Commit Graph

43 Commits

Author SHA1 Message Date
c932ad59e1 HS: repeat property for-loops + where (+3 tests)
Re-applied from worktree-agent-a7c6dca2be5bbada0 (commit c4241d57)
onto HEAD that already has clusters 30, 26, 27 runtime changes —
straight cherry-pick conflicted on the cluster-30 log-all block
and cluster-27 intersection helper, so the logical diff was
replayed surgically.

Parser (parse-atom object-literal):
- obj-collect now `append`s pairs in source order instead of
  `cons`'ing, so `{foo:1, bar:2, baz:3}` reaches hs-make-object
  as `((foo 1) (bar 2) (baz 3))`.

Compiler (emit-for, array-index emission):
- emit-for detects `for x in COLL where COND` (parser wraps COLL
  as `(coll-where INNER COND)`) and rewrites the filter lambda
  to bind the for-loop variable name rather than the default
  `it`, so `where x.val > 10` sees the right binding. Also
  unwraps `coll-where` so filter targets the real inner coll.
- emit-for now wraps a symbol collection with `cek-try` (not the
  broken `hs-safe-call`, which has an uninitialised CEK call-ref
  in the WASM build) so `for prop in x` after `set x to {…}`
  iterates x's keys instead of nil.
- array-index emits `(hs-index obj key)` instead of
  `(nth obj key)`, which only worked on lists.

Runtime:
- New polymorphic `hs-index` dispatches to get / nth / host-get
  based on target type (dict / list / string / otherwise).
- `hs-put-at!` default branch now detects DOM elements via
  `hs-element?` and delegates to `hs-put!`, so `put X at end of
  elt` on a DOM node appends innerHTML instead of crashing.
- `hs-make-object` tracks insertion order in a hidden `_order`
  list; `hs-for-each` and `hs-coerce` (Keys / Entries / Map
  branches) prefer `_order` when present, filtering the marker
  out of output.

Suite hs-upstream-repeat: 25/30 → 28/30 (+3).
Smoke 0-195 unchanged at 165/195.
2026-04-24 11:02:49 +00:00
0c31dd2735 HS: intersection observer mock + on intersection (+3 tests)
Applied from worktree-agent-ad6e17cbc4ea0c94b (commit 0a0fe314)
with manual re-apply onto post-cluster-26 HEAD:

- Parser: parse-on-feat collects `having margin X threshold Y`
  clauses between `from X` and the body; packs them into a
  `:having {"margin" M "threshold" T}` dict on the parts list.
- Compiler: scan-on threads a new `having-info` parameter through
  all recursions; when event-name is "intersection", wraps the
  hs-on call with `(do on-call (hs-on-intersection-attach! target
  margin threshold))`.
- Runtime: hs-on-intersection-attach! constructs an
  IntersectionObserver with {rootMargin, threshold} options and a
  callback that dispatches an "intersection" DOM event carrying
  {intersecting, entry} detail.
- Runner: HsIntersectionObserver mock fires the callback
  synchronously on observe() with isIntersecting=true so handlers
  run during activation; ignores margin/threshold (tests assert
  only that the handler fires).

Suite hs-upstream-on: 33/70 -> 36/70 (on intersection: 0/3 -> 3/3).
Smoke 0-195 unchanged at 165/195.
2026-04-24 10:44:01 +00:00
0d38a75b21 HS: closest parent <sel> traversal (+1 test)
parse-trav recognises `parent` as an ident modifier after the
`closest` keyword — consumes it and re-invokes with kind
`closest-parent`, producing AST `(closest-parent "div" (me))` instead
of the generic trailing-ident-as-unit shape
`(string-postfix (closest "*" (me)) "parent")`.

Compiler translates `(closest-parent sel target)` to
`(dom-closest (host-get target "parentElement") sel)` so `me` is
skipped and only strict ancestors match. `closest-parent` also
joined the `put X into <trav>` inner-html shortcut alongside
next/previous/closest.

