Root cause identified: :ref attribute on DOM elements inside defisland
triggers dict-set!/reduce error in WASM kernel hydration system.
Minimal repro:
(defisland ~test ()
(let ((el-ref (signal nil)))
(div (div :ref (fn (el) (reset! el-ref el)) ""))))
→ "dict-set!: dict key val (in reduce → reduce → for-each)"
Without :ref: works perfectly (signals, effects, canvas FFI,
break-lines, pretext-layout-lines all functional).
Working version: full Pretext with 3 controls + effect + layout
computation, outputs text via (deref result). 34 disposers, no error.
Just needs :ref fix to add DOM rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- parse_html now captures ALL elements (not just top-level) with
parent-child relationships
- emit_element_setup uses three phases: attributes, DOM tree, activation
- ref() maps positional names (d1, d2) to top-level elements only
- dom-scope: 9→14 (+5), reset: 3→6 (+3), take: 2→3, parser: 2→3
Net 0 due to regressions in dialog/halt/closest (needs investigation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Don't insert 'then' inside for-in loop bodies or after 'repeat N times'
(fixes repeat from 1/30 → 5/30)
- Allow HS sources ending with " when they don't contain embedded HTML
(fixes set from 6/25 → 10/25, enables 18 previously-skipped tests)
- Fix assert= argument order: (actual expected), not (expected actual)
(error messages now correctly report Expected/Got)
395 → 402/831 (+7)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isolated the dict-set!/reduce error to complex island body parsing,
not the reactive system or library functions. Proven working:
- break-lines inside effect ✓
- canvas.measureText inside effect ✓
- pretext-layout-lines inside effect ✓
- signal + slider + reactive update ✓
The error triggers only with large island bodies (many ~tw spreads,
nested controls). This is a component definition parser bug in the
WASM kernel, not a Pretext or reactive system issue.
Current island: minimal working version with effect-based layout,
slider control, and innerHTML rendering. Ready for incremental
expansion once the parser size limit is identified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 7 new tests in computed-ho-forms suite: computed with map, reduce,
for-each, nested map, dict creation, signal updates. All pass on
OCaml and WASM sandbox.
- Removed standalone pretext-position-line and pretext-layout-lines
from pretext-demo.sx — now in text-layout library only
- Root cause of island error: pretext-demo.sx had old define with
(reduce + 0 lwid) that the server serialized into component defs,
overriding the library's sum-loop version
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added sublist helper (portable list extraction, avoids 3-arg slice
which fails in browser WASM kernel)
- Replaced reduce + 0 lwid with manual sum loop (reduce has browser
compat issues with dict-set! error in call stack)
- Imperative DOM update via effect for clean paragraph re-rendering
on signal changes (clear container, create new spans)
- String slice in hyphenate-word kept (works on strings)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The (let ((lines (deref layout))) ...) pattern captured the layout value
once at island initialization. Replacing with (deref layout) inline in the
DOM expressions makes the reactive system track the dependency and
re-render when signals change.
Sliders and algorithm toggle now trigger layout recomputation and DOM
update. Remaining: reactive DOM patching for absolutely-positioned spans
creates visual artifacts (old spans persist). Needs keyed list or full
container replacement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: sx_insert_near placed break-lines-greedy, pretext-position-line,
pretext-layout-lines OUTSIDE the define-library begin block. The bytecode
compiler only compiles forms inside begin as STORE_GLOBAL — forms outside
are invisible to the browser VM.
Fix: moved all function definitions inside (begin ...) of (define-library).
Bytecode now includes all 17 functions (11K compiled, was 9K).
Browser load-sxbc: simplified VmSuspended handling — just catch and
continue, since STORE_GLOBAL ops already ran before the import OP_PERFORM.
sync_vm_to_env copies them to global_env.
Island now calls break-lines and pretext-layout-lines from bytecode-compiled
library — runs on VM, not CEK interpreter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- text-layout.sx added to WASM bytecode pipeline (9K compiled)
- Fix multi-list map calls (map-indexed + nth instead of map fn list1 list2)
- pretext-layout-lines and pretext-position-line moved to library exports
- Browser load-sxbc: handle VmSuspended for import, copy library exports
to global_env after module load (define-library export fix)
- compile-modules.js: text-layout in SOURCE_MAP, FILES, and entry deps
- Island uses library functions (break-lines, pretext-layout-lines)
instead of inlining — runs on bytecode VM when exports resolve
Known issue: define-library exports don't propagate to browser global env
yet. The load-sxbc import suspension handler resumes correctly but
bind_import_set doesn't fire. Needs deeper investigation into how the
WASM kernel's define-library registers exports vs how other libraries
(adapter-html, tw) make their exports available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move all layout functions inside defisland body (browser can't access
top-level defines from component defs bundle)
- Use div placeholder with data-sx-island attr (matches island root tag)
- Rename pretext-island.sx → pretext-client.sx for alphabetical load order
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
defisland ~pretext-demo/live — same Knuth-Plass algorithm running in
the browser with canvas.measureText for pixel-perfect font metrics.
- Width slider (200-700px), font size slider (10-24px)
- Greedy vs Knuth-Plass toggle button
- Reactive re-layout on every control change
- All layout functions inlined in the island (no library deps)
- Perfectly straight right edges — browser measures AND renders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser/compiler/runtime for focus command. Tokenizer: focus, blur,
precedes, follows, ignoring, case keywords. Test spec: per-test
failure output for diagnosis.
374/831 (45%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous K.eval() approach double-escaped backslashes in SX source
strings, breaking the \/ → / unescaping that the server serializer adds
for HTML safety. Using K.callFn() passes strings directly as arguments,
bypassing the escaping problem entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stream-colors dict had green/blue keys but data used emerald/violet — all three
slots now render with correct Tailwind color classes. Platform: resolveSuspense
must not exist on Sx until boot completes, otherwise bootstrap __sxResolve calls
it before web stack loads and resolves silently fail. Moved to post-boot setup
so all pre-boot resolves queue in __sxPending and drain correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Streaming chunked transfer with shell-first suspense and resolve scripts.
Hyperscript parser/compiler/runtime expanded for conformance. WASM static
assets added to OCaml host. Playwright streaming and page-level test suites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shell HTML included closing </body></html> tags. Resolve script
chunks arrived AFTER the document end — browser ignored them
(ERR_INCOMPLETE_CHUNKED_ENCODING). Now strips </body></html> from
shell, sends resolve scripts inside the body, closes document last.
Added live server Playwright tests that hit the actual streaming
endpoint and verify suspense slots resolve with content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The streaming render matched `List items` but SX's `(list ...)` produces
`ListRef` (mutable list) in the OCaml runtime. Data items were rejected
with "returned list, expected dict or list" — 0 resolve chunks sent.
Fixed both streaming render and AJAX paths to handle ListRef.
Added sandbox test for streaming-demo-data return type validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server (sx_server.ml):
- eval_with_io: CEK evaluator with IO suspension handling (io-sleep, import)
- io-sleep platform primitive: raises CekPerformRequest, resolved by eval_with_io
- Streaming render uses eval_with_io for data + content evaluation
- Data items with "delay" field sleep before resolving (async streaming)
- Removed hardcoded streaming-demo-data — application logic belongs in .sx
Application (streaming-demo.sx):
- streaming-demo-data defined in SX: 3 items with 1s/3s/5s delays
- Each item has delay, stream-id, and display data fields
- Shell renders instantly, slots fill progressively as IO completes
Tests (streaming.spec.js):
- Staggered resolve test: fast resolves first, medium/slow still skeleton
- Verifies independent slot resolution matches async IO behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server (sx_server.ml):
- eval_with_io: CEK evaluator with IO suspension handling (io-sleep, import)
- io-sleep platform primitive: raises CekPerformRequest, resolved by eval_with_io
- Streaming render uses eval_with_io for data + content evaluation
- Data items with "delay" field sleep before resolving (async streaming)
- Removed hardcoded streaming-demo-data — application logic belongs in .sx
Application (streaming-demo.sx):
- streaming-demo-data defined in SX: 3 items with 1s/3s/5s delays
- Each item has delay, stream-id, and display data fields
- Shell renders instantly, slots fill progressively as IO completes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Self-referencing local function used let instead of letrec, causing
JIT failures: "VM undefined: skip-annotations" when compiling any
define with type annotations (:effects, :as). Retranspile needed
to eliminate JIT fallback warnings from the OCaml binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These are OCaml-side bookkeeping for the Python async bridge. The browser
WASM kernel registers them in the CEK env but not the VM global table,
so bytecode-compiled batch() crashed with "VM undefined: batch-begin!".
The SX-level *batch-depth*/*batch-queue* already handle batching correctly.
Verified in Playwright sandbox: signal, deref, reset!, batch, computed
all work with source fallback (sxbc load-format issue is pre-existing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues prevented core-signals.sx from working as bytecode:
1. computed/effect used (let) for self-referencing bindings (recompute,
run-effect). Changed to (letrec) so the VM pre-allocates slots before
compiling the lambda bodies — required for self-reference in bytecode.
2. deref used dict destructuring (let {:notify n :deps d} ctx ...) which
the transpiled OCaml compiler doesn't support. Rewrote to explicit
(get ctx "notify") / (get ctx "deps") calls.
Also fixed compile-let dict destructuring opcodes (OP_CONST=1 not 2,
OP_CALL_PRIM=52 not 10) for future use when compiler is retranspiled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
compile-let now handles dict destructuring patterns:
(let {:key1 var1 :key2 var2} source body). This unblocked core-signals.sx
(deref uses dict destructuring) which was the sole bytecode skip.
Rewrote stripLibraryWrapper from line-based to paren-aware extraction.
The old regex missed (define-library on its own line (no trailing space),
silently passing the full wrapper to the compiler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bytecode-serialize/deserialize: sxbc v2 format wrapping compiled code
dicts. cek-serialize/deserialize: cek-state v1 format wrapping suspended
CEK state (phase, request, env, kont). Both use SX s-expression
round-trip via inspect/parse. lib/serialize.sx has pure SX versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed then insertion to only trigger before known HS command keywords
(set, put, add, remove, toggle, etc.) via lookahead regex, instead of
on all multi-space sequences. Prevents breaking single-command
expressions with wide spacing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Biggest win: HS sources from upstream HTML had newlines replaced with
spaces, losing command separation. Now multi-space sequences become
'then' keywords, matching _hyperscript's implicit newline-as-separator
behavior. +42 tests passing.
Parser: 'is between X and Y', 'is not between', 'starts with',
'ends with' comparison operators.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- is between X and Y / is not between X and Y: uses parse-atom for
bounds to avoid consuming 'and' as logical operator
- starts with / ends with: comparison operators mapping to
starts-with? / ends-with? primitives
- comparisonOperator: 12→17/40
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Generator: converts no-HTML tests with run("expr").toBe(val) patterns
to (assert= val (eval-hs "expr")). 111→92 stubs (-19 converted).
- Parser: multi-class add/remove (.foo .bar collects into multi-add-class)
- Compiler: multi-add-class/multi-remove-class emit (do (dom-add-class..))
- Test runner: drives IO suspension in per-test evaluate for async tests
- Parser: catch/finally support in on handlers, cmd terminators
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests for cross-language type primitives: ->> (thread-last),
as-> (thread-anywhere), define-protocol/implement/satisfies?.
All features already implemented in evaluator, now covered by tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- parse-cmd: catch/finally/end/else/otherwise are now terminators that
stop parse-cmd-list (return nil from parse-cmd)
- parse-on-feat: optional catch var handler / finally handler clauses
after the command body, before 'end'
- emit-on: scan-on passes catch-info/finally-info through recursion,
wraps compiled body in (guard (var (true catch-body)) body) when
catch clause is present
- Runtime: hs-put! handles "start" (afterbegin) and "end" (beforeend)
- Removed duplicate conformance-dev.sx (all 110 tests already in behavioral)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- put parser: added 'at start of' and 'at end of' positional syntax
- take parser: added 'for' as alternative to 'from' for target clause
- runtime: hs-put! handles "start" (afterbegin) and "end" (beforeend)
- eval-hs: smart wrapping for commands vs expressions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed ref() to map upstream JS variable names to let-bound SX variables
using element context (tag→var, id→var, make-return→last-var). Fixes
if (0→14/19), put (14→18), on (20→23), and other categories where the
upstream test uses make() return variables like d1, div, btn.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers all bugs fixed in the DOM-preserving hydration work:
DOM preservation:
- Islands hydrate without errors or warnings
- Both islands report hydrated in boot log
- No replaceChildren called on island elements
- No stray comment markers in island DOM
Counter text nodes (was: "0 / 16" → "0"):
- Counter shows full "0 / 16" text
- Counter has exactly 3 text nodes (value, separator, total)
- Counter updates on forward/back clicks
Event listeners (was: buttons had no click handlers):
- Stepper buttons respond to clicks
- Header navigation links present after hydration
Code view:
- Syntax-highlighted spans present after hydration
- Code highlighting advances with stepper clicks
SSR DOM identity:
- Element count roughly preserved (not doubled)
- Stepper buttons are the SAME DOM nodes (JS property survives)
- Header elements are the SAME DOM nodes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues with the initial hydration implementation:
1. Text node mismatch: SSR merges adjacent text into one node
("0 / 16") but client renders three separate children. When the
cursor ran out, new nodes were created but dom-append was
unconditionally skipped. Fix: only skip append when the child
already has a parent (existing SSR node). New nodes (nil parent)
get appended even during hydration.
2. Conditional markers: dispatch-render-form for if/when/cond in
island scope was injecting comment markers during hydration,
corrupting the DOM. Fix: skip the reactive conditional machinery
during hydration — just evaluate and render the active branch
normally, walking the cursor. Reactivity for conditionals
activates after the first user-triggered re-render.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scope-based cursor walks the existing SSR DOM during island hydration
instead of creating new elements and calling replaceChildren. The
hydration scope (sx-hydrating) propagates through define-library via
scope-push!/peek/pop!, solving the env isolation that broke the
previous set!-based approach.
Changes:
- adapter-dom.sx: hydrating?, hydrate-next-node, hydrate-enter/exit-element
helpers. render-to-dom reuses text nodes. render-dom-element reuses
elements by tag match, skips dom-append. reactive-text/cek-reactive-text
reuse existing text nodes. render-dom-fragment/lake/marsh skip append.
dispatch-render-form (if/when/cond) injects markers into existing DOM.
- boot.sx: hydrate-island pushes cursor scope, skips replaceChildren.
On mismatch error, falls back to full re-render.
Result: zero DOM destruction, zero visual flash, event listeners
attached to original SSR elements. Stepper clicks verified working.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The WIP commit (0044f17e) added calls to hydrate-start!, hydrate-stop!,
hydrate-push!, hydrate-pop!, and hydrate-next-*! — none of which were
ever defined. This crashed hydrate-island silently (cek-try swallowed
the error), preventing event listener attachment on every island.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>