- return compiles to (raise (list "hs-return" value)) instead of
silently discarding the return keyword
- def wraps function body in guard that catches hs-return exceptions,
enabling early exit from repeat-forever loops via return
- def params correctly extract name from (ref name) AST nodes
Note: IO suspension kernel changes reduced baseline from 519→487.
The HS parser/compiler/runtime fixes are all intact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Compiler: return compiles to (raise (list "hs-return" value))
- Compiler: def wraps body in guard to catch hs-return exceptions
- Compiler: def params extract name from (ref name) nodes
- Test generator: extract <script type="text/hyperscript"> blocks
and compile def functions as setup before tests
- Test generator: add eval-hs-with-me for {me: N} opts
The return mechanism enables repeat-forever with early exit via return.
Direct SX guard/raise works (returns correct value), but the compiled
HS repeat-forever thunk body needs further debugging for full coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
parse-poss-tail now handles style token type after 's operator.
#div2's *color, #foo's *width etc. now correctly produce
(style prop owner) AST which compiles to dom-set/get-style.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parser: take @attr=value with replacement restored (was reverted)
- Runtime: take @attr bare doesn't remove from scope (hyperscript keeps
source attr, only sets on target). Only take @attr=val with replacement
modifies scope elements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parser: multi-property transition (width from 0px to 100px height from...)
with collect-transitions loop. CSS value parsing uses parse-atom + manual
number+unit concat to avoid greedy string-postfix chaining.
- Compiler: take! passes attr-val and with-val (restored from revert)
- Runtime: hs-empty-target! handles FORM by iterating child inputs,
hs-starts-with-ic/hs-ends-with-ic for case-insensitive comparison
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- htmx-boot-subtree! wired into process-elements for auto-activation
- Fixed cond compilation bug in hx-verb-info (Clojure-style flat cond)
- Platform io-fetch upgraded: method/body/headers support, full response dict
- Replaced perform IO ops with browser primitives (set-timeout, browser-confirm, etc)
- SX→HTML rendering in hx-do-swap with OOB section filtering
- hx-collect-params: collects input name/value for all methods
- Handler naming: ex-{slug} convention, removed perform IO dependencies
- Test runner page at (test.(applications.(htmx))) with iframe-based runner
- Header "test" link on every page linking to test URL
- Page file restructure: 285 files moved to URL-matching paths (a/b/c/index.sx)
- page-functions.sx: ~100 component name references updated
- _test added to skip_dirs, test- file prefix convention for test files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
- 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>
- sx_browser.ml: restore VmSuspended handler in api_call_fn with
make_js_callFn_suspension for IO suspension chains (wait, fetch)
- runtime.sx: delete host-get stub that shadowed platform native —
hs-toggle-class! now uses real FFI host-get for classList access
All three live demo examples work:
Toggle Color — classList.toggle on click
Bounce — add .animate-bounce, wait 1s suspend, remove
Count Clicks — increment @data-count, put into innerHTML
4/4 bytecode regression tests pass (was 0/4 without VmSuspended).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs fixed:
1. host-get in sx-platform.js: return true for function-valued properties
so dom-get-attr/dom-set-attr guards pass (functions can't cross WASM boundary)
2. hs-runtime.sx: renamed host-get→hs-host-get and dom-query→hs-dom-query to
stop shadowing platform natives when loaded as .sx source
3. compile-modules.js: HS dependency chain (integration→runtime→compiler→parser→tokenizer)
so lazy loading pulls in all deps. Non-library modules load as .sx source
for CEK env visibility.
Result: 8/8 elements activate, hs-on attaches listeners. Click handler needs
IO suspension support (VmSuspended in sx_browser.ml) to fire — next step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- orchestration.sx: add hs-boot-subtree! call to process-elements
- integration.sx: remove load-library! calls (browser loads via manifest)
- sx_vm.ml: add __resolve-symbol hook to OP_GLOBAL_GET for lazy loading
- compile-modules.js: add HS modules as lazy_deps in manifest
HS compilation works in browser (tokenize→parse→compile verified).
Activation pipeline partially working — hs-activate! needs debugging
(dom-get-data/dom-set-data interaction with WASM host-get on functions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reset to last known-good state (908f4f80) where links, stepper, and
islands all work, then recovered all hyperscript implementation,
conformance tests, behavioral tests, Playwright specs, site sandbox,
IO-aware server loading, and upstream test suite from f271c88a.
Excludes runtime changes (VM resolve hook, VmSuspended browser handler,
sx_ref.ml guard recovery) that need careful re-integration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Compiler match for type-check-strict was still using old name type-check!
- Deploy updated HS source files to shared/static/wasm/sx/
- Sandbox runner validates 16/16 hard cases pass with cek-eval
(no runtime let-binding hacks needed in WASM context)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser:
- null-literal: null/undefined produce (null-literal) AST, not bare nil
- is a/an String!: check ! as next token, not suffix in string
- type-check! renamed to type-check-strict (! in symbol names)
Compiler:
- the first/last of: emit hs-first/hs-last instead of (get x "first")
- empty? dispatch: match parser-emitted empty?, emit hs-empty?
- modulo: emit modulo instead of % symbol
Runtime:
- hs-contains?: recursive implementation (avoids some primitive)
- hs-empty?: len-based checks (avoids empty? primitive in tree-walker)
- hs-falsy?: handles empty lists and zero
- hs-first/hs-last: wrappers for tree-walker context
- hs-type-check-strict: renamed from hs-type-check!
Test infrastructure:
- eval-hs: try-call wraps both compile AND eval steps
- Mutable _hs-result captures value through try-call boundary
- Removed DOM-dependent fixtures that cause uncatchable OCaml crashes
(selectors <body/>, .class refs in exists/empty tests)
Scorecard: 62/109 tests passing (55%), up from 57/112.
3 fixtures removed (DOM-only crashers), net +5 passing tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The when form's continuation for the second body expression was lost
across perform/cek_resume cycles. Wrapping (thunk) and (do-repeat)
in an explicit (do ...) gives when a single body, and do's own
continuation handles the sequencing correctly.
Sandbox confirms: 6/6 io-sleep suspensions now chain through
host-callback → _driveAsync → resume_vm (was 1/6 before fix).
Also fix sandbox async timing: _asyncPending counter tracks in-flight
IO chains so page.evaluate waits for all resumes to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New sx_playwright mode="sandbox" — injects the WASM kernel into about:blank
with full FFI, IO suspension tracing, and real DOM. No server needed.
Predefined stacks: core (kernel only), web (full web stack), hs (+ hyperscript),
test (+ test framework). Custom files and setup expressions supported.
Reproduces the host-callback IO suspension bug: direct callFn chains 6/6
suspensions correctly, but host-callback → addEventListener → _driveAsync
only completes 1/6. Bug is in the _driveAsync resume chain context.
Also: debug.sx mock DOM harness, test_hs_repeat.js Node.js reproduction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sx_eval now accepts files (smart-loaded by mtime — unchanged files skip),
trace_io (harness-wrapped IO capture), mock (evaluated platform overrides),
and setup params. Definitions survive between calls. sx_harness_eval also
uses smart loading. sx_write_file can create new files.
New lib/hyperscript/debug.sx: mock DOM platform for instant hyperscript
testing — compile and execute HS expressions against simulated elements,
see every DOM mutation and wait in the IO trace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runtime visibility fix:
- eval-hs now injects runtime helpers (hs-add, hs-falsy?, hs-strict-eq,
hs-type-check, hs-matches?, hs-contains?, hs-coerce) via outer let
binding so the tree-walker evaluator can resolve them
Parser fixes:
- null/undefined: return (null-literal) AST node instead of bare nil
(nil was indistinguishable from "no parse result" sentinel)
- === / !== tokenized as single 3-char operators
- mod operator: emit (modulo) instead of (%) — modulo is a real primitive
Compiler fixes:
- null-literal → nil
- % → modulo
- contains? → hs-contains? (avoids tree-walker primitive arity conflict)
Runtime additions:
- hs-contains?: wraps list membership + string containment
Tokenizer:
- Added keywords: a, an (removed — broke all tokenization), exist
- Triple operators: === and !== now tokenized correctly
Scorecard: 54/112 test groups passing, +23 from baseline.
Unlocked: really-equals, english comparisons, is-in, null is empty,
null exists, type checks, strict equality, mod.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract pure expression tests from the official _hyperscript test suite
and implement parser/compiler/runtime extensions to pass them.
Test infrastructure:
- 222 fixtures extracted from evalHyperScript calls (no DOM dependency)
- SX data format with eval-hs bridge and run-hs-fixture runner
- 24 suites covering expressions, comparisons, coercion, logic, etc.
Parser extensions (parser.sx):
- mod as infix arithmetic operator
- English comparison phrases (is less than, is greater than or equal to)
- is a/an Type typecheck syntax
- === / !== strict equality operators
- I as me synonym, am as is for comparisons
- does not exist/match/contain postfix
- some/every ... with quantifier expressions
- undefined keyword → nil
Compiler updates (compiler.sx):
- + emits hs-add (type-dispatching: string concat or numeric add)
- no emits hs-falsy? (HS truthiness: empty string is falsy)
- matches? emits hs-matches? (string regex in non-DOM context)
- New cases: not-in?, in?, type-check, strict-eq, some, every
Runtime additions (runtime.sx):
- hs-coerce: Int/Integer truncation via floor
- hs-add: string concat when either operand is string
- hs-falsy?: HS-compatible truthiness (nil, false, "" are falsy)
- hs-matches?: string pattern matching
- hs-type-check/hs-type-check!: lenient/strict type checking
- hs-strict-eq: type + value equality
Tokenizer (tokenizer.sx):
- Added keywords: I, am, does, some, mod, equal, equals, really,
include, includes, contain, undefined, exist
Scorecard: 47/112 test groups passing. 0 non-HS regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>