Skip to content

Commit

Permalink
- basic for parsing/interpreting
Browse files Browse the repository at this point in the history
Also implements __iter__ on Dict and List
added main handling on interpreter
Added enumerate global function
Added items function on Dict
tests also check real python
  • Loading branch information
Jacco committed Nov 29, 2023
1 parent 15a5388 commit feef1bc
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 2 deletions.
87 changes: 86 additions & 1 deletion src/interpreted/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import deque
from typing import Any
from unittest import mock
import sys

from interpreted import nodes
from interpreted.nodes import (
Expand Down Expand Up @@ -42,6 +43,7 @@ def __init__(self, parent=None) -> None:
self.set("int", Int())
self.set("float", Float())
self.set("deque", DequeConstructor())
self.set("enumerate", Enumerate())

def get(self, name) -> Any:
return self.data.get(name, NOT_SET)
Expand Down Expand Up @@ -122,6 +124,19 @@ def call(self, _: Interpreter, args: list[Object]) -> Object:
raise InterpreterError(f"{type(item).__name__} has no len()")


class Enumerate(Function):
def as_string(self) -> str:
return "<function 'enumerate'>"

def arg_count(self) -> int:
return 1

def call(self, _: Interpreter, args: list[Object]) -> Object:
super().ensure_args(args)
for idx, val in enumerate(args[0]):
yield Tuple([Value(idx), val])


class Int(Function):
def as_string(self) -> str:
return "<function 'int'>"
Expand Down Expand Up @@ -257,6 +272,23 @@ def call(self, _: Interpreter, args: list[Object]) -> None:
self.wrapper._data.append(item)


class Items(Function):
def __init__(self, wrapper: Dict) -> None:
super().__init__()
self.wrapper = wrapper

def as_string(self) -> str:
return f"<method 'items' of {self.wrapper.repr()}>"

def arg_count(self) -> int:
return 0

def call(self, _: Interpreter, args: list[Object]) -> Any:
super().ensure_args(args)
for kvp in self.wrapper._dict.items():
yield Tuple(kvp)


class PopLeft(Function):
def __init__(self, deque: Deque) -> None:
super().__init__()
Expand Down Expand Up @@ -362,6 +394,9 @@ def __init__(self, elements) -> None:
def as_string(self) -> str:
return "[" + ", ".join(item.repr() for item in self._data) + "]"

# TODO Review this
def __iter__(self):
return self._data.__iter__()

class Tuple(Object):
def __init__(self, elements) -> None:
Expand All @@ -375,8 +410,8 @@ def as_string(self) -> str:
class Dict(Object):
def __init__(self, keys: list[Object], values: list[Object]) -> None:
super().__init__()

self._dict = {key: value for key, value in zip(keys, values, strict=True)}
self.methods["items"] = Items(self)

def as_string(self) -> str:
return (
Expand All @@ -387,6 +422,9 @@ def as_string(self) -> str:
+ "}"
)

# TODO review this
def __iter__(self):
return self._dict.__iter__()

def is_truthy(obj: Object) -> bool:
if isinstance(obj, Value):
Expand Down Expand Up @@ -544,6 +582,32 @@ def visit_If(self, node: If) -> None:
for stmt in node.orelse:
self.visit(stmt)

def visit_For(self, node: For) -> None:
if len(node.iterable) == 1:
elements = self.visit(node.iterable[0])
else:
elements = [self.visit(e) for e in node.iterable]
for element in elements:
value = element
if len(node.target) > 1:
# TODO Review this: must be tuple? (or can it be list too?)
if len(node.target) > len(value._data):
raise Exception(f"ValueError: too many values to unpack (expected {len(node.target)})")
for idx, t in enumerate(node.target):
if isinstance(t, Name):
self.scope.set(t.id, value._data[idx])
else:
target = node.target[0]
if isinstance(target, Name):
self.scope.set(target.id, value)
for stmt in node.body:
try:
self.visit(stmt)
except Break:
return
except Continue:
break

def visit_While(self, node: While) -> None:
while is_truthy(self.visit(node.condition)):
for stmt in node.body:
Expand Down Expand Up @@ -792,3 +856,24 @@ def interpret(source: str) -> None:
return

Interpreter().visit(module)


def main() -> None:
source = sys.stdin.read()
module = interpret(source)
if module is None:
return

if "--pretty" in sys.argv:
try:
import black
except ImportError:
print("Error: `black` needs to be installed for `--pretty` to work.")

print(black.format_str(repr(module), mode=black.Mode()))
else:
print(module)


if __name__ == "__main__":
main()
22 changes: 21 additions & 1 deletion src/interpreted/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ def parse_multiline_statement(self) -> FunctionDef | For | If | While:
if keyword == "while":
return self.parse_while()

# TODO: for
if keyword == "for":
return self.parse_for()

raise NotImplementedError()

def parse_function_def(self) -> FunctionDef:
Expand Down Expand Up @@ -283,6 +285,24 @@ def parse_while(self) -> While:

return While(condition=condition, body=body, orelse=orelse)

def parse_for(self) -> For:
targets = []
targets.append(self.parse_primary())
while self.match_op(","):
# TODO Review this?
if self.peek().token_type == TokenType.NAME and self.peek().string == 'in':
break
targets.append(self.parse_primary())

self.expect_name('in')

iterable = self.parse_expressions()

self.expect_op(':')
body = self.parse_block()

return For(target=targets, iterable=iterable, body=body, orelse=None)

def parse_block(self) -> list[Statement]:
self.expect(TokenType.NEWLINE)
self.expect(TokenType.INDENT)
Expand Down
84 changes: 84 additions & 0 deletions tests/interpreted_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,90 @@ def test_interpret(source, output) -> None:
assert process.stderr == b""
assert process.stdout.decode() == dedent(output)

@pytest.mark.parametrize(
("source", "output"),
(
(
"""\
for e in [1,2]:
print(e)
""",
"1\n2\n"
),
(
"""\
lst = ['test','test123']
for e in lst:
print(e)
""",
"test\ntest123\n"
),
(
"""\
for x in 1, 2:
print(x)
""",
"1\n2\n"
),
(
"""\
dct = { "one": 1, "two": 2 }
for k in dct:
print(k, dct[k])
""",
"one 1\ntwo 2\n"
),
(
"""\
dct = { "one": 1, "two": 2 }
for k,v in dct.items():
print(k, v)
""",
"one 1\ntwo 2\n"
),
(
"""\
dct = { "one": 1, "two": 2 }
for k in dct.items():
print(k)
""",
"('one', 1)\n('two', 2)\n"
),
(
"""\
dct = { "one": 1, "two": 2 }
for idx, tup in enumerate(dct):
print(idx, tup)
""",
"0 one\n1 two\n"
),
),
)
def test_for(source, output) -> None:
"""Tests the interpreter CLI."""
with tempfile.NamedTemporaryFile("w+") as file:
file.write(dedent(source))
file.seek(0)

process = subprocess.run(
["interpreted", file.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)

# also test real python output
# TODO this gives a lot of coverage warnings
file.seek(0)
process2 = subprocess.run(
["python", file.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)

assert process2.stdout.decode() == dedent(output)
assert process.stderr == b""
assert process.stdout.decode() == dedent(output)


def test_file_not_found() -> None:
"""Tests the file not found prompt."""
Expand Down

0 comments on commit feef1bc

Please sign in to comment.