diff --git a/juic/builtins.py b/juic/builtins.py index 23b45fd..f4a97b1 100644 --- a/juic/builtins.py +++ b/juic/builtins.py @@ -2,7 +2,7 @@ from juic.datatypes import * from juic.eval import * def juiPrint(*args): - print("[print]", *args) + print("[print]", *(juiValueString(a) for a in args)) def juiLen(x): if type(x) not in [str, list]: diff --git a/juic/datatypes.py b/juic/datatypes.py index 654cc75..7e5d48f 100644 --- a/juic/datatypes.py +++ b/juic/datatypes.py @@ -1,5 +1,9 @@ from dataclasses import dataclass, field -from typing import Callable, Union +from typing import Callable, Union, TYPE_CHECKING +import juic + +if TYPE_CHECKING: + import juic.eval # Mapping of Jui types to Python types: # Null None @@ -62,9 +66,9 @@ class Record: # A record type if it's pure, or a thunk with the base object otherwise. base: Union[RecordType, "juic.eval.Thunk"] # Standard key-value attribute pairs - attr: dict[str, "juic.value.Thunk"] + attr: dict[str, "juic.eval.Thunk"] # Children elements - children: list["juic.value.Thunk"] + children: list["juic.eval.Thunk"] # TODO: Keep track of variables that are not fields, i.e. "methods"? # scope: dict[str, "JuiValue"] # TODO: Labels @@ -93,7 +97,7 @@ def juiIsCallable(value): return type(value) in [BuiltinFunction, Function] def juiIsProjectable(value): - return type(value) in [ThisRef, Record] + return type(value) in [Record, juic.eval.PartialRecordSnapshot] def juiIsConstructible(value): return type(value) in [RecordType, Function] @@ -133,15 +137,25 @@ def juiValueString(v): s += "; ".join(juiValueString(x) for x in r.children) return s + "}" raise NotImplementedError - case Thunk() as th: + case juic.eval.Thunk() as th: return str(th) - case PartialRecordSnapshot() as prs: + case juic.eval.PartialRecordSnapshot() as prs: return str(prs) case _: raise NotImplementedError # Check whether two *forced* values are equal def juiValuesEqual(v1, v2): + def unwrapThunk(v): + match v: + case juic.eval.Thunk() as th: + assert th.evaluated and not th.invalid + return unwrapThunk(th.result) + case _: + return v + + v1 = unwrapThunk(v1) + v2 = unwrapThunk(v2) match v1, v2: case None, None: return True @@ -177,11 +191,8 @@ def juiValuesEqual(v1, v2): for v1, v2 in zip(r1.children, r2.children)): return False return True - case Thunk() as th1, Thunk() as th2: - assert th1.evaluated and not th1.invalid - assert th2.evaluated and not th2.invalid - return juiValuesEqual(th1.result, th2.result) - case PartialRecordSnapshot(), PartialRecordSnapshot(): + case juic.eval.PartialRecordSnapshot(), \ + juic.eval.PartialRecordSnapshot(): raise Exception("cannot compare PRSs") case _, _: raise Exception(f"invalid types for comparison: {type(v1)}, {type(v2)}") diff --git a/juic/eval.py b/juic/eval.py index cddb46d..12890d5 100644 --- a/juic/eval.py +++ b/juic/eval.py @@ -106,17 +106,17 @@ class Thunk: def __str__(self): s = "Thunk(" - if th._running: + if self._running: s += "running, " - if th.evaluated: + if self.evaluated: s += "evaluated" - if th.invalid: + if self.invalid: s += ", invalid" else: - s += " = " + juiValueString(th.result) + s += " = " + juiValueString(self.result) else: s += "unevaluated" - s += "){ " + str(th.ast) + " }" + s += "){ " + str(self.ast) + " }" return s # Object representing the interpretation of a partially-built record. In a @@ -137,7 +137,7 @@ class PartialRecordSnapshot: base: None | Thunk = None def __str__(self): - return "" # TODO + return "" # TODO def copy(self): return PartialRecordSnapshot(self.fieldThunks.copy(), self.base) @@ -344,10 +344,21 @@ class Context: return self.evalCall(f, [X]) case Node.T.THIS, []: - raise NotImplementedError + v = self.lookup("this") + if v is None: + raise JuiNameError(f"no 'this' in current context") + assert isinstance(v, PartialRecordSnapshot) + return v + + case Node.T.PROJ, [R, field]: + r = self.evalExpr(R) + requireType(r, juiIsProjectable) + f = self.project(r, field) + if isinstance(f, Thunk): + return self.evalThunk(f) + else: + return f - case Node.T.PROJ, []: - raise NotImplementedError case Node.T.CALL, [F, *A]: f = self.evalExpr(F) @@ -376,6 +387,22 @@ class Context: case _, _: raise Exception("invalid expr o(x_x)o: " + str(node)) + def project(self, v: JuiValue, field: str) -> JuiValue: + match v: + case Record() as r: + if field not in r.attr: + raise Exception(f"access to undefined field {field}") + return r.attr[field] + case PartialRecordSnapshot() as prs: + if field in prs.fieldThunks: + return prs.fieldThunks[field] + elif prs.base is None: + raise Exception(f"access to undefined field {field} of 'this'") + else: + return self.project(prs.base, v) + case _: + raise NotImplementedError # unreachable + def execStmt(self, node: Node) -> JuiValue: match node.ctor, node.args: case Node.T.LET_DECL, [name, X]: @@ -397,9 +424,9 @@ class Context: ve = self.force(ve) if not juiValuesEqual(vs, ve): print("unit test failed:") - print(" " + str(vs)) + print(" " + juiValueString(vs)) print("vs.") - print(" " + str(ve)) + print(" " + juiValueString(ve)) case _, _: return self.evalExpr(node) diff --git a/juic/examples/unit_tests.jui b/juic/examples/unit_tests.jui index 90c347c..3cfc3b9 100644 --- a/juic/examples/unit_tests.jui +++ b/juic/examples/unit_tests.jui @@ -27,3 +27,16 @@ let z = 4; let helloworld = "Hello, World!"; len("xyz" + helloworld) + 1; //^ 17; + +record {}; +//^ record {}; + +let r = record {x: 2; y: 3+5}; +r; +//^ record {y:8; x:2}; + +r.x; +//^ 2; + +record {x: r.x; y: this.x + 2; x: 4; z: this.x + 5}; +//^ record {x: 4; y: 4; z: 9}; diff --git a/juic/main.py b/juic/main.py index a7245b3..f2b5b9e 100644 --- a/juic/main.py +++ b/juic/main.py @@ -56,8 +56,8 @@ def main(argv): parser = juic.parser.JuiParser(lexer) ast = parser.scope() if opts.get("--debug") == "parser": - for e in ast.args: - e.dump() + for node in ast.args: + node.dump() print("---") return 0 @@ -69,13 +69,13 @@ def main(argv): ctx = juic.eval.Context(juic.builtins.builtinClosure) if "--unit-tests" in opts: - for e in ast.args: - ctx.execStmt(e) + for node in ast.args: + ctx.execStmt(node) return 0 - for e in ast.args: - e.dump() - v = ctx.execStmt(e) + for node in ast.args: + node.dump() + v = ctx.execStmt(node) v = ctx.force(v) print(">>>>>>>", juic.eval.juiValueString(v)) diff --git a/juic/parser.py b/juic/parser.py index eaa203b..d1429ac 100644 --- a/juic/parser.py +++ b/juic/parser.py @@ -391,7 +391,7 @@ class JuiParser(LL1Parser): node = Node(Node.T.CALL, [node, *args]) # Postfix update or record creation operation - while self.la.type in ["{", "<{"]: + while self.la is not None and self.la.type in ["{", "<{"]: entries = self.record_literal() node = Node(Node.T.RECORD, [node, *entries])