Skip to content

Commit 12429a0

Browse files
Basic for parsing/interpreting (#32)
Co-authored-by: Tushar Sadhwani <[email protected]>
1 parent f426cd0 commit 12429a0

File tree

4 files changed

+257
-12
lines changed

4 files changed

+257
-12
lines changed

src/interpreted/interpreter.py

+107-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

3+
import sys
34
from collections import deque
4-
from typing import Any
5+
from typing import Any, Iterable
56
from unittest import mock
67

78
from interpreted import nodes
@@ -42,6 +43,7 @@ def __init__(self, parent=None) -> None:
4243
self.set("int", Int())
4344
self.set("float", Float())
4445
self.set("deque", DequeConstructor())
46+
self.set("enumerate", Enumerate())
4547

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

124126

127+
class Enumerate(Function):
128+
def as_string(self) -> str:
129+
return "<function 'enumerate'>"
130+
131+
def arg_count(self) -> int:
132+
return 1
133+
134+
def call(self, _: Interpreter, args: list[Object]) -> Object:
135+
super().ensure_args(args)
136+
# We don't have generator support yet :^)
137+
pairs = [Tuple([Value(idx), val]) for idx, val in enumerate(args[0])]
138+
return List(pairs)
139+
140+
125141
class Int(Function):
126142
def as_string(self) -> str:
127143
return "<function 'int'>"
@@ -257,6 +273,24 @@ def call(self, _: Interpreter, args: list[Object]) -> None:
257273
self.wrapper._data.append(item)
258274

259275

276+
class Items(Function):
277+
def __init__(self, wrapper: Dict) -> None:
278+
super().__init__()
279+
self.wrapper = wrapper
280+
281+
def as_string(self) -> str:
282+
return f"<method 'items' of {self.wrapper.repr()}>"
283+
284+
def arg_count(self) -> int:
285+
return 0
286+
287+
def call(self, _: Interpreter, args: list[Object]) -> Any:
288+
super().ensure_args(args)
289+
# We don't have generator support yet :^)
290+
pairs = [Tuple(key_value_pair) for key_value_pair in self.wrapper._dict.items()]
291+
return List(pairs)
292+
293+
260294
class PopLeft(Function):
261295
def __init__(self, deque: Deque) -> None:
262296
super().__init__()
@@ -354,29 +388,35 @@ def call(self, _: Interpreter, args: list[Object]) -> Value:
354388

355389

356390
class List(Object):
357-
def __init__(self, elements) -> None:
391+
def __init__(self, elements: Iterable[Object]) -> None:
358392
super().__init__()
359393
self._data = elements
360394
self.methods["append"] = Append(self)
361395

362396
def as_string(self) -> str:
363397
return "[" + ", ".join(item.repr() for item in self._data) + "]"
364398

399+
def __iter__(self) -> Iterable[Object]:
400+
return iter(self._data)
401+
365402

366403
class Tuple(Object):
367-
def __init__(self, elements) -> None:
404+
def __init__(self, elements: Iterable[Object]) -> None:
368405
super().__init__()
369406
self._data = elements
370407

371408
def as_string(self) -> str:
372409
return "(" + ", ".join(item.repr() for item in self._data) + ")"
373410

411+
def __iter__(self) -> Iterable[Object]:
412+
return iter(self._data)
413+
374414

375415
class Dict(Object):
376416
def __init__(self, keys: list[Object], values: list[Object]) -> None:
377417
super().__init__()
378-
379-
self._dict = {key: value for key, value in zip(keys, values, strict=True)}
418+
self._dict = {key: value for key, value in zip(keys, values)}
419+
self.methods["items"] = Items(self)
380420

381421
def as_string(self) -> str:
382422
return (
@@ -387,6 +427,9 @@ def as_string(self) -> str:
387427
+ "}"
388428
)
389429

430+
def __iter__(self) -> Iterable[Object]:
431+
return iter(list(self._dict))
432+
390433

391434
def is_truthy(obj: Object) -> bool:
392435
if isinstance(obj, Value):
@@ -486,14 +529,16 @@ def visit_FunctionDef(self, node: FunctionDef) -> None:
486529

487530
self.scope.set(node.name, function)
488531

489-
def visit_Assign(self, node: Assign) -> None:
490-
value = self.visit(node.value)
491-
assert len(node.targets) == 1 # TODO
492-
target = node.targets[0]
493-
532+
def assign(self, target: Node, value: Object) -> None:
494533
if isinstance(target, Name):
495534
self.scope.set(target.id, value)
496535

536+
elif isinstance(target, (nodes.List, nodes.Tuple)) and isinstance(
537+
value, (List, Tuple, Deque, Dict)
538+
):
539+
for element, value in zip(target.elements, value):
540+
self.assign(element, value)
541+
497542
elif isinstance(target, Subscript):
498543
obj = self.visit(target.value)
499544

@@ -517,7 +562,14 @@ def visit_Assign(self, node: Assign) -> None:
517562
)
518563

