Adds the bracket postfix branch:
a[0] / a[i] / a[i+1] / m["key"] → (list :index OBJ IDX)
a[:] / a[1:] / a[:2] / a[1:2] / a[1:2:3] → (list :slice OBJ LOW HIGH MAX)
LOW/HIGH/MAX are AST nodes or nil for omitted indices. The 4th MAX
slot is only populated by the three-index full-slice form.
Two new lib/guest/ast.sx kit gaps surfaced (logged in plans/go-on-sx.md
Blockers):
* No :index node — universal across guests with arrays/maps.
* No :slice node — Python/Rust/Swift/JS/Ruby all need at minimum the
two-index form. Go's three-index variant is more specialised but
fits in the same shape with an optional fourth slot.
Parser is permissive on a[1::3] (strict Go rejects, but the type phase
can enforce the grammar; lexer/parser stays loose).
Chained (a[0][1]) and mixed-with-selector (a[0].field) cases work via
the existing left-associative postfix loop.
parse 61/61, total 190/190.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds postfix expression forms per Go spec:
f() f(x) f(x, y, z) — function calls
x.y x.y.z obj.method(x) — selector / member access
gp-parse-postfix sits between gp-parse-unary and gp-parse-primary,
so calls and selectors bind tighter than any unary prefix — `-f(x)`
parses as `-(f(x))`, not `(-f)(x)`. Postfix is left-associative
(`x.y.z` = `(x.y).z`), so the loop iterates rather than recurses
on the LHS.
AST shapes:
Call: (ast-app FN ARGS) — canonical
Selector: (list :select OBJ "field") — Go-specific tag
The selector shape is a kit gap — lib/guest/ast.sx ships ast-app but
no ast-select, despite `obj.field` being universal across Go, Rust,
Swift, TS, JS, Python, Ruby, Java, C#. Logged in Blockers; tagging
[proposes-ast]. Worth promoting on the next nominally-typed guest.
parse 49/49, total 178/178.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go unary prefix operators per Go spec § Operators:
+x -x !x ^x *p &v <-ch
gp-parse-unary is recursive (so !!x and -^x chain correctly) and
sits between gp-parse-expr and gp-parse-primary — unary therefore
always binds tighter than any binary op without needing a unary
entry in the precedence table.
Symbols +, -, *, &, ^ are shared between unary and binary forms;
the positional split (expression-start sees unary, mid-expression
sees binary) disambiguates them cleanly with no lookback.
Unary nodes are single-arg ast-app:
(ast-app (ast-var OP) (list OPERAND))
parse 37/37, total 166/166.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gp-parse-expr / gp-pratt-loop implement classic Pratt climbing
against go-precedence-table (entry shape from lib/guest/pratt.sx).
The kit gives us pratt-op-lookup + accessors; the climbing loop
itself stays per-language (per kit header — Lua and Prolog have
opposite conventions).
Left-associative ops raise the right-recursion min by 1; right-
associative would keep prec. All Go binary operators are left-assoc.
AST shape: a binary node is emitted as
(ast-app (ast-var OP) [LHS RHS])
— canonical ast-app rather than a Go-specific binary node, since a
future evaluator can recognise operator-named apps without losing
information.
Coverage: equal-prec left-to-right, * tighter than +, && tighter
than ||, comparison tighter than &&, long left-assoc chains, mixed
literal+ident operands.
parse 26/26, total 155/155.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Starts Phase 2. lib/go/parse.sx defines:
* go-precedence-table — Go's five operator-precedence levels in the
(NAME PREC ASSOC) entry shape from lib/guest/pratt.sx, ready for the
binary-operator iteration to consume via pratt-op-lookup.
* go-parse(src) — tokenises and parses ONE primary expression: int,
float, imag, string, rune literals become (ast-literal VALUE);
identifiers become (ast-var NAME). Built directly on lib/guest/ast.sx
constructors — no intermediate AST shape.
Conformance.sh extended to load lib/guest/{ast,pratt}.sx and run the
new parse suite. Scoreboard cleanup: drop the "pending" parse row since
the suite is now real.
parse 17/17 (lex still 129/129). Total 146/146.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing tilde operator '~' (Go 1.18+ generics type-set
constraint, e.g. 'interface { ~int | ~float64 }') to the longest-match
operator table. Adds an exhaustive 'op-audit:' test block covering
every Go operator/punctuation token by category — arithmetic +
assignment, bitwise + assignment, comparison + logical, decls /
arrows / variadic / inc-dec, punctuation, and tilde.
Phase 1 (tokenizer) is now complete. Two kit gaps surfaced and logged
in plans/go-on-sx.md Blockers for the substrate maintainer / next
statically-typed guest loop:
* lib/guest/lex.sx lacks lex-oct-digit? / lex-bin-digit?
(we rolled local gl-* equivalents for 0o.. and 0b.. literals).
* lib/guest/lex.sx lacks a table-driven longest-prefix operator
matcher; our gl-match-op is a 25-clause cond ladder. Rust/Swift/TS
will each hit the same shape with 50+ ops apiece.
lex 129/129. Phase 2 (parser) next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go raw string literals per Go spec § String literals:
backtick-delimited, no escape processing, may span multiple
lines, '\r' chars discarded from the value.
gl-read-raw-string! mirrors gl-read-string! but skips escape
handling and the \r filter. scan! routes the leading backtick
to it; emits "string" type (same as interpreted strings — no
need to distinguish at parse/type time).
lex 123/123.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go float and imaginary literal forms per Go spec § Floating-point
literals and § Imaginary literals:
3.14 .5 1. 1e10 1.5e-3 2.0e+2 1E5 (floats)
2i 3.14i 1e2i (imag)
gl-read-number! returns one of "int" / "float" / "imag"; gl-finish-number!
factors out the post-mantissa exponent + 'i' suffix logic so the int /
float / leading-dot-float paths all share it. scan! adds a .<digit>
branch ahead of the operator matcher so '.5' tokenises as float.
ASI trigger list extended to include float + imag (Go spec § Semicolons:
all literal types trigger).
Greedy-grammar pin (a single test '1.method' lexes as float ident),
since the Go spec says the '.' after a digit always belongs to the
number, never to a following identifier.
Hex floats (0x1.fp0) deferred — not commonly used.
lex 114/114.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds prefixed integer forms per Go spec § Integer literals:
0x.. / 0X.. (hex), 0b.. / 0B.. (binary), 0o.. / 0O.. (octal),
legacy 0123 octal also accepted. Underscores allowed between digits
in any run; lexer is permissive (parser/types phase can enforce
strict placement).
Dispatch lives in gl-read-number! against the first 1-2 chars;
hex digit run consumes lex-hex-digit? from lib/guest/lex.sx. Octal
and binary use local gl-oct-digit?/gl-bin-digit? — narrow enough
that promoting them to the kit is premature.
lex 92/92.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First Go-on-SX iteration. Tokenizer consumes lib/guest/lex.sx character-class
predicates. Automatic semicolon insertion per Go spec § Semicolons fires on
newline, EOF, and block comments containing a newline, after
ident/int/string/rune/{break,continue,fallthrough,return}/{++,--,),],}}.
Scoreboard + conformance.sh wired; lex 78/78. Plan Phase 1 sub-items
checked; floats/raw-strings/hex-ints still ⬜.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- go-on-sx.md: rewrite of 2026-04-26 draft to integrate lib/guest framework.
Adds Phase 3 (independent bidirectional type checker — first static-typed
guest), Phase 10 (extraction enabler), chisel discipline, conformance
scoreboard model. Phases 1-2 now consume lib/guest/core lex+pratt+ast.
- lib-guest-scheduler.md: NEW. Extraction plan for the fork/yield/block/
resume scheduler shared by Erlang (addressed processes + mailboxes) and
Go (anonymous channels + goroutines). Two-language rule blocks extraction
until both consumers independently work; rejected-extraction is a valid
outcome.
- lib-guest-static-types-bidirectional.md: NEW. Sister to lib/guest/hm.sx.
Bidirectional checker kit (synth/check judgments, pluggable subtype +
unify) for the languages HM doesn't fit — Go, Rust, TS, Swift, Kotlin,
Scala 3, Hack. First consumer: Go-on-SX. Second TBD; recommendation
TypeScript.
The three plans cross-reference each other. Go-on-SX implements scheduler +
checker independently of the kits; extraction is its own workstream once
two consumers exist.
Pure-OCaml crypto/CBOR/CID/Ed25519/RSA + native HTTP server in
hosts/ocaml/, the host-primitive surface Erlang Phase 8 BIFs and
fed-sx Milestone 1 are blocked on. WASM-safe lib boundary enforced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.