mirror of
https://git.planet-casio.com/Lephenixnoir/JustUI.git
synced 2025-01-01 06:23:38 +01:00
612 lines
24 KiB
Python
612 lines
24 KiB
Python
|
from dataclasses import dataclass, field
|
||
|
from typing import Tuple, Union
|
||
|
|
||
|
import juic.parser
|
||
|
from juic.parser import Node
|
||
|
from juic.datatypes import *
|
||
|
|
||
|
# Storage of a scope's internal data, including all definitions in the scope.
|
||
|
# The data in this object is never copied, so for instance, the name -> value
|
||
|
# mappings are always unique and can only be found in the relevant's scope
|
||
|
# unique MutableScopeData instance. This data is, of course, not constant,
|
||
|
# which makes it unsuitable for building closures. The Closure type captures an
|
||
|
# immutable snapshot of a scope's contents at one point in time.
|
||
|
@dataclass
|
||
|
class MutableScopeData:
|
||
|
# List of definitions. Each value has a timestamp integer that indicates
|
||
|
# when it was added; this allows snapshots to ignore definitions newer than
|
||
|
# the snapshot by simply comparing timestamp values. The value itself is
|
||
|
# either a thunk (evaluated or not), or a runtime value.
|
||
|
defs: dict[str, Tuple[int, Union[JuiValue, "Thunk"]]] = \
|
||
|
field(default_factory=dict)
|
||
|
|
||
|
# Current timestamp, changed every time a value is added.
|
||
|
timestamp: int = 0
|
||
|
|
||
|
def addDefinition(self, name: str, content: Union[JuiValue, "Thunk"]):
|
||
|
if name in self.defs:
|
||
|
raise JuiRuntimeError(f"{name} already defined in current scope")
|
||
|
self.defs[name] = (self.timestamp, content)
|
||
|
self.timestamp += 1
|
||
|
|
||
|
def lookup(self, name: str, maxTimestamp: int = -1) \
|
||
|
-> Union[JuiValue, "Thunk", None]:
|
||
|
if name not in self.defs:
|
||
|
return None
|
||
|
if maxTimestamp >= 0 and self.defs[name][0] >= maxTimestamp:
|
||
|
return None
|
||
|
return self.defs[name][1]
|
||
|
|
||
|
def dump(self):
|
||
|
digits = len(str(self.timestamp))
|
||
|
print(f"Scope: timestamp={self.timestamp}, defs:")
|
||
|
for name, (ts, content) in self.defs.items():
|
||
|
print(f" {ts: >{digits}} {name}:", juiValueString(content))
|
||
|
|
||
|
# An immutable snapshot of all definitions that can be referred to at a given
|
||
|
# point in time. This specifies the interpretation of all defined identifiers
|
||
|
# but not of the "this" keyword.
|
||
|
@dataclass
|
||
|
class Closure:
|
||
|
# Parent snapshot on which this one is based, which defines the contents of
|
||
|
# surrounding scopes at that time.
|
||
|
parent: Union["Closure", None]
|
||
|
# Reference to the scope in which the closure was created.
|
||
|
scope: MutableScopeData
|
||
|
# Timestamp at which the closure was created. Definitions from `scope`
|
||
|
# created later than the closure will be ignored during name lookup.
|
||
|
timestamp: int = field(init=False)
|
||
|
|
||
|
def __post_init__(self):
|
||
|
# Automatically get the timestamp at the time of creation
|
||
|
self.timestamp = self.scope.timestamp
|
||
|
|
||
|
def lookup(self, name: str) -> Union[JuiValue, "Thunk", None]:
|
||
|
value = self.scope.lookup(name, self.timestamp)
|
||
|
if value is not None or self.parent is None:
|
||
|
return value
|
||
|
return self.parent.lookup(name)
|
||
|
|
||
|
def dump(self, level=0):
|
||
|
print(f"Closure at depth {level}: timestamp={self.timestamp}:")
|
||
|
self.scope.dump()
|
||
|
if self.parent is not None:
|
||
|
self.parent.dump(level+1)
|
||
|
|
||
|
# Thunk representing a suspended/lazy computation. Thunks are evaluated when
|
||
|
# needed (which is the call-by-need evaluation strategy). They are always
|
||
|
# interpreted in the context of the associated closure.
|
||
|
#
|
||
|
# While the object is technically mutable, the result of the computation
|
||
|
# (whether a value or a cyclic invalid result) is entirely determined by the
|
||
|
# expression AST and closure, so the result is just a lazily computed/memoized
|
||
|
# member. That said, the result might not exist if there is a cyclic dependency
|
||
|
# between this and other thunks referenced by the closure.
|
||
|
@dataclass
|
||
|
class Thunk:
|
||
|
# Expression AST that the thunk evaluates to
|
||
|
ast: Node
|
||
|
# Closure in which the thunk can be evaluated, plus the definition for
|
||
|
# "this"; if there is one, it is always a PRS
|
||
|
closure: Closure
|
||
|
thisReference: Union[None, "PartialRecordSnapshot"] = None
|
||
|
|
||
|
# Whether the thunk has been evaluated yet
|
||
|
evaluated: bool = False
|
||
|
# Whether the thunk has been evaluated to an invalid result because of a
|
||
|
# cyclic dependency with other thunks captured by the closure
|
||
|
invalid: bool = False
|
||
|
# If the thunk has been evaluated and is not invalid, resulting value. None
|
||
|
# is a possible resulting value (`null` in the language); do not use this
|
||
|
# field to determine whether the thunk has been successfully evaluted.
|
||
|
result: JuiValue = None
|
||
|
|
||
|
# Whether the thunk is currently under evaluation
|
||
|
_running: bool = False
|
||
|
|
||
|
# 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
|
||
|
# underlying record otherwise, if there is one. Because the use of record
|
||
|
# constructor arguments can create forwards dependencies, all these definitions
|
||
|
# are thunked and computed lazily in a somewhat unpredictable order. A partial
|
||
|
# record snapshot fully specifies the interpretation of all fields of a
|
||
|
# partially-constructed record (i.e. `this`) at the location of one record
|
||
|
# entry. It does so by mapping defined field names to the correct thunks. This
|
||
|
# object is how dependencies between fields are discovered and followed.
|
||
|
@dataclass
|
||
|
class PartialRecordSnapshot:
|
||
|
# Mapping from fields of `this` to thunks
|
||
|
fieldThunks: dict[str, Thunk] = field(default_factory=dict)
|
||
|
# Base object for fields not captured in `fieldThunks`
|
||
|
base: None | Thunk = None
|
||
|
|
||
|
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
|
||
|
|
||
|
class JuiNameError(Exception):
|
||
|
pass
|
||
|
|
||
|
class JuiStaticError(Exception):
|
||
|
pass
|
||
|
|
||
|
class JuiRuntimeError(Exception):
|
||
|
pass
|
||
|
|
||
|
# TODO: Better diagnostics for type errors
|
||
|
def requireType(v: JuiValue, predicate: Callable[[JuiValue], bool]):
|
||
|
if not predicate(v):
|
||
|
raise JuiTypeError(f"type error: got {v}, needed {predicate}")
|
||
|
|
||
|
def requireSameType(vs: list[JuiValue], predicate: Callable[[JuiValue], bool]):
|
||
|
if len(vs) == 0:
|
||
|
return
|
||
|
if len(set(type(v) for v in vs)) != 1:
|
||
|
vals = ", ".join(str(v) for v in vs)
|
||
|
raise JuiTypeError(f"type error: heterogeneous types for {vals}")
|
||
|
|
||
|
if not predicate(vs[0]):
|
||
|
raise JuiTypeError(f"type error: got {vs[0]}, needed {predicate}")
|
||
|
|
||
|
# Evaluation context. This tracks the current scope and provides evaluation
|
||
|
# functions, error tracking, etc.
|
||
|
class Context:
|
||
|
# Current top-level scope. Can be None. This is the only scope in which we
|
||
|
# can add definitions.
|
||
|
currentScope: MutableScopeData
|
||
|
# Current closure defining the scopes that surround `currentScope`.
|
||
|
currentClosure: Closure | None
|
||
|
|
||
|
def __init__(self, initialClosure = None):
|
||
|
self.currentScope = MutableScopeData()
|
||
|
self.currentClosure = initialClosure
|
||
|
self._contextStack = []
|
||
|
|
||
|
#=== Context switch commodity ===#
|
||
|
|
||
|
_contextStack: list[Tuple[MutableScopeData, Closure]]
|
||
|
|
||
|
class ContextSwitchContextManager:
|
||
|
def __init__(self, parent: "Context", c: Closure):
|
||
|
self.parent = parent
|
||
|
self.targetClosure = c
|
||
|
|
||
|
def __enter__(self):
|
||
|
p = self.parent
|
||
|
p._contextStack.append((p.currentScope, p.currentClosure))
|
||
|
p.currentScope = MutableScopeData()
|
||
|
p.currentClosure = self.targetClosure
|
||
|
|
||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||
|
p = self.parent
|
||
|
s, c = p._contextStack.pop()
|
||
|
p.currentScope = s
|
||
|
p.currentClosure = c
|
||
|
|
||
|
# Temporarily switch the execution context. This is used when we need e.g.
|
||
|
# to evaluate a thunk in its associated closure. Morally one could
|
||
|
# construct a different Context object, but it's easier to accumulate error
|
||
|
# messages and other things in the same Context object.
|
||
|
# Example:
|
||
|
# with self.contextSwitchAndPushNewScope(myClosure):
|
||
|
# myResult = self.eval(myExprNode)
|
||
|
def contextSwitchAndPushNewScope(self, c: Closure):
|
||
|
return Context.ContextSwitchContextManager(self, c)
|
||
|
|
||
|
# Freeze the current scope and push a new empty one to start working in.
|
||
|
# Example:
|
||
|
# with self.pushNewScope():
|
||
|
# self.addDefinition(myParamName, myParamValue)
|
||
|
# myResult = self.eval(myFunctionBody)
|
||
|
def pushNewScope(self):
|
||
|
return self.contextSwitchAndPushNewScope(self.currentStateClosure())
|
||
|
|
||
|
#=== State management ===#
|
||
|
|
||
|
# Build a the closure of the current state
|
||
|
def currentStateClosure(self):
|
||
|
return Closure(parent=self.currentClosure, scope=self.currentScope)
|
||
|
|
||
|
# Add a new definition in the innermost scope
|
||
|
def addDefinition(self, name: str, content: JuiValue | Thunk):
|
||
|
return self.currentScope.addDefinition(name, content)
|
||
|
|
||
|
# Lookup a name
|
||
|
def lookup(self, name: str) -> JuiValue | Thunk | None:
|
||
|
return self.currentStateClosure().lookup(name)
|
||
|
|
||
|
#=== Evaluation helpers ===#
|
||
|
|
||
|
def makeFunction(self, params: list[Tuple[str, bool]], body: Node) \
|
||
|
-> Function:
|
||
|
# Check validity of the variadic parameter specification
|
||
|
variadics = [i for i, (_, v) in enumerate(params) if v]
|
||
|
|
||
|
for (name, _) in params:
|
||
|
count = len([0 for (n, _) in params if n == name])
|
||
|
if count > 1:
|
||
|
raise JuiStaticError(f"duplicate argument {name}")
|
||
|
|
||
|
if len(variadics) > 1:
|
||
|
names = ", ".join(params[i][0] for i in variadics)
|
||
|
raise JuiStaticError(f"multiple variadic arguments: {names}")
|
||
|
|
||
|
if variadics and variadics != [len(params) - 1]:
|
||
|
name = params[variadic][0]
|
||
|
raise JuiStaticError(f"variadic argument {name} not at the end")
|
||
|
variadic = params[variadics[0]][0] if variadics else None
|
||
|
|
||
|
# Build function object
|
||
|
return Function(closure = self.currentStateClosure(),
|
||
|
body = body,
|
||
|
params = [name for (name, v) in params if not v],
|
||
|
variadic = variadic)
|
||
|
|
||
|
#=== Main evaluation functions ===#
|
||
|
|
||
|
# Expression evaluator; this function is pure. It can cause thunks to be
|
||
|
# evaluated and thus reveal previously-undiscovered cyclic dependencies,
|
||
|
# but it doesn't modify the context.
|
||
|
def evalExpr(self, node: Node) -> JuiValue:
|
||
|
match node.ctor, node.args:
|
||
|
case Node.T.LIT, [x]:
|
||
|
return x
|
||
|
|
||
|
case Node.T.IDENT, [name]:
|
||
|
match self.lookup(name):
|
||
|
case None:
|
||
|
raise JuiNameError(f"name {name} not defined")
|
||
|
case Thunk() as t:
|
||
|
return self.evalThunk(t)
|
||
|
case value:
|
||
|
return value
|
||
|
|
||
|
case Node.T.OP, ["+", X, Y]:
|
||
|
x = self.evalExpr(X)
|
||
|
y = self.evalExpr(Y)
|
||
|
requireSameType([x, y], juiIsAddable)
|
||
|
return x + y
|
||
|
|
||
|
# TODO: Arithmetic: integer division, type conversions
|
||
|
case Node.T.OP, [("+" | "-" | "*" | "/" | "%") as op, X, Y]:
|
||
|
x = self.evalExpr(X)
|
||
|
y = self.evalExpr(Y)
|
||
|
requireSameType([x, y], juiIsArith)
|
||
|
match op:
|
||
|
case "+": return x + y
|
||
|
case "-": return x - y
|
||
|
case "*": return x * y
|
||
|
case "/": return x / y if type(x) == float else x // y
|
||
|
case "%": return x % y
|
||
|
|
||
|
case Node.T.OP, [("!" | "+" | "-") as op, X]:
|
||
|
x = self.evalExpr(X)
|
||
|
requireType(x, juiIsArith)
|
||
|
match op:
|
||
|
case "!": return not x
|
||
|
case "+": return +x
|
||
|
case "-": return -x
|
||
|
|
||
|
case Node.T.OP, [(">"|">="|"<"|"<="|"=="|"!=") as op, X, Y]:
|
||
|
x = self.evalExpr(X)
|
||
|
y = self.evalExpr(Y)
|
||
|
requireSameType([x, y], juiIsComparable)
|
||
|
match op:
|
||
|
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 Node.T.OP, [("&&" | "||") as op, X, Y]:
|
||
|
x = self.evalExpr(X)
|
||
|
y = self.evalExpr(Y)
|
||
|
requireSameType([x, y], juiIsLogical)
|
||
|
match op:
|
||
|
case "&&": return x and y
|
||
|
case "||": return x or y
|
||
|
|
||
|
case Node.T.OP, ["...", X]:
|
||
|
x = self.evalExpr(X)
|
||
|
requireType(x, juiIsUnpackable)
|
||
|
raise NotImplementedError("unpack operator o(x_x)o")
|
||
|
|
||
|
case Node.T.OP, ["|", X, F]:
|
||
|
f = self.evalExpr(F)
|
||
|
requireType(f, juiIsCallable)
|
||
|
return self.evalCall(f, [X])
|
||
|
|
||
|
case Node.T.OP, ["<|", F, X]:
|
||
|
f = self.evalExpr(F)
|
||
|
requireType(f, juiIsCallable)
|
||
|
return self.evalCall(f, [X])
|
||
|
|
||
|
case Node.T.THIS, []:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case Node.T.PROJ, []:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case Node.T.CALL, [F, *A]:
|
||
|
f = self.evalExpr(F)
|
||
|
requireType(f, juiIsCallable)
|
||
|
return self.evalCall(f, A)
|
||
|
|
||
|
case Node.T.IF, [C, T, E]:
|
||
|
c = self.evalExpr(C)
|
||
|
requireType(c, juiIsLogical)
|
||
|
if bool(c):
|
||
|
return self.evalExpr(T)
|
||
|
else:
|
||
|
return None if E is None else self.evalExpr(E)
|
||
|
|
||
|
case Node.T.RECORD, [R, *A]:
|
||
|
r = self.evalExpr(R)
|
||
|
requireType(r, juiIsConstructible)
|
||
|
return self.evalRecordConstructor(r, A)
|
||
|
|
||
|
case Node.T.REC_ATTR, _:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case Node.T.REC_VALUE, _:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case _, _:
|
||
|
raise Exception("invalid expr o(x_x)o: " + str(node))
|
||
|
|
||
|
def execStmt(self, node: Node) -> JuiValue:
|
||
|
match node.ctor, node.args:
|
||
|
case Node.T.LET_DECL, [name, X]:
|
||
|
self.addDefinition(name, self.evalExpr(X))
|
||
|
|
||
|
case Node.T.FUN_DECL, [name, params, body]:
|
||
|
self.addDefinition(name, self.makeFunction(params, body))
|
||
|
|
||
|
case Node.T.REC_DECL, _:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case Node.T.SET_STMT, _:
|
||
|
raise NotImplementedError
|
||
|
|
||
|
case _, _:
|
||
|
return self.evalExpr(node)
|
||
|
|
||
|
# TODO: Context.eval*: continue failed computations to find other errors?
|
||
|
def evalThunk(self, th: Thunk) -> JuiValue:
|
||
|
if th.evaluated:
|
||
|
if th.invalid:
|
||
|
raise JuiRuntimeError("cyclic dependency result encountered")
|
||
|
return th.result
|
||
|
if th._running:
|
||
|
raise JuiRuntimeError("cyclic dependency detected!")
|
||
|
|
||
|
with self.contextSwitchAndPushNewScope(th.closure):
|
||
|
if th.thisReference is not None:
|
||
|
self.addDefinition("this", th.thisReference)
|
||
|
|
||
|
th._running = True
|
||
|
result = self.evalExpr(th.ast)
|
||
|
th._running = False
|
||
|
|
||
|
# TODO: Set invalid = True if we continue failed computations
|
||
|
th.evaluated = True
|
||
|
th.result = result
|
||
|
return result
|
||
|
|
||
|
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
|
||
|
if type(f) == BuiltinFunction:
|
||
|
return f.func(*[self.evalExpr(a) for a in args])
|
||
|
|
||
|
assert type(f) == Function and "evalCall: bad type check precondition"
|
||
|
|
||
|
# Check number of arguments
|
||
|
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(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 f.variadic is not None:
|
||
|
raise NotImplementedError("list node for building varargs o(x_x)o")
|
||
|
|
||
|
# Run into the function's scope
|
||
|
with self.contextSwitchAndPushNewScope(f.closure):
|
||
|
for name, node in zip(f.params, args):
|
||
|
th = Thunk(ast=node, closure=self.currentStateClosure())
|
||
|
self.addDefinition(name, th)
|
||
|
# self.currentScope.dump()
|
||
|
# self.currentClosure.dump()
|
||
|
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")
|
||
|
|
||
|
# 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
|
||
|
th = Thunk(ast=node, closure=self.currentStateClosure())
|
||
|
th.thisReference = prs.copy()
|
||
|
|
||
|
r.attr[name] = th
|
||
|
prs.fieldThunks[name] = th
|
||
|
|
||
|
return r
|
||
|
|
||
|
# TODO: Factor this with function. In fact, this should reduce to a call
|
||
|
# Check number of arguments
|
||
|
req = str(len(ctor.params)) + ("+" if ctor.variadic is not None else "")
|
||
|
if len(args) < len(ctor.params):
|
||
|
raise JuiRuntimeError(f"not enough args (need {req}, got {len(args)})")
|
||
|
if len(args) > len(ctor.params) and ctor.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:
|
||
|
raise NotImplementedError("list node for building varargs o(x_x)o")
|
||
|
|
||
|
# Otherwise, it's a function so it might use
|
||
|
raise NotImplementedError
|
||
|
|
||
|
assert type(r) == Function and \
|
||
|
"evalRecordConstructor: not a record type nor a function"
|
||
|
|
||
|
# 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.
|
||
|
|
||
|
# - 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
|
||
|
|
||
|
def force(self, v: JuiValue) -> JuiValue:
|
||
|
match v:
|
||
|
case Record() as r:
|
||
|
for a in r.attr:
|
||
|
r.attr[a] = self.force(r.attr[a])
|
||
|
return r
|
||
|
case Thunk() as th:
|
||
|
self.evalThunk(th)
|
||
|
if th.evaluated:
|
||
|
return self.force(th.result)
|
||
|
return th
|
||
|
case _:
|
||
|
return v
|