Suite hs-upstream-core/regressions: 10/16 → 11/16.
Smoke 0-195: 162/195 → 163/195.
2026-04-24 09:33:32 +00:00
d862efe811 HS: select returns selected text (+1 test)
Runtime gains hs-get-selection: prefers window.__test_selection stash,
falls back to real getSelection().toString(). Compiler rewrites
`(ref "selection")` to `(hs-get-selection)`. Generator detects the
createRange + setStart/setEnd + addRange block and emits a single
host-set! on __test_selection with the text slice; sidesteps the need
for a fully propagating DOM range/text-node mock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 07:13:28 +00:00
bd821c0445 HS: toggle multi-class + until event (+2 tests)
Parser `parse-toggle-cmd`: after the leading class ref, collect any
additional class refs and treat `toggle .foo .bar` as `toggle-between`
(pair-only). Recognise a `until EVENT [from SOURCE]` modifier and emit
a new `toggle-class-until` AST node. Compiler handles the new node by
emitting `(begin (hs-toggle-class! tgt cls) (hs-wait-for src ev)
(hs-toggle-class! tgt cls))` which uses the existing event-waiter
machinery to flip the class back when the specified event fires.

Remaining toggle test (`can toggle for a fixed amount of time`)
depends on the mock's sync io-sleep resuming immediately — the click
handler toggles on/off synchronously, so the pre-timeout assertion
can never see the `.foo` class present. Needs an async scheduler in
the mock to handle.

Suite hs-upstream-toggle: 22/25 → 24/25. Smoke 0-195: 162/195
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 06:35:30 +00:00
f79f96c1c3 HS: wait on event basics (+4 tests)
Five parts: (a) tests/hs-run-filtered.js `io-wait-event` mock now
registers a one-shot listener on the target element and resumes with
the event, instead of immediately resuming with nil. (b) Added
hs-wait-for-or runtime form carrying a timeout-ms; mock resumes
immediately when a timeout is present (0ms tests). (c) parser
parse-wait-cmd recognises `wait for EV(v1, v2)` destructure syntax,
emits :destructure list on wait-for AST. (d) compiler emit-wait-for
updated for :from/:or combos; a new `__bind-from-detail__` form
compiles to `(define v (host-get (host-get it "detail") v))`, and the
`do`-sequence handler preprocesses wait-for to splice these synthetic
bindings after the wait. (e) generator extracts `detail: ...` from
`CustomEvent` options so dispatched events carry their payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 06:05:53 +00:00
ed8d71c9b8 HS: send can reference sender (+1 test)
Three-part fix: (a) emit-send now builds detail=(dict "sender" me) on
(send NAME target) and bare (send NAME) instead of nil, so the receiving
handler has access to the sending element. (b) parser parse-atom now
recognises the `sender` keyword (previously swallowed as noise) and
emits it as (sender). (c) compiler translates bare `sender` symbol and
(sender) list-head to (hs-sender event) — a new runtime helper that
reads (get (host-get event "detail") "sender").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:37:18 +00:00
108e25d418 HS: string template \${x} (+2 tests)
`\$window.foo` / `\${window.foo}` couldn't resolve. Two fixes:
(a) compiler.sx: in a dot-chain base position, known globals (window,
    document, navigator, location, history, screen, localStorage,
    sessionStorage, console) emit `(host-global "name")` instead of a
    bare unbound symbol.
(b) generator: `eval-hs-locals` now also sets each binding on
    `window.<name>` via `host-set!`, so tests that translated
    `window.X = Y` as a local pair still see `window.X` at eval time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:52:55 +00:00
6b0334affe HS: remove bare @attr, set X @attr, JSON clean, FormEncoded, HTML join
- parser remove/set: accept bare @attr (not just [@attr])
- parser set: wrap tgt as (attr name tgt) when @attr follows target
- runtime: hs-json-stringify walks sx-dict/list to emit plain JSON
  (strips _type key which leaked via JSON.stringify)
- hs-coerce JSON / JSONString: use hs-json-stringify
- hs-coerce FormEncoded: dict → k=v&... (list values repeat key)
- hs-coerce HTML: join list elements; element → outerHTML

+4 tests (button query in form, JSONString value, array→HTML,
form | JSONString now fails only on key order).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:14:03 +00:00
a5f0325935 HS empty + .length/.size on SX collections
- compiler emit-empty-target: for empty :local, rebind via emit-set so
  scoped locals persist.
