Add signal test suite (17/17) and Island type to evaluator

test-signals.sx: 17 tests covering signal basics (create, deref, reset!,
swap!), computed (derive, update, chain), effects (run, re-run, dispose,
cleanup), batch (deferred deduped notifications), and defisland (create,
call, children).

types.py: Island dataclass mirroring Component but for reactive boundaries.
evaluator.py: sf_defisland special form, Island in call dispatch.
run.py: Signal platform primitives (make-signal, tracking context, etc)
  and native effect/computed/batch implementations that bridge Lambda
  calls across the Python↔SX boundary.
signals.sx: Updated batch to deduplicate subscribers across signals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 09:44:18 +00:00
parent a97f4c0e39
commit 26320abd64
5 changed files with 580 additions and 15 deletions

View File

@@ -33,7 +33,7 @@ from __future__ import annotations
from typing import Any
from .types import Component, Continuation, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
from .types import Component, Continuation, HandlerDef, Island, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol, _ShiftSignal
from .primitives import _PRIMITIVES
@@ -147,13 +147,13 @@ def _eval(expr: Any, env: dict[str, Any]) -> Any:
fn = _trampoline(_eval(head, env))
args = [_trampoline(_eval(a, env)) for a in expr[1:]]
if callable(fn) and not isinstance(fn, (Lambda, Component)):
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
return fn(*args)
if isinstance(fn, Lambda):
return _call_lambda(fn, args, env)
if isinstance(fn, Component):
if isinstance(fn, (Component, Island)):
return _call_component(fn, expr[1:], env)
raise EvalError(f"Not callable: {fn!r}")
@@ -555,6 +555,51 @@ def _sf_defcomp(expr: list, env: dict) -> Component:
return comp
def _sf_defisland(expr: list, env: dict) -> Island:
"""``(defisland ~name (&key ...) body)``"""
if len(expr) < 4:
raise EvalError("defisland requires name, params, and body")
name_sym = expr[1]
if not isinstance(name_sym, Symbol):
raise EvalError(f"defisland 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("defisland 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)
elif isinstance(p, str):
params.append(p)
body = expr[-1]
island = Island(
name=comp_name,
params=params,
has_children=has_children,
body=body,
closure=dict(env),
)
env[name_sym.name] = island
return island
def _defcomp_kwarg(expr: list, key: str, default: str) -> str:
"""Extract a keyword annotation from defcomp, e.g. :affinity :client."""
# Scan from index 3 to second-to-last for :key value pairs
@@ -592,7 +637,7 @@ def _sf_thread_first(expr: list, env: dict) -> Any:
else:
fn = _trampoline(_eval(form, env))
args = [result]
if callable(fn) and not isinstance(fn, (Lambda, Component)):
if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
result = fn(*args)
elif isinstance(fn, Lambda):
result = _trampoline(_call_lambda(fn, args, env))
@@ -1021,6 +1066,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
"define": _sf_define,
"defstyle": _sf_defstyle,
"defcomp": _sf_defcomp,
"defisland": _sf_defisland,
"defrelation": _sf_defrelation,
"begin": _sf_begin,
"do": _sf_begin,