mirror of
https://git.planet-casio.com/Lephenixnoir/JustUI.git
synced 2024-12-28 04:23:40 +01:00
Compare commits
5 commits
24f3e14061
...
aa736bd12a
Author | SHA1 | Date | |
---|---|---|---|
|
aa736bd12a | ||
|
5f198ff6b0 | ||
|
ce39929bb4 | ||
|
4579acc0f4 | ||
|
e79ba0056b |
8 changed files with 417 additions and 232 deletions
|
@ -156,6 +156,8 @@ BIG TODO:
|
|||
- Don't have a scope and ability to let- or fun-declare inside function body!!
|
||||
- `rec x() = y {}; x {}` is it of record type `x` or `y`? How to say that `x`
|
||||
is based on `y`?
|
||||
-> it's of type y, which is derived from x and may or may not have custom
|
||||
codegen that overrides that of x
|
||||
|
||||
IMPLEMENTATION TODO:
|
||||
- List syntax
|
||||
|
@ -177,17 +179,6 @@ The syntax for assigning at labels is a bit dodgy at the moment. It should be
|
|||
-> function-based interface param("FX")
|
||||
- "Control-flow": for(each) loops, what else needed for basic ops?
|
||||
|
||||
Limitation of lazy record construction: if you use a function in the middle of
|
||||
a record construction, the record update `<{ }` will force the evaluation of
|
||||
fields even though it would be possible not to force that.
|
||||
|
||||
```
|
||||
fun f(e) = e <{ width: e.width + 10 };
|
||||
rec r1(x) = f <| base { width: x };
|
||||
rec r2(x) = r1 { height: this.width; x };
|
||||
r2 { 10; }
|
||||
```
|
||||
|
||||
Capturing "this" in a local variable to get a snapshot is required, otherwise
|
||||
we can't refer to parents.
|
||||
|
||||
|
|
|
@ -2,19 +2,28 @@ 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]:
|
||||
raise JuiTypeError("len")
|
||||
return len(x)
|
||||
|
||||
R_record = RecordType("record", None)
|
||||
R_subrecord = RecordType("subrecord", R_record)
|
||||
R_jwidget = RecordType("jwidget", None)
|
||||
R_jlabel = RecordType("jlabel", R_jwidget)
|
||||
R_jfkeys = RecordType("jfkeys", R_jwidget)
|
||||
|
||||
builtinClosure = Closure(parent=None, scope=MutableScopeData(defs={
|
||||
"print": (0, BuiltinFunction(juiPrint)),
|
||||
"len": (0, BuiltinFunction(juiLen)),
|
||||
|
||||
# TODO: Remove the built-in record type "record" used for testing
|
||||
"record": (0, RecordType.makePlainRecordType()),
|
||||
"subrecord": (0, RecordType.makePlainRecordType()),
|
||||
"record": (0, R_record),
|
||||
"subrecord": (0, R_subrecord),
|
||||
"jwidget": (0, R_jwidget),
|
||||
"jlabel": (0, R_jlabel),
|
||||
"jfkeys": (0, R_jfkeys),
|
||||
|
||||
}, timestamp=1))
|
||||
|
|
|
@ -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
|
||||
|
@ -29,41 +33,31 @@ class Function:
|
|||
# Expression node to evaluate when calling the function
|
||||
body: "juic.parser.Node"
|
||||
# Parameter names, must all be unique. May be empty
|
||||
params: list[str] = field(default_factory=[])
|
||||
params: list[str] = field(default_factory=list)
|
||||
# Name of variadic argument if one; must also be unique
|
||||
variadic: str | None = None
|
||||
|
||||
@dataclass
|
||||
class RecordType:
|
||||
# Record name
|
||||
name: str
|
||||
# Base type for inheritance/subconstructor logic
|
||||
base: Union[None, "RecordType"]
|
||||
|
||||
# TODO: Record prototypes?
|
||||
|
||||
@staticmethod
|
||||
def makePlainRecordType():
|
||||
return RecordType()
|
||||
|
||||
# @dataclass
|
||||
# class RecordCtor:
|
||||
# # Context in which the record body is evaluated
|
||||
# closure: "juic.eval.Closure"
|
||||
# # List of entries to produce, of type `REC_ATTR` or `REC_VALUE`
|
||||
# entries: list["juic.parser.Node"]
|
||||
# # Parameter names, must all be unique. May be empty
|
||||
# params: list[str] = field(default_factory=[])
|
||||
# # Name of variadic argument if one; must also be unique
|
||||
# variadic: str | None = None
|
||||
|
||||
# @staticmethod
|
||||
# def makePlainRecordCtor():
|
||||
# return RecordCtor(closure=None, entries=[], params=[], variadic=None)
|
||||
@dataclass
|
||||
class RecordCtor:
|
||||
func: Function
|
||||
|
||||
@dataclass
|
||||
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, Union["JuiValue", "juic.eval.Thunk"]]
|
||||
# Children elements
|
||||
children: list["juic.value.Thunk"]
|
||||
children: list[Union["JuiValue", "juic.eval.Thunk"]]
|
||||
# TODO: Keep track of variables that are not fields, i.e. "methods"?
|
||||
# scope: dict[str, "JuiValue"]
|
||||
# TODO: Labels
|
||||
|
@ -71,7 +65,7 @@ class Record:
|
|||
|
||||
JuiValue = Union[None, bool, int, float, str, CXXQualid, list,
|
||||
"juic.eval.PartialRecordSnapshot", BuiltinFunction, Function,
|
||||
RecordType, Record]
|
||||
RecordType, RecordCtor, Record]
|
||||
|
||||
def juiIsArith(value):
|
||||
return type(value) in [bool, int, float]
|
||||
|
@ -92,10 +86,110 @@ 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]
|
||||
return type(value) in [RecordType, RecordCtor]
|
||||
|
||||
def juiIsRecordUpdatable(value):
|
||||
return type(value) == Record
|
||||
|
||||
# Generic functions on values
|
||||
|
||||
# String representation of a value
|
||||
def juiValueString(v):
|
||||
match v:
|
||||
case None:
|
||||
return "null"
|
||||
case bool():
|
||||
return str(v).lower()
|
||||
case int() | float():
|
||||
return str(v)
|
||||
case str():
|
||||
return repr(v)
|
||||
case CXXQualid():
|
||||
return "&" + v.qualid
|
||||
case list():
|
||||
return "[" + ", ".join(juiValueString(x) for x in v) + "]"
|
||||
case BuiltinFunction():
|
||||
return str(v)
|
||||
case Function() as f:
|
||||
p = f.params + (["..." + f.variadic] if f.variadic else [])
|
||||
s = "fun(" + ", ".join(p) + ") => " + str(f.body)
|
||||
s += " (in some closure)"
|
||||
return s
|
||||
case RecordCtor() as rc:
|
||||
f = rc.func
|
||||
p = f.params + (["..." + f.variadic] if f.variadic else [])
|
||||
s = "rec(" + ", ".join(p) + ") => " + str(f.body)
|
||||
s += " (in some closure)"
|
||||
return s
|
||||
case RecordType() as rt:
|
||||
return str(rt)
|
||||
case Record() as r:
|
||||
s = r.base.name + " {"
|
||||
s += "; ".join(x + ": " + juiValueString(y) for x, y in r.attr.items())
|
||||
if len(r.attr) and len(r.children):
|
||||
s += "; "
|
||||
s += "; ".join(juiValueString(x) for x in r.children)
|
||||
return s + "}"
|
||||
raise NotImplementedError
|
||||
case juic.eval.Thunk() as th:
|
||||
return str(th)
|
||||
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
|
||||
case bool(), bool():
|
||||
return v1 == v2
|
||||
case int(), int():
|
||||
return v1 == v2
|
||||
case float(), float():
|
||||
return v1 == v2
|
||||
case str(), str():
|
||||
return v1 == v2
|
||||
case CXXQualid(), CXXQualid():
|
||||
return v1.qualid == v2.qualid
|
||||
case list() as l1, list() as l2:
|
||||
return len(l1) == len(l2) and \
|
||||
all(juiValuesEqual(v1, v2) for v1, v2 in zip(l1, l2))
|
||||
case BuiltinFunction(), BuiltinFunction():
|
||||
return id(v1) == id(v2)
|
||||
case Function(), Function():
|
||||
raise Exception("cannot compare functions")
|
||||
case RecordType(), RecordType():
|
||||
return id(v1) == id(v2)
|
||||
case Record() as r1, Record() as r2:
|
||||
if not juiValuesEqual(r1.base, r2.base):
|
||||
return False
|
||||
if r1.attr.keys() != r2.attr.keys():
|
||||
return False
|
||||
if any(not juiValuesEqual(r1.attr[k], r2.attr[k]) for k in r1.attr):
|
||||
return False
|
||||
if len(r1.children) != len(r2.children):
|
||||
return False
|
||||
if any(not juiValuesEqual(v1, v2)
|
||||
for v1, v2 in zip(r1.children, r2.children)):
|
||||
return False
|
||||
return True
|
||||
case juic.eval.PartialRecordSnapshot(), \
|
||||
juic.eval.PartialRecordSnapshot():
|
||||
raise Exception("cannot compare PRSs")
|
||||
case _, _:
|
||||
raise Exception(f"invalid types for comparison: {type(v1)}, {type(v2)}")
|
||||
|
|
314
juic/eval.py
314
juic/eval.py
|
@ -104,6 +104,21 @@ class Thunk:
|
|||
# Whether the thunk is currently under evaluation
|
||||
_running: bool = False
|
||||
|
||||
def __str__(self):
|
||||
s = "Thunk("
|
||||
if self._running:
|
||||
s += "running, "
|
||||
if self.evaluated:
|
||||
s += "evaluated"
|
||||
if self.invalid:
|
||||
s += ", invalid"
|
||||
else:
|
||||
s += " = " + juiValueString(self.result)
|
||||
else:
|
||||
s += "unevaluated"
|
||||
s += "){ " + str(self.ast) + " }"
|
||||
return s
|
||||
|
||||
# Object representing the interpretation of a partially-built record. In a
|
||||
# record construction or update operation, fields of `this` refer to the latest
|
||||
# definition before the point of use, if there is one, or the definition in the
|
||||
|
@ -121,58 +136,12 @@ class PartialRecordSnapshot:
|
|||
# Base object for fields not captured in `fieldThunks`
|
||||
base: None | Thunk = None
|
||||
|
||||
def __str__(self):
|
||||
return "<PartialRecordSnapshot>" # TODO
|
||||
|
||||
def copy(self):
|
||||
return PartialRecordSnapshot(self.fieldThunks.copy(), self.base)
|
||||
|
||||
# TODO: Make this a general value printing function
|
||||
def juiValueString(v):
|
||||
match v:
|
||||
case None:
|
||||
return "null"
|
||||
case bool():
|
||||
return str(v).lower()
|
||||
case int() | float():
|
||||
return str(v)
|
||||
case str():
|
||||
return repr(v)
|
||||
case CXXQualid():
|
||||
return "&" + v.qualid
|
||||
case list():
|
||||
return "[" + ", ".join(juiValueString(x) for x in v) + "]"
|
||||
case BuiltinFunction():
|
||||
return str(v)
|
||||
case Function():
|
||||
p = v.params + (["..." + v.variadic] if v.variadic else [])
|
||||
s = "fun(" + ", ".join(p) + ") => " + str(v.body)
|
||||
s += " (in some closure)"
|
||||
return s
|
||||
case RecordType():
|
||||
return "<RecordType>"
|
||||
case Record() as r:
|
||||
s = juiValueString(r.base) + " " + "{"
|
||||
s += "; ".join(x + ": " + juiValueString(y) for x, y in r.attr.items())
|
||||
s += "; ".join(juiValueString(x) for x in r.children)
|
||||
return s + "}"
|
||||
raise NotImplementedError
|
||||
case Thunk() as th:
|
||||
s = "Thunk("
|
||||
if th._running:
|
||||
s += "running, "
|
||||
if th.evaluated:
|
||||
s += "evaluated"
|
||||
if th.invalid:
|
||||
s += ", invalid"
|
||||
else:
|
||||
s += " = " + juiValueString(th.result)
|
||||
else:
|
||||
s += "unevaluated"
|
||||
s += "){ " + str(th.ast) + " }"
|
||||
return s
|
||||
case PartialRecordSnapshot():
|
||||
return "<A PRS AAAAAAH>"
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
|
||||
class JuiTypeError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -284,16 +253,21 @@ class Context:
|
|||
raise JuiStaticError(f"multiple variadic arguments: {names}")
|
||||
|
||||
if variadics and variadics != [len(params) - 1]:
|
||||
name = params[variadic][0]
|
||||
name = params[variadics[0]][0]
|
||||
raise JuiStaticError(f"variadic argument {name} not at the end")
|
||||
variadic = params[variadics[0]][0] if variadics else None
|
||||
|
||||
# Build function object
|
||||
# TODO: Make a full scope not just an expr
|
||||
return Function(closure = self.currentStateClosure(),
|
||||
body = body,
|
||||
body = Node(Node.T.SCOPE_EXPR, [body]),
|
||||
params = [name for (name, v) in params if not v],
|
||||
variadic = variadic)
|
||||
|
||||
def makeRecordCtor(self, params: list[Tuple[str, bool]], body: Node) \
|
||||
-> RecordCtor:
|
||||
return RecordCtor(func=self.makeFunction(params, body))
|
||||
|
||||
#=== Main evaluation functions ===#
|
||||
|
||||
# Expression evaluator; this function is pure. It can cause thunks to be
|
||||
|
@ -328,7 +302,10 @@ class Context:
|
|||
case "+": return x + y
|
||||
case "-": return x - y
|
||||
case "*": return x * y
|
||||
case "/": return x / y if type(x) == float else x // y
|
||||
case "/":
|
||||
match x, y:
|
||||
case float(), float(): return (x / y)
|
||||
case _, _: return (x // y)
|
||||
case "%": return x % y
|
||||
|
||||
case Node.T.OP, [("!" | "+" | "-") as op, X]:
|
||||
|
@ -375,10 +352,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)
|
||||
|
@ -401,28 +389,60 @@ class Context:
|
|||
case Node.T.REC_ATTR, _:
|
||||
raise NotImplementedError
|
||||
|
||||
case Node.T.REC_VALUE, _:
|
||||
case Node.T.REC_VALUE, args:
|
||||
raise NotImplementedError
|
||||
|
||||
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 self.evalValueOrThunk(r.attr[field])
|
||||
case PartialRecordSnapshot() as prs:
|
||||
if field in prs.fieldThunks:
|
||||
return self.evalThunk(prs.fieldThunks[field])
|
||||
elif prs.base is None:
|
||||
raise Exception(f"access to undefined field {field} of 'this'")
|
||||
else:
|
||||
return self.project(self.evalThunk(prs.base), field)
|
||||
case _:
|
||||
raise NotImplementedError # unreachable
|
||||
|
||||
def execStmt(self, node: Node) -> JuiValue:
|
||||
match node.ctor, node.args:
|
||||
case Node.T.LET_DECL, [name, X]:
|
||||
self.addDefinition(name, self.evalExpr(X))
|
||||
return None
|
||||
|
||||
case Node.T.FUN_DECL, [name, params, body]:
|
||||
self.addDefinition(name, self.makeFunction(params, body))
|
||||
return None
|
||||
|
||||
case Node.T.REC_DECL, _:
|
||||
raise NotImplementedError
|
||||
case Node.T.REC_DECL, [name, params, body]:
|
||||
self.addDefinition(name, self.makeRecordCtor(params, body))
|
||||
return None
|
||||
|
||||
case Node.T.SET_STMT, _:
|
||||
raise NotImplementedError
|
||||
|
||||
case _, _:
|
||||
return self.evalExpr(node)
|
||||
case Node.T.UNIT_TEST, [subject, expected]:
|
||||
vs = self.evalExpr(subject)
|
||||
ve = self.evalExpr(expected)
|
||||
vs = self.force(vs)
|
||||
ve = self.force(ve)
|
||||
if not juiValuesEqual(vs, ve):
|
||||
print("unit test failed:")
|
||||
print(" " + juiValueString(vs))
|
||||
print("vs.")
|
||||
print(" " + juiValueString(ve))
|
||||
return None
|
||||
|
||||
case Node.T.SCOPE_EXPR, [e]:
|
||||
return self.evalExpr(e)
|
||||
|
||||
raise Exception(f"execStmt: unrecognized node {node.ctor.name}")
|
||||
|
||||
# TODO: Context.eval*: continue failed computations to find other errors?
|
||||
def evalThunk(self, th: Thunk) -> JuiValue:
|
||||
|
@ -446,6 +466,9 @@ class Context:
|
|||
th.result = result
|
||||
return result
|
||||
|
||||
def evalValueOrThunk(self, v: JuiValue | Thunk) -> JuiValue:
|
||||
return self.evalThunk(v) if isinstance(v, Thunk) else v
|
||||
|
||||
def evalCall(self, f: JuiValue, args: list[Node]) -> JuiValue:
|
||||
# Built-in functions: just evaluate arguments and go
|
||||
# TODO: Check types of built-in function calls
|
||||
|
@ -472,140 +495,113 @@ class Context:
|
|||
self.addDefinition(name, th)
|
||||
# self.currentScope.dump()
|
||||
# self.currentClosure.dump()
|
||||
assert f.body.ctor == Node.T.SCOPE_EXPR
|
||||
return self.execStmt(f.body)
|
||||
|
||||
def evalRecordConstructor(self, ctor: JuiValue, entries: list[Node]) \
|
||||
-> JuiValue:
|
||||
|
||||
# Collect indices of fields and arguments separately; keep the order
|
||||
args = []
|
||||
attr = []
|
||||
for i, e in enumerate(entries):
|
||||
if e.ctor == Node.T.REC_ATTR:
|
||||
attr.append(i)
|
||||
elif e.ctor == Node.T.REC_VALUE:
|
||||
args.append(i)
|
||||
else:
|
||||
assert False and "record node has weird children o(x_x)o"
|
||||
|
||||
# Base record constructor: starts out with an empty record. All
|
||||
# arguments are children. Easy.
|
||||
if type(ctor) == RecordType:
|
||||
r = Record(base=ctor, attr=dict(), children=dict())
|
||||
|
||||
if len(args) > 0:
|
||||
raise JuiRuntimeError(f"arguments given to type rec ctor")
|
||||
if isinstance(ctor, RecordType):
|
||||
r = Record(base=ctor, attr=dict(), children=[])
|
||||
|
||||
# Create thunks for all entries while providing them with
|
||||
# progressively more complete PRS of r.
|
||||
prs = PartialRecordSnapshot()
|
||||
|
||||
for i in attr:
|
||||
name, label, node = entries[i].args
|
||||
for i, e in enumerate(entries):
|
||||
if e.ctor == Node.T.REC_ATTR:
|
||||
name, label, node = e.args
|
||||
elif e.ctor == Node.T.REC_VALUE:
|
||||
name, label, node = None, None, e.args[0]
|
||||
|
||||
th = Thunk(ast=node, closure=self.currentStateClosure())
|
||||
th.thisReference = prs.copy()
|
||||
|
||||
r.attr[name] = th
|
||||
prs.fieldThunks[name] = th
|
||||
if name is not None:
|
||||
r.attr[name] = th
|
||||
prs.fieldThunks[name] = th
|
||||
else:
|
||||
r.children.append(th)
|
||||
|
||||
return r
|
||||
|
||||
# TODO: Factor this with function. In fact, this should reduce to a call
|
||||
# NOTE: NO WAY TO SPECIFY AN ATTRIBUTE IN A NON-STATIC WAY.
|
||||
|
||||
# Create thunks for all entries that have everything but the "this".
|
||||
entry_thunks = []
|
||||
for e in entries:
|
||||
if e.ctor == Node.T.REC_ATTR:
|
||||
name, label, node = e.args
|
||||
elif e.ctor == Node.T.REC_VALUE:
|
||||
name, label, node = None, None, e.args[0]
|
||||
th = Thunk(ast=node, closure=self.currentStateClosure())
|
||||
entry_thunks.append(th)
|
||||
|
||||
# Collect arguments to the constructor and build a thunk the call.
|
||||
args = [entry_thunks[i]
|
||||
for i, e in enumerate(entries) if e.ctor == Node.T.REC_VALUE]
|
||||
|
||||
# TODO: Merge with an internal version of evalCall()
|
||||
#---
|
||||
|
||||
assert isinstance(ctor, RecordCtor)
|
||||
f = ctor.func
|
||||
|
||||
# Check number of arguments
|
||||
req = str(len(ctor.params)) + ("+" if ctor.variadic is not None else "")
|
||||
if len(args) < len(ctor.params):
|
||||
req = str(len(f.params)) + ("+" if f.variadic is not None else "")
|
||||
if len(args) < len(f.params):
|
||||
raise JuiRuntimeError(f"not enough args (need {req}, got {len(args)})")
|
||||
if len(args) > len(ctor.params) and ctor.variadic is None:
|
||||
if len(args) > len(f.params) and f.variadic is None:
|
||||
raise JuiRuntimeError(f"too many args (need {req}, got {len(args)})")
|
||||
|
||||
# TODO: In order to build variadic set I need a LIST node
|
||||
if ctor.variadic is not None:
|
||||
if f.variadic is not None:
|
||||
raise NotImplementedError("list node for building varargs o(x_x)o")
|
||||
|
||||
# Otherwise, it's a function so it might use
|
||||
raise NotImplementedError
|
||||
# Run into the function's scope to build the thunk
|
||||
with self.contextSwitchAndPushNewScope(f.closure):
|
||||
for name, th in zip(f.params, args):
|
||||
self.addDefinition(name, th)
|
||||
assert f.body.ctor == Node.T.SCOPE_EXPR
|
||||
call_thunk = Thunk(ast=f.body.args[0],
|
||||
closure=self.currentStateClosure())
|
||||
#---
|
||||
|
||||
assert type(r) == Function and \
|
||||
"evalRecordConstructor: not a record type nor a function"
|
||||
# Use the call as base for a PRS and assign "this" in all thunks.
|
||||
prs = PartialRecordSnapshot()
|
||||
prs.base = call_thunk
|
||||
|
||||
# Thunks should be created from a closure of the entire environment
|
||||
# -> This environment is always known, _except_ for
|
||||
# partially-constructed records
|
||||
# So, naturally... patch the PRS for "this" later on?
|
||||
#
|
||||
# ## In a normal function call
|
||||
#
|
||||
# Invariant: context of evaluating the expression that has the call is
|
||||
# complete. Use it for all the arguments. Done.
|
||||
#
|
||||
# ## In a record constructor with no parameters
|
||||
#
|
||||
# All attribute values have the same context when excluding "this",
|
||||
# it's just the context surrounding the record constructor, and
|
||||
# (invariant) it is complete. Use it to create all of the thunks.
|
||||
#
|
||||
# Later, iterate through the thunks in top-down order and patch the
|
||||
# "this" by constructing PRSs.
|
||||
#
|
||||
# Invariant is satisified because each thunk's closure is complete and
|
||||
# thus the context for evaluating their contents will also be
|
||||
# complete.
|
||||
#
|
||||
# ## In a record constructor with parameters
|
||||
#
|
||||
# Again, the entire context except for "this" is well-defined and
|
||||
# complete. Use it to create thunks for all record entries, inline or
|
||||
# not.
|
||||
#
|
||||
# Prepare a thunk for the call's evaluation, based on the function's
|
||||
# environment, assigning parameter-related thunks (or lists thereof) as
|
||||
# definitions for the function's arguments.
|
||||
#
|
||||
# Use the call's thunk as base for the PRS and sweep top-to-bottom
|
||||
# assigning PRSs in every record-entry thunk.
|
||||
#
|
||||
# Return a literal record with all non-parameter entries as elements.
|
||||
# Erm, remove duplicates.
|
||||
#
|
||||
# NOTE: NO WAY TO SPECIFY AN ATTRIBUTE IN A NON-STATIC WAY.
|
||||
for i, e in enumerate(entries):
|
||||
entry_thunks[i].thisReference = prs.copy()
|
||||
if e.ctor == Node.T.REC_ATTR:
|
||||
name, label, node = e.args
|
||||
prs.fieldThunks[name] = th
|
||||
|
||||
# - Build thunks for all entries, with one layer of "this" data
|
||||
# - Call ctor1, pass it the argument thunks
|
||||
# - Move into ctor1's scope, add new scope with arguments
|
||||
# - Build a thunk for ctor1's body, this is second layer of "this"
|
||||
# /!\ It's not per-field! It's not per-field!
|
||||
# - Go back to toplevel, add second layer of "this" to all thunks
|
||||
# - Return a PartialRecord object that has ctor1's returned thunk as
|
||||
# base and the other fields as overrides.
|
||||
#
|
||||
# ... Looks too opaque at ctor1's level.
|
||||
# ... But then again for lazy evaluation all we need to know is it's a
|
||||
# record, and then we evaluated only when we project fields. If the
|
||||
# same thing happens with <{} there should be no problem.
|
||||
# ... Having exactly two layers is suspicious. What happens when we try
|
||||
# to get a field?
|
||||
#
|
||||
# 1. Lookup the field in the PartialRecord. If it has it, it's been
|
||||
# added by the latest constructor, and we know which it is. Evaluate
|
||||
# the thunk inside.
|
||||
# 2. Otherwise, evaluate the base thunk. In the "f <| rec { ... }" it's
|
||||
# a record update. This will yield another PartialRecord. Use it.
|
||||
# 3. This should work, but we need a PartialRecord.
|
||||
#
|
||||
# When do we convert back to a full Record?
|
||||
# How about that's what Record does, and it just has accessors?
|
||||
raise NotImplementedError
|
||||
baseRecord = self.evalThunk(call_thunk)
|
||||
if not isinstance(baseRecord, Record):
|
||||
raise Exception("record ctor did not return a record")
|
||||
|
||||
def force(self, v: JuiValue) -> JuiValue:
|
||||
for i, e in enumerate(entries):
|
||||
if e.ctor == Node.T.REC_ATTR:
|
||||
name, label, node = e.args
|
||||
baseRecord.attr[name] = entry_thunks[i]
|
||||
|
||||
return baseRecord
|
||||
|
||||
def force(self, v: JuiValue | Thunk) -> JuiValue:
|
||||
match v:
|
||||
case Record() as r:
|
||||
for a in r.attr:
|
||||
r.attr[a] = self.force(r.attr[a])
|
||||
for i, e in enumerate(r.children):
|
||||
r.children[i] = self.force(e)
|
||||
return r
|
||||
case Thunk() as th:
|
||||
self.evalThunk(th)
|
||||
if th.evaluated:
|
||||
return self.force(th.result)
|
||||
return th
|
||||
return th.result
|
||||
case _:
|
||||
return v
|
||||
|
|
|
@ -31,18 +31,26 @@ record { attr: 2 + 3; };
|
|||
|
||||
record { x: 1; y: 2; z: subrecord { u: "42"; }; };
|
||||
|
||||
/*
|
||||
record {
|
||||
fun stack(x) = x;
|
||||
fun stretch(x) = x;
|
||||
|
||||
jwidget {
|
||||
fullscreen: true;
|
||||
@title jlabel { title; };
|
||||
@title jlabel { "title"; };
|
||||
@stack jwidget {} | stack | stretch;
|
||||
|
||||
let x = 4;
|
||||
jfkeys { x };
|
||||
// let x = 4;
|
||||
// jfkeys { x };
|
||||
jfkeys { 4 };
|
||||
|
||||
fun test(x, y, ...all) = x + y + sum(all);
|
||||
// fun test(x, y, ...all) = x + y + sum(all);
|
||||
};
|
||||
|
||||
rec jlabel2(str) = jwidget { text: str };
|
||||
|
||||
jlabel2 {"Hello"};
|
||||
|
||||
/*
|
||||
fun _(fx, cg) = if(param("FX")) fx else cg;
|
||||
fun stack(elem) = elem <{ layout: stack };
|
||||
fun stretch(elem) = elem <{ stretch_x: 1; stretch_y: 1; strech_beyond_limits: false };
|
||||
|
|
42
juic/examples/unit_tests.jui
Normal file
42
juic/examples/unit_tests.jui
Normal file
|
@ -0,0 +1,42 @@
|
|||
null;
|
||||
//^ null;
|
||||
|
||||
1+2;
|
||||
//^ 3;
|
||||
|
||||
let zero = 0;
|
||||
zero + 2 * 20 - 8 / 4;
|
||||
//^ 38;
|
||||
|
||||
"hello" + "," + "world";
|
||||
//^ "hello,world";
|
||||
|
||||
if(2 < 4) 8;
|
||||
//^ 8;
|
||||
|
||||
if(2 > 4) 8;
|
||||
//^ null;
|
||||
|
||||
if(2 > 4) 8 else 14;
|
||||
//^ 14;
|
||||
|
||||
let z = 4;
|
||||
(z + 10) * 3;
|
||||
//^ 42;
|
||||
|
||||
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};
|
30
juic/main.py
30
juic/main.py
|
@ -11,8 +11,9 @@ usage: juic [OPTIONS] INPUT
|
|||
JustUI high-level description compiler.
|
||||
|
||||
Options:
|
||||
--debug=lexer Dump the lexer output and stop.
|
||||
=parser Dump the parser output and stop.
|
||||
--debug=lexer Dump the lexer output and stop
|
||||
=parser Dump the parser output and stop
|
||||
--unit-tests Check unit tests specified by "//^" comments
|
||||
""".strip()
|
||||
|
||||
def usage(exitcode=None):
|
||||
|
@ -24,7 +25,8 @@ def usage(exitcode=None):
|
|||
def main(argv):
|
||||
# Read command-line arguments
|
||||
try:
|
||||
opts, args = getopt.gnu_getopt(argv[1:], "", ["help", "debug="])
|
||||
opts, args = getopt.gnu_getopt(argv[1:], "",
|
||||
["help", "debug=", "unit-tests"])
|
||||
opts = dict(opts)
|
||||
|
||||
if len(argv) == 1 or "-h" in opts or "--help" in opts:
|
||||
|
@ -45,7 +47,8 @@ def main(argv):
|
|||
source = fp.read()
|
||||
|
||||
try:
|
||||
lexer = juic.parser.JuiLexer(source, args[0])
|
||||
lexer = juic.parser.JuiLexer(source, args[0],
|
||||
keepUnitTests="--unit-tests" in opts)
|
||||
if opts.get("--debug") == "lexer":
|
||||
lexer.dump()
|
||||
return 0
|
||||
|
@ -53,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
|
||||
|
||||
|
@ -65,11 +68,16 @@ def main(argv):
|
|||
|
||||
ctx = juic.eval.Context(juic.builtins.builtinClosure)
|
||||
|
||||
for e in ast.args:
|
||||
e.dump()
|
||||
v = ctx.execStmt(e)
|
||||
v = ctx.force(v)
|
||||
print(">>>>>>>", juic.eval.juiValueString(v))
|
||||
if "--unit-tests" in opts:
|
||||
for node in ast.args:
|
||||
ctx.execStmt(node)
|
||||
return 0
|
||||
|
||||
for node in ast.args:
|
||||
v = ctx.execStmt(node)
|
||||
if node.ctor == juic.parser.Node.T.SCOPE_EXPR:
|
||||
v = ctx.force(v)
|
||||
print(">>>>>>>", juic.eval.juiValueString(v))
|
||||
|
||||
ctx.currentScope.dump()
|
||||
ctx.currentClosure.dump()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
import typing
|
||||
from typing import Any, Tuple
|
||||
import enum
|
||||
import sys
|
||||
import re
|
||||
|
@ -23,8 +23,8 @@ class SyntaxError(Exception):
|
|||
|
||||
@dataclass
|
||||
class Token:
|
||||
type: typing.Any
|
||||
value: typing.Any
|
||||
type: Any
|
||||
value: Any
|
||||
loc: Loc
|
||||
|
||||
def __str__(self):
|
||||
|
@ -45,7 +45,8 @@ class NaiveRegexLexer:
|
|||
# Override with list of (regex, token type, token value). Both the token
|
||||
# type and value can be functions, in which case they'll be called with the
|
||||
# match object as parameter.
|
||||
TOKEN_REGEX = []
|
||||
Rule = Tuple[str, Any, Any] | Tuple[str, Any, Any, int]
|
||||
TOKEN_REGEX: list[Rule] = []
|
||||
# Override with token predicate that matches token to be discarded and not
|
||||
# sent to the parser (typically, whitespace and comments).
|
||||
TOKEN_DISCARD = lambda _: False
|
||||
|
@ -76,12 +77,18 @@ class NaiveRegexLexer:
|
|||
if not len(self.input):
|
||||
return None
|
||||
|
||||
highestPriority = 0
|
||||
longestMatch = None
|
||||
longestMatchIndex = -1
|
||||
|
||||
for i, (regex, _, _) in enumerate(self.TOKEN_REGEX):
|
||||
for i, (regex, _, _, *rest) in enumerate(self.TOKEN_REGEX):
|
||||
priority = rest[0] if len(rest) else 0
|
||||
|
||||
if (m := re.match(regex, self.input)):
|
||||
if longestMatch is None or len(m[0]) > len(longestMatch[0]):
|
||||
score = (priority, len(m[0]))
|
||||
if longestMatch is None or \
|
||||
score > (highestPriority, len(longestMatch[0])):
|
||||
highestPriority = priority
|
||||
longestMatch = m
|
||||
longestMatchIndex = i
|
||||
|
||||
|
@ -90,7 +97,7 @@ class NaiveRegexLexer:
|
|||
self.raiseError(f"unknown syntax '{nextWord}'")
|
||||
|
||||
# Build the token
|
||||
_, type_info, value_info = self.TOKEN_REGEX[longestMatchIndex]
|
||||
_, type_info, value_info, *rest = self.TOKEN_REGEX[longestMatchIndex]
|
||||
m = longestMatch
|
||||
|
||||
typ = type_info(m) if callable(type_info) else type_info
|
||||
|
@ -169,6 +176,7 @@ class LL1Parser:
|
|||
|
||||
# Rule combinators implementing unary and binary operators with precedence
|
||||
|
||||
@staticmethod
|
||||
def binaryOpsLeft(ctor, ops):
|
||||
def decorate(f):
|
||||
def symbol(self):
|
||||
|
@ -179,6 +187,7 @@ class LL1Parser:
|
|||
return symbol
|
||||
return decorate
|
||||
|
||||
@staticmethod
|
||||
def binaryOps(ctor, ops, *, rassoc=False):
|
||||
def decorate(f):
|
||||
def symbol(self):
|
||||
|
@ -191,9 +200,11 @@ class LL1Parser:
|
|||
return symbol
|
||||
return decorate
|
||||
|
||||
@staticmethod
|
||||
def binaryOpsRight(ctor, ops):
|
||||
return LL1Parser.binaryOps(ctor, ops, rassoc=True)
|
||||
|
||||
@staticmethod
|
||||
def unaryOps(ctor, ops, assoc=True):
|
||||
def decorate(f):
|
||||
def symbol(self):
|
||||
|
@ -212,15 +223,17 @@ def unescape(s: str) -> str:
|
|||
|
||||
class JuiLexer(NaiveRegexLexer):
|
||||
T = enum.Enum("T",
|
||||
["WS", "KW", "COMMENT",
|
||||
"INT", "FLOAT", "STRING",
|
||||
["WS", "KW", "COMMENT", "UNIT_TEST_MARKER",
|
||||
"TEXTLIT", "INT", "FLOAT", "STRING",
|
||||
"IDENT", "ATTR", "VAR", "LABEL", "FIELD", "CXXIDENT"])
|
||||
|
||||
RE_UTMARKER = r'//\^'
|
||||
|
||||
RE_COMMENT = r'(#|//)[^\n]*|/\*([^/]|/[^*])+\*/'
|
||||
RE_INT = r'0|[1-9][0-9]*|0b[0-1]+|0o[0-7]+|0[xX][0-9a-fA-F]+'
|
||||
# RE_FLOAT = r'([0-9]*\.[0-9]+|[0-9]+\.[0-9]*|[0-9]+)([eE][+-]?{INT})?f?'
|
||||
|
||||
RE_KW = r'\b(else|fun|if|let|rec|set|this)\b'
|
||||
RE_KW = r'\b(else|fun|if|let|rec|set|this|null|true|false)\b'
|
||||
RE_IDENT = r'[\w_][\w0-9_]*'
|
||||
RE_ATTR = r'({})\s*(?:@({}))?\s*:'.format(RE_IDENT, RE_IDENT)
|
||||
RE_VAR = r'\$(\.)?' + RE_IDENT
|
||||
|
@ -253,14 +266,22 @@ class JuiLexer(NaiveRegexLexer):
|
|||
]
|
||||
TOKEN_DISCARD = lambda t: t.type in [JuiLexer.T.WS, JuiLexer.T.COMMENT]
|
||||
|
||||
def __init__(self, input, inputFilename, *, keepUnitTests):
|
||||
if keepUnitTests:
|
||||
unit_rule = (self.RE_UTMARKER, JuiLexer.T.UNIT_TEST_MARKER, None, 1)
|
||||
self.TOKEN_REGEX.insert(0, unit_rule)
|
||||
|
||||
super().__init__(input, inputFilename)
|
||||
|
||||
@dataclass
|
||||
class Node:
|
||||
T = enum.Enum("T", [
|
||||
"LIT", "IDENT", "OP", "THIS", "PROJ", "CALL", "IF", "SCOPE",
|
||||
"RECORD", "REC_ATTR", "REC_VALUE",
|
||||
"LET_DECL", "FUN_DECL", "REC_DECL", "SET_STMT"])
|
||||
"LET_DECL", "FUN_DECL", "REC_DECL", "SET_STMT",
|
||||
"SCOPE_EXPR", "UNIT_TEST"])
|
||||
ctor: T
|
||||
args: list[typing.Any]
|
||||
args: list[Any]
|
||||
|
||||
def dump(self, indent=0):
|
||||
print(" " * indent + self.ctor.name, end=" ")
|
||||
|
@ -339,7 +360,7 @@ class JuiParser(LL1Parser):
|
|||
case T.KW if t.value == "null":
|
||||
node = Node(Node.T.LIT, [None])
|
||||
case T.KW if t.value in ["true", "false"]:
|
||||
node = Node(Node.T.LIT, t.value == "true")
|
||||
node = Node(Node.T.LIT, [t.value == "true"])
|
||||
case "(":
|
||||
node = self.expr()
|
||||
self.expect(")")
|
||||
|
@ -352,7 +373,7 @@ class JuiParser(LL1Parser):
|
|||
# | expr0 "<{" record_entry,* "}" (record update)
|
||||
# | expr0 "(" expr,* ")" (function call)
|
||||
# | expr0 "." ident (projection, same prec as call)
|
||||
@LL1Parser.binaryOpsRight(mkOpNode, ["|"])
|
||||
@LL1Parser.binaryOpsLeft(mkOpNode, ["|"])
|
||||
@LL1Parser.binaryOpsLeft(mkOpNode, ["<|"])
|
||||
@LL1Parser.binaryOpsLeft(mkOpNode, ["||"])
|
||||
@LL1Parser.binaryOpsLeft(mkOpNode, ["&&"])
|
||||
|
@ -375,7 +396,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])
|
||||
|
||||
|
@ -480,7 +501,20 @@ class JuiParser(LL1Parser):
|
|||
def scope(self):
|
||||
isNone = lambda t: t is None
|
||||
entries = self.separatedList(self.scope_stmt, sep=";", term=isNone)
|
||||
return Node(Node.T.SCOPE, entries)
|
||||
|
||||
# Rearrange unit tests around their predecessors
|
||||
entries2 = []
|
||||
i = 0
|
||||
while i < len(entries):
|
||||
if i < len(entries) - 1 and entries[i+1].ctor == Node.T.UNIT_TEST:
|
||||
entries[i+1].args[0] = entries[i]
|
||||
entries2.append(entries[i+1])
|
||||
i += 2
|
||||
else:
|
||||
entries2.append(entries[i])
|
||||
i += 1
|
||||
|
||||
return Node(Node.T.SCOPE, entries2)
|
||||
|
||||
def scope_stmt(self):
|
||||
match self.la.type, self.la.value:
|
||||
|
@ -490,5 +524,8 @@ class JuiParser(LL1Parser):
|
|||
return self.fun_rec_decl()
|
||||
case JuiLexer.T.KW, "set":
|
||||
return self.set_stmt()
|
||||
case JuiLexer.T.UNIT_TEST_MARKER, _:
|
||||
self.expect(JuiLexer.T.UNIT_TEST_MARKER)
|
||||
return Node(Node.T.UNIT_TEST, [None, self.expr()])
|
||||
case _:
|
||||
return self.expr()
|
||||
return Node(Node.T.SCOPE_EXPR, [self.expr()])
|
||||
|
|
Loading…
Reference in a new issue