- tests/hs-run-filtered.js host-get: when reading length/size from an SX
  list, return items.length. Similarly for SX dict size (non-_type keys).
  Unlocks :arr.length / :set.size / :map.size in compiled HS.

Net: empty 8→11, remove 17→18, add (set dedup) exposed as separate issue.
2026-04-23 17:29:16 +00:00
7ecdd59335 HS trigger: compound event names + detail, event-refs via host-get
- parse-trigger-cmd: use parse-compound-event-name so 'trigger foo:bar' and
  'trigger foo.bar' preserve the full event name. Also parse an optional
  detail dict '(x:42)' like parse-send-cmd.
- compiler: 3-arg (trigger NAME DETAIL TGT) emits dom-dispatch with the
  detail dict. 2-arg (trigger NAME TGT) unchanged.
- emit-on event-ref bindings now use (host-get event 'detail') → the event
  carries detail as a JS object, so the SX 'get' primitive returned nil
  and tests checking 'on foo(x) … x' saw empty values.

Net: trigger 2→6 (100%).
2026-04-23 17:22:25 +00:00
d6137f0d6f HS reset/first/last: defaultValue tracking, list-of-elements reset, find().first|last
Mock DOM:
- El now tracks defaultValue/defaultChecked/defaultSelected and a reset()
  method that walks descendant form controls, restoring them.
- setAttribute(value|checked|selected) sets the matching default-* too, so
  the initial HTML state can be restored later.
- parseHTMLFragments + _setInnerHTML capture a textarea's textContent as
  its value AND defaultValue.

Generator (pw-body):
- add_action / add_assertion extract .first() / .last() / .nth(N) modifiers
  into (nth (dom-query-all …) i) or a (let ((_all …)) (nth _all (- … 1)))
  tail so multi-match helpers hit the right element.

Compiler:
- emit-reset! with a .<class>/.<sel> query target now compiles to hs-query-all
  so 'reset .resettable' resets every matching control (not just the first).

Net: reset 1→8 (100%).
2026-04-23 17:15:40 +00:00
5b31d935bd HS pick runtime: guard nil inputs so pick first/last/items/match/matches don't hang
- hs-pick-first/last/random/items/slice: short-circuit nil or non-list
  (strings flow through unchanged).
- New hs-pick-match / hs-pick-matches wrappers around regex-match /
  regex-find-all, also nil-safe; compiler routes pick-match / pick-matches
  through them. Unblocks 'pick first from null returns null' and
  'pick match from null returns null' which previously looped past
  step_limit.
2026-04-23 17:08:04 +00:00
601fdc1c34 HS take command: class/attr with+giving, attr removal from scope, giving keyword
- tokenizer: add 'giving' as keyword so parse-take-cmd can detect it.
- parser.sx parse-take-cmd: loop over 'with <class>' / 'giving <class>' /
  'from <sel>' / 'for <tgt>' clauses in any order for both the class and
  attribute cases. Emits uniform (take! kind name from-sel for-tgt
  attr-val with-val) 7-slot AST.
- compiler emit-take: pass with-cls for the class case through to runtime.
- runtime hs-take!: with a class 'with' replacement, toggle both classes
  across scope + target. For attribute take, always strip the attr from
  the scope 'others' (setting to with-val if given, otherwise removing).
- generator pw-body: translate evaluate(() => document.querySelector(s).
  click()) and .dispatchEvent(new Event('name', …)) into dom-dispatch ops
  so bubbling-click assertions in 'parent takes…' tests work.
- generator toHaveClass: strip JS regex word-boundaries (\\b) from the
  expected class name.
- shared/static/wasm/sx/dom.sx: dom-child-list / dom-child-nodes mirror
  the dom-query-all SX-list passthrough — childNodes arrives pre-SXified.