519564
else:
520-
raise NotImplementedError(target) # TODO
565+
raise NotImplementedError(target, value) # TODO
566+
567+
def visit_Assign(self, node: Assign) -> None:
568+
value = self.visit(node.value)
569+
assert len(node.targets) == 1 # TODO
570+
target = node.targets[0]
571+
572+
self.assign(target, value)
521573

522574
def visit_AugAssign(self, node: AugAssign) -> None:
523575
increment = self.visit(node.value)
@@ -544,6 +596,29 @@ def visit_If(self, node: If) -> None:
544596
for stmt in node.orelse:
545597
self.visit(stmt)
546598

599+
def visit_For(self, node: nodes.For) -> None:
600+
if isinstance(node.iterable, (nodes.List, nodes.Tuple)):
601+
elements = [self.visit(element) for element in node.iterable.elements]
602+
elif isinstance(node.iterable, nodes.Dict):
603+
elements = [self.visit(element) for element in node.iterable.keys]
604+
else:
605+
elements = self.visit(node.iterable)
606+
if not isinstance(elements, (List, Tuple, Deque, Dict)):
607+
raise InterpreterError(
608+
f"Object of type {type(elements).__name__} is not iterable"
609+
)
610+
611+
for element in elements:
612+
self.assign(node.target, element)
613+
614+
for stmt in node.body:
615+
try:
616+
self.visit(stmt)
617+
except Break:
618+
return
619+
except Continue:
620+
break
621+
547622
def visit_While(self, node: While) -> None:
548623
while is_truthy(self.visit(node.condition)):
549624
for stmt in node.body:
@@ -792,3 +867,24 @@ def interpret(source: str) -> None:
792867
return
793868

794869
Interpreter().visit(module)
870+
871+
872+
def main() -> None:
873+
source = sys.stdin.read()
874+
module = interpret(source)
875+
if module is None:
876+
return
877+
878+
if "--pretty" in sys.argv:
879+
try:
880+
import black
881+
except ImportError:
882+
print("Error: `black` needs to be installed for `--pretty` to work.")
883+
884+
print(black.format_str(repr(module), mode=black.Mode()))
885+
else:
886+
print(module)
887+
888+
889+
if __name__ == "__main__":
890+
main()

src/interpreted/parser.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,9 @@ def parse_multiline_statement(self) -> FunctionDef | For | If | While:
226226
if keyword == "while":
227227
return self.parse_while()
228228

229-
# TODO: for
229+
if keyword == "for":
230+
return self.parse_for()
231+
230232
raise NotImplementedError()
231233

232234
def parse_function_def(self) -> FunctionDef:
@@ -283,6 +285,34 @@ def parse_while(self) -> While:
283285

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

288+
def parse_for(self) -> For:
289+
targets = []
290+
targets.append(self.parse_primary())
291+
while self.match_op(","):
292+
# as soon as we see the first `in` keyword, we assume target to have ended
293+
if self.peek().token_type == TokenType.NAME and self.peek().string == "in":
294+
break
295+
296+
targets.append(self.parse_primary())
297+
298+
if len(targets) == 1:
299+
target = targets[0]
300+
else:
301+
target = Tuple(targets)
302+
303+
self.expect_name("in")
304+
305+
expressions = self.parse_expressions()
306+
if len(expressions) == 1:
307+
iterable = expressions[0]
308+
else:
309+
iterable = Tuple(expressions)
310+
311+
self.expect_op(":")
312+
body = self.parse_block()
313+
314+
return For(target=target, iterable=iterable, body=body, orelse=None)
315+
286316
def parse_block(self) -> list[Statement]:
287317
self.expect(TokenType.NEWLINE)
288318
self.expect(TokenType.INDENT)

