lib/guest/reflective/env.sx — added refl-env-find-frame-with (returns
the scope where NAME is bound, or nil). Needed by consumers like
Smalltalk that mutate variables at the source frame rather than
shadowing at the current one. Also added refl-env-find-frame for
the canonical shape.
lib/smalltalk/eval.sx — new st-frame-cfg adapter for the kit.
st-lookup-local now delegates parent-walk to refl-env-find-frame-with
while preserving its Smalltalk-flavoured {:found :value :frame}
return shape (which is used to mutate at the binding's source
frame, not the current one).
lib/smalltalk/test.sh + compare.sh — load lib/guest/reflective/env.sx
before lib/smalltalk/eval.sx.
Three genuinely different wire shapes now share the parent-walk:
- Kernel: {:refl-tag :env :bindings :parent} mutable bindings
- Tcl: {:level :locals :parent} functional update
- Smalltalk: {:self :method-class :locals :parent mutable bindings,
:return-k :active-cell} rich metadata
All three consumers' full test suites unchanged: Smalltalk 847/847,
Kernel 322/322, Tcl 427/427. The cfg adapter pattern (modelled after
lib/guest/match.sx) cleanly handles all three.
Phase 1 of the lib-guest-reflective extraction plan.
lib/guest/reflective/env.sx — canonical wire shape
{:refl-tag :env :bindings DICT :parent ENV-OR-NIL} with mutable
defaults (dict-set!), plus *-with adapter-cfg variants for consumers
with their own shape (modelled after lib/guest/match.sx). 13 forms,
~5 KB.
lib/kernel/eval.sx — env block collapses from ~30 lines to 6 thin
wrappers (kernel-env? = refl-env?, etc.). No semantic change; envs
now carry :refl-tag :env instead of :knl-tag :env. All 322 Kernel
tests pass unchanged across 7 suites (parse 62, eval 36, vau 38,
standard 127, encap 19, hygiene 26, metacircular 14).
Next: Phase 2 — Tcl adapter cfg in lib/tcl/runtime.sx using
refl-env-lookup-with against the existing :level/:locals/:parent
frame shape.
Ships the algebra for HM-style type inference, riding on
lib/guest/match.sx (terms + unify) and ast.sx (canonical AST):
• Type constructors: hm-tv, hm-arrow, hm-con, hm-int, hm-bool, hm-string
• Schemes: hm-scheme / hm-monotype + accessors
• Free type-vars: hm-ftv, hm-ftv-scheme, hm-ftv-env
• Substitution: hm-apply, hm-apply-scheme, hm-apply-env, hm-compose
• Generalize / Instantiate (with shared fresh-tv counter)
• hm-fresh-tv (counter is a (list N) the caller threads)
• hm-infer-literal (the only fully-closed inference rule)
24 self-tests in lib/guest/tests/hm.sx covering every function above.
The lambda / app / let inference rules — the substitution-threading
core of Algorithm W — intentionally live in HOST CODE rather than the
kit, because each host's AST shape and substitution-threading idiom
differ subtly enough that forcing one shared assembly here proved
brittle in practice (an earlier inline-assembled hm-infer faulted with
"Not callable: nil" only when defined in the kit, despite working when
inline-eval'd or in a separate file — a load/closure interaction not
worth chasing inside this step's budget). The host gets the algebra
plus a spec; assembly stays close to the AST it reasons over.
PARTIAL — algebra + literal rule shipped; full Algorithm W deferred
to host consumers (haskell/infer.sx, lib/ocaml/types.sx when
OCaml-on-SX Phase 5 lands per the brief's sequencing note). Haskell
infer.sx untouched; haskell scoreboard still 156/156 baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configurable layout pass that inserts virtual open / close / separator
tokens based on indentation. Supports both styles the brief calls out:
• Haskell-flavour: layout opens AFTER a reserved keyword
(let/where/do/of) and resolves to the next token's column. Module
prelude wraps the whole input in an implicit block. Explicit `{`
after the keyword suppresses virtual layout.
• Python-flavour: layout opens via an :open-trailing-fn predicate
fired AFTER the trigger token (e.g. trailing `:`) — and resolves
to the column of the next token, which in real source is on a
fresh line. No module prelude.
Public entry: (layout-pass cfg tokens). Token shape: dict with at
least :type :value :line :col; everything else passes through. Newline
filler tokens are NOT used — line-break detection is via :line.
lib/guest/tests/layout.sx — 6 tests covering both flavours:
haskell-do-block / haskell-explicit-brace / haskell-do-inline /
haskell-module-prelude / python-if-block / python-nested.
Per the brief's gotcha note ("Don't ship lib/guest/layout.sx unless
the haskell scoreboard equals baseline") — haskell/layout.sx is left
UNTOUCHED. The kit isn't yet a drop-in replacement for the full
Haskell 98 algorithm (Note 5, multi-stage pre-pass, etc.) and forcing
a port would risk the 156 currently passing programs. Haskell
scoreboard remains at 156/156 baseline because no haskell file
changed. The synthetic Python-ish fixture is the second consumer per
the brief's wording.
PARTIAL — kit + synthetic fixture shipped; haskell port deferred until
the kit grows the missing Haskell-98 wrinkles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-functional pattern-match + unification, shipped for miniKanren
(minikraken) / Datalog and any other logic-flavoured guest that wants
immutable unification without writing it from scratch.
Canonical wire format (config callbacks let other shapes plug in):
var (:var NAME)
constructor (:ctor HEAD ARGS)
literal number / string / boolean / nil
Public API:
empty-subst walk walk* extend occurs?
unify (symmetric, with occurs check)
unify-with (cfg-driven for non-canonical term shapes)
match-pat (asymmetric pattern→value, vars only in pattern)
match-pat-with (cfg-driven)
lib/guest/tests/match.sx — 25 tests covering walk chains, occurs,
unify (literal/var/ctor, head + arity mismatch, transitive vars),
match-pat. All passing.
The brief flagged this as the highest-risk step ("revert and redesign
on any regression"). The two existing engines — haskell/match.sx
(pure asymmetric, lazy, returns env-or-nil) and prolog runtime.sx
pl-unify! (mutating symmetric, trail-based, returns bool) — are
structurally divergent and forcing a shared core under either of their
contracts would risk the 746 tests they currently pass. Both are
untouched; they remain at baseline (haskell 156/156, prolog 590/590)
because none of their source files were modified.
PARTIAL — kit shipped, prolog/haskell ports deferred until a guest
chooses to migrate or until a third consumer (minikraken / datalog)
provides a less risky migration path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defines the 10 canonical node kinds called out in the brief — literal,
var, app, lambda, let, letrec, if, match-clause, module, import — plus
predicates, ast-kind dispatch, and per-field accessors. Each node is a
tagged keyword-headed list: (:literal V), (:var N), (:app FN ARGS), …
Also lib/guest/tests/ast.sx — 33 tests exercising every constructor +
predicate + accessor, runnable via (gast-tests-run!) which returns the
{:passed :failed :total} dict the shared conformance driver expects.
PARTIAL — pending real consumers. The brief calls Step 5 "Optional —
guests may keep their own AST" and forcing lua/prolog to switch their
internal AST shape risks regressing 775 passing tests for tooling that
nothing yet calls. Both internal ASTs are untouched; lua still 185/185,
prolog still 590/590. Datalog-on-sx (in flight, see plans/datalog-on-sx.md)
will be the natural first real consumer; lua/prolog converters can land
when a cross-language tool wants them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracted the data-half of Pratt-style precedence parsing: the operator
table format and lookup. The climbing loop stays per-language because
the two canaries use opposite conventions (lua: higher prec = tighter;
prolog: lower prec = tighter, with xfx/xfy/yfx assoc tags) — forcing
one shared loop adds callback indirection that obscures more than it
shares. The brief's literal ask is "Grammar is a dict, not hardcoded
cond" and that's what gets shared.
Entry shape: (NAME PREC ASSOC). Three accessors: pratt-op-name /
pratt-op-prec / pratt-op-assoc. One traversal: pratt-op-lookup.
Ported lua/parser.sx — replaced 18-clause cond and the
lua-binop-right? hardcoded `or` with a 15-entry lua-op-table, now
queried via pratt-op-lookup. Ported prolog/parser.sx — pl-op-find
(linear walk reimpl) deleted; pl-op-lookup wraps pratt-op-lookup;
pl-token-op simplified to return the entry directly.
Verification:
- lua/test.sh: 185/185 = baseline.
- prolog/conformance.sh: 590/590 = baseline (timestamp-only diff).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>