Net: take 6→15 (100%), remove 16→17, fetch 11→15.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:51:17 +00:00
5b100cac17 HS runtime + generator: make, Values, toggle styles, scoped storage, array ops, fetch coercion, scripts in PW bodies
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>
2026-04-23 16:08:07 +00:00
e4773ec336 HS: split type-check (predicate) from type-assert (:) — +5 comparisonOperator
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>
2026-04-23 11:37:12 +00:00
a11d0941e9 HS test generator: fix toHaveCSS, locals, and \"-escapes — +28 tests
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>
2026-04-23 09:18:21 +00:00
0515295317 HS: extend parser/runtime + new node test runner; ignore test-results/
- Parser: `--` line comments, `|` op, `result` → `the-result`, query-scoped
  `<sel> in <expr>`, `is a/an <type>` predicate, multi-`as` chaining with `|`,
  `match`/`precede` keyword aliases, `[attr]` add/toggle, between attr forms
- Runtime: per-element listener registry + hs-deactivate!, attr toggle
  variants, set-inner-html boots subtree, hs-append polymorphic on
  string/list/element, default? / array-set! / query-all-in / list-set
  via take+drop, hs-script idempotence guard
- Integration: skip reserved (me/it/event/you/yourself) when collecting vars
- Tokenizer: emit `--` comments and `|` op
- Test framework + conformance runner updates; new tests/hs-run-filtered.js
  (single-process Node runner using OCaml VM step-limit to bound infinite
  loops); generate-sx-conformance-dev.py improvements
- mcp_tree.ml + run_tests.ml: harness extensions
- .gitignore: top-level test-results/ (Playwright artifacts)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:11:07 +00:00
802ccd23e8 HS: fix empty/halt/morph/reset/dialog — 17 upstream tests pass
- 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>
2026-04-22 15:36:01 +00:00
7357988af6 Rebuild hyperscript WASM bytecode bundles (hs-*.sxbc + manifest)
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>
2026-04-22 09:09:56 +00:00
3ba819d9ae HS parser/compiler/runtime: fix 8 parse errors, add/remove arrays, return guard
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>
2026-04-18 11:06:46 +00:00
84996d74e2 Test runner: return-value error handling, no guard/cek-try/throws
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>
2026-04-17 23:09:38 +00:00
db8e680caf Revert do→let/it chaining (caused 80-test regression)
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>
2026-04-17 23:01:23 +00:00
ac193e8839 HS: do→let/it chaining, single-IO fetch, fetch URL parser, IO mock
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>
2026-04-17 22:54:31 +00:00
76f7e3b68a HS: return/guard, repeat while/until, if-then fix, script extraction
Parser: if-then consumes 'then' keyword before parsing then-body.
Compiler: return→raise, def→guard, repeat while/until dispatch.
Runtime: hs-repeat-while, hs-repeat-until.
Test gen: script block extraction for def functions.
repeat suite: 10→13/30.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:33:55 +00:00
97818c6de1 HS compiler: return via raise/guard, def param fix
- 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>
2026-04-16 17:55:20 +00:00
ca9196a693 Stub VM uses real globals for CEK resume after IO suspension
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>
2026-04-16 17:26:38 +00:00
8819d7cbd1 HS fixes: multi-property transition, take attr with-val, empty form, css-value parsing
- 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>
2026-04-16 13:44:59 +00:00
684a46297d HS behavioral tests: 478→509/831 (57%→61%), parser/compiler/runtime fixes
Parser: am-a/am-not-a type checks, transition element/selector targeting,
take @attr=value with replacement, toggle my/the possessive, <selector/>
syntax in parse-atom, the-of for style/attr/class/selector, when-clause
filtering for add, starts/ends-with ignoring case.

Compiler: take attr passthrough, toggle-style nil→me default, scoped
querySelectorAll for add/remove/toggle-class, has-class? entry, matches?
extracts selector from (query sel), add-class-when with for-each filter,
starts/ends-with-ic entries, hs-add replaces + for polymorphic add.

Runtime: hs-take! proper attr values, hs-type-check Element/Node via
host-typeof, hs-toggle-style! opacity 0↔1, hs-coerce +8 coercions
(Keys/Values/Entries/Reversed/Unique/Flat/JSON/Object), hs-query-all
bypasses broken dom-query-all (WASM auto-converts arrays), hs-matches?
handles DOM el.matches(selector), hs-add list+string+number polymorphic,
hs-starts/ends-with-ic for case-insensitive comparison.

