Add `_hs-command-registry` and `_hs-converter-registry` dicts plus
`hs-register-command!` / `hs-register-converter!` to
`lib/hyperscript/compiler.sx`. Inside `hs-to-sx`, before the existing
`cond` over head symbols, check both registries: an `as` form whose
type-name has a registered converter dispatches to that converter; any
list head whose name (`(str head)`) is in the command registry
dispatches to that compile-fn. On registry miss, the original ~180
hardcoded branches handle the form.
Each registered fn receives a ctx dict (built per call) exposing
`:hs-to-sx` for recursion plus the AST fields the dispatch needs
(`:ast :head` for commands; `:ast :value-ast :type-name` for
converters). Mirrors Step 9's parser feature registry shape.
Smoke tested: register custom command + converter, both dispatch;
built-in `(as x \"Int\")` still produces `(hs-coerce x \"Int\")`.
Mirror `shared/static/wasm/sx/hs-compiler.sx` copied byte-identical.
OCaml: 4545/1339, JS: 2591/2465 — both match baseline, zero regressions.
Second piece of plans/designs/hs-plugin-system.md (Step 11 next).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `_hs-feature-registry` dict and `hs-register-feature!` to
`lib/hyperscript/parser.sx`. Replace `parse-feat`'s hardcoded `cond`
on feature names with a registry lookup; the paren-open and
default-expression branches remain as fallthroughs.
Each parse-fn receives a `ctx` dict (built per call by `parse-feat-ctx`)
exposing parser internals (`:adv!`, `:tp-val`, `:tp-type`, `:at-end?`,
`:parse-cmd-list`, `:parse-expr`) and the per-feature handlers
(`:parse-on-feat` … `:parse-socket-feat`). All nine builtins
(`on`, `init`, `def`, `behavior`, `live`, `when`, `worker`, `bind`,
`socket`) are registered at file load time, so plugins added later via
`hs-register-feature!` persist across `hs-parse` calls.
Worker stub still raises identically. Mirror `shared/static/wasm/sx/hs-parser.sx`
copied byte-identical. OCaml: 4545/1339, JS: 2591/2465 — both match
baseline, zero regressions.
First piece of plans/designs/hs-plugin-system.md (Steps 10/11 follow).
lib/guest/prefix.sx defines a single (defmacro prefix-rename PREFIX ENTRIES)
form that takes a prefix string and a quoted list of entries. Each entry
is either a bare symbol (same-name alias: cl-foo = foo) or a 2-element
list (alias target) for renames (cl-mod = modulo).
Ported lib/common-lisp/runtime.sx: 47 hand-written (define cl-X Y) lines
across 13 contiguous groups now collapse into prefix-rename calls. Loaded
lib/guest/prefix.sx in the conformance preamble so the macro is available
when runtime.sx is parsed.
Verification: cl scoreboard 518/518, up from a stale baseline of 309/309
— Phase 2 (evaluator, +182) and Phase 6 (stdlib, +27) had under-counted
historical results, not affected by this change. No regressions; baseline
updated to reflect true counts.
PARTIAL — pending second consumer. lua/runtime.sx (the brief's specified
second consumer) has zero pure same-name aliases — every lua- definition
wraps custom logic. Step left [partial — pending lua] until a consumer
fits, or the second-consumer choice is revisited (js/runtime.sx has 2
candidates: isFinite/isNaN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted the duplicated conformance plumbing into a single driver:
- lib/guest/conformance.sx — two helper fns that emit (gc-result NAME P F T)
lines for the bash side to grep: gc-dict-result for runners returning
a {:passed :failed :total} dict, and gc-counters-result for guests that
bump a global pass/fail counter from a test file load.
- lib/guest/conformance.sh — config-driven bash driver. Sources a per-lang
conf, locates sx_server, runs sx_server in either single-session "dict"
mode (one preload + many suite evals) or per-suite "counters" mode
(fresh sx_server per suite, with shared preloads). Aggregates and writes
scoreboard.{json,md} via per-lang emit_scoreboard_* functions.
- Ported lib/prolog/conformance.sh and lib/haskell/conformance.sh down to
one-line wrappers that exec the shared driver against their .conf file.
Verification:
- Prolog: 590/590 — diff vs baseline is timestamp-only.
- Haskell: 156/156 — significantly higher than the 0/18 in baseline. The
old conformance.sh was buggy (its `(ok-len 3 ...)` grep never matched,
defaulting every program to 0 pass / 1 fail). Updated baseline to the
true count; no actual test regressed. Plan baseline cell updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend hs-make-token to (type value pos &rest extras) producing dicts
{:pos :end :line :value :type}. End defaults to pos+len(value); line
defaults to 1. Both tokenize loops now track current-line via newline
counting in advance!. hs-emit! and t-emit! pass the right end and
start-line to the constructor; redundant dict-set! after construction
removed.
Mirror copied to shared/static/wasm/sx/hs-tokenizer.sx (byte-identical).
Verify: (hs-make-token "NUMBER" "1" 0) returns
{:pos 0 :end 1 :line 1 :value "1" :type "NUMBER"}.
OCaml suite: 4529 pass, 1339 pre-existing failures (baseline). All
4/4 hs-upstream-core/sourceInfo tests now pass (was 2/4 — closes E38).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parser, layout, desugar, lazy eval, ADTs, HM inference, typeclasses
(Eq/Ord/Show/Num/Functor/Monad), real IO monad, full Prelude. 775/775
green across 13 program suites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrote the coroutine implementation to use lib/fiber.sx (make-fiber,
fiber-resume, fiber-done?) instead of eagerly running the proc body and
collecting all yields into a list. Each coroutine is now a live fiber —
calls to the coro command invoke fiber-resume, yield suspends via call/cc.
- make-tcl-interp: remove :coroutines/:in-coro/:coro-yields, add :coro-yield-fn nil
- tcl-cmd-yield: calls :coro-yield-fn (fiber's yield fn) to truly suspend
- tcl-cmd-yieldto: same pattern, yields "" to resumer
- make-coro-cmd: takes fiber (not coro-name), calls fiber-resume on each invoke
- tcl-cmd-coroutine: creates a fiber whose body runs the proc with :coro-yield-fn set
- tcl-call-proc result merge: drop :coro-yields/:coroutines propagation
- test.sh: load lib/fiber.sx before lib/tcl/runtime.sx in epoch 4
All 337/337 tests pass including all 20 coro tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parse-int "2.0" returns nil in SX (strict integer parse); fixed by adding
tcl-num-float? (char scan for ./e/E) and tcl-parse-num (routes to
parse-float when float-shaped). Applied in tcl-apply-binop (all arith +
comparisons), tcl-apply-func (parse-float for all math args), unary minus,
and tcl-expr-parse-power (**). Real sqrt/floor/ceil/round/pow/sin/cos/tan/
exp/log now used instead of integer stubs. Integer division still truncates
when both operands are integer-shaped. 329/329 tests green.
Parser: limit `from SOURCE` to parse-collection/cmp/arith/poss/atom
(stops before parse-logical so `or` is not consumed as binary op),
then collect `or EVENT from SOURCE` pairs via recursive collect-ors!.
Adds :or-sources key to the on-feature parts list.
Compiler: scan-on gains or-sources param (11th); new :or-sources cond
clause extracts the list; terminal `true` branch wraps on-call in
(do on-call (hs-on target event handler) ...) for each extra source.
Test: "can handle an or after a from clause" moved from skip-list to
MANUAL_TEST_BODIES and now passes (1478/1496).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When `from #doesntExist` resolves to nil, hs-on silently skips
listener registration instead of crashing on dom-listen nil.
Removes "can ignore when target doesn't exist" from skip-list.
Also adds host-make-js-thrower native utility (plain JS throwing
function, no K.callFn re-entry) — investigated for the js-exceptions
catch test but that test stays skipped: native JS throws from host
calls escape OCaml WASM try-with guards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parser: bracket-open in obj-collect key cond → (computed-key expr).
Compiler: detect computed-key list at object-literal pair key and compile
the inner expression instead of emitting a literal string.
Generator: special case for 'expressions work in object literal field names'
using eval-hs-locals with host-callback so hs-win-call can find the fn.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parser parses optional deriving clause; only appended to AST when non-empty.
hk-bind-decls! data arm generates dictShow_Con / dictEq_Con per constructor.
hk-binop == and /= now deep-force both sides (SX dict equality is by
reference — two thunks wrapping the same value compared as not-equal without
this). Three token-type fixes in the deriving parser (lparen/rparen/comma,
not "special").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements namespace eval, current, which, exists, delete, export,
import, forget, path, and ensemble create (auto-map + -map). Procs
defined inside namespace eval are stored as fully-qualified names
(::ns::proc), resolved relative to the calling namespace at lookup
time. Proc bodies execute in their defining namespace so sibling
calls work without qualification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds lib/tcl/conformance.sh: runs .tcl programs through the epoch
protocol, compares against # expected: annotations, writes
scoreboard.json and scoreboard.md. All 3 classic programs pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 3 headline feature: everything falls out of SX's first-class env chain.
- make-tcl-interp extended with :frame-stack and :procs fields
- proc: user-defined commands with param binding, rest args, isolated scope
- uplevel: run script in ancestor frame with correct frame propagation
- upvar: alias local name to remote frame variable (get/set follow alias)
- global/variable: sugar for upvar #0
- info: level, vars, locals, globals, commands, procs, args, body
- tcl-call-proc propagates updated frames back to caller after proc returns
- test.sh timeout bumped to 90s for larger runtime
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pl-hs-query, pl-hs-predicate/1,2,3, pl-hs-install in hs-bridge.sx.
No parser/compiler changes: Hyperscript already compiles
`when allowed(user, action)` to (allowed user action).
Total 590/590.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hs-coerce HTML list case: use outerHTML for element items, not str.
hs-coerce Fragment case: actually build a DocumentFragment — element
items are appended directly; strings are parsed via a temp div.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>