5 phases (A-E) per plans/sx-vm-opcode-extension.md:
- A: Sx_vm dispatch fallthrough for opcodes ≥200 + Invalid_opcode + extension_dispatch_ref
- B: Sx_vm_extension interface + Sx_vm_extensions registry (register / dispatch /
id_of_name / state_of_extension), installs into the dispatch_ref at module init
- C: extension-opcode-id SX primitive for compiler-side lookup
- D: lib/extensions/ subtree wired via include_subdirs, test_ext.ml as the canonical
worked example, opcode_name forward-ref so disassemble shows ext opcodes by name
- E: bytecode_uses_extension_opcodes scanner + JIT skip path so lambdas containing
extension opcodes run interpreted via CEK
26 new foundation tests across 5 suites, all green. Zero regressions across 11
language-port conformance suites (erlang 530, haskell 285, datalog 276, prolog 590,
smalltalk 847, common-lisp 487, apl 562, js 148, forth 632, tcl 3, ocaml-on-sx unit 607).
Hand-off: lib/erlang/vm/dispatcher.sx (Phase 9b stub) can now be replaced with a real
hosts/ocaml/lib/extensions/erlang.ml consumer.
Adds Sx_vm.bytecode_uses_extension_opcodes — an operand-aware
bytecode scanner that walks past CONST u16, CALL_PRIM u16+u8, and
CLOSURE u16+dynamic upvalue descriptors so operand bytes that happen
to be ≥200 don't false-positive as extension opcodes.
jit_compile_lambda calls the scanner on the inner closure's bytecode.
On hit it returns None — the lambda then runs through CEK
interpretation. The VM's dispatch fallthrough still routes the
extension opcodes themselves through the registry; this change just
prevents the JIT from claiming code it has no plan for.
Tests: 7 new foundation cases — pure core eligible, head/middle/
post-CLOSURE detection, CONST + CALL_PRIM + CLOSURE-descriptor false-
positive avoidance. +7 pass vs Phase D baseline, no regressions
across 11 conformance suites.
Loop complete: acceptance criteria 1-4 met. Hand-off to the Erlang
loop — lib/erlang/vm/dispatcher.sx's Phase 9b stub can now be
replaced with a real hosts/ocaml/lib/extensions/erlang.ml consumer.
lib/extensions/ becomes the new home for VM extensions, wired in via
(include_subdirs unqualified). README documents the registration
pattern, opcode-ID range conventions (200-209 guest_vm, 210-219
inline test, 220-229 test_ext, 230-247 ports), and naming rules.
extensions/test_ext.ml is the canonical worked example — two
operand-less opcodes (220 push 42, 221 double TOS) carrying a per-
extension state slot (TestExtState invocation counter). Test_ext.register
called from run_tests.ml at the start of the Phase D suite, on top of
the inline test_reg from earlier suites (disjoint opcode IDs).
Sx_vm.opcode_name now consults extension_opcode_name_ref (forward ref
in the same style as extension_dispatch_ref), so disassemble shows
extension opcodes by name instead of UNKNOWN_n. Registry maintains
name_of_id_table and installs the lookup at module init.
Tests: 5 new foundation cases — primitive resolves test_ext name,
end-to-end bytecode (push + double + return → 84), disassemble shows
"test_ext.OP_TEST_PUSH_42" / "test_ext.OP_TEST_DOUBLE_TOS",
unregistered ext opcodes still fall back to UNKNOWN_n, invocation
counter records the two dispatches. +5 pass vs Phase C baseline, no
regressions across 11 conformance suites.
Registers extension-opcode-id from sx_vm_extensions.ml module init.
Lives downstream of both sx_primitives and sx_vm to avoid a build
cycle. Accepts a string or symbol; returns Integer id when the opcode
is registered, Nil otherwise.
Compilers (lib/compiler.sx) call this to emit extension opcodes by
name. Returning Nil rather than failing on unknown names lets a port's
optimization opt in per-build — missing extensions degrade to slower
correct execution.
Tests: 5 new foundation cases — registered lookup, unknown → nil,
symbol arg, zero-arg + integer-arg rejection. +5 pass vs Phase B
baseline, no regressions across 11 conformance suites.
sx_vm_extension.ml: handler type, extensible extension_state variant,
EXTENSION first-class module signature.
sx_vm_extensions.ml: register / dispatch / id_of_name /
state_of_extension. install_dispatch () runs at module init,
swapping Phase A's stub for the real registry. Rejects out-of-range
opcode IDs (must be 200-247), duplicate IDs, duplicate names, and
duplicate extension names.
Tests: 9 new foundation cases — lookup hits/misses, end-to-end VM
dispatch including opcode composition, all four rejection paths.
+9 pass vs Phase A baseline, no regressions across 11 conformance
suites.
Adds Invalid_opcode of int exception and extension_dispatch_ref forward
ref (default raises Invalid_opcode op), plus the |op when op >= 200 arm
before the catch-all in the bytecode dispatch loop. Partition comment
documents 1-199 core / 200-247 extensions / 248-255 reserved.
Phase B will install the real registry's dispatch into the ref at module
init, replacing this stub.
Tests: 4 new foundation cases (Invalid_opcode for 200/224/247, Eval_error
for 199 to pin the threshold). +4 pass vs baseline, no regressions.
plans/sx-vm-opcode-extension.md ports over from loops/erlang (f6a68656)
with the opcode partition adjusted to match real VM usage: 1-199 core
(current ceiling 175 = OP_DEC), 200-247 extensions, 248-255 reserved.
plans/agent-briefings/sx-vm-extensions-loop.md captures the per-fire
workflow and ground rules.
Lua now joins tcl/ocaml/kernel/common-lisp in consuming lib/guest/lex.sx via
prefix-rename. Removes 28 lines of duplicated character-class helpers
(lua-make-token, lua-digit?, lua-hex-digit?, lua-letter?, lua-ident-start?,
lua-ident-char?, lua-ws?) and replaces with the 8-line prefix-rename block.
The byte-table additions from loops/lua (__ascii-tok, __lua-127-255-tok,
lua-byte-to-char) are preserved at the top of tokenizer.sx — those provide
Lua's 8-bit-clean string semantics on top of the shared lex layer.
test.sh updated to preload lib/guest/lex.sx + lib/guest/prefix.sx before
lua sources, matching the load order arch's pre-merge test.sh used.
393/395 maintained. The 2 pre-existing failures are unrelated:
- math.random(n) primitive arity issue
- os.clock returns rational instead of number (SX division semantics)
Skipped from the planned follow-up: delay/force port. Arch's lua-force was
defined but never referenced anywhere — dead code, not worth porting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates Phase 7 status:
- env.sx ✓ extracted (three live consumers: Kernel, Tcl, Smalltalk,
with Scheme also using it directly)
- class-chain.sx ✓ extracted (bonus — not on the original six-file
list but surfaced by the same chiselling discipline; Smalltalk +
CLOS consumers)
- quoting.sx ✓ extracted (Kernel + Scheme consumers)
- evaluator.sx DECLINED — too thin to be its own kit; the shared
content is protocol/API surface, not algorithm. Documented
in-plan, no file created.
- combiner.sx, short-circuit.sx — still need fexpr-having
second consumers
- hygiene.sx — still awaits Scheme Phase 6c (research-grade
scope-set work)
Three kits live, one declined, three still gated.
lib/guest/reflective/quoting.sx — quasiquote walker with adapter cfg.
Three forms:
- refl-quasi-walk-with CFG FORM ENV (top-level)
- refl-quasi-walk-list-with CFG FORMS ENV (list walker, splice-aware)
- refl-quasi-list-concat XS YS (pure-SX helper)
Adapter cfg keys:
- :unquote-name — string keyword ("$unquote" or "unquote")
- :unquote-splicing-name — string keyword
- :eval — fn (form env) → value
The shared algorithm is identical in Kernel and Scheme; the only
divergences are the keyword names (`$unquote` vs `unquote`) and
which host evaluator runs at unquote points (`kernel-eval` vs
`scheme-eval`). Both surface through the cfg.
Migrations:
- lib/kernel/runtime.sx: knl-quasi-walk reduces to a 3-line wrapper
that builds knl-quasi-cfg and delegates. Removed knl-quasi-walk-
list + knl-list-concat (~40 LoC) — now provided by the kit.
- lib/scheme/eval.sx: scm-quasi-walk reduces to a 3-line wrapper
around scm-quasi-cfg. Removed scm-quasi-walk-list + scm-list-
concat. scm-collect-exports (module impl) was a hidden consumer
of scm-list-concat — rewired to refl-quasi-list-concat.
lib/scheme/test.sh — loads lib/guest/reflective/quoting.sx before
lib/scheme/parser.sx so the kit is available when eval.sx loads.
Both consumers' tests green:
- Kernel: 322 tests across 7 suites
- Scheme: 296 tests across 9 suites
**Second reflective-kit extraction landed.** The kit-extraction
playbook from env.sx and class-chain.sx — adapter-cfg pattern from
lib/guest/match.sx, same algorithm bridges different keyword names —
works again on a third structurally different problem (quasiquote
walking). The cumulative extraction story: env.sx → class-chain.sx
→ quoting.sx, three independent kits, all using the same pattern.
`evaluator.sx` (the other deferred candidate the Scheme port
unlocked) is NOT extracted — the genuinely shared content is too
thin (one helper for closure-capturing interaction-environment).
The eval-protocol is more about API surface than algorithm.
Documented as a non-extraction.
Two additions from loops/hs needed for the new WebSocket socket tests:
- unhandledRejection suppressor — synchronous test harness doesn't await RPC promises
- Fake setTimeout/clearTimeout + __hsFlushTimers — drain RPC timeout tests synchronously
Plan update: mark E36 WebSocket as DONE (previously "design-done, pending review").
Skipped: loops/hs's tests/playwright/generate-sx-tests.py — architecture's version
is 1468 lines vs loops/hs's 290; arch's is the further-evolved version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loop closer documenting what 10 feature commits landed across the
session. Phase-by-phase outcomes captured, including the SX cond
multi-expression bug found and fixed during Phase 4.
Chisel ledger:
- env.sx already EXTRACTED with Scheme as third consumer
- evaluator.sx + quoting.sx second-consumer-ready for follow-on
kit-extraction commits
- hygiene.sx still awaits the deferred Phase 6c (scope-set work)
- combiner.sx and short-circuit.sx don't apply (Scheme has no
fexprs and uses syntactic and/or)
Deferred phases listed: full hygiene, nested quasi-depth, R7RS
module rich features, dotted-pair syntax, full call/cc-wind
interaction.
Loop's defining feature: lib/guest CHISELLING discipline — every
commit had a chisel note, and the cumulative work satisfies the
two-consumer rule for three new kit extractions.
lib/scheme/test.sh — single-process test runner. Loads parser/eval/
runtime + lib/guest/reflective/env.sx once, then for each test
suite loads its file and calls its (*-tests-run!) function. Parses
the {:passed N :failed N ...} dict output and aggregates.
Usage:
bash lib/scheme/test.sh # summary
bash lib/scheme/test.sh -v # per-suite breakdown
Output: "ok 296/296 scheme-on-sx tests passed (9 suites)"
lib/scheme/scoreboard.md — per-suite passing counts, phase status,
deferred items, reflective-kit consumption ledger.
The scoreboard documents the chisel value of the Scheme port:
three reflective kits unlocked (env.sx — already extracted with
Scheme as third consumer; evaluator.sx + quoting.sx — second-
consumer-ready for extraction whenever a follow-up commit is run).
Loop status: 11 phases done (1, 2, 3, 3.5, 4, 5abc, 6ab, 7, 8, 9,
10, 11). Two deferred (6c hygiene, full call/cc-wind interaction).
296 tests, 1830 LoC of Scheme implementation. Zero substrate fixes
required across the loop.
eval.sx adds module support:
(define-library NAME EXPR...)
Where EXPR is one of:
(export NAME ...)
(import LIB-NAME ...)
(begin BODY ...)
(import LIB-NAME ...)
Looks up each library by key, copies its exported names
into the current env.
Library values: {:scm-tag :library :name :exports :env}
Stored in scheme-library-registry keyed by joined library-name
(`(my math)` → `"my/math"`).
Library body runs in a FRESH standard env (each library is its
own namespace). Only :exports are visible after import; private
internal definitions stay in the library's env. Internal calls
between library functions use the library's env, so public-facing
exports can rely on private helpers.
Multiple imports work — each library is independent.
NOT yet supported: cond-expand, include, include-library-
declarations, renaming (`(only ...)`, `(except ...)`, `(prefix ...)`,
`(rename ...)`). Standard R7RS modules use these but the core
two-operation flow (define-library / import) covers most everyday
module use.
7 tests: single export, multi-export, private-not-visible,
internal-calls-private, two-libs-both-imported, unknown-lib-error,
single-symbol library name.
296 total Scheme tests (62+23+49+78+25+20+13+10+9+7).
Phases done: 1, 2, 3, 3.5, 4, 5abc, 6ab, 7, 8, 9, 10.
Deferred: 6c (hygiene/scope-set — research-grade), 11 (conformance).
eval.sx adds the define-record-type syntactic operator:
(define-record-type NAME
(CONSTRUCTOR ARG...)
PREDICATE
(FIELD ACCESSOR [MUTATOR])...)
Records are tagged dicts:
{:scm-record TYPE-NAME :fields {FIELD VALUE ...}}
For each record type, the operator binds:
- Constructor: takes the listed ARGs, populates :fields, returns
the record. Fields not in CONSTRUCTOR ARGs default to nil.
- Predicate: returns true iff its arg is a record of THIS type
(tag-match via :scm-record).
- Accessor per field: extracts the field value; errors if not
a record of the right type.
- Mutator per field (optional): sets the field via dict-set!;
same type-check.
Distinct types are isolated via their tag — point? returns false
on a circle, even if both have the same shape.
9 tests cover: constructor + predicate + accessors, mutator,
distinct-types-via-tag, records as first-class values (in lists,
passed to map/filter), constructor arity errors.
289 total Scheme tests (62+23+49+78+25+20+13+10+9).
eval.sx adds quasiquote / unquote / unquote-splicing as syntactic
operators with the canonical R7RS walker:
- (quasiquote X) — top-level entry to scm-quasi-walk
- (unquote X) — at depth-0, evaluates X in env
- (unquote-splicing X) — inside a list, splices X's list value
- Reader-macro sugar: `X / ,X / ,@X work via Phase 1 parser
Algorithm identical to lib/kernel/runtime.sx's knl-quasi-walk:
- Walk template recursively
- Non-list: pass through
- ($unquote/unquote X) head form: eval X
- Inside a list, ($unquote-splicing/unquote-splicing X) head:
eval X, splice list into surrounding context
- Otherwise: recurse on each element
No depth-tracking yet — nested quasiquotes are not properly
handled (matches Kernel's deferred state).
10 tests: plain atom/list, unquote substitution, splicing at
start/middle/end, nested list with unquote, unquote evaluates
expression, error on non-list splice, error on bare unquote.
**Second consumer for lib/guest/reflective/quoting.sx unlocked.**
Both Kernel and Scheme have structurally identical walkers; the
extraction would parameterise just the unquote/splicing keyword
names (Kernel uses $unquote / $unquote-splicing; Scheme uses
unquote / unquote-splicing — pure cfg, no algorithmic change).
280 total Scheme tests (62+23+49+78+25+20+13+10).
Three reflective-kit extractions unlocked in this Scheme port:
- env.sx — Phase 2 (consumed directly, third overall consumer)
- evaluator.sx — Phase 7 (second consumer via eval/interaction-env)
- quoting.sx — Phase 10 (second consumer via scm-quasi-walk)
The kit extractions themselves remain follow-on commits when
desired. hygiene.sx still awaits a real second consumer
(Scheme phase 6c with scope-set algorithm).
runtime.sx binds R7RS reflective primitives:
- eval EXPR ENV
- interaction-environment — returns env captured by closure
- null-environment VERSION — fresh empty env (ignores version)
- scheme-report-environment N — fresh full standard env
- environment? V
interaction-environment closes over the standard env being built;
each invocation of scheme-standard-env produces a distinct
interaction env that returns ITSELF when queried — so user-side
(define name expr) inside (eval ... (interaction-environment))
persists for subsequent (eval 'name ...) lookups.
13 tests cover:
- eval over quoted forms (literal + constructed via list)
- define-then-lookup through interaction-environment
- eqv? identity of interaction-environment across calls
- sandbox semantics: eval in null-environment errors on +
- scheme-report-environment is fresh and distinct from interaction
**Second consumer for lib/guest/reflective/evaluator.sx unlocked.**
Scheme's eval/interaction-environment/null-environment triple is
the same protocol Kernel exposes via eval-applicative /
get-current-environment / make-environment. Extraction now
satisfies the two-consumer rule — same playbook as env.sx and
class-chain.sx, awaits a follow-up commit to actually extract
the kit.
270 total Scheme tests (62 + 23 + 49 + 78 + 25 + 20 + 13).
scm-match-list now detects `<pat> ...` at the END of a pattern list
and binds <pat> (must be a symbol — single-variable rest) to the
remaining forms as a list. Nested-list patterns under ellipsis and
middle-of-list ellipses are NOT supported yet (rare in practice;
deferred).
scm-instantiate-list mirrors: when it encounters `<var> ... `
inside a list template, it splices the list-valued binding of <var>
in place. Internal list-append-all helper for the splice.
Removes the `(length pat) = (length form)` strict-equality check
in scm-match-step's list case — that gate blocked ellipsis. The
length-1-or-more relaxed check now lives in scm-match-list itself.
8 ellipsis tests cover:
- Empty rest (my-list)
- Non-empty rest (my-list 1 2 3 4)
- my-when with multi-body
- Variadic sum-em via fold-left
- Recursive my-and pattern (short-circuit AND defined as macro)
257 total Scheme tests (62 + 23 + 49 + 78 + 25 + 20).
Phase 6c (proper hygiene) is the next step and will be the
**second consumer for lib/guest/reflective/hygiene.sx** — the
deferred research-grade kit from the kernel-on-sx loop.
eval.sx adds macro infrastructure:
- {:scm-tag :macro :literals (LIT...) :rules ((PAT TMPL)...) :env E}
- scheme-macro? predicate
- scm-match / scm-match-list — pattern matching against literals,
pattern variables, and structural list shapes
- scm-instantiate — template substitution with bindings
- scm-expand-rules — try each rule in order
- (syntax-rules (LITS) (PAT TMPL)...) → macro value
- (define-syntax NAME FORM) → bind macro in env
- scheme-eval: when head looks up to a macro, expand and re-eval
Pattern matching supports:
- _ → match anything, no bind
- literal symbols from the LITERALS list → must equal-match
- other symbols → pattern variables, bind to matched form
- list patterns → must be same length, each element matches
NO ellipsis (`...`) support yet — that's Phase 6b. NO hygiene
yet (introduced symbols can shadow caller bindings) — that's
Phase 6c, which will be the second consumer for
lib/guest/reflective/hygiene.sx.
12 tests cover: simple substitution, multi-rule selection,
nested macro use, swap-idiom (state mutation via set!), control-
flow wrappers, literal-keyword pattern matching, macros inside
lambdas.
249 total Scheme tests now (62 + 23 + 49 + 78 + 25 + 12).
(dynamic-wind BEFORE THUNK AFTER)
- Calls BEFORE; runs THUNK; calls AFTER; returns THUNK's value.
- If THUNK raises, AFTER still runs before the raise propagates.
- Implementation: outcome-sentinel pattern (same trick as guard
and with-exception-handler) — catch THUNK's raise inside a
host guard, run AFTER unconditionally, then either return the
value or re-raise outside the catch.
Not implemented: call/cc-escape tracking. R7RS specifies that
dynamic-wind's BEFORE and AFTER thunks should re-run when control
re-enters or exits the dynamic extent via continuations. That
requires explicit dynamic-extent stack tracking, deferred until
a consumer needs it (probably never needed for pure-eval Scheme
programs; matters for first-class-continuation-heavy code).
5 tests: success ordering, return value, after-on-raise,
raise propagation, nested wind.
237 total Scheme tests now (62 + 23 + 49 + 78 + 25).
eval.sx adds the `guard` syntactic operator with R7RS-compliant
clause dispatch: var binds to raised value in a fresh child env;
clauses tried in order; `else` is catch-all; no match re-raises.
Implementation uses a "catch-once-then-handle-outside" pattern to
avoid the handler self-raise loop:
outcome = host-guard {body} ;; tag raise vs success
if outcome was raise:
try clauses → either result or sentinel
if sentinel: re-raise OUTSIDE the host-guard scope
runtime.sx binds R7RS exception primitives:
- raise V
- error MSG IRRITANT... → {:scm-error MSG :irritants LIST}
- error-object?, error-object-message, error-object-irritants
- with-exception-handler HANDLER THUNK
(same outcome-sentinel pattern — handler's own raises propagate
outward instead of re-entering)
12 tests cover: catch on raise, predicate dispatch, else catch-all,
no-error pass-through, first-clause-wins, re-raise-on-no-match,
error-object construction and accessors.
232 total Scheme tests now (62 + 23 + 49 + 78 + 20).
scheme-standard-env binds:
- call/cc — primary
- call-with-current-continuation — alias
Implementation wraps SX's host call/cc, presenting the captured
continuation k as a Scheme procedure that accepts a single value
(or a list of values for multi-arg invocation). Single-shot
escape semantics: when k is invoked, control jumps out of the
surrounding call/cc form. Multi-shot re-entry isn't safely
testable without delimited-continuation infrastructure (the
captured continuation re-enters indefinitely if invoked after
the call/cc returns) — deferred to a follow-up commit if needed.
Tests cover:
- No-escape return value
- Escape past arithmetic frames
- Detect/early-exit idiom over for-each
- Procedure? on the captured k
220 total Scheme tests now (62 + 23 + 49 + 78 + 8).
Adopts loops/hs's cleaner WebSocket API on top of arch's hyperscript:
- Runtime: replace 5 arch socket functions (hs-try-json-parse, hs-socket-normalise-url,
hs-socket-bind-name!, hs-socket-resolve-rpc!, hs-socket-register!) with loops/hs's
versions. Wrapper fields now use external-style names (url, timeout, pending, handler,
json?, closedFlag, dispatchEvent) instead of internal-style underscores (_url,
_timeout, _pending, _hsSetupSocket).
- Tests: replace arch's 257-line hs-upstream-socket suite (which probed _pending,
_hsSetupSocket etc.) with loops/hs's 162-line suite that checks the new field names.
Both suites cover the same 16 E36 behavioral cases.
Parser/compiler unchanged: both branches emit (hs-socket-register! name-path url
timeout handler json?) so the call signature is compatible with either runtime.
Arch's parse-socket-feat / emit-socket are preserved.
Local hs test.sh: 23/25 (the 2 failures are pre-existing hide/show cmd compiler
issues, not socket-related).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lib/scheme/runtime.sx — full R7RS-base surface:
- Arithmetic: variadic +/-/*//, abs, min, max, modulo, quotient,
remainder. Predicates zero?/positive?/negative?.
- Comparison: chained =/</>/<=/>=.
- Type predicates: number?/boolean?/symbol?/string?/char?/vector?/
null?/pair?/procedure?/not.
- List: cons/car/cdr/list/length/reverse/append.
- Higher-order: map/filter/fold-left/fold-right/for-each/apply.
These re-enter scheme-apply to invoke user-supplied procs.
- String: string-length/string=?/string-append/substring.
- Char: char=?.
- Vector: vector/vector-length/vector-ref/vector->list/list->vector/
make-vector.
- Equality: eqv?/equal?/eq? (all = under the hood for now).
Built via small adapters: scm-unary, scm-binary, scm-fold (variadic
left-fold with identity + one-arity special), scm-chain (n-ary
chained comparison).
**Bugfix in eval.sx set! handler.** The :else branch had two
expressions `(dict-set! ...) val` — SX cond branches don't run
multiple expressions, they return nil silently (or evaluate only
the first, depending on shape). Wrapped in (begin ...) to force
sequential execution. This fix also unblocks 4 set!-dependent
tests in lib/scheme/tests/syntax.sx that were silently raising
during load (and thus not counted) — syntax test count jumps
from 45 → 49.
Classic programs verified:
- factorial 10 → 3628800
- fib 10 → 55
- recursive list reverse → working
- sum of squares via fold-left + map → 55
212 total Scheme tests: parse 62 + eval 23 + syntax 49 + runtime 78.
All green.
The env-as-value section in runtime tests demonstrates
scheme-standard-env IS a refl-env? — kit primitives operate on it
directly, confirming the third-consumer adoption with zero adapter.
Adds the rest of the standard syntactic operators, all built on the
existing eval/closure infrastructure from Phase 3:
- let — parallel bindings in fresh child env; values evaluated in
outer env (RHS sees pre-let bindings only). Multi-body via
scheme-eval-body.
- let* — sequential bindings, each in a nested child env; later
bindings see earlier ones.
- cond — clauses walked in order; first truthy test wins. `else`
symbol is the catch-all. Test-only clauses (no body) return the
test value. Scheme truthiness: only #f is false.
- when / unless — single-test conditional execution, multi-body
body via scheme-eval-body.
- and / or — short-circuit boolean. Empty `(and)` = true,
`(or)` = false. Both return the actual value at the point
of short-circuit (not coerced to bool), matching R7RS.
130 total Scheme tests (62 parse + 23 eval + 45 syntax). The
Scheme port is now self-hosting enough to write any non-stdlib
program — factorial, list operations via primitives, closures
with mutable state, all working.
Next phase: standard env (runtime.sx) with variadic +/-, list
ops as Scheme-visible applicatives.
eval.sx grows: five new syntactic operators wired via the table-
driven dispatch from Phase 2. lambda creates closures
{:scm-tag :closure :params :rest :body :env} that capture the
static env; scheme-apply-closure binds formals + rest-arg, evaluates
multi-expression body in (extend static-env), returns last value.
Supports lambda formals shapes:
() → no args
(a b c) → fixed arity
args → bare symbol; binds all call-args as a list
Dotted-pair tail (a b . rest) deferred until parser supports it.
define has both flavours:
(define name expr) — direct binding
(define (name . formals) body...) — lambda sugar
set! walks the env chain via refl-env-find-frame, mutates at the
binding's source frame (no shadowing). Raises on unbound name.
24 new tests in lib/scheme/tests/syntax.sx, including:
- Factorial 5 → 120 and 10 → 3628800 (recursion + closures)
- make-counter via closed-over set! state
- Curried (((curry+ 1) 2) 3) → 6
- (lambda args args) rest-arg binding
- Multi-body lambdas with internal define
109 total Scheme tests (62 parse + 23 eval + 24 syntax).
lib/scheme/eval.sx — R7RS evaluator skeleton:
- Self-evaluating: numbers, booleans, characters, vectors, strings
- Symbol lookup: refl-env-lookup
- Lists: syntactic-operator table dispatch, else applicative call
- Table-driven syntactic ops (Phase 2 wires `quote` only; full set
in Phase 3)
- Apply: callable host fn or scheme closure (closure stub for Phase 3)
scheme-make-env / scheme-env-bind! / etc. are THIN ALIASES for the
refl-env-* primitives from lib/guest/reflective/env.sx. No adapter
cfg needed — Scheme's lexical-scope semantics ARE the canonical
wire shape. This is the THIRD CONSUMER for env.sx after Kernel and
Tcl + Smalltalk's variant adapters; the first to use it without
any bridging code. Validates the kit handles canonical-shape
adoption with zero ceremony.
23 tests in lib/scheme/tests/eval.sx cover literals, symbol
lookup with parent-chain shadowing, quote (special form + sugar),
primitive application with nested calls, and an env-as-value
section explicitly demonstrating the kit primitives work on
Scheme envs.
85 total Scheme tests (62 parse + 23 eval).
chisel: consumes-env (third consumer for lib/guest/reflective/env.sx).
11-phase plan from parser through R7RS conformance. Explicitly maps
which reflective kits Scheme consumes:
- env.sx (Phase 2) — third consumer, no cfg needed
- evaluator.sx (Phase 7) — second consumer, unblocks extraction
- hygiene.sx (Phase 6) — second consumer, drives the deferred
scope-set / lifted-symbol work
- quoting.sx (Phase 10) — second consumer, unblocks extraction
- combiner.sx — N/A (Scheme has no fexprs)
Correction to earlier session claim: a Scheme port unlocks THREE
more reflective kits, not four. combiner.sx stays Kernel-only.
The OCaml epoch-protocol printer serializes raw SX dicts. JS object literals
now carry __proto__ / __js_order__ bookkeeping that points into Object.prototype,
a complex dict containing lambdas that close over Object — the printer
recurses indefinitely and hangs.
js-display walks the value once, dropping any dict key that matches the
__name__ dunder convention. js-eval calls it on its return value so the
output is the user-facing shape only. Restores 587/593 passing (up from
191/593 post-merge and 492/585 pre-merge) — the surviving 6 failures are
legitimate pre-existing test mismatches (illegal return/break/continue,
parseFloat float vs rational, escaped backtick).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new WASM ABI wraps numbers, strings, and other atoms as opaque
value-handles ({_type, __sx_handle}) inside the perform request args.
The io-wait-event mock checks typeof against 'number' and 'string'
directly, so under the new ABI:
- typeof timeout === 'number' → false (timeout is a handle)
- typeof items[2] === 'string' → false (event name is a handle)
so the "timeout wins" branch never triggered, and the test fell into
the "neither timeout nor event" else that resumed with nil but never
fired the post-wait `then add .bar` command.
Apply _unwrapHandle to the three args (target, evName, timeout) before
the type checks. This is the same pattern the rest of the host-* native
sweep already follows (commit 29ef89d4).
Effect: hs-upstream-wait goes from 5/7 → 7/7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records that the 1514/1514 claim was relative to the kernel as of
92619301; the value-handle ABI + numeric tower + JIT Phase 2 commits
introduced three regressions (1 dict-eq, now fixed in 4db1f85f, and 2
event-or-timeout wait tests still pending).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related kernel bugs were causing the HS conformance test
"arrays containing objects work" to fail with the misleading message
"Expected ({:a 1} {:b 2}) but got ({:a 1} {:b 2})".
1. sx_primitives.ml safe_eq: Dict/Dict only returned true for DOM-wrapped
dicts (those carrying __host_handle); all other dict pairs returned
false unconditionally. Plain dict literals can never have been =
to each other. Add the structural-equality fallback: when neither
side has a host handle, compare lengths and walk keys.
2. sx_browser.ml deep_equal (the kernel binding for equal?): had a
Number/Number branch but no Integer/Integer or cross-Integer/Number
branches, so since the numeric tower change Integer 1 vs Integer 1
was falling through to the catch-all and returning false. Mirror the
cases from run_tests.ml deep_equal which already had them.
Verified via direct kernel probe:
(= {:a 1} {:a 1}) => true (was false)
(= {:a 1 :b 2} {:b 2 :a 1}) => true (was false)
(equal? 1 1) => true (was false)
(equal? {:a 1} {:a 1}) => true (was false)
(equal? (list {:a 1}) (list {:a 1})) => true (was false)
HS suite arrayLiteral: 7/8 → 8/8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the extraction (Smalltalk + CLOS migrated, kit landed,
counts unchanged), lists plausible third consumers (JS proto chain,
Ruby ancestors, Python MRO), and notes which other patterns stayed
unextracted and why (method-cache invalidation, inline cache, and
the five reflective siblings all need consumers that don't exist
yet in the codebase).
Closes the session's extraction work at five branches: env (3
consumers), class-chain (2), test-runner (POC), plus the chain
of intermediate branches. The Scheme port is the next high-leverage
move; it would unlock four more reflective kits in one stroke.
lib/guest/reflective/class-chain.sx — class inheritance walker with
adapter cfg for single-parent (Smalltalk) and multi-parent (CLOS)
hierarchies. Three primitives:
- refl-class-chain-find-with CFG CN PROBE
DFS through parents, returns first non-nil probe result.
Smalltalk method lookup uses this.
- refl-class-chain-depth-with CFG CN ANCESTOR
Min hop distance via any parent path, or nil if unreachable.
CLOS method specificity uses this.
- refl-class-chain-ancestors-with CFG CN
Flat DFS-ordered list of all reachable ancestor names.
Adapter cfg has two keys: :parents-of (CN → list of parent names,
possibly empty) and :class? (predicate; short-circuits walk on
non-existent class names mid-chain).
Migrations:
- lib/smalltalk/runtime.sx: st-method-lookup-walk now a 9-line
thin probe through the kit (was 20 lines of inline recursion);
st-class-cfg wraps the single-parent :superclass field into a
1-element list for the cfg.
- lib/common-lisp/clos.sx: clos-specificity is a one-line wrapper
around refl-class-chain-depth-with (was 28 lines); clos-class-cfg
reads the multi-parent :parents field.
Both consumers green:
- Smalltalk: 847/847 (unchanged)
- CL: 222/240 (unchanged baseline; 18 pre-existing failures, all
in stdlib functions like cl-set-memberp, unrelated to CLOS).
This is the second extracted reflective kit (env.sx was first).
The adapter-cfg pattern continues to bridge structurally divergent
consumers (Smalltalk single-inheritance vs CLOS multiple-inheritance
with method-precedence distance) via a uniform :parents-of callback.
The shared/static/wasm/sx_browser.bc.js artifact now reflects the OCaml
kernel with JIT Phase 1 (tiered compilation), Phase 2 (LRU eviction),
and Phase 3 (manual reset) — same source as previously committed,
just the rebuilt binary so test/dev consumers pick it up without
needing a local sx_build.
tests/hs-run-batched.js: TOTAL default 1496 → 1514. The conformance
suite grew by 18 tests since the constant was last set; without this
the batched runner stops short of the final 14 tests.
Verified via batched run (75-test batches, parallelism=2):
1436 / 1439 reported pass (3 failures, all in suites where the
underlying parser/dict-equality gap is independent of WASM).
Batch 150-225 didn't complete inside 15 min — 75 reactivity /
regressions / runtime tests at 5-11s each blow past the wall; a
per-batch deadline raise is the right knob, not a kernel change.
Per-test timing (new vs old WASM, slice 170-195) is comparable
(60s vs 78s on new/threshold=4 — Phase 1+2 is NOT a perf regression
on HS code; the slow tests are slow on both kernels because the
underlying CEK path doesn't get JIT-compiled either way — HS emits
anonymous lambdas that bypass the named-only JIT gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>