juic: add some record processing, start checking with mypy

This commit is contained in:
Lephenixnoir 2024-08-28 13:31:17 +02:00
parent e79ba0056b
commit 4579acc0f4
No known key found for this signature in database
GPG key ID: 1BBA026E13FC0495
6 changed files with 82 additions and 31 deletions

View file

@ -2,7 +2,7 @@ from juic.datatypes import *
from juic.eval import * from juic.eval import *
def juiPrint(*args): def juiPrint(*args):
print("[print]", *args) print("[print]", *(juiValueString(a) for a in args))
def juiLen(x): def juiLen(x):
if type(x) not in [str, list]: if type(x) not in [str, list]:

View file

@ -1,5 +1,9 @@
from dataclasses import dataclass, field 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: # Mapping of Jui types to Python types:
# Null None # Null None
@ -62,9 +66,9 @@ class Record:
# A record type if it's pure, or a thunk with the base object otherwise. # A record type if it's pure, or a thunk with the base object otherwise.
base: Union[RecordType, "juic.eval.Thunk"] base: Union[RecordType, "juic.eval.Thunk"]
# Standard key-value attribute pairs # Standard key-value attribute pairs
attr: dict[str, "juic.value.Thunk"] attr: dict[str, "juic.eval.Thunk"]
# Children elements # Children elements
children: list["juic.value.Thunk"] children: list["juic.eval.Thunk"]
# TODO: Keep track of variables that are not fields, i.e. "methods"? # TODO: Keep track of variables that are not fields, i.e. "methods"?
# scope: dict[str, "JuiValue"] # scope: dict[str, "JuiValue"]
# TODO: Labels # TODO: Labels
@ -93,7 +97,7 @@ def juiIsCallable(value):
return type(value) in [BuiltinFunction, Function] return type(value) in [BuiltinFunction, Function]
def juiIsProjectable(value): def juiIsProjectable(value):
return type(value) in [ThisRef, Record] return type(value) in [Record, juic.eval.PartialRecordSnapshot]
def juiIsConstructible(value): def juiIsConstructible(value):
return type(value) in [RecordType, Function] return type(value) in [RecordType, Function]
@ -133,15 +137,25 @@ def juiValueString(v):
s += "; ".join(juiValueString(x) for x in r.children) s += "; ".join(juiValueString(x) for x in r.children)
return s + "}" return s + "}"
raise NotImplementedError raise NotImplementedError
case Thunk() as th: case juic.eval.Thunk() as th:
return str(th) return str(th)
case PartialRecordSnapshot() as prs: case juic.eval.PartialRecordSnapshot() as prs:
return str(prs) return str(prs)
case _: case _:
raise NotImplementedError raise NotImplementedError
# Check whether two *forced* values are equal # Check whether two *forced* values are equal
def juiValuesEqual(v1, v2): 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: match v1, v2:
case None, None: case None, None:
return True return True
@ -177,11 +191,8 @@ def juiValuesEqual(v1, v2):
for v1, v2 in zip(r1.children, r2.children)): for v1, v2 in zip(r1.children, r2.children)):
return False return False
return True return True
case Thunk() as th1, Thunk() as th2: case juic.eval.PartialRecordSnapshot(), \
assert th1.evaluated and not th1.invalid juic.eval.PartialRecordSnapshot():
assert th2.evaluated and not th2.invalid
return juiValuesEqual(th1.result, th2.result)
case PartialRecordSnapshot(), PartialRecordSnapshot():
raise Exception("cannot compare PRSs") raise Exception("cannot compare PRSs")
case _, _: case _, _:
raise Exception(f"invalid types for comparison: {type(v1)}, {type(v2)}") raise Exception(f"invalid types for comparison: {type(v1)}, {type(v2)}")

View file

