Rebrand sexp → sx across web platform (173 files)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s
Rename all sexp directories, files, identifiers, and references to sx. artdag/ excluded (separate media processing DSL). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
620
shared/sx/evaluator.py
Normal file
620
shared/sx/evaluator.py
Normal file
@@ -0,0 +1,620 @@
|
||||
"""
|
||||
S-expression evaluator.
|
||||
|
||||
Walks a parsed s-expression tree and evaluates it in an environment.
|
||||
|
||||
Special forms:
|
||||
(if cond then else?)
|
||||
(when cond body)
|
||||
(cond clause...) — Scheme-style ((test body)...) or Clojure-style (test body...)
|
||||
(case expr val body... :else default)
|
||||
(and expr...) (or expr...)
|
||||
(let ((name val)...) body) or (let (name val name val...) body)
|
||||
(lambda (params...) body) or (fn (params...) body)
|
||||
(define name value)
|
||||
(defcomp ~name (&key param...) body)
|
||||
(defrelation :name :from "type" :to "type" :cardinality :card ...)
|
||||
(begin expr...)
|
||||
(quote expr)
|
||||
(do expr...) — alias for begin
|
||||
(-> val form...) — thread-first macro
|
||||
|
||||
Higher-order forms (operate on lambdas):
|
||||
(map fn coll)
|
||||
(map-indexed fn coll)
|
||||
(filter fn coll)
|
||||
(reduce fn init coll)
|
||||
(some fn coll)
|
||||
(every? fn coll)
|
||||
(for-each fn coll)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .types import Component, Keyword, Lambda, NIL, RelationDef, Symbol
|
||||
from .primitives import _PRIMITIVES
|
||||
|
||||
|
||||
class EvalError(Exception):
|
||||
"""Error during expression evaluation."""
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def evaluate(expr: Any, env: dict[str, Any] | None = None) -> Any:
|
||||
"""Evaluate *expr* in *env* and return the result."""
|
||||
if env is None:
|
||||
env = {}
|
||||
return _eval(expr, env)
|
||||
|
||||
|
||||
def make_env(**kwargs: Any) -> dict[str, Any]:
|
||||
"""Convenience: create an environment dict with initial bindings."""
|
||||
return dict(kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal evaluator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _eval(expr: Any, env: dict[str, Any]) -> Any:
|
||||
# --- literals ---------------------------------------------------------
|
||||
if isinstance(expr, (int, float, str, bool)):
|
||||
return expr
|
||||
if expr is None or expr is NIL:
|
||||
return NIL
|
||||
|
||||
# --- symbol lookup ----------------------------------------------------
|
||||
if isinstance(expr, Symbol):
|
||||
name = expr.name
|
||||
if name in env:
|
||||
return env[name]
|
||||
if name in _PRIMITIVES:
|
||||
return _PRIMITIVES[name]
|
||||
if name == "true":
|
||||
return True
|
||||
if name == "false":
|
||||
return False
|
||||
if name == "nil":
|
||||
return NIL
|
||||
raise EvalError(f"Undefined symbol: {name}")
|
||||
|
||||
# --- keyword → its string name ----------------------------------------
|
||||
if isinstance(expr, Keyword):
|
||||
return expr.name
|
||||
|
||||
# --- dict literal -----------------------------------------------------
|
||||
if isinstance(expr, dict):
|
||||
return {k: _eval(v, env) for k, v in expr.items()}
|
||||
|
||||
# --- list = call or special form --------------------------------------
|
||||
if not isinstance(expr, list):
|
||||
return expr
|
||||
|
||||
if not expr:
|
||||
return []
|
||||
|
||||
head = expr[0]
|
||||
|
||||
# If head is not a symbol/lambda/list, treat entire list as data
|
||||
if not isinstance(head, (Symbol, Lambda, list)):
|
||||
return [_eval(x, env) for x in expr]
|
||||
|
||||
# --- special forms ----------------------------------------------------
|
||||
if isinstance(head, Symbol):
|
||||
name = head.name
|
||||
handler = _SPECIAL_FORMS.get(name)
|
||||
if handler is not None:
|
||||
return handler(expr, env)
|
||||
|
||||
# Higher-order forms (need lazy eval of lambda arg)
|
||||
ho = _HO_FORMS.get(name)
|
||||
if ho is not None:
|
||||
return ho(expr, env)
|
||||
|
||||
# --- function / lambda call -------------------------------------------
|
||||
fn = _eval(head, env)
|
||||
args = [_eval(a, env) for a in expr[1:]]
|
||||
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
return fn(*args)
|
||||
|
||||
if isinstance(fn, Lambda):
|
||||
return _call_lambda(fn, args, env)
|
||||
|
||||
if isinstance(fn, Component):
|
||||
return _call_component(fn, expr[1:], env)
|
||||
|
||||
raise EvalError(f"Not callable: {fn!r}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lambda / component invocation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _call_lambda(fn: Lambda, args: list[Any], caller_env: dict[str, Any]) -> Any:
|
||||
if len(args) != len(fn.params):
|
||||
raise EvalError(f"{fn!r} expects {len(fn.params)} args, got {len(args)}")
|
||||
local = dict(fn.closure)
|
||||
local.update(caller_env)
|
||||
for p, v in zip(fn.params, args):
|
||||
local[p] = v
|
||||
return _eval(fn.body, local)
|
||||
|
||||
|
||||
def _call_component(comp: Component, raw_args: list[Any], env: dict[str, Any]) -> Any:
|
||||
"""Evaluate a component invocation with keyword arguments.
|
||||
|
||||
``(~card :title "Hello" (p "child"))``
|
||||
→ comp.params gets ``title="Hello"``, comp children gets ``[(p "child")]``
|
||||
"""
|
||||
kwargs: dict[str, Any] = {}
|
||||
children: list[Any] = []
|
||||
i = 0
|
||||
while i < len(raw_args):
|
||||
arg = raw_args[i]
|
||||
if isinstance(arg, Keyword) and i + 1 < len(raw_args):
|
||||
kwargs[arg.name] = _eval(raw_args[i + 1], env)
|
||||
i += 2
|
||||
else:
|
||||
children.append(_eval(arg, env))
|
||||
i += 1
|
||||
|
||||
local = dict(comp.closure)
|
||||
local.update(env)
|
||||
for p in comp.params:
|
||||
if p in kwargs:
|
||||
local[p] = kwargs[p]
|
||||
else:
|
||||
local[p] = NIL
|
||||
if comp.has_children:
|
||||
local["children"] = children
|
||||
return _eval(comp.body, local)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Special forms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _sf_if(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 3:
|
||||
raise EvalError("if requires condition and then-branch")
|
||||
cond = _eval(expr[1], env)
|
||||
if cond and cond is not NIL:
|
||||
return _eval(expr[2], env)
|
||||
if len(expr) > 3:
|
||||
return _eval(expr[3], env)
|
||||
return NIL
|
||||
|
||||
|
||||
def _sf_when(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 3:
|
||||
raise EvalError("when requires condition and body")
|
||||
cond = _eval(expr[1], env)
|
||||
if cond and cond is not NIL:
|
||||
result = NIL
|
||||
for body_expr in expr[2:]:
|
||||
result = _eval(body_expr, env)
|
||||
return result
|
||||
return NIL
|
||||
|
||||
|
||||
def _sf_cond(expr: list, env: dict) -> Any:
|
||||
clauses = expr[1:]
|
||||
if not clauses:
|
||||
return NIL
|
||||
# Detect scheme-style: first clause is a 2-element list that isn't a comparison
|
||||
if (
|
||||
isinstance(clauses[0], list)
|
||||
and len(clauses[0]) == 2
|
||||
and not (isinstance(clauses[0][0], Symbol) and clauses[0][0].name in (
|
||||
"=", "<", ">", "<=", ">=", "!=", "and", "or",
|
||||
))
|
||||
):
|
||||
for clause in clauses:
|
||||
if not isinstance(clause, list) or len(clause) < 2:
|
||||
raise EvalError("cond clause must be (test result)")
|
||||
test = clause[0]
|
||||
if isinstance(test, Symbol) and test.name in ("else", ":else"):
|
||||
return _eval(clause[1], env)
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return _eval(clause[1], env)
|
||||
if _eval(test, env):
|
||||
return _eval(clause[1], env)
|
||||
else:
|
||||
i = 0
|
||||
while i < len(clauses) - 1:
|
||||
test = clauses[i]
|
||||
result = clauses[i + 1]
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return _eval(result, env)
|
||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||
return _eval(result, env)
|
||||
if _eval(test, env):
|
||||
return _eval(result, env)
|
||||
i += 2
|
||||
return NIL
|
||||
|
||||
|
||||
def _sf_case(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 2:
|
||||
raise EvalError("case requires expression to match")
|
||||
match_val = _eval(expr[1], env)
|
||||
clauses = expr[2:]
|
||||
i = 0
|
||||
while i < len(clauses) - 1:
|
||||
test = clauses[i]
|
||||
result = clauses[i + 1]
|
||||
if isinstance(test, Keyword) and test.name == "else":
|
||||
return _eval(result, env)
|
||||
if isinstance(test, Symbol) and test.name in (":else", "else"):
|
||||
return _eval(result, env)
|
||||
if match_val == _eval(test, env):
|
||||
return _eval(result, env)
|
||||
i += 2
|
||||
return NIL
|
||||
|
||||
|
||||
def _sf_and(expr: list, env: dict) -> Any:
|
||||
result: Any = True
|
||||
for arg in expr[1:]:
|
||||
result = _eval(arg, env)
|
||||
if not result:
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
def _sf_or(expr: list, env: dict) -> Any:
|
||||
result: Any = False
|
||||
for arg in expr[1:]:
|
||||
result = _eval(arg, env)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
def _sf_let(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 3:
|
||||
raise EvalError("let requires bindings and body")
|
||||
bindings = expr[1]
|
||||
local = dict(env)
|
||||
|
||||
if isinstance(bindings, list):
|
||||
if bindings and isinstance(bindings[0], list):
|
||||
# Scheme-style: ((name val) ...)
|
||||
for binding in bindings:
|
||||
if len(binding) != 2:
|
||||
raise EvalError("let binding must be (name value)")
|
||||
var = binding[0]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = _eval(binding[1], local)
|
||||
elif len(bindings) % 2 == 0:
|
||||
# Clojure-style: (name val name val ...)
|
||||
for i in range(0, len(bindings), 2):
|
||||
var = bindings[i]
|
||||
vname = var.name if isinstance(var, Symbol) else var
|
||||
local[vname] = _eval(bindings[i + 1], local)
|
||||
else:
|
||||
raise EvalError("let bindings must be (name val ...) pairs")
|
||||
else:
|
||||
raise EvalError("let bindings must be a list")
|
||||
|
||||
# Evaluate body expressions, return last
|
||||
result: Any = NIL
|
||||
for body_expr in expr[2:]:
|
||||
result = _eval(body_expr, local)
|
||||
return result
|
||||
|
||||
|
||||
def _sf_lambda(expr: list, env: dict) -> Lambda:
|
||||
if len(expr) < 3:
|
||||
raise EvalError("lambda requires params and body")
|
||||
params_expr = expr[1]
|
||||
if not isinstance(params_expr, list):
|
||||
raise EvalError("lambda params must be a list")
|
||||
param_names = []
|
||||
for p in params_expr:
|
||||
if isinstance(p, Symbol):
|
||||
param_names.append(p.name)
|
||||
elif isinstance(p, str):
|
||||
param_names.append(p)
|
||||
else:
|
||||
raise EvalError(f"Invalid lambda param: {p}")
|
||||
return Lambda(param_names, expr[2], dict(env))
|
||||
|
||||
|
||||
def _sf_define(expr: list, env: dict) -> Any:
|
||||
if len(expr) < 3:
|
||||
raise EvalError("define requires name and value")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"define name must be symbol, got {type(name_sym).__name__}")
|
||||
value = _eval(expr[2], env)
|
||||
if isinstance(value, Lambda) and value.name is None:
|
||||
value.name = name_sym.name
|
||||
env[name_sym.name] = value
|
||||
return value
|
||||
|
||||
|
||||
def _sf_defcomp(expr: list, env: dict) -> Component:
|
||||
"""``(defcomp ~name (&key param1 param2 &rest children) body)``"""
|
||||
if len(expr) < 4:
|
||||
raise EvalError("defcomp requires name, params, and body")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"defcomp name must be symbol, got {type(name_sym).__name__}")
|
||||
comp_name = name_sym.name.lstrip("~")
|
||||
|
||||
params_expr = expr[2]
|
||||
if not isinstance(params_expr, list):
|
||||
raise EvalError("defcomp params must be a list")
|
||||
|
||||
params: list[str] = []
|
||||
has_children = False
|
||||
in_key = False
|
||||
for p in params_expr:
|
||||
if isinstance(p, Symbol):
|
||||
if p.name == "&key":
|
||||
in_key = True
|
||||
continue
|
||||
if p.name == "&rest":
|
||||
has_children = True
|
||||
continue
|
||||
if in_key or has_children:
|
||||
if not has_children:
|
||||
params.append(p.name)
|
||||
else:
|
||||
params.append(p.name)
|
||||
# Skip children param name after &rest
|
||||
elif isinstance(p, str):
|
||||
params.append(p)
|
||||
|
||||
comp = Component(
|
||||
name=comp_name,
|
||||
params=params,
|
||||
has_children=has_children,
|
||||
body=expr[3],
|
||||
closure=dict(env),
|
||||
)
|
||||
env[name_sym.name] = comp
|
||||
return comp
|
||||
|
||||
|
||||
def _sf_begin(expr: list, env: dict) -> Any:
|
||||
result: Any = NIL
|
||||
for sub in expr[1:]:
|
||||
result = _eval(sub, env)
|
||||
return result
|
||||
|
||||
|
||||
def _sf_quote(expr: list, _env: dict) -> Any:
|
||||
return expr[1] if len(expr) > 1 else NIL
|
||||
|
||||
|
||||
def _sf_thread_first(expr: list, env: dict) -> Any:
|
||||
"""``(-> val (f a) (g b))`` → ``(g (f val a) b)``"""
|
||||
if len(expr) < 2:
|
||||
raise EvalError("-> requires at least a value")
|
||||
result = _eval(expr[1], env)
|
||||
for form in expr[2:]:
|
||||
if isinstance(form, list):
|
||||
fn = _eval(form[0], env)
|
||||
args = [result] + [_eval(a, env) for a in form[1:]]
|
||||
else:
|
||||
fn = _eval(form, env)
|
||||
args = [result]
|
||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||
result = fn(*args)
|
||||
elif isinstance(fn, Lambda):
|
||||
result = _call_lambda(fn, args, env)
|
||||
else:
|
||||
raise EvalError(f"-> form not callable: {fn!r}")
|
||||
return result
|
||||
|
||||
|
||||
def _sf_set_bang(expr: list, env: dict) -> Any:
|
||||
"""``(set! name value)`` — mutate existing binding."""
|
||||
if len(expr) != 3:
|
||||
raise EvalError("set! requires name and value")
|
||||
name_sym = expr[1]
|
||||
if not isinstance(name_sym, Symbol):
|
||||
raise EvalError(f"set! name must be symbol, got {type(name_sym).__name__}")
|
||||
value = _eval(expr[2], env)
|
||||
# Walk up scope if using Env objects; for plain dicts just overwrite
|
||||
env[name_sym.name] = value
|
||||
return value
|
||||
|
||||
|
||||
_VALID_CARDINALITIES = {"one-to-one", "one-to-many", "many-to-many"}
|
||||
_VALID_NAV = {"submenu", "tab", "badge", "inline", "hidden"}
|
||||
|
||||
|
||||
def _sf_defrelation(expr: list, env: dict) -> RelationDef:
|
||||
"""``(defrelation :name :from "t" :to "t" :cardinality :card ...)``"""
|
||||
if len(expr) < 2:
|
||||
raise EvalError("defrelation requires a name")
|
||||
|
||||
name_kw = expr[1]
|
||||
if not isinstance(name_kw, Keyword):
|
||||
raise EvalError(f"defrelation name must be a keyword, got {type(name_kw).__name__}")
|
||||
rel_name = name_kw.name
|
||||
|
||||
# Parse keyword args from remaining elements
|
||||
kwargs: dict[str, str | None] = {}
|
||||
i = 2
|
||||
while i < len(expr):
|
||||
key = expr[i]
|
||||
if isinstance(key, Keyword):
|
||||
if i + 1 < len(expr):
|
||||
val = expr[i + 1]
|
||||
if isinstance(val, Keyword):
|
||||
kwargs[key.name] = val.name
|
||||
else:
|
||||
kwargs[key.name] = _eval(val, env) if not isinstance(val, str) else val
|
||||
i += 2
|
||||
else:
|
||||
kwargs[key.name] = None
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
for field in ("from", "to", "cardinality"):
|
||||
if field not in kwargs:
|
||||
raise EvalError(f"defrelation {rel_name} missing required :{field}")
|
||||
|
||||
card = kwargs["cardinality"]
|
||||
if card not in _VALID_CARDINALITIES:
|
||||
raise EvalError(
|
||||
f"defrelation {rel_name}: invalid cardinality {card!r}, "
|
||||
f"expected one of {_VALID_CARDINALITIES}"
|
||||
)
|
||||
|
||||
nav = kwargs.get("nav", "hidden")
|
||||
if nav not in _VALID_NAV:
|
||||
raise EvalError(
|
||||
f"defrelation {rel_name}: invalid nav {nav!r}, "
|
||||
f"expected one of {_VALID_NAV}"
|
||||
)
|
||||
|
||||
defn = RelationDef(
|
||||
name=rel_name,
|
||||
from_type=kwargs["from"],
|
||||
to_type=kwargs["to"],
|
||||
cardinality=card,
|
||||
inverse=kwargs.get("inverse"),
|
||||
nav=nav,
|
||||
nav_icon=kwargs.get("nav-icon"),
|
||||
nav_label=kwargs.get("nav-label"),
|
||||
)
|
||||
|
||||
from .relations import register_relation
|
||||
register_relation(defn)
|
||||
|
||||
env[f"relation:{rel_name}"] = defn
|
||||
return defn
|
||||
|
||||
|
||||
_SPECIAL_FORMS: dict[str, Any] = {
|
||||
"if": _sf_if,
|
||||
"when": _sf_when,
|
||||
"cond": _sf_cond,
|
||||
"case": _sf_case,
|
||||
"and": _sf_and,
|
||||
"or": _sf_or,
|
||||
"let": _sf_let,
|
||||
"let*": _sf_let,
|
||||
"lambda": _sf_lambda,
|
||||
"fn": _sf_lambda,
|
||||
"define": _sf_define,
|
||||
"defcomp": _sf_defcomp,
|
||||
"defrelation": _sf_defrelation,
|
||||
"begin": _sf_begin,
|
||||
"do": _sf_begin,
|
||||
"quote": _sf_quote,
|
||||
"->": _sf_thread_first,
|
||||
"set!": _sf_set_bang,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Higher-order forms (need to evaluate the fn arg first)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ho_map(expr: list, env: dict) -> list:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("map requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"map requires lambda, got {type(fn).__name__}")
|
||||
return [_call_lambda(fn, [item], env) for item in coll]
|
||||
|
||||
|
||||
def _ho_map_indexed(expr: list, env: dict) -> list:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("map-indexed requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"map-indexed requires lambda, got {type(fn).__name__}")
|
||||
if len(fn.params) < 2:
|
||||
raise EvalError("map-indexed lambda needs (i item) params")
|
||||
return [_call_lambda(fn, [i, item], env) for i, item in enumerate(coll)]
|
||||
|
||||
|
||||
def _ho_filter(expr: list, env: dict) -> list:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("filter requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"filter requires lambda, got {type(fn).__name__}")
|
||||
return [item for item in coll if _call_lambda(fn, [item], env)]
|
||||
|
||||
|
||||
def _ho_reduce(expr: list, env: dict) -> Any:
|
||||
if len(expr) != 4:
|
||||
raise EvalError("reduce requires fn, init, and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
acc = _eval(expr[2], env)
|
||||
coll = _eval(expr[3], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"reduce requires lambda, got {type(fn).__name__}")
|
||||
for item in coll:
|
||||
acc = _call_lambda(fn, [acc, item], env)
|
||||
return acc
|
||||
|
||||
|
||||
def _ho_some(expr: list, env: dict) -> Any:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("some requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"some requires lambda, got {type(fn).__name__}")
|
||||
for item in coll:
|
||||
result = _call_lambda(fn, [item], env)
|
||||
if result:
|
||||
return result
|
||||
return NIL
|
||||
|
||||
|
||||
def _ho_every(expr: list, env: dict) -> bool:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("every? requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"every? requires lambda, got {type(fn).__name__}")
|
||||
for item in coll:
|
||||
if not _call_lambda(fn, [item], env):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _ho_for_each(expr: list, env: dict) -> Any:
|
||||
if len(expr) != 3:
|
||||
raise EvalError("for-each requires fn and collection")
|
||||
fn = _eval(expr[1], env)
|
||||
coll = _eval(expr[2], env)
|
||||
if not isinstance(fn, Lambda):
|
||||
raise EvalError(f"for-each requires lambda, got {type(fn).__name__}")
|
||||
for item in coll:
|
||||
_call_lambda(fn, [item], env)
|
||||
return NIL
|
||||
|
||||
|
||||
_HO_FORMS: dict[str, Any] = {
|
||||
"map": _ho_map,
|
||||
"map-indexed": _ho_map_indexed,
|
||||
"filter": _ho_filter,
|
||||
"reduce": _ho_reduce,
|
||||
"some": _ho_some,
|
||||
"every?": _ho_every,
|
||||
"for-each": _ho_for_each,
|
||||
}
|
||||
Reference in New Issue
Block a user