mirror of
https://git.planet-casio.com/Lephenixnoir/JustUI.git
synced 2024-12-29 13:03: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!!
|
- 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`
|
- `rec x() = y {}; x {}` is it of record type `x` or `y`? How to say that `x`
|
||||||
is based on `y`?
|
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:
|
IMPLEMENTATION TODO:
|
||||||
- List syntax
|
- 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")
|
-> function-based interface param("FX")
|
||||||
- "Control-flow": for(each) loops, what else needed for basic ops?
|
- "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
|
Capturing "this" in a local variable to get a snapshot is required, otherwise
|
||||||
we can't refer to parents.
|
we can't refer to parents.
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,28 @@ 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]:
|
||||||
raise JuiTypeError("len")
|
raise JuiTypeError("len")
|
||||||
return len(x)
|
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={
|
builtinClosure = Closure(parent=None, scope=MutableScopeData(defs={
|
||||||
"print": (0, BuiltinFunction(juiPrint)),
|
"print": (0, BuiltinFunction(juiPrint)),
|
||||||
"len": (0, BuiltinFunction(juiLen)),
|
"len": (0, BuiltinFunction(juiLen)),
|
||||||
|
|
||||||
# TODO: Remove the built-in record type "record" used for testing
|
# TODO: Remove the built-in record type "record" used for testing
|
||||||
"record": (0, RecordType.makePlainRecordType()),
|
"record": (0, R_record),
|
||||||
"subrecord": (0, RecordType.makePlainRecordType()),
|
"subrecord": (0, R_subrecord),
|
||||||
|
"jwidget": (0, R_jwidget),
|
||||||
|
"jlabel": (0, R_jlabel),
|
||||||
|
"jfkeys": (0, R_jfkeys),
|
||||||
|
|
||||||
}, timestamp=1))
|
}, timestamp=1))
|
||||||
|
|
|
@ -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
|
||||||
|
@ -29,41 +33,31 @@ class Function:
|
||||||
# Expression node to evaluate when calling the function
|
# Expression node to evaluate when calling the function
|
||||||
body: "juic.parser.Node"
|
body: "juic.parser.Node"
|
||||||
# Parameter names, must all be unique. May be empty
|
# 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
|
# Name of variadic argument if one; must also be unique
|
||||||
variadic: str | None = None
|
variadic: str | None = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RecordType:
|
class RecordType:
|
||||||
|
# Record name
|
||||||
|
name: str
|
||||||
|
# Base type for inheritance/subconstructor logic
|
||||||
|
base: Union[None, "RecordType"]
|
||||||
|
|
||||||
# TODO: Record prototypes?
|
# TODO: Record prototypes?
|
||||||
|
|
||||||
@staticmethod
|
@dataclass
|
||||||
def makePlainRecordType():
|
class RecordCtor:
|
||||||
return RecordType()
|
func: Function
|
||||||
|
|
||||||
# @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
|
@dataclass
|
||||||
class Record:
|
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, Union["JuiValue", "juic.eval.Thunk"]]
|
||||||
# Children elements
|
# 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"?
|
# TODO: Keep track of variables that are not fields, i.e. "methods"?
|
||||||
# scope: dict[str, "JuiValue"]
|
# scope: dict[str, "JuiValue"]
|
||||||
# TODO: Labels
|
# TODO: Labels
|
||||||
|
@ -71,7 +65,7 @@ class Record:
|
||||||
|
|
||||||
JuiValue = Union[None, bool, int, float, str, CXXQualid, list,
|
JuiValue = Union[None, bool, int, float, str, CXXQualid, list,
|
||||||
"juic.eval.PartialRecordSnapshot", BuiltinFunction, Function,
|
"juic.eval.PartialRecordSnapshot", BuiltinFunction, Function,
|
||||||
RecordType, Record]
|
RecordType, RecordCtor, Record]
|
||||||
|
|
||||||
def juiIsArith(value):
|
def juiIsArith(value):
|
||||||
return type(value) in [bool, int, float]
|
return type(value) in [bool, int, float]
|
||||||
|
@ -92,10 +86,110 @@ 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, RecordCtor]
|
||||||
|
|
||||||
def juiIsRecordUpdatable(value):
|
def juiIsRecordUpdatable(value):
|
||||||
return type(value) == Record
|
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
|
# Whether the thunk is currently under evaluation
|
||||||
_running: bool = False
|
_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
|
# Object representing the interpretation of a partially-built record. In a
|
||||||
# record construction or update operation, fields of `this` refer to the latest
|
# 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
|
# 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 object for fields not captured in `fieldThunks`
|
||||||
base: None | Thunk = None
|
base: None | Thunk = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "<PartialRecordSnapshot>" # TODO
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
return PartialRecordSnapshot(self.fieldThunks.copy(), self.base)
|
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):
|
class JuiTypeError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -284,16 +253,21 @@ class Context:
|
||||||
raise JuiStaticError(f"multiple variadic arguments: {names}")
|
raise JuiStaticError(f"multiple variadic arguments: {names}")
|
||||||
|
|
||||||
if variadics and variadics != [len(params) - 1]:
|
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")
|
raise JuiStaticError(f"variadic argument {name} not at the end")
|
||||||
variadic = params[variadics[0]][0] if variadics else None
|
variadic = params[variadics[0]][0] if variadics else None
|
||||||
|
|
||||||
# Build function object
|
# Build function object
|
||||||
|
# TODO: Make a full scope not just an expr
|
||||||
return Function(closure = self.currentStateClosure(),
|
return Function(closure = self.currentStateClosure(),
|
||||||
body = body,
|
body = Node(Node.T.SCOPE_EXPR, [body]),
|
||||||
params = [name for (name, v) in params if not v],
|
params = [name for (name, v) in params if not v],
|
||||||
variadic = variadic)
|
variadic = variadic)
|
||||||
|
|
||||||
|
def makeRecordCtor(self, params: list[Tuple[str, bool]], body: Node) \
|
||||||
|
-> RecordCtor:
|
||||||
|
return RecordCtor(func=self.makeFunction(params, body))
|
||||||
|
|
||||||
#=== Main evaluation functions ===#
|
#=== Main evaluation functions ===#
|
||||||
|
|
||||||
# Expression evaluator; this function is pure. It can cause thunks to be
|
# 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
|
||||||
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 "%": return x % y
|
||||||
|
|
||||||
case Node.T.OP, [("!" | "+" | "-") as op, X]:
|
case Node.T.OP, [("!" | "+" | "-") as op, X]:
|
||||||
|
@ -375,10 +352,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)
|
||||||
|
@ -401,28 +389,60 @@ class Context:
|
||||||
case Node.T.REC_ATTR, _:
|
case Node.T.REC_ATTR, _:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
case Node.T.REC_VALUE, _:
|
case Node.T.REC_VALUE, args:
|
||||||
raise NotImplementedError
|
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:
|
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]:
|
||||||
self.addDefinition(name, self.evalExpr(X))
|
self.addDefinition(name, self.evalExpr(X))
|
||||||
|
return None
|
||||||
|
|
||||||
case Node.T.FUN_DECL, [name, params, body]:
|
case Node.T.FUN_DECL, [name, params, body]:
|
||||||
self.addDefinition(name, self.makeFunction(params, body))
|
self.addDefinition(name, self.makeFunction(params, body))
|
||||||
|
return None
|
||||||
|
|
||||||
case Node.T.REC_DECL, _:
|
case Node.T.REC_DECL, [name, params, body]:
|
||||||
raise NotImplementedError
|
self.addDefinition(name, self.makeRecordCtor(params, body))
|
||||||
|
return None
|
||||||
|
|
||||||
case Node.T.SET_STMT, _:
|
case Node.T.SET_STMT, _:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
case _, _:
|
case Node.T.UNIT_TEST, [subject, expected]:
|
||||||
return self.evalExpr(node)
|
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?
|
# TODO: Context.eval*: continue failed computations to find other errors?
|
||||||
def evalThunk(self, th: Thunk) -> JuiValue:
|
def evalThunk(self, th: Thunk) -> JuiValue:
|
||||||
|
@ -446,6 +466,9 @@ class Context:
|
||||||
th.result = result
|
th.result = result
|
||||||
return 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:
|
def evalCall(self, f: JuiValue, args: list[Node]) -> JuiValue:
|
||||||
# Built-in functions: just evaluate arguments and go
|
# Built-in functions: just evaluate arguments and go
|
||||||
# TODO: Check types of built-in function calls
|
# TODO: Check types of built-in function calls
|
||||||
|
@ -472,140 +495,113 @@ class Context:
|
||||||
self.addDefinition(name, th)
|
self.addDefinition(name, th)
|
||||||
# self.currentScope.dump()
|
# self.currentScope.dump()
|
||||||
# self.currentClosure.dump()
|
# self.currentClosure.dump()
|
||||||
|
assert f.body.ctor == Node.T.SCOPE_EXPR
|
||||||
return self.execStmt(f.body)
|
return self.execStmt(f.body)
|
||||||
|
|
||||||
def evalRecordConstructor(self, ctor: JuiValue, entries: list[Node]) \
|
def evalRecordConstructor(self, ctor: JuiValue, entries: list[Node]) \
|
||||||
-> JuiValue:
|
-> 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
|
# Base record constructor: starts out with an empty record. All
|
||||||
# arguments are children. Easy.
|
# arguments are children. Easy.
|
||||||
if type(ctor) == RecordType:
|
if isinstance(ctor, RecordType):
|
||||||
r = Record(base=ctor, attr=dict(), children=dict())
|
r = Record(base=ctor, attr=dict(), children=[])
|
||||||
|
|
||||||
if len(args) > 0:
|
|
||||||
raise JuiRuntimeError(f"arguments given to type rec ctor")
|
|
||||||
|
|
||||||
# Create thunks for all entries while providing them with
|
# Create thunks for all entries while providing them with
|
||||||
# progressively more complete PRS of r.
|
# progressively more complete PRS of r.
|
||||||
prs = PartialRecordSnapshot()
|
prs = PartialRecordSnapshot()
|
||||||
|
|
||||||
for i in attr:
|
for i, e in enumerate(entries):
|
||||||
name, label, node = entries[i].args
|
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 = Thunk(ast=node, closure=self.currentStateClosure())
|
||||||
th.thisReference = prs.copy()
|
th.thisReference = prs.copy()
|
||||||
|
|
||||||
r.attr[name] = th
|
if name is not None:
|
||||||
prs.fieldThunks[name] = th
|
r.attr[name] = th
|
||||||
|
prs.fieldThunks[name] = th
|
||||||
|
else:
|
||||||
|
r.children.append(th)
|
||||||
|
|
||||||
return r
|
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
|
# Check number of arguments
|
||||||
req = str(len(ctor.params)) + ("+" if ctor.variadic is not None else "")
|
req = str(len(f.params)) + ("+" if f.variadic is not None else "")
|
||||||
if len(args) < len(ctor.params):
|
if len(args) < len(f.params):
|
||||||
raise JuiRuntimeError(f"not enough args (need {req}, got {len(args)})")
|
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)})")
|
raise JuiRuntimeError(f"too many args (need {req}, got {len(args)})")
|
||||||
|
|
||||||
# TODO: In order to build variadic set I need a LIST node
|
# 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")
|
raise NotImplementedError("list node for building varargs o(x_x)o")
|
||||||
|
|
||||||
# Otherwise, it's a function so it might use
|
# Run into the function's scope to build the thunk
|
||||||
raise NotImplementedError
|
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 \
|
# Use the call as base for a PRS and assign "this" in all thunks.
|
||||||
"evalRecordConstructor: not a record type nor a function"
|
prs = PartialRecordSnapshot()
|
||||||
|
prs.base = call_thunk
|
||||||
|
|
||||||
# Thunks should be created from a closure of the entire environment
|
for i, e in enumerate(entries):
|
||||||
# -> This environment is always known, _except_ for
|
entry_thunks[i].thisReference = prs.copy()
|
||||||
# partially-constructed records
|
if e.ctor == Node.T.REC_ATTR:
|
||||||
# So, naturally... patch the PRS for "this" later on?
|
name, label, node = e.args
|
||||||
#
|
prs.fieldThunks[name] = th
|
||||||
# ## 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.
|
|
||||||
|
|
||||||
# - Build thunks for all entries, with one layer of "this" data
|
baseRecord = self.evalThunk(call_thunk)
|
||||||
# - Call ctor1, pass it the argument thunks
|
if not isinstance(baseRecord, Record):
|
||||||
# - Move into ctor1's scope, add new scope with arguments
|
raise Exception("record ctor did not return a record")
|
||||||
# - 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
|
|
||||||
|
|
||||||
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:
|
match v:
|
||||||
case Record() as r:
|
case Record() as r:
|
||||||
for a in r.attr:
|
for a in r.attr:
|
||||||
r.attr[a] = self.force(r.attr[a])
|
r.attr[a] = self.force(r.attr[a])
|
||||||
|
for i, e in enumerate(r.children):
|
||||||
|
r.children[i] = self.force(e)
|
||||||
return r
|
return r
|
||||||
case Thunk() as th:
|
case Thunk() as th:
|
||||||
self.evalThunk(th)
|
self.evalThunk(th)
|
||||||
if th.evaluated:
|
if th.evaluated:
|
||||||
return self.force(th.result)
|
return self.force(th.result)
|
||||||
return th
|
return th.result
|
||||||
case _:
|
case _:
|
||||||
return v
|
return v
|
||||||
|
|
|
@ -31,18 +31,26 @@ record { attr: 2 + 3; };
|
||||||
|
|
||||||
record { x: 1; y: 2; z: subrecord { u: "42"; }; };
|
record { x: 1; y: 2; z: subrecord { u: "42"; }; };
|
||||||
|
|
||||||
/*
|
fun stack(x) = x;
|
||||||
record {
|
fun stretch(x) = x;
|
||||||
|
|
||||||
|
jwidget {
|
||||||
fullscreen: true;
|
fullscreen: true;
|
||||||
@title jlabel { title; };
|
@title jlabel { "title"; };
|
||||||
@stack jwidget {} | stack | stretch;
|
@stack jwidget {} | stack | stretch;
|
||||||
|
|
||||||
let x = 4;
|
// let x = 4;
|
||||||
jfkeys { x };
|
// 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 _(fx, cg) = if(param("FX")) fx else cg;
|
||||||
fun stack(elem) = elem <{ layout: stack };
|
fun stack(elem) = elem <{ layout: stack };
|
||||||
fun stretch(elem) = elem <{ stretch_x: 1; stretch_y: 1; strech_beyond_limits: false };
|
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.
|
JustUI high-level description compiler.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--debug=lexer Dump the lexer output and stop.
|
--debug=lexer Dump the lexer output and stop
|
||||||
=parser Dump the parser output and stop.
|
=parser Dump the parser output and stop
|
||||||
|
--unit-tests Check unit tests specified by "//^" comments
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
def usage(exitcode=None):
|
def usage(exitcode=None):
|
||||||
|
@ -24,7 +25,8 @@ def usage(exitcode=None):
|
||||||
def main(argv):
|
def main(argv):
|
||||||
# Read command-line arguments
|
# Read command-line arguments
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.gnu_getopt(argv[1:], "", ["help", "debug="])
|
opts, args = getopt.gnu_getopt(argv[1:], "",
|
||||||
|
["help", "debug=", "unit-tests"])
|
||||||
opts = dict(opts)
|
opts = dict(opts)
|
||||||
|
|
||||||
if len(argv) == 1 or "-h" in opts or "--help" in opts:
|
if len(argv) == 1 or "-h" in opts or "--help" in opts:
|
||||||
|
@ -45,7 +47,8 @@ def main(argv):
|
||||||
source = fp.read()
|
source = fp.read()
|
||||||
|
|
||||||
try:
|
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":
|
if opts.get("--debug") == "lexer":
|
||||||
lexer.dump()
|
lexer.dump()
|
||||||
return 0
|
return 0
|
||||||
|
@ -53,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
|
||||||
|
|
||||||
|
@ -65,11 +68,16 @@ def main(argv):
|
||||||
|
|
||||||
ctx = juic.eval.Context(juic.builtins.builtinClosure)
|
ctx = juic.eval.Context(juic.builtins.builtinClosure)
|
||||||
|
|
||||||
for e in ast.args:
|
if "--unit-tests" in opts:
|
||||||
e.dump()
|
for node in ast.args:
|
||||||
v = ctx.execStmt(e)
|
ctx.execStmt(node)
|
||||||
v = ctx.force(v)
|
return 0
|
||||||
print(">>>>>>>", juic.eval.juiValueString(v))
|
|
||||||
|
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.currentScope.dump()
|
||||||
ctx.currentClosure.dump()
|
ctx.currentClosure.dump()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import typing
|
from typing import Any, Tuple
|
||||||
import enum
|
import enum
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
@ -23,8 +23,8 @@ class SyntaxError(Exception):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Token:
|
class Token:
|
||||||
type: typing.Any
|
type: Any
|
||||||
value: typing.Any
|
value: Any
|
||||||
loc: Loc
|
loc: Loc
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -45,7 +45,8 @@ class NaiveRegexLexer:
|
||||||
# Override with list of (regex, token type, token value). Both the token
|
# 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
|
# type and value can be functions, in which case they'll be called with the
|
||||||
# match object as parameter.
|
# 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
|
# Override with token predicate that matches token to be discarded and not
|
||||||
# sent to the parser (typically, whitespace and comments).
|
# sent to the parser (typically, whitespace and comments).
|
||||||
TOKEN_DISCARD = lambda _: False
|
TOKEN_DISCARD = lambda _: False
|
||||||
|
@ -76,12 +77,18 @@ class NaiveRegexLexer:
|
||||||
if not len(self.input):
|
if not len(self.input):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
highestPriority = 0
|
||||||
longestMatch = None
|
longestMatch = None
|
||||||
longestMatchIndex = -1
|
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 (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
|
longestMatch = m
|
||||||
longestMatchIndex = i
|
longestMatchIndex = i
|
||||||
|
|
||||||
|
@ -90,7 +97,7 @@ class NaiveRegexLexer:
|
||||||
self.raiseError(f"unknown syntax '{nextWord}'")
|
self.raiseError(f"unknown syntax '{nextWord}'")
|
||||||
|
|
||||||
# Build the token
|
# Build the token
|
||||||
_, type_info, value_info = self.TOKEN_REGEX[longestMatchIndex]
|
_, type_info, value_info, *rest = self.TOKEN_REGEX[longestMatchIndex]
|
||||||
m = longestMatch
|
m = longestMatch
|
||||||
|
|
||||||
typ = type_info(m) if callable(type_info) else type_info
|
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
|
# Rule combinators implementing unary and binary operators with precedence
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def binaryOpsLeft(ctor, ops):
|
def binaryOpsLeft(ctor, ops):
|
||||||
def decorate(f):
|
def decorate(f):
|
||||||
def symbol(self):
|
def symbol(self):
|
||||||
|
@ -179,6 +187,7 @@ class LL1Parser:
|
||||||
return symbol
|
return symbol
|
||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def binaryOps(ctor, ops, *, rassoc=False):
|
def binaryOps(ctor, ops, *, rassoc=False):
|
||||||
def decorate(f):
|
def decorate(f):
|
||||||
def symbol(self):
|
def symbol(self):
|
||||||
|
@ -191,9 +200,11 @@ class LL1Parser:
|
||||||
return symbol
|
return symbol
|
||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def binaryOpsRight(ctor, ops):
|
def binaryOpsRight(ctor, ops):
|
||||||
return LL1Parser.binaryOps(ctor, ops, rassoc=True)
|
return LL1Parser.binaryOps(ctor, ops, rassoc=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def unaryOps(ctor, ops, assoc=True):
|
def unaryOps(ctor, ops, assoc=True):
|
||||||
def decorate(f):
|
def decorate(f):
|
||||||
def symbol(self):
|
def symbol(self):
|
||||||
|
@ -212,15 +223,17 @@ def unescape(s: str) -> str:
|
||||||
|
|
||||||
class JuiLexer(NaiveRegexLexer):
|
class JuiLexer(NaiveRegexLexer):
|
||||||
T = enum.Enum("T",
|
T = enum.Enum("T",
|
||||||
["WS", "KW", "COMMENT",
|
["WS", "KW", "COMMENT", "UNIT_TEST_MARKER",
|
||||||
"INT", "FLOAT", "STRING",
|
"TEXTLIT", "INT", "FLOAT", "STRING",
|
||||||
"IDENT", "ATTR", "VAR", "LABEL", "FIELD", "CXXIDENT"])
|
"IDENT", "ATTR", "VAR", "LABEL", "FIELD", "CXXIDENT"])
|
||||||
|
|
||||||
|
RE_UTMARKER = r'//\^'
|
||||||
|
|
||||||
RE_COMMENT = r'(#|//)[^\n]*|/\*([^/]|/[^*])+\*/'
|
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_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_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_IDENT = r'[\w_][\w0-9_]*'
|
||||||
RE_ATTR = r'({})\s*(?:@({}))?\s*:'.format(RE_IDENT, RE_IDENT)
|
RE_ATTR = r'({})\s*(?:@({}))?\s*:'.format(RE_IDENT, RE_IDENT)
|
||||||
RE_VAR = r'\$(\.)?' + 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]
|
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
|
@dataclass
|
||||||
class Node:
|
class Node:
|
||||||
T = enum.Enum("T", [
|
T = enum.Enum("T", [
|
||||||
"LIT", "IDENT", "OP", "THIS", "PROJ", "CALL", "IF", "SCOPE",
|
"LIT", "IDENT", "OP", "THIS", "PROJ", "CALL", "IF", "SCOPE",
|
||||||
"RECORD", "REC_ATTR", "REC_VALUE",
|
"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
|
ctor: T
|
||||||
args: list[typing.Any]
|
args: list[Any]
|
||||||
|
|
||||||
def dump(self, indent=0):
|
def dump(self, indent=0):
|
||||||
print(" " * indent + self.ctor.name, end=" ")
|
print(" " * indent + self.ctor.name, end=" ")
|
||||||
|
@ -339,7 +360,7 @@ class JuiParser(LL1Parser):
|
||||||
case T.KW if t.value == "null":
|
case T.KW if t.value == "null":
|
||||||
node = Node(Node.T.LIT, [None])
|
node = Node(Node.T.LIT, [None])
|
||||||
case T.KW if t.value in ["true", "false"]:
|
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 "(":
|
case "(":
|
||||||
node = self.expr()
|
node = self.expr()
|
||||||
self.expect(")")
|
self.expect(")")
|
||||||
|
@ -352,7 +373,7 @@ class JuiParser(LL1Parser):
|
||||||
# | expr0 "<{" record_entry,* "}" (record update)
|
# | expr0 "<{" record_entry,* "}" (record update)
|
||||||
# | expr0 "(" expr,* ")" (function call)
|
# | expr0 "(" expr,* ")" (function call)
|
||||||
# | expr0 "." ident (projection, same prec as call)
|
# | expr0 "." ident (projection, same prec as call)
|
||||||
@LL1Parser.binaryOpsRight(mkOpNode, ["|"])
|
@LL1Parser.binaryOpsLeft(mkOpNode, ["|"])
|
||||||
@LL1Parser.binaryOpsLeft(mkOpNode, ["<|"])
|
@LL1Parser.binaryOpsLeft(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])
|
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])
|
||||||
|
|
||||||
|
@ -480,7 +501,20 @@ class JuiParser(LL1Parser):
|
||||||
def scope(self):
|
def scope(self):
|
||||||
isNone = lambda t: t is None
|
isNone = lambda t: t is None
|
||||||
entries = self.separatedList(self.scope_stmt, sep=";", term=isNone)
|
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):
|
def scope_stmt(self):
|
||||||
match self.la.type, self.la.value:
|
match self.la.type, self.la.value:
|
||||||
|
@ -490,5 +524,8 @@ class JuiParser(LL1Parser):
|
||||||
return self.fun_rec_decl()
|
return self.fun_rec_decl()
|
||||||
case JuiLexer.T.KW, "set":
|
case JuiLexer.T.KW, "set":
|
||||||
return self.set_stmt()
|
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 _:
|
case _:
|
||||||
return self.expr()
|
return Node(Node.T.SCOPE_EXPR, [self.expr()])
|
||||||
|
|
Loading…
Reference in a new issue