tests/interpreted_test.py

+75
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,81 @@ def test_interpret(source, output) -> None:
168168
assert process.stdout.decode() == dedent(output)
169169

170170

171+
@pytest.mark.parametrize(
172+
("source", "output"),
173+
(
174+
(
175+
"""\
176+
for e in [1,2]:
177+
print(e)
178+
""",
179+
"1\n2\n",
180+
),
181+
(
182+
"""\
183+
lst = ['test','test123']
184+
for e in lst:
185+
print(e)
186+
""",
187+
"test\ntest123\n",
188+
),
189+
(
190+
"""\
191+
for x in 1, 2:
192+
print(x)
193+
""",
194+
"1\n2\n",
195+
),
196+
(
197+
"""\
198+
dct = { "one": 1, "two": 2 }
199+
for k in dct:
200+
print(k, dct[k])
201+
""",
202+
"one 1\ntwo 2\n",
203+
),
204+
(
205+
"""\
206+
dct = { "one": 1, "two": 2 }
207+
for k,v in dct.items():
208+
print(k, v)
209+
""",
210+
"one 1\ntwo 2\n",
211+
),
212+
(
213+
"""\
214+
dct = { "one": 1, "two": 2 }
215+
for k in dct.items():
216+
print(k)
217+
""",
218+
"('one', 1)\n('two', 2)\n",
219+
),
220+
(
221+
"""\
222+
dct = { "one": 1, "two": 2 }
223+
for idx, tup in enumerate(dct):
224+
print(idx, tup)
225+
""",
226+
"0 one\n1 two\n",
227+
),
228+
),
229+
)
230+
def test_for(source, output) -> None:
231+
"""Tests the interpreter CLI."""
232+
with tempfile.NamedTemporaryFile("w+") as file:
233+
file.write(dedent(source))
234+
file.seek(0)
235+
236+
process = subprocess.run(
237+
["interpreted", file.name],
238+
stdout=subprocess.PIPE,
239+
stderr=subprocess.PIPE,
240+
)
241+
242+
assert process.stderr == b""
243+
assert process.stdout.decode() == dedent(output)
244+
245+
171246
def test_file_not_found() -> None:
172247
"""Tests the file not found prompt."""
173248
process = subprocess.run(

tests/parser_test.py

+44
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
Compare,
1010
Constant,
1111
ExprStmt,
12+
For,
1213
Import,
1314
ImportFrom,
1415
Module,
1516
Name,
17+
Tuple,
1618
While,
1719
alias,
1820
)
@@ -129,6 +131,48 @@
129131
]
130132
),
131133
),
134+
(
135+
"""\
136+
for a in b:
137+
42
138+
for i, j in x, t, u in y in a:
139+
print(1)
140+
""",
141+
Module(
142+
body=[
143+
For(
144+
target=Name(id="a"),
145+
iterable=Name(id="b"),
146+
body=[ExprStmt(value=Constant(value=42))],
147+
orelse=None,
148+
),
149+
For(
150+
target=Tuple(elements=[Name(id="i"), Name(id="j")]),
151+
iterable=Tuple(
152+
elements=[
153+
Name(id="x"),
154+
Name(id="t"),
155+
Compare(
156+
left=Compare(
157+
left=Name(id="u"), op="in", right=Name(id="y")
158+
),
159+
op="in",
160+
right=Name(id="a"),
161+
),
162+
]
163+
),
164+
body=[
165+
ExprStmt(
166+
value=Call(
167+
function=Name(id="print"), args=[Constant(value=1)]
168+
)
169+
)
170+
],
171+
orelse=None,
172+
),
173+
]
174+
),
175+
),
132176
),
133177
)
134178
def test_parser(source: str, tree: Module) -> None:

0 commit comments

Comments
 (0)