DOM mock: mkStyle() with setProperty/getPropertyValue, fndAll.item().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:53:43 +00:00
e5e3e90ee7 HS compiler: emit-set handles @attr of target expressions
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>
2026-04-14 19:47:30 +00:00
b1666a5fe2 HS tests: VM step limit fix, callFn error propagation, compiler emit-set fixes
- 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>
2026-04-14 19:27:03 +00:00
e2fe070dd4 Fix :ref callback bug in adapter-dom — Pretext island fully working
Root cause: adapter-dom.sx line 345 handled :ref by calling
(dict-set! attr-val "current" el), assuming React-style ref objects.
Callback-style refs (fn (el) ...) passed a function, not a dict,
causing dict-set! to fail with "dict key val" error.

Fix: (if (callable? attr-val) (attr-val el) (dict-set! attr-val "current" el))
Supports both callback refs and dict refs.

Pretext island now fully working:
- 3 controls: width slider, font size slider, algorithm toggle
- Knuth-Plass + greedy line breaking via bytecode-compiled library
- canvas.measureText for pixel-perfect browser font metrics
- Effect-based imperative DOM rendering (createElement + appendChild)
- Reactive: slider drag → re-measure → re-break → re-render

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:26:48 +00:00
7ec42386fb Fix Pretext island: library functions inside define-library begin block
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>
2026-04-12 17:53:50 +00:00
699dd5ad69 Step 17b: bytecode-compiled text-layout, WASM library import fix
- 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>
2026-04-12 16:37:04 +00:00
f60d22e86e Hyperscript: focus command, diagnostic test output, blur keyword
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>
2026-04-12 12:38:05 +00:00
ae32254dfb Hyperscript: hide/show strategy (opacity/visibility), add/remove query-all
Parser: hide/show handle `with opacity/visibility/display` strategy
and properly detect target vs command boundaries. Compiler: emit
correct CSS property per strategy, add-class/remove-class use
for-each+query-all for class selectors. Runtime: hs-query-all uses
dom-body, hs-each helper for collection iteration.

Generator: inline run().toEqual() pattern for eval-only tests.

372/831 (45%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:07:06 +00:00
854ed9c027 Hyperscript conformance: 372→373 — hide/show strategy, generator toEqual
Parser: hide/show handle `with opacity/visibility/display` strategy,
target detection for then-less chaining (add/remove/set/put as boundary).
Generator: inline run().toEqual([...]) pattern for eval-only tests.
Compiler: hide/show emit correct CSS property per strategy.

373/831 (45%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:42:28 +00:00
6e27442d57 Step 17: streaming render — hyperscript enhancements, WASM builds, live server tests
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>
2026-04-12 08:41:38 +00:00
5fe97d8481 Multi-class add/remove, async IO in test runner — 280/831 (34%)
- Parser: add .foo .bar collects multiple class refs into multi-add-class AST
- Compiler: multi-add-class/multi-remove-class emit (do (dom-add-class...) ...)
- Test runner: drives IO suspension chains (wait/fetch/settle) via _driveAsync
  so async HS tests (wait 100ms, settle, fetch) can complete
- Assertion failed: 51→49

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:24:16 +00:00
08cd82ed65 Parser: catch/finally in on handlers, cmd terminators — 279/831 (34%)
- 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>
2026-04-10 21:51:43 +00:00
42a7747d02 Fix HS put-into and query: compiler emits hs-query-first, runtime uses real DOM
Two bugs found by automated test suite:
1. compiler.sx: query → hs-query-first (was dom-query, a deleted stub)
2. compiler.sx: emit-set with query target → dom-set-inner-html (was set!)
3. runtime.sx: hs-query-first uses real document.querySelector
4. runtime.sx: delete hs-dom-query stub (returned empty list)

All 8/8 HS elements pass: toggle, bounce+wait, count, add-class,
toggle-between, set-innerHTML-eval, put-into-target, repeat-3-times.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 06:50:57 +00:00
7492ceac4e Restore hyperscript work on stable site base (908f4f80)
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>
2026-04-09 19:29:56 +00:00
67d2f32512 Fix type-check-strict compiler match + deploy HS to WASM
- 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>
2026-04-08 21:25:23 +00:00