Compiler:
- append to symbol → (set! target (hs-append target value))
- append to DOM → (hs-append! value target)
Runtime:
- hs-append: pure function for string concat and list append
- hs-append!: DOM insertAdjacentHTML for element targets
Mock DOM:
- dom_stringify handles List by joining elements with commas
(matching JS Array.toString() behavior)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integration:
- hs-collect-vars: scan compiled SX for set! targets, collect symbols
- hs-handler: pre-declare collected variables in closure let-bindings
so increment/decrement work on first use (variable persists across
event handler calls via closure scope)
Compiler:
- Fix emit-inc/emit-dec: use expr (variable) not tgt-override (element)
- Simplify to plain (set! x (+ x amount)) since vars are pre-declared
Mock DOM:
- Add mock console object to host-global
- Add console handler (no-op) to host-call dispatch
- Override console-log/debug/error as no-op primitives to avoid
str hitting circular refs in mock DOM elements
Fixes 4 log timeouts, 2+ increment/decrement failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser:
- Reorder toggle style parsing: target before between clause
- Handle "indexed" keyword, "indexed by" syntax
- Use parse-atom (not parse-expr) for between values to avoid
consuming "and" as boolean operator
- Support 3-4 value cycles via toggle-style-cycle
Compiler:
- Add toggle-style-cycle dispatch → hs-toggle-style-cycle!
Runtime:
- Add hs-toggle-style-between! (2-value toggle)
- Add hs-toggle-style-cycle! (N-value round-robin)
Mock DOM:
- Parse CSS strings from setAttribute "style" into style sub-dict
so dom-get-style/dom-set-style work correctly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser:
- Relax (number? v) to v in parse-one-transition so (expr)unit works
- Add (match-kw "then") before parse-cmd-list in parse-for-cmd
- Handle "indexed by" syntax alongside "index" in for loops
- Add "indexed" to hs-keywords to prevent unit-suffix consumption
Compiler:
- Use map-indexed instead of for-each for indexed for-loops
Test generator:
- Preserve \" escapes in process_hs_val via placeholder/restore
Mock DOM:
- Coerce insertAdjacentHTML values via dom_stringify (match browser)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser:
- `add VALUE to :var` → (add-value) for array append
- `remove VALUE from :var` → (remove-value) for array removal
- `toggle .foo for 10ms` → (toggle-class-for) with duration
- `append VALUE` without `to` → implicit target (it)
- `set {obj} on target` → (set-on) for object property spread
- `repeat in` body: remove spurious nil (body at index 3→2)
- Keywords followed by `(` parsed as function calls (fixes `increment()`)
Compiler:
- Handle add-value, remove-value, toggle-class-for, set-on AST nodes
- Local variables (`set :var`) use `define` instead of `set!`
Runtime:
- hs-add-to!: append value to list
- hs-remove-from!: filter value from list
- hs-set-on!: spread dict properties onto target
- `as String` for lists: comma-join (JS Array.toString compat)
Tests:
- eval-hs/eval-hs-with-me: guard for hs-return exceptions
(return compiles to raise, needs handler to extract value)
Parse errors: 20→12 (8 fixed). Remaining: 6 embedded HTML quotes
(tokenizer), 6 transition template values `(expr)px`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compiler: do-blocks containing IO commands (hs-fetch, hs-wait, perform)
are compiled as (let ((it cmd1)) (let ((it cmd2)) ...)) to chain the
it variable through IO suspensions. Non-IO do-blocks stay as plain
(do cmd1 cmd2). This enables fetch X then put it into me pattern.
Parser: then-separator handled via __then__ markers (stripped in output).
fetch URL /path parsing. Default format "text".
Runtime: hs-fetch simplified to single perform (io-fetch url format).
Test runner: mock fetch routes with format-specific responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The let/it wrapping changed semantics of ALL multi-command sequences,
breaking independent side-effect chains like (do (add-class) (add-class)).
Need a targeted approach — chain it only for then-separated commands.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compiler: do-blocks now compile to (let ((it cmd1)) (let ((it cmd2)) ...))
instead of (do cmd1 cmd2 ...). This chains the `it` variable through
command sequences, enabling `fetch X then put it into me` pattern.
Each command's result is bound to `it` for the next command.
Runtime: hs-fetch simplified to single perform (io-fetch url format)
instead of two-stage io-fetch + io-parse-text/json.
Parser: fetch URL /path handled by reading /+ident tokens.
Default fetch format changed to "text" (was "json").
Test runner: mock fetch routes with format-specific responses.
io-fetch handler returns content directly based on format param.
Fetch tests still need IO suspension to chain through let continuations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
Two hyperscript extensions beyond stock:
render ~component :key val [into|before|after target]
Tokenizer: ~ + ident → component token type
Parser: render command with kwargs and optional position
Compiler: emits (render-to-html ~comp :key val) or
(hs-put! (render-to-html ...) pos target)
Bridges hyperscript flow to SX component rendering
eval (sx-expression) — SX escape hatch
Inside eval (...), content is SX syntax (not hyperscript)
Parser: collect-sx-source extracts balanced parens from raw source
Compiler: sx-parse at compile time, inlines AST directly
Result: SX runs in handler scope — hyperscript variables visible!
Also supports string form: eval '(+ 1 2)' for backward compat
set name to "Giles"
set greeting to eval (str "Hello " name) -- name is visible!
16 new tests (parser + compiler + integration).
3127/3127 full build, zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>