parser.sx: detect bare ident "initial" after "to" in parse-one-transition,
emit string sentinel instead of (ref "initial") which evaluated to nil.
runtime.sx: hs-transition stores pre-first-transition style as
data-hs-init-{prop}; restores it when value=="initial".
Also commits E37 tokenizer and E40 fetch test implementations that
accumulated in the working tree but weren't staged in prior commits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements hs-tokens-of, hs-stream-token, hs-stream-consume,
hs-stream-has-more, hs-token-type, hs-token-value, hs-token-op?,
hs-raw->api-token, hs-eof-sentinel in runtime.sx.
Tokenizer emits whitespace tokens after the first content token;
stream functions skip them for look-ahead and consume. Parser
filters whitespace tokens at hs-parse entry. Dot/hash after close
brackets split into PERIOD/POUND + IDENTIFIER. Template escape \$
produces literal $.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merge c36fd5b2 stripped the source-info dict unwrapping from hs-to-sx
(the (let ((ast (if (and (dict? ast) (:hs-ast)) ...) wrapper) and also
introduced E37 tokenizer whitespace-token changes that broke the parser.
Reverts tokenizer/runtime to pre-E37 HEAD~1 state, restores hs-to-sx
with AST unwrapping from 61c9697f, and adds back the hs-id= dispatch
clause. Baseline: 178/195.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add hs-raw->api-token, hs-eof-sentinel, hs-api-list, hs-tokens-of,
hs-stream-token, hs-stream-consume, hs-stream-has-more, hs-token-type,
hs-token-value, hs-token-op? to runtime. Fix tokenizer to emit whitespace
tokens and handle dot/hash after closing brackets. Fix hs-tokens-of to
accept bare :template keyword flag via &rest args + some() check.
Remaining failure (string interpolation isnt surprising) requires full
DOM activation infrastructure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lost when resolving E37 reformat conflicts — re-added:
- hs-id= function in runtime.sx (JS === for elements, = for scalars)
- hs-id= dispatch in compiler.sx (after = clause)
Parser already uses hs-id= for != operator (unchanged).
- hs-id= uses JS === for DOM elements (hs-ref-eq), = for scalars
- != operator now uses hs-id= for structural correctness
- compound tag[attr=val] selector matching in test runner
- dom-query-all replaces host-call querySelectorAll
- DOM tree structure corrected in 4 generated tests (elements were
appended to wrong parents)
Fix compiler: (block-literal () body) was emitting bare body instead of
(fn () body). Now always wraps in fn regardless of param count.
Generator: MANUAL_TEST_BODIES for all 4 blockLiteral tests using apply
and SX map rather than JS array.map.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parser: parse-js-block extracts raw JS source by character positions.
Compiler: js-block AST → hs-js-exec call, stores result in it.
Runtime: hs-js-exec creates JS Function, handles promise rejection.
Test runner: host-new-function/host-promise-state natives + promise monkey-patch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hs-make-object no longer appends _order to every HS object literal.
Generator emit_eval now uses assert-equal (equal?) for dict-containing
expected values instead of assert= (= reference equality).
Together these fix arrayLiteral "arrays containing objects work".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
empty .class compiled (empty-target (query ".class")) to
(hs-empty-target! (hs-query-first ".class")) via hs-to-sx — only
emptying the first match. Fix: detect (query ...) target in the
empty-target compiler case and emit (for-each (fn (_el)
(hs-empty-target! _el)) (hs-query-all sel)) instead, mirroring the
add-class pattern. Suite hs-upstream-empty: 12/13 → 13/13.
Smoke 0-195: 175/195 unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
${}{"val"} pattern in add {prop: ${}{"val"}} uses two consecutive brace
groups: empty ${} followed by {"val"} for the actual expression. The prior
fix called parse-expr when already at the brace-close of the empty group,
returning nil. New fix: detect empty ${} (brace-open then brace-close),
skip the close, then read the actual value from the following {…} block.
Also handles non-empty ${expr} directly as before.
Suite hs-upstream-add: 17/19 → 18/19. Smoke 0-195: 174/195 → 175/195.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
call X then put it into Y was emitting (hs-win-call ...) without
wrapping in emit-set, so it remained nil. Wrap call result in
emit-set(the-result) so it/the-result are updated. Fixes +1 test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents click events from bubbling into ancestor elements that also
have hs handlers (e.g. parent re-inserting HTML after child click).
Fixes put-reprocessing tests 1147/1149/1150 (+3 tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tell now rebinds beingTold/you/yourself without overwriting me.
Parser implicit targets use beingTold; handler wrapper seeds beingTold=me.
Fixes: attributes refer to the thing being told, does not overwrite me,
your symbol represents the thing being told.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _NO_STEP_LIMIT set exempts hypertrace tests from the 200k step cap
- globalThis.__hs_deadline exposed so cek_step_loop wall-clock check
(every 10k steps) can terminate runaway async loops without needing
to go through host-call or _driveAsync
- meta + _hs-on-caller added to hs-runtime.sx (both lib and bundled):
on-event handlers now set meta.caller to an object with
meta.feature.type = "onFeature" before calling the handler
Tests 196 (async hypertrace), 198 (meta.caller), 199, 200 now pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add hs-tokenize-template: scans " as single STRING token, ${ ... }
as dollar+brace+inner-tokens (inner tokenized with hs-tokenize), and
} as brace-close. Update hs-tokens-of to call hs-tokenize-template
when :template keyword arg is passed. Unlocks tests 1 and 15.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add \r \b \f \v and \xNN escape handling to read-string. Use
char-from-code for non-SX-literal chars. Throw "Unterminated string"
on EOF inside a string literal. Throw "Invalid hexadecimal escape: \x"
on bad \xNN. Add hs-hex-digit? and hs-hex-val helpers. Unlocks
tests 2, 6, 13, 14 once generator lands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add hs-eof-sentinel, hs-op-type, hs-raw->api-token, hs-tokens-of,
hs-stream-token, hs-stream-consume, hs-stream-has-more, and the
three token accessors (hs-token-type, hs-token-value, hs-token-op?).
No test delta yet — API-only, generator comes in step 6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`1 in [1, 2, 3]` must return (list 1) not true. Root cause: in? compiled
to hs-contains? which returns boolean for scalar items. Fix: new hs-in?
returns filtered list; new in-bool? operator for is/am-in comparison
contexts so those still return boolean. Parser generates in-bool? for
`X is in Y` / `X am in Y`; plain `in` keeps in? → list return.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove guard wrapper from hs-win-call emit (direct call is sufficient now)
- def command also registers fn on window[name] so hs-win-call finds it
- Generator: fix \"-escaped quotes in hs-compile string literal (was splitting "here" into three SX nodes)
- Hand-rolled deftest for 'can refer to function in init blocks' now passes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add host-call-fn FFI primitive to test runner (calls SX lambdas or JS fns)
- Add hs-win-call runtime helper: looks up fn by name in window globals
- Compiler call case: emit guard-wrapped hs-win-call for bare (ref ...) calls
- Compiler method-call else: same guard pattern for non-dot method calls
- Compiler do case: hoist define forms before init/other forms (def hoisting)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three orthogonal fixes that pick up tests now unblocked by earlier
cluster-34 (count filters) and cluster-35 (hs-method-call fallback) work:
(1) parser.sx parse-hide-cmd / parse-show-cmd — added `on` to the keyword
list that signals an implicit-`me` target. Without this, `on click 1
hide on click 2 show` silently parsed as `(hide nil)` because parse-expr
greedily started consuming `on` and returned nil. With the bail-out,
hide/show default to me when the next token is `on` (a sibling feature).
(2) runtime.sx hs-method-call fallback — when method isn't a built-in
collection op, look up obj[method] via host-get; if it's an SX-callable
(lambda) use apply, but if it's a JS-native function (e.g. cookies.clear
on the cookies Proxy) dispatch via `(apply host-call (cons obj (cons
method args)))` so the JS native receives the args correctly. SX
callable? returns false for JS-native function values, hence the split.
(3) generator hs-cleanup! — wrapped body in begin (fn body evaluates
only the last expression) and reset two pieces of mutable global runtime
state between tests: hs-set-default-hide-strategy! nil and
hs-set-log-all! false. The prior `can set default to custom strategy`
test (cluster 11) was leaking _hs-default-hide-strategy to subsequent
tests, breaking `hide element then show element retains original
display` because hs-hide-one! resolved its "display" strategy through
the leaked override.
Also added cluster-33 hand-roll for `basic clear cookie values work`
(uses the new method-call fallback to dispatch cookies.clear via
host-call).
hs-upstream-hide: 15/16 → 16/16. hs-upstream-expressions/cookies: 3/5
→ 4/5. Smoke 0-195 unchanged at 172/195.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime: hs-method-call gains a fallback case — when method isn't one of
the built-in collection ops (map/push/filter/join/indexOf), look up the
method name as a property on obj via host-get; if the value is callable,
invoke via apply with the call args. This makes namespaced calls like
`utils.foo()` work when utils is an SX dict whose foo entry is an SX fn.
Generator: hand-rolled deftests for the 3 cluster-35 tests:
- `is called synchronously` and `can call asynchronously`: pre-evaluate
the script-tag def via `(eval-expr-cek (hs-to-sx (first (hs-parse
(hs-tokenize "def foo() ... end")))))` so foo lands in the global eval
env, then build a click div via dom-set-attr + hs-boot-subtree! and
exercise it via dom-dispatch click.
- `functions can be namespaced`: hand-build `(define utils (dict))` then
`(host-set! utils "foo" __utils_foo)` (the def is registered under a
fresh sym since the parser doesn't yet support `def utils.foo()` dotted
names), and rely on the new hs-method-call fallback to dispatch
`utils.foo()` through host-get/apply.
Removed the 3 def entries from SKIP_TEST_NAMES.
hs-upstream-def: 24/27 → 27/27. Smoke 0-195 unchanged at 172/195.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser: parse-on-feat now consumes `elsewhere` (or `from elsewhere`) as
a modifier between event-name and source. When matched, sets a flag and
emits :elsewhere true on parts. The `from elsewhere` form peeks one
token ahead before consuming both keywords so plain `from #x` continues
to parse as a source expression.
Compiler: scan-on threads elsewhere?; when present, target becomes
(dom-body) (so the listener attaches to body and bubbles see all clicks)
and the handler body is wrapped with `(when (not (host-call me "contains"
(host-get event "target"))) BODY)` so the handler fires only when the
click originated outside the activated element.
Generator: dropped supports "elsewhere" modifier and supports "from
elsewhere" modifier from skip-list.
hs-upstream-on: 48/70 → 50/70. Smoke 0-195 unchanged at 172/195.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser: parse-on-feat now consumes `first` keyword before event-name (sets
count-min/max to 1) and a count expression after event-name — `N` (single),
`N to M` (range), `N and on` (unbounded above). Number tokens are coerced
via parse-number. Emits :count-filter {"min" N "max" M | -1} part.
Compiler: scan-on threads count-filter-info; the handler binding wraps the
fn body in a let-bound __hs-count counter. Each event fire increments the
counter and (when count is in range) executes the original body. Each
on-clause registers an independent handler with its own counter, so
`on click 1 ... on click 2 ... on click 3` produces three handlers that
fire on their respective Nth click (mix-ranges test).
Generator: dropped 5 cluster-34 tests from skip-list — `can filter events
based on count`, `... count range`, `... unbounded count range`, `can mix
ranges`, `on first click fires only once`.
hs-upstream-on: 43/70 → 48/70. Smoke 0-195 unchanged at 172/195.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
integration.sx hs-activate! now wraps the activation block in a cancelable
hyperscript:before:init event (dispatched on the el via dom-dispatch which
returns the dispatchEvent boolean — true unless preventDefault was called).
On success it dispatches hyperscript:after:init at the end. Both events
bubble so listeners on a containing wa work-area receive them. Generator
gets two hand-rolled deftests that exercise the new dispatch via
hs-boot-subtree!: one captures both events into a list, the other
preventDefaults before:init and asserts data-hyperscript-powered is absent.
hs-upstream-core/bootstrap: 20/26 → 22/26. Smoke 0-195: 170 → 172.
Remaining 4 cluster-29 tests need stricter parser error-rejection
(hs-upstream-core/parser, parse-error event); larger than a single
cluster budget — leave as untranslated for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser: parse-on-feat now consumes `of FILTER` after `mutation` event-name,
where FILTER is `attributes`/`childList`/`characterData` ident or `@a [or @b]*`
attr-token chain. Emits :of-filter dict on parts. Compiler: scan-on threads
of-filter-info; mutation event-name emits `(do (hs-on …) (hs-on-mutation-attach!
TARGET MODE ATTRS))`. Runtime: hs-on-mutation-attach! constructs a real
MutationObserver with config matched to filter and dispatches "mutation" event
with records detail. Runner: HsMutationObserver mock with global registry;
prototype hooks on El.setAttribute/appendChild/removeChild/_setInnerHTML fire
matching observers synchronously, with __hsMutationActive guard preventing
recursion. Generator: dropped 7 mutation tests from skip-list, added
evaluate(setAttribute) and evaluate(appendChild) body patterns.
hs-upstream-on: 36/70 → 43/70. Smoke 0-195 unchanged at 170/195.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shared formatter in sx_types.ml. Small integer-valued floats still print
as plain ints; floats outside safe-int range (|n| >= 1e16) now print as
%.17g (full precision) instead of silently wrapping to negative or 0.
Non-integer values keep %g 6-digit behavior — no existing SX tests regress.
Unblocks Number.MAX_VALUE / Math.pow(2,N) style tests in js-on-sx where
iterative float loops were collapsing to 0 at ~2^63.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire up the `ask` and `answer` commands end-to-end:
- tokenizer.sx: register `ask` and `answer` as hs-keywords.
- parser.sx: cmd-kw? gains both; parse-cmd dispatches to new
parse-ask-cmd (emits `(ask MSG)`) and parse-answer-cmd, which
reads `answer MSG [with YES or NO]`. The with/or pair reads
yes/no via parse-atom — parse-expr would collapse
`"Yes" or "No"` into `(or "Yes" "No")` before match-kw "or"
could fire. The no-`with` form emits `(answer-alert MSG)`.
- compiler.sx: three new cond branches (ask, answer, answer-alert)
compile to a let that binds __hs-a, sets `the-result` and `it`,
and returns the value — so `then put it into ...` works.
- runtime.sx: hs-ask / hs-answer / hs-answer-alert call
window.prompt / confirm / alert via host-call + host-global.
- tests/hs-run-filtered.js: test-name-keyed globalThis.{alert,
confirm,prompt}; __currentHsTestName is updated before each
test. Host-set! for innerHTML/textContent now coerces JS
null → "null" (browser behaviour) so `prompt → null` →
`put it into #out` renders literal text "null", which the
fourth test depends on.
Suite hs-upstream-askAnswer: 1/5 -> 5/5.
Smoke 0-195: 166/195 -> 170/195.
Three parser additions so scripts like `(on click (log me) (trigger foo))`
parse into a single feature with both commands in its body:
1. parse-feat: new cond branch for `paren-open` — advance, recurse
parse-feat, consume `paren-close`. Allows a feature like `(on click
...)` to be grouped in parens.
2. parse-cmd: two new cond branches — on `paren-close` return nil (so
cl-collect terminates at an outer group close), and on `paren-open`
advance / recurse / close. Allows single parenthesized commands like
`(log me)`.
3. cl-collect: previously only recursed when the next token was a
recognised command keyword (`cmd-kw?`), so after `(log me)` the
sibling `(trigger foo)` would end the feature body and re-surface as
a top-level feature. Extended the recursion predicate to also fire
when the next token is `paren-open`.
Suite hs-upstream-core/parser: 9/14 -> 10/14.
Smoke 0-195: 165/195 -> 166/195.
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.
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.
Add `_hs-config-log-all` runtime flag + captured log list. When set
via `hs-set-log-all!`, `hs-activate!` pushes "hyperscript:init" onto
`_hs-log-captured` and mirrors to console.log. Covers cluster 30.
Generator side: eval-only path now detects the logAll body pattern
(`_hyperscript.config.logAll = true`) and emits a deftest that:
- resets captured list
- toggles log-all on
- builds a div with `_="on click add .foo"` and `hs-boot-subtree!`s
- asserts `(some string-contains? "hyperscript:")` over captured logs.
hs-upstream-core/bootstrap: 19/26 -> 20/26. Smoke 0-195: 164 -> 165.
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.
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>