Level 2-3: lake morphing — server content flows through reactive islands

Lake tag (lake :id "name" children...) creates server-morphable slots
within islands. During morph, the engine enters hydrated islands and
updates data-sx-lake elements by ID while preserving surrounding
reactive DOM (signals, effects, event listeners).

Specced in .sx, bootstrapped to JS and Python:
- adapter-dom.sx: render-dom-lake, reactive-attr marks data-sx-reactive-attrs
- adapter-html.sx: render-html-lake SSR output
- adapter-sx.sx: lake serialized in wire format
- engine.sx: morph-island-children (lake-by-ID matching),
  sync-attrs skips reactive attributes
- ~sx-header uses lakes for logo and copyright
- Hegelian essay updated with lake code example

Also includes: lambda nil-padding for missing args, page env ordering fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 14:29:54 +00:00
parent d5e416e478
commit 9b9fc6b6a5
15 changed files with 351 additions and 63 deletions

View File

@@ -205,12 +205,16 @@ async def _parse_io_args(
async def _async_call_lambda(
fn: Lambda, args: list[Any], caller_env: dict[str, Any], ctx: RequestContext,
) -> Any:
if len(args) != len(fn.params):
# Too many args is an error; too few pads with nil
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
# Pad missing params with nil
for p in fn.params[len(args):]:
local[p] = None
return _AsyncThunk(fn.body, local, ctx)
@@ -1332,6 +1336,10 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
return await _aser_call(name, expr[1:], env, ctx)
return await sf(expr, env, ctx)
# Lake — serialize (server-morphable slot within island)
if name == "lake":
return await _aser_call(name, expr[1:], env, ctx)
# HTML tag — serialize (don't render to HTML)
if name in HTML_TAGS:
return await _aser_call(name, expr[1:], env, ctx)