@ -106,17 +106,17 @@ class Thunk:
def __str__(self): def __str__(self):
s = "Thunk(" s = "Thunk("
if th._running: if self._running:
s += "running, " s += "running, "
if th.evaluated: if self.evaluated:
s += "evaluated" s += "evaluated"
if th.invalid: if self.invalid:
s += ", invalid" s += ", invalid"
else: else:
s += " = " + juiValueString(th.result) s += " = " + juiValueString(self.result)
else: else:
s += "unevaluated" s += "unevaluated"
s += "){ " + str(th.ast) + " }" s += "){ " + str(self.ast) + " }"
return s return s
# Object representing the interpretation of a partially-built record. In a # Object representing the interpretation of a partially-built record. In a
@ -137,7 +137,7 @@ class PartialRecordSnapshot:
base: None | Thunk = None base: None | Thunk = None
def __str__(self): def __str__(self):
return "<A PRS AAAAAAH>" # TODO return "<PartialRecordSnapshot>" # TODO
def copy(self): def copy(self):
return PartialRecordSnapshot(self.fieldThunks.copy(), self.base) return PartialRecordSnapshot(self.fieldThunks.copy(), self.base)
@ -344,10 +344,21 @@ class Context:
return self.evalCall(f, [X]) return self.evalCall(f, [X])
case Node.T.THIS, []: 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]: case Node.T.CALL, [F, *A]:
f = self.evalExpr(F) f = self.evalExpr(F)
@ -376,6 +387,22 @@ class Context:
case _, _: case _, _:
raise Exception("invalid expr o(x_x)o: " + str(node)) 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: def execStmt(self, node: Node) -> JuiValue:
match node.ctor, node.args: match node.ctor, node.args:
case Node.T.LET_DECL, [name, X]: case Node.T.LET_DECL, [name, X]:
@ -397,9 +424,9 @@ class Context:
ve = self.force(ve) ve = self.force(ve)
if not juiValuesEqual(vs, ve): if not juiValuesEqual(vs, ve):
print("unit test failed:") print("unit test failed:")
print(" " + str(vs)) print(" " + juiValueString(vs))
print("vs.") print("vs.")
print(" " + str(ve)) print(" " + juiValueString(ve))
case _, _: case _, _:
return self.evalExpr(node) return self.evalExpr(node)

View file

@ -27,3 +27,16 @@ let z = 4;
let helloworld = "Hello, World!"; let helloworld = "Hello, World!";
len("xyz" + helloworld) + 1; len("xyz" + helloworld) + 1;
//^ 17; //^ 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};

View file

@ -56,8 +56,8 @@ def main(argv):
parser = juic.parser.JuiParser(lexer) parser = juic.parser.JuiParser(lexer)
ast = parser.scope() ast = parser.scope()
if opts.get("--debug") == "parser": if opts.get("--debug") == "parser":
for e in ast.args: for node in ast.args:
e.dump() node.dump()
print("---") print("---")
return 0 return 0
@ -69,13 +69,13 @@ def main(argv):
ctx = juic.eval.Context(juic.builtins.builtinClosure) ctx = juic.eval.Context(juic.builtins.builtinClosure)
if "--unit-tests" in opts: if "--unit-tests" in opts:
for e in ast.args: for node in ast.args:
ctx.execStmt(e) ctx.execStmt(node)
return 0 return 0
for e in ast.args: for node in ast.args:
e.dump() node.dump()
v = ctx.execStmt(e) v = ctx.execStmt(node)
v = ctx.force(v) v = ctx.force(v)
print(">>>>>>>", juic.eval.juiValueString(v)) print(">>>>>>>", juic.eval.juiValueString(v))

View file

@ -391,7 +391,7 @@ class JuiParser(LL1Parser):
node = Node(Node.T.CALL, [node, *args]) node = Node(Node.T.CALL, [node, *args])
# Postfix update or record creation operation # 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() entries = self.record_literal()
node = Node(Node.T.RECORD, [node, *entries]) node = Node(Node.T.RECORD, [node, *entries])