Patterns: wildcard, literal, var, ctor (nullary + arg, flattens tuple
args so Pair(a,b) -> (:pcon "Pair" PA PB)), tuple, list literal, cons
:: (right-assoc), unit. Match: leading | optional, (:match SCRUT
CLAUSES) with each clause (:case PAT BODY). Body parsed via parse-expr
because | is below level-1 binop precedence.
ocaml-parse-program: program = decls + bare exprs, ;;-separated.
Each decl is (:def …), (:def-rec …), or (:expr …). Body parsing
re-feeds the source slice through ocaml-parse — shared-state refactor
deferred.
new (new Object("")) hung because js-new-call called
js-get-ctor-proto -> js-ctor-id -> inspect, and inspect on a
wrapper-with-proto-chain recurses through the prototype's
lambdas forever. Added (js-function? ctor) precheck at the top
of js-new-call that raises a TypeError instance instead.
conformance.sh: 148/148.
parser.sx parse-toggle-cmd: when seeing 'toggle .foo for', peek the
following two tokens. If they are '<ident> in', it is a for-in loop
and toggle does NOT consume 'for' as a duration clause. Restores the
trailing for-in to the command list.
parser.sx parse-on (handler modifiers): recognize 'throttled at <ms>'
and 'debounced at <ms>' as handler modifiers. Captured as :throttle /
:debounce kwargs in the on-form parts list.
compiler.sx emit-on: pre-extract :throttle / :debounce from parts via
new _strip-throttle-debounce helper before scan-on, then wrap the built
handler with (hs-throttle! handler ms) or (hs-debounce! handler ms).
runtime.sx: hs-throttle! — closure with __hs-last-fire timestamp,
fires immediately and drops events arriving within ms of the last fire.
hs-debounce! — closure with __hs-timer, clears any pending timer and
schedules a new setTimeout(handler, ms) so only the last burst event
fires.
Both formerly-architectural skips now pass:
- "toggle does not consume a following for-in loop"
- "throttled at <time> drops events within the window"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings the architecture branch (559 commits ahead — R7RS step 4-6, JIT
expansion, host_error wrapping, bytecode compiler, etc.) into the
loops/haskell line of work. Conflict in lib/haskell/conformance.sh:
architecture replaced the inline driver with a thin wrapper delegating
to lib/guest/conformance.sh + a config file. Resolved by taking the
wrapper and extending lib/haskell/conformance.conf with all programs
added under loops/haskell (caesar, runlength-str, showadt, showio,
partial, statistics, newton, wordfreq, mapgraph, uniquewords, setops,
shapes, person, config, counter, accumulate, safediv, trycatch) plus
adding map.sx and set.sx to PRELOADS.
plans/haskell-completeness.md gains three new follow-up phases:
- Phase 17 — parser polish (`(x :: Int)` annotations, mid-file imports)
- Phase 18 — one ambitious conformance program (lambda-calc / Dijkstra /
JSON parser candidate list)
- Phase 19 — conformance speed (batch all suites in one sx_server
process to compress the 25-min run to single-digit minutes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SX strictly arity-checks lambdas; JS allows passing more args than
declared (extras accessible via arguments). Was raising "f expects
1 args, got 2" whenever Array.from passed (value, index) to a
1-arg mapFn. Fixed in js-build-param-list: every JS param list
now ends with &rest __extra_args__ unless an explicit rest is
present, so extras are silently absorbed.
conformance.sh: 148/148.
The 2^32-1 threshold still allowed indices like 2147483648 to pad
billions of undefineds. Without sparse-array support there's no
semantic value in >1M padding; lowering the bail turns those tests
into fast assertion fails instead of timeouts.
built-ins/Array timeouts: 2 → 1. conformance.sh: 148/148.
arr[4294967295] = 'x' and arr.length = 4294967295 were padding
the SX list with js-undefined for ~4 billion entries — instant
timeout. Per ES spec, indices >= 2^32-1 aren't array indices
anyway (regular properties, which we can't store on lists).
Added (>= i 4294967295) bail clauses to js-list-set! and the
length setter.
built-ins/Array: 21/45 → 23/45 (5 timeouts → 2).
conformance.sh: 148/148.
String.fromCharCode.length, Math.max.length, Array.from.length
were returning 0 because their SX lambdas use &rest args with no
required params — but spec assigns each a specific length.
Added js-builtin-fn-length mapping JS name to spec length (12
entries). js-fn-length consults the table first and falls back to
counting real params.
built-ins/String: 79/99 → 80/99, built-ins/Array: 20/45 → 21/45.
conformance.sh: 148/148.
Was hardcoded to "[object Object]" for everything; per ES it should
return "[object Array]", "[object Function]", "[object Number]",
etc. by class. Added js-object-tostring-class helper that switches
on type-of and dict-internal markers (__js_*_value__,
__callable__). Prototype-identity checks ensure
Object.prototype.toString.call(Number.prototype) returns
"[object Number]" (similar for String/Boolean/Array).
built-ins/Array: 18/45 → 20/45, built-ins/Number: 43/50 → 44/50.
conformance.sh: 148/148.
Per ES, every function instance's constructor slot points to the
Function global. Was returning undefined for (function () {})
.constructor. Added constructor to the function-property cond in
js-get-prop; returns js-function-global.
conformance.sh: 148/148.
new Object(func) should return func itself (per ES spec - "if value
is a native ECMAScript object, return it"), but js-new-call only
kept the ctor's return when it was dict or list — functions fell
through to the empty wrapper. Added (js-function? ret) to the
accept set.
built-ins/Object: 42/50 → 44/50. conformance.sh: 148/148.
JS var is function-scoped, but the transpiler only collected
top-level vars and re-emitted (define) everywhere; for-body var
shadowed the outer (un-hoisted) scope. Three-part fix:
1. js-collect-var-names recurses into js-block/js-for/js-while
/js-do-while/js-if/js-try/js-switch/js-for-of-in;
2. var-kind decls emit (set! ...) instead of (define ...) since
the binding is already created at function scope;
3. js-block uses js-transpile-stmt-list (no re-hoist) instead of
js-transpile-stmts.
built-ins/Array: 17/45 → 18/45, String: 77/99 → 78/99.
conformance.sh: 148/148.
js-list-set! was a no-op for the length key. Added a clause that
pads with js-undefined via js-pad-list! when target > current.
Truncation skipped: the pop-last! SX primitive doesn't actually
mutate the list (length unchanged after the call), so no clean
way to shrink in place from SX. Extension covers common cases.
built-ins/Array: 16/45 → 17/45. conformance.sh: 148/148.
js-get-prop for SX lists fell through to js-undefined for any key
not in its hardcoded method list, so Array.prototype.myprop and
Object.prototype.hasOwnProperty were invisible to arrays.
Switched the fallback to walk Array.prototype via js-dict-get-walk,
which already chains to Object.prototype.
built-ins/Array: 14/45 → 16/45. conformance.sh: 148/148.
JS arrays must treat string indices that look like numbers ("0",
"42") as the corresponding integer slot. js-get-prop and js-list-set!
only handled numeric key, falling through to undefined / no-op for
string keys. Added a (and (string-typed key) (numeric? key)) clause
that converts via js-string-to-number and recurses with the integer
key. built-ins/Array: 13/45 → 14/45. conformance.sh: 148/148.
hk-bind-exceptions! in eval.sx registers throwIO, throw, evaluate, catch,
try, handle, displayException. SomeException constructor pre-registered
in runtime.sx (arity 1, type SomeException).
throwIO and the existing error primitive both raise via SX `raise` with a
uniform "hk-error: msg" string. catch/try/handle parse it back into a
SomeException via hk-exception-of, which strips nested
'Unhandled exception: "..."' host wraps (CEK's host_error formatter) and
the "hk-error: " prefix.
catch and handle evaluate the handler outside the guard scope (build an
"ok"/"exn" outcome tag inside guard, then dispatch outside) so that a
re-throw from the handler propagates past this catch — matching Haskell
semantics rather than infinite-looping in the same guard.
14 unit tests in tests/exceptions.sx (catch success, catch error, try
Right/Left, handle, throwIO + catch/try, evaluate, nested catch, do-bind
through catch, branch on try result, IORef-mutating handler).
Conformance: safediv.hs (8/8) and trycatch.hs (8/8). Scoreboard now
285/285 tests, 36/36 programs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JS top-level var was emitting (define <name> X) at SX top level,
permanently rebinding any SX primitive of that name (e.g. var list
= X broke (list ...) globally). Two-part fix:
1. wrap transpiled program in (let () ...) in js-eval so defines
scope to the eval and don't leak.
2. rename call-args constructor in js-transpile-args from list to
js-args (a variadic alias) so even within the eval's own scope,
JS vars named list don't shadow arg construction.
Array-literal transpile keeps list (arrays must be mutable).
built-ins/Object: 41/50 → 42/50. conformance.sh: 148/148.
js-new-call Object had set obj.__proto__ correctly, but then the
__callable__ returned a fresh (dict), which js-new-call's "use
returned dict over obj" rule honoured — losing the proto. Added
is-new check (this.__proto__ === Object.prototype) and return
this instead of a new dict when invoked as a constructor with
no/null args. Now new Object().__proto__ === Object.prototype.
built-ins/Object: 37/50 → 41/50. conformance.sh: 148/148.
Trivial wrapper: apl-run-file = apl-run ∘ file-read, where
file-read is built-in to OCaml SX.
Tests verify primes.apl, life.apl, quicksort.apl all parse
end-to-end (their last form is a :dfn AST). Source-then-call
test confirms the loaded file's defined fn is callable, even
when the algorithm itself can't fully execute (primes' inline
⍵ rebinding still missing — :glyph-token, not :name-token).
js-loose-eq only had a __js_string_value__ unwrap clause, so
Object(1.1) == 1.1 returned false. Added parallel clauses for
__js_number_value__ and __js_boolean_value__ in both directions.
Now new Number(5) == 5, Object(true) == true, etc.
built-ins/Object: 26/50 → 37/50. conformance.sh: 148/148.
Per ES spec, Object('s') instanceof String, Object(42).constructor
=== Number, etc. Was passing primitives through as-is. Added cond
clauses to Object.__callable__ that dispatch by type and call
(js-new-call String/Number/Boolean (list arg)). The wrapper
constructors already store __js_*_value__ on this.
built-ins/Object: 16/50 → 26/50. conformance.sh: 148/148.
Parser: :name clause now detects 'name ← rhs' patterns inside
expressions. When seen, consumes the remaining tokens as RHS,
parses recursively, and emits a (:assign-expr name parsed-rhs)
value segment.
Eval-ast :dyad and :monad: when the right operand is an
:assign-expr node, capture the binding into env before
evaluating the left operand. This realises the primes idiom:
apl-run "(2 = +⌿ 0 = a ∘.| a) / a ← ⍳ 30"
→ 2 3 5 7 11 13 17 19 23 29
Also: top-level x←5 now evaluates to scalar 5 (apl-eval-ast
:assign just unwraps to its RHS value).
Caveat: ⍵-rebinding (the original primes.apl uses
'⍵←⍳⍵') is a :glyph-token; only :name-tokens are handled.
A regular variable name (like 'a') works.
Per ES spec, Object(value) returns a new object when value is null
or undefined. Was returning the argument itself, breaking
Object(null).toString(). Added a cond clause to Object.__callable__
that detects nil/js-undefined and falls through to (dict).
built-ins/Object: 15/50 → 16/50. conformance.sh: 148/148.
Was computing m * pow(10, e) for "1.2345e-3" forms; floating-point
multiplication introduced rounding (Number(".12345e-3") -
0.00012345 == 2.7e-20). The SX string->number primitive parses the
whole literal in one IEEE round, matching JS literal parsing. Falls
back to manual m * pow(10, e) only when string->number returns nil.
built-ins/Number: 42/50 → 43/50. conformance.sh: 148/148.
Haskell strings are [Char]. Calling reverse / head / length on a SX raw
string transparently produces a cons-list of char codes (via hk-str-head /
hk-str-tail in runtime.sx), but (==) then compared the original raw string
against the char-code cons-list and always returned False — so
"racecar" == reverse "racecar" was False.
Added hk-try-charlist-to-string and hk-normalize-for-eq in eval.sx; routed
== and /= through hk-normalize-for-eq so a string compares equal to any
cons-list whose elements are valid Unicode code points spelling the same
characters, and "[]" ↔ "".
palindrome.hs lifts from 9/12 → 12/12; conformance 33/34 → 34/34 programs,
266/269 → 269/269 tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Object/Array/Number/String/Boolean had no __proto__, so
Function.prototype mutations were invisible to them. Added a
post-init (begin (dict-set! ...)) at the end of runtime.sx
that wires each constructor to js-function-global.prototype.
Combined with the recent Object.prototype fallback, the chain
now terminates correctly: ctor → Function.prototype → Object.prototype.
built-ins/Number: 41/50 → 42/50, built-ins/String: 75/99 → 78/99,
built-ins/Array: 12/45 → 13/45. conformance.sh: 148/148.
Investigation of the long-standing 'why does the runner say 1494/1494 not
1496/1496?' question. The answer is in tests/hs-run-filtered.js:969 — two
tests are skipped via _SKIP_TESTS for documented architectural reasons:
1. 'until event keyword works' — uses 'repeat until event click from #x',
which suspends the OCaml kernel waiting for a click that is never
dispatched from outside K.eval. The sync test runner has no way to
fire the click while the kernel is suspended.
2. 'throttled at <time> drops events within the window' — the HS parser
does not implement the 'throttled at <ms>' modifier. The compiled SX
for the handler is malformed: handler body is the literal symbol
'throttled', the time expression dangles outside the closure as
stray (do 200 ...). Genuinely needs parser+compiler+runtime work,
not just a deadline bump.
Both are documented at the skip site with a comment explaining why they
can't run synchronously. The conformance number is 1494/1494 = 100% on
counted tests, with 2 explicit, justified skips out of 1496 total.
This was the source of the cumulative-vs-isolated test-count discrepancy.
Suite filter runs see them as 'not in this suite,' batched runs see them
as 'continued past'. Either way: not failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apl-permutations was doing (append acc <new-perms>) which is
O(|acc|) and acc grows ~N! big — total cost O(N!²).
Swapped to (append <new-perms> acc) — append is O(|first|)
so cost is O((n+1)·N!_prev) per layer, total O(N!). q(7)
went from 32s to 12s; q(8)=92 now finishes well within the
300s timeout, so the queens(8) test is restored.
497/497. Phase 8 complete.
JS -0 was returning rational integer 0; the (- 0 x) form loses the
sign-of-zero. Switched js-neg to (* -1 (exact->inexact (js-to-number a))),
which produces a float and preserves -0.0. Now 1/(-0) === -Infinity
and Math.asinh(-0) preserves the sign as required by the spec.
built-ins/Math: 41/45 → 42/45. conformance.sh: 148/148.
hk-bind-data-ioref! registers newIORef / readIORef / writeIORef /
modifyIORef / modifyIORef' under the import alias (default IORef).
Representation: dict {"hk-ioref" true "hk-value" v} allocated inside IO.
modifyIORef' uses hk-deep-force on the new value before write.
Side-effect: fixed pre-existing bug in import handler — modname was
reading (nth d 1) (the qualified flag) instead of (nth d 2). All
'import qualified … as Foo' paths were silently no-ops; map.sx unit
suite jumps from 22→26 passing.
Conformance now 33/34 programs, 266/269 tests (only pre-existing
palindrome.hs 9/12 still failing on string-as-list reversal, present
on prior commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tests/hs-run-batched.js — fresh-kernel-per-batch conformance runner.
Solves the WASM kernel JIT-cache-saturation problem (compiled VmClosures
accumulate over a single process and slow tests at the tail of the run)
by spawning a child Node process per batch. Each batch starts with an
empty cache, so tests at index 1400 perform identically to tests at
index 100. Configurable batch size (HS_BATCH_SIZE, default 150) and
parallelism (HS_PARALLEL, default 1).
This is option 2 from the cache-architecture plan — the lowest-risk fix:
zero kernel changes, deterministic results, runs in the same time as the
single-process version when parallelism matches CPU count.
plans/jit-cache-architecture.md — sketches the SX-wide architectural
fix in three phases:
1. Tiered compilation — call counter on lambdas; only JIT after K
invocations. Filters out one-shot lambdas (test harness, dynamic
eval, REPLs) at the source.
2. LRU eviction — central cache with fixed budget. Predictable memory
ceiling regardless of input pattern.
3. Reset API — jit-reset!, jit-clear-cold!, jit-stats, jit-pin!
primitives for app-driven cache management.
Layer split: cache datastructure + LRU in hosts/ocaml/lib/sx_jit_cache.ml
(new), VM integration in sx_vm.ml, primitives registered in
sx_primitives.ml, declarative spec in spec/primitives.sx, and SX-level
ergonomics (with-jit-threshold, with-fresh-jit, jit-report) in lib/jit.sx.
This is host-specific to the OCaml WASM kernel but the SX API surface is
shared across all hosted languages (HS, Common Lisp, Erlang, etc.).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(js-div 1 0) with rational integer literals throws "rational: division
by zero" instead of producing Infinity. Wrapped the divisor in
(exact->inexact ...) so integer-by-zero now returns inf/-inf/nan
matching JS semantics. Hit by the harness's _isSameValue +0/-0 check
which calls (js-div 1 a) on JS literal arguments.
built-ins/Number: 37/50 → 41/50. built-ins/String: 77/99.
conformance.sh: 148/148.