Runtime (lib/hyperscript/ + shared/static/wasm/sx/hs-*.sx):
- make: parser accepts `<tag.class#id/>` selectors and `from <expr>,…`; compiler
emits via scoped-set so `called <name>` persists; `called $X` lands on
window; runtime dispatches element vs host-new constructor by type.
- Values: `x as Values` walks form inputs/selects/textareas, producing
{name: value | [value,…]}; duplicates promote to array; multi-select and
checkbox/radio handled.
- toggle *display/*visibility/*opacity: paired with sensible inline defaults
in the mock DOM so toggle flips block/visible/1 ↔ none/hidden/0.
- add/remove/put at array: emit-set paths route list mutations back through
the scoped binding; add hs-put-at! / hs-splice-at! / hs-dict-without.
- remove OBJ.KEY / KEY of OBJ: rebuild dict via hs-dict-without and reassign,
since SX dicts are copy-on-read across the bridge.
- dom-set-data: use (host-new "Object") rather than (dict) so element-local
storage actually persists between reads.
- fetch: hs-fetch normalizes JSON/Object/Text/Response format aliases;
compiler sets `the-result` when wrapping a fetch in the `let ((it …))`
chain, and __get-cmd shares one evaluation via __hs-g.
Mock DOM (tests/hs-run-filtered.js):
- parseHTMLFragments accepts void elements (<input>, <br>, …);
- setAttribute tracks name/type/checked/selected/multiple;
- select.options populated on appendChild;
- insertAdjacentHTML parses fragments and inserts real El children into the
parent so HS-activated handlers attach.
Generator (tests/playwright/generate-sx-tests.py):
- process_hs_val strips `//` / `--` line comments before newline→then
collapse, and strips spurious `then` before else/end/catch/finally.
- parse_dev_body interleaves window-setup ops and DOM resets between
actions/assertions; pre-html setups still emit up front.
- generate_test_pw compiles any `<script type=text/hyperscript>` (flattened
across JS string-concat) under guard, exposing def blocks.
- Ordered ops for `run()`-style tests check window.obj.prop via new
_js_window_expr_to_sx; add DOM-constructing evaluate + _hyperscript
pattern for `as Values` tests (result.key[i].toBe(…)).
- js_val_to_sx handles backticks and escapes embedded quotes.
Net delta across suites:
- if 16→18, make 0→8, toggle 12→21, add 9→10, remove 11→16, put 29→31,
fetch 11→15, repeat 14→26, expressions/asExpression 20→25, set 27→28,
core/scoping 12→14, when 39→39 (no regression).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last commit's `hs-type-check` rewrite collapsed predicate and assertion
into one runtime fn that always raised on mismatch. That fixed `: Type`
but broke `is a Type` / `is not a Type` (which need a bool):
null is a String expected true, got nil (raised)
null is not a String expected false, got true (default boolean)
Restored the split. Parser now emits `(type-assert ...)` for `:` and
keeps `(type-check ...)` for `is a` / `is not a`. Runtime adds:
- `hs-type-check` — predicate, never raises (nil passes)
- `hs-type-check-strict` — predicate, false on nil
- `hs-type-assert` — value or raises
- `hs-type-assert-strict` — value or raises (also raises on nil)
Compiler maps `type-assert` / `type-assert-strict` to the new runtime fns.
comparisonOperator 74/83 → 79/83 (+5: `is a/an`, `is not a/an` four tests
plus a fifth that depended on them). typecheck stays 2/5 (no regression).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`set element x to 10` was compiling to `(set! (string-postfix (ref "element") "x") 10)`
because parse-expr greedily consumed `element x` as a string-postfix expression.
Recognise the bare `element` / `global` / `local` ident at the start of the
set target and skip it so `tgt` parses as just `x`. The variable lives in
the closure scope of the handler — close enough for handler-local use; a
real per-element store would need extra work in the compiler.
core/scoping: 9/20 → 12/20 (+3): "element scoped variables work",
"element scoped variables span features", "global scoped variables work".
The `:x` / `$x` short-syntax variants still fail because their listeners
aren't registering in the test mock — separate issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`'foo' : String` and `'foo' : String!` were returning `true` because
`hs-type-check` was a predicate. Per upstream hyperscript semantics,
`value : Type` is a type-asserted pass-through:
- nil passes the basic check (use `Type!` for non-null)
- mismatched type → raise "Typecheck failed!"
- match → return the original value
`hs-type-check-strict` now also raises on nil rather than returning
false, so the `String!` form actually rejects null.
hs-upstream-expressions/typecheck: 0/5 → 2/5.
asExpression unchanged (uses different `as Type` runtime path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser (lib/hyperscript/parser.sx):
- parse-poss case for "(" (function call) was building (call ...) and
returning without recursing, so `f().x` lost the `.x` suffix and the
compiler emitted (let ((it (f))) (hs-query-first ".x")). Now it tail-
calls parse-poss on the constructed call so chains like f().x.y(),
obj.method().prop, etc. parse correctly.
Generator (tests/playwright/generate-sx-tests.py):
- New js_expr_to_sx: translates arrow functions ((args) => body), object
literals, simple property access / method calls / arith. Falls back
through js_val_to_sx for primitives.
- New extract_window_setups: scans `evaluate(() => { window.X = Y })`
blocks (with balanced-brace inner-body extraction) and returns
(name, sx_value) pairs.
- Pattern 1 / Pattern 2 in generate_eval_only_test merge those window
setups into the locals passed to eval-hs-locals, so HS expressions
can reference globals defined by the test prelude.
- Object literal value parsing now goes through js_expr_to_sx first,
so `{x: x, y: y}` yields `{:x x :y y}` (was `{:x "x" :y "y"}`).
Net: hs-upstream-expressions/functionCalls 0/12 → 5/12 (+5).
Smoke-checked put/set/scoping/possessiveExpression — no regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generator changes (tests/playwright/generate-sx-tests.py):
- toHaveCSS regex: balance parens so `'rgb(255, 0, 0)'` is captured intact
(was truncating at first `)`)
- Map browser-computed colors `rgb(R,G,B)` back to CSS keywords
(red/green/blue/black/white) — our DOM mock returns the inline value
- js_val_to_sx now handles object literals `{a: 1, b: {c: 2}}` → `{:a 1 :b {:c 2}}`
- Pattern 2 (`var x = await run(...)`) now captures locals via balanced-brace
scan and emits `eval-hs-locals` instead of `eval-hs`
- Pattern 1 with locals: emit `eval-hs-locals` (was wrapping in `let`, which
doesn't reach the inner HS env)
- Stop collapsing `\"` → `"` in raw HTML (line 218): the backslash escapes
are legitimate in single-quoted `_='...'` HS attribute values containing
nested HS scripts
Test-framework changes (regenerated into spec/tests/test-hyperscript-behavioral.sx):
- `_hs-wrap-body`: returns expression value if non-nil, else `it`. Lets bare
expressions (`foo.foo`) and `it`-mutating scripts (`pick first 3 of arr;
set $test to it`) both round-trip through the same wrapper
- `eval-hs-locals` now injects locals via `(let ((name (quote val)) ...) sx)`
rather than `apply handler (cons nil vals)` — works around a JIT loop on
some compiled forms (e.g. `bar.doh of foo` with undefined `bar`)
Also synced lib/hyperscript/*.sx → shared/static/wasm/sx/hs-*.sx (the WASM
test runner reads from the wasm/sx/ copies).
Net per-cluster pass counts (vs prior baseline):
- put: 23 → 29 (+6)
- set: 21 → 28 (+7)
- show: 7 → 15 (+8)
- expressions/propertyAccess: 3 → 9 (+6)
- expressions/possessiveExpression: 17 → 18 (+1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- parser `empty` no-target → (ref "me") (was bogus (sym "me"))
- parser `halt` modes distinguish: "all"/"bubbling"/"default" halt execution
(raise hs-return), "the-event"/"the event's" only stop propagation/default.
"'s" now matched as op token, not keyword.
- parser `get` cmd: dispatch + cmd-kw list + parse-get-cmd (parses expr with
optional `as TYPE`). Required for `get result as JSON` in fetch chains.
- compiler empty-target for (local X): emit (set! X (hs-empty-like X)) so
arrays/sets/maps clear the variable, not call DOM empty on the value.
- runtime hs-empty-like: container-of-same-type empty value.
- runtime hs-empty-target!: drop dead FORM branch that was short-circuiting
to innerHTML=""; the querySelectorAll-over-inputs branch now runs.
- runtime hs-halt!: take ev param (was free `event` lookup); raise hs-return
to stop execution unless mode is "the-event".
- runtime hs-reset!: type-aware — FORM → reset, INPUT/TEXTAREA → value/checked
from defaults, SELECT → defaultSelected option.
- runtime hs-open!/hs-close!: toggle `open` attribute on details elements
(not just the prop) so dom-has-attr? assertions work.
- runtime hs-coerce JSON: json-stringify dict/list (was str).
- test-runner mock: host-get on List + "length"/"size" (was only Dict);
dom-set-attr tracks defaultChecked / defaultSelected / defaultValue;
mock_query_all supports comma-separated selector groups.
- generator: emit boolean attrs (checked/selected/etc) even with null value;
drop overcautious "skip HS with bare quotes or embedded HTML" guard so
morph tests (source contains embedded <div>) emit properly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the pre-bundled HS tokenizer/parser/compiler/runtime/integration
sx + sxbc pairs plus module-manifest.json in shared/static/wasm/sx/,
matching the current HS source after recent patches (call command,
event destructuring, halt/append, break/continue, CSS block syntax, etc.).
Co-Authored-By: Claude Opus 4.7 (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>
Previously these primitives threw Eval_error if either arg was non-string.
Now they return false, preventing crashes when DOM attributes return nil
values during element processing (e.g. htmx-boot-subtree! iterating
elements with undefined attribute names).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
orchestration.sx process-elements iterates DOM attributes and calls
starts-with? on the name. Some attributes have nil names (e.g. from
malformed elements). Added (string? name) guard before starts-with?.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The htmx-boot-subtree! function (defined in lib/hyperscript/htmx.sx)
was never loaded in the browser because hs-htmx.sx wasn't in the
bundle or compile-modules lists. Added to:
- bundle.sh: copy htmx.sx as hs-htmx.sx to dist
- compile-modules.js: compile to hs-htmx.sxbc, add to deps and lazy list
This was the root cause of "Load Content" button not working —
hx-* attributes were never activated because htmx-boot-subtree!
was undefined.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parser: fetch command consumes {method:"POST"}, with {opts}, and
handles as-format both before and after options.
Mock: Number format case-insensitive, /test route has number field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
guard and cek-try both create CEK frames that don't survive async
perform/resume. Instead, run-action returns nil on success and an
error string on failure. The for-each loop checks the return value
and sets fail-msg. No exceptions cross async boundaries.
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>
When an error occurs during resumed VM execution (after perform/hs-wait),
resume_vm now checks the VM's handler_stack. If a handler exists (from a
compiled guard form's OP_PUSH_HANDLER), it unwinds frames and jumps to
the catch block — exactly like OP_RAISE. This enables try/catch across
async perform/resume boundaries.
The guard form compiles to OP_PUSH_HANDLER which lives on the vm struct
and survives across setTimeout-based async resume. Previously, errors
during resume escaped to the JS console as unhandled exceptions.
Also restored guard in the test runner (was cek-try which doesn't survive
async) and restored error-throwing assertions in run-action.
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>
The guard form (call/cc + handler-bind expansion) doesn't survive async
IO suspension — the CEK continuation from guard's call/cc captures frames
that become invalid after the VM resumes from hs-wait. Replacing guard
with cek-try (which compiles to VM-native OP_PUSH_HANDLER/OP_POP_HANDLER)
avoids the CEK boundary crossing.
The test runner now executes: suspends on hs-wait, resumes, runs test
actions, and test assertions fire correctly. The "Not callable: nil"
error is eliminated. Remaining: test assertion errors from iframe content
not loading fast enough (timing issue, not a framework bug).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Not callable: nil error happens on a stub VM (frames=[], sp=0) during
cek_resume with 12 CEK kont frames. The error is from a reactive signal
subscriber (reset! current ...) that triggers during run vm after resume.
The subscriber callback goes through CEK via cek_call_or_suspend and the
CEK continuation tries to call nil.
This is a reactive subscriber notification issue, not a perform/resume
frame management issue. The VM frames are correctly restored — the error
happens during a synchronous reset! call within the resumed VM execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows pending_cek, reuse_stack count, and frames count in the error.
Also transfers reuse_stack from _active_vm at VmSuspended catch sites.
Finding: the Not callable: nil happens during cek_resume (pending_cek=false,
kont=12 frames). The CEK continuation tries to call a letrec function that
is nil because letrec bindings are in VM local SLOTS, not in the CEK env.
The VM→CEK boundary crossing during suspension loses the local slot values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bootstrap.py regenerated cek_run as a simple "raise if suspended" without
the _cek_io_resolver and _cek_io_suspend_hook checks. Also lost the
CekPerformRequest catch in cek_step_loop and step_limit checks.
This was the direct cause of "IO suspension in non-IO context" when island
click handlers called perform (via hs-wait). The CEK had no way to propagate
the suspension to the VM/JS boundary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The callFn suspension returns requests as {op: "io-sleep", args: {items: [100]}}
(dict format) but _driveAsync only handled list format (op-name arg ...).
Result: io-sleep/wait resumes never fired — tests hung after first suspension.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: when perform fires inside a VM closure chain (call_closure_reuse),
the caller frames are saved to reuse_stack on the ACTIVE VM. But the
_cek_io_suspend_hook and _cek_eval_lambda_ref create a NEW stub VM for the
VmSuspended exception. On resume, resume_vm runs on the STUB VM which has
an empty reuse_stack — the caller frames are orphaned on the original VM.
Fix: transfer reuse_stack from _active_vm to the stub VM before raising
VmSuspended. This ensures resume_vm -> restore_reuse can find and restore
the caller's frames after async resume via _driveAsync/setTimeout.
Also restore step_limit/step_count refs dropped by bootstrap.py regeneration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: sx_browser.ml registered all HTML tags (a, b, i, p, s, u, g, etc.)
as custom special forms. The evaluator's step_eval_list checked custom special
forms BEFORE checking local env bindings. So (let ((a (fn () 42))) (a))
matched the HTML tag <a> instead of calling the local function a.
Fix: skip custom special forms AND render-check when the symbol is bound in
the local env. Added (not (env-has? env name)) guard to both checks in
step-eval-list (spec/evaluator.sx and transpiled sx_ref.ml).
This was the root cause of "[sx] resume: Not callable: nil" — after hs-wait
resumed, calling letrec-bound functions like wait-boot (which is not an HTML
tag) worked, but any function whose name collided with an HTML tag failed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
callable? in boot-helpers.sx checked for "native-fn" but type-of returns
"function" for NativeFn — broke make-spread and all native fn dispatch
in aser. Restore 20 behavioral tests replaced with NOT IMPLEMENTED stubs
by the test regeneration commit. Add host-* platform primitive stubs to
sx_server.ml so boot-helpers.sx loads without errors server-side.
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>
The _cek_io_suspend_hook creates a stub VM to carry the suspended CEK
state. Previously used empty globals, which caused "Not callable: nil"
when the CEK resume needed platform functions. Now uses _default_vm_globals
(set to _vm_globals by sx_browser.ml) so all platform functions and
definitions are available during resume.
Remaining issue: still getting "resume: Not callable: nil" — the CEK
continuation env may not include letrec bindings from the island body.
The suspension point is inside reload-frame → hs-wait, and the resume
needs to call wait-boot (a letrec binding).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _cek_io_suspend_hook was only added to cek_run_iterative (line 986)
but the actual code path went through cek_run (line 624). Added the hook
check to both functions.
This fixes the "IO suspension in non-IO context" error that blocked
hs-wait/perform from propagating through event handler → trampoline →
eval_expr call chains. IO suspension now converts to VmSuspended via the
hook, which the value_to_js wrapper catches and drives with _driveAsync.
+42 OCaml test passes (3924→3966). IO suspension verified working in
browser WASM: dom-on click handler → hs-wait → perform → suspend →
_driveAsync → setTimeout → resume.
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>
Recompiled WASM kernel (wasm_of_ocaml) to include the VM reuse_stack
fix from sx_vm.ml. Recompiled boot.sxbc with the clear-and-replace
hydration (replaceChildren + nil hydrating scope + dom-append).
sx-platform.js deployed with island preload, isMultiDefine fix, and
K.load error checking.
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>
Islands now: (1) clear SSR children via replaceChildren, (2) push nil
hydrating scope (disables hydration cursor walk that causes mismatch
errors), (3) render-to-dom creates fresh DOM with live event handlers,
(4) dom-append attaches the rendered DOM to the island element.
This fixes the hydrate-mismatch:div error caused by SSR/client attribute
differences (~tw generates different class strings server vs client).
NOTE: needs WASM rebuild (sx_build target=wasm) to compile boot.sxbc.
The .sx source is updated but the bytecoded module is stale.
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>
Islands now clear SSR children before render-to-dom and append the
fresh DOM result. Avoids hydrate-mismatch errors from SSR/client
attribute differences (~tw generates different class strings).
The hydrating scope is set to nil (no cursor walk) so render-to-dom
creates new DOM nodes instead of trying to reuse SSR elements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VM frame merging bug: call_closure_reuse now saves caller continuations
on a reuse_stack instead of merging frames. resume_vm restores them in
innermost-first order. Fixes frame count corruption when nested closures
suspend via OP_PERFORM. Zero test regressions (3924/3924).
Island hydration: hydrate-island now looks up components from (global-env)
instead of render-env, triggering the symbol resolve hook. Added JS-level
preload-island-defs that scans DOM for data-sx-island and loads definitions
from the content-addressed manifest BEFORE hydration — avoids K.load
reentrancy when the resolve hook fires inside env_get.
loadDefinitionByHash: fixed isMultiDefine check — defcomp/defisland bodies
containing nested (define ...) forms no longer suppress name insertion.
Added K.load return value checking for silent error string returns.
sx_browser.ml: resolve hook falls back to global_env.bindings when
_vm_globals miss (sync gap). Snapshot reuse_stack alongside pending_cek.
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>