Parser: skip unit suffix when next ident is a comparison keyword
(starts, ends, contains, matches, is, does, in, precedes, follows).
Fixes "123 starts with '12'" returning "123starts" instead of true.
eval-hs: use hs-compile directly instead of hs-to-sx-from-source with
"return " prefix, which was causing the parser to consume the comparison
as a string suffix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- eval-hs: new test helper that compiles+evaluates a HS expression and
returns its result. Uses hs-to-sx-from-source with "return " prefix.
- Generator now emits eval-hs calls for expression-only tests
- no suite: 4/5 pass (was 0/5)
- evalStatically: 5/8 pass (was 0/8 stubs)
- pick: 7/7 pass (was 0/7 stubs)
- mathOperator: 3/5 pass (type issues on array concat)
477/831 (57.4%), +69 from session baseline of 408.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser now handles 'does not start with' and 'does not end with'
comparison operators, compiling to (not (starts-with? ...)) and
(not (ends-with? ...)) respectively.
Test runner: host-set!/host-get stringify innerHTML/textContent.
437/831 (52.6%) — parser fix doesn't change count yet (comparison tests
use 'is a' type checks which need separate fix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
host-set! now stringifies values for innerHTML/textContent properties.
host-get returns string for innerHTML/textContent/value/className.
Fixes "Expected X, got X" type mismatch failures where number 22 != string "22".
437/831 (52.6%), +20 tests from stringify fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds attribute reference case to the 'of' branch in emit-set:
(set @bar of #div2 to "foo") now compiles to (dom-set-attr target "bar" "foo")
instead of falling through to the broken (set! (host-get ...)) catchall.
417/831 (50.2%), +2 from attr-of fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sx_vm.ml: VM timeout now compares vm_insn_count > step_limit instead of
unconditionally throwing after 65536 instructions when limit > 0
- sx_browser.ml: Expose setStepLimit/resetStepCount APIs on SxKernel;
callFn now returns {__sx_error, message} on Eval_error instead of null
- compiler.sx: emit-set handles array-index targets (host-set! instead of
nth) and 'of' property chains (dom-set-prop with chain navigation)
- hs-run-fast.js: New Node.js test runner with step-limit timeouts,
SX-level guard for error detection, insertAdjacentHTML mock,
range selection (HS_START/HS_END), wall-clock timeout in driveAsync
- hs-debug-test.js: Single-test debugger with DOM state inspection
- hs-verify.js: Assertion verification (proves pass/fail detection works)
Test results: 415/831 (50%), up from 408/831 (49%) baseline.
Fixes: set my style["color"], set X of Y, put at end of (insertAdjacentHTML).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When innerHTML is set on a mock element, textContent now updates to
match (with HTML tags stripped). Many HS tests do `put "foo" into me`
(which sets innerHTML) then check textContent. Previously textContent
stayed empty because only innerHTML was updated.
Also fixes innerHTML="" to fully detach children from parent.
393 → 408/831 HS tests (+15).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setting innerHTML="" on a mock element now detaches and removes all
children, matching browser behavior. Previously hs-cleanup! (which
sets body.innerHTML="") left stale children attached, causing
querySelector to find elements from prior tests.
Also clears children when textContent is set (browser behavior).
375 → 393/831 HS tests (+18).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In a real browser, innerHTML/textContent/value are always strings.
The mock was storing raw SX values (Number, Bool, Nil), causing type
mismatches like "Expected 1, got 1" where the value was correct but
Number 1.0 != String "1".
Now coerces to string on host-set! for innerHTML, textContent, value,
outerHTML, innerText. Fixes 10 increment tests that were doing
`put value into me` with numeric results.
367 → 375/831 HS tests (+8 net, +10 new passes, -2 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The infinite loops in the HS parser are in transpiled native OCaml code,
not in the VM or CEK step loop. Neither step counters (in cek_step_loop,
cek_step, trampoline) nor VM instruction checks caught them because
the loops are in direct OCaml recursion.
Fix: SIGALRM handler raises Eval_error to break out of native loops.
Also sets step_limit flag to catch VM loops. Combined approach handles
both native OCaml recursion (alarm+raise) and VM bytecode (step check).
The alarm+raise can become unreliable after ~13 timeouts in a single
process, but handles the common case well. Reverts the fork-based
approach which lost inter-test state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two critical fixes for the mock DOM test runner:
1. host-get returns truthy for DOM method names on mock elements.
dom.sx guards like `(and el (host-get el "setAttribute"))` were
silently skipping setAttribute/getAttribute calls because the mock
dict had no "setAttribute" key. Now returns Bool true for known
DOM method names, fixing hs-activate! → dom-set-attr → dom-get-attr
chain. Also adds firstElementChild, nextElementSibling, etc. as
computed properties.
2. Fork-based per-test timeout (5 seconds). The HS parser has infinite
loops on certain syntax ([@attr], complex put targets). Signal-based
alarm doesn't work reliably in OCaml 5. Fork + waitpid + select
gives hard OS-level timeout protection.
Also adds step_limit/step_count to sx_ref.ml trampoline (currently
unused but available for future CEK-level timeout).
Result: 525/963 total, up from 498. Many more add/remove/toggle/set
tests now pass because hs-activate! actually wires up event handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the monolithic 500KB <script data-components> block with a 25KB
JSON manifest mapping names to content hashes. Every definition —
components, islands, macros, client libraries, bytecode modules, and
WASM binaries — is now content-addressed and loaded on demand.
Server (sx_server.ml):
- build_hash_index: Merkle DAG over all definitions — topological sort,
hash leaves first, component refs become @h:{hash} in instantiated form
- /sx/h/{hash} endpoint: serves definitions with Cache-Control: immutable
- Per-page manifest in <script data-sx-manifest> with defs + modules + boot
- Client library .sx files hashed as whole units (tw.sx, tw-layout.sx, etc.)
- .sxbc modules and WASM kernel hashed individually
Browser (sx-platform.js):
- Content-addressed boot: inline script loads kernel + platform by hash
- loadDefinitionByHash: recursive dep resolution with @h: rewriting
- resolveHash: 3-tier cache (memory → localStorage → fetch /sx/h/{hash})
- __resolve-symbol extended for manifest-based component + library loading
- Cache API wrapper intercepts .wasm fetches for offline caching
- Eager pre-loading of plain symbol deps for CEK evaluator compatibility
Shell template (shell.sx):
- Monolithic <script data-components> removed
- data-sx-manifest script with full hash manifest
- Inline bootstrap replaces <script src="...?v="> with CID-based loading
Second visit loads zero bytes from network. Changed content gets a new
hash — only that item refetched (Merkle propagation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add mock DOM layer to run_tests.ml so hyperscript behavioral tests
(spec/tests/test-hyperscript-behavioral.sx) can run in the OCaml test
runner without a browser. Previously these tests required Playwright
which crashed after 10 minutes from WASM page reboots.
Mock DOM implementation:
- host-global, host-get, host-set!, host-call, host-new, host-callback,
host-typeof, host-await — OCaml primitives operating on SX Dict elements
- Mock elements with classList, style, attributes, event dispatch + bubbling
- querySelector/querySelectorAll with #id, .class, tag, [attr] selectors
- Load web/lib/dom.sx and web/lib/browser.sx for dom-* wrappers
- eval-hs function for expression-only tests (comparisonOperator, etc.)
Result: 367/831 HS tests pass in ~30 seconds (was: Playwright crash).
14 suites at 100%: live, component, liveTemplate, scroll, call, go,
focus, log, reactive-properties, resize, measure, attributeRef,
objectLiteral, queryRef.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- dom-visible?: check element display != none (web/lib/dom.sx)
- json-stringify: JSON.stringify via host-call (web/lib/browser.sx)
- hs-coerce Boolean: use hs-falsy? for JS-compatible truthiness
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The effect form returns a VM closure (disposer) which the island DOM
renderer displayed as text. Moving it to a let binding (_eff) captures
the return value without rendering it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
as Boolean now uses hs-falsy? for JS-compatible truthiness (0, "", nil, false → false)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parse `is X ignoring case` → (eq-ignore-case left right)
- Parse `is not X ignoring case` → (not (eq-ignore-case left right))
- Compiler: eq-ignore-case → hs-eq-ignore-case
- Runtime: hs-eq-ignore-case using downcase/str
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>