Adds Go's switch and select statements:
switch TAG { case V1, V2: a; case V3: b; default: c }
switch { case cond: ... } — tagless
select { case x := <-ch: a; case ch <- v: b; default: c }
AST shapes:
(list :switch TAG CASES) — TAG nil for tagless
(list :case VALUES BODY) — VALUES is expr-list
(list :select CASES)
(list :select-case COMM-STMT BODY) — COMM-STMT is send/recv-assign/bare-recv
(list :default BODY)
gp-parse-case-body reads stmts until the next case/default/}/eof
without consuming the terminator — used by both switch and select.
select-case parsing reuses gp-parse-stmt for the comm-stmt, so all
four shapes (send, x := <-ch, x = <-ch, bare <-ch) fall out from the
existing stmt parser. Composite-lit suppression is engaged for the
switch tag expression.
Type-switch (`switch v := x.(type) { case int: ... }`) is the one
deferred shape; needs the `.(type)` pseudo-syntax recognised in the
expression layer. Phase 2 statement coverage is otherwise complete.
This is also a chiselling iteration for scheduler sister kit. Diary
updated with select-case design insights:
* All four select-case shapes share (list :select-case STMT BODY)
— kit primitive sched-select accepts a uniform list of cases.
* Default vs no-default determines blocking semantics. Erlang's
`receive ... after Timeout -> ...` is the analogue — both fit
"non-blocking fallback case" in the kit API.
parse 169/169, total 298/298.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go's concurrency + iteration primitives to the statement parser:
go EXPR → (list :go EXPR)
defer EXPR → (list :defer EXPR)
ch <- v → (list :send CHAN VALUE)
for range COLL { ... } → (list :range-for nil nil nil COLL BODY)
for k := range C { ... } → (list :range-for :short-decl KEY nil COLL BODY)
for k, v := range C { } → (list :range-for :short-decl KEY VAL COLL BODY)
for k, v = range C { ... } → (list :range-for :assign KEY VAL COLL BODY)
gp-for-find-range pre-scans the for-header (to '{' or eof) looking
for the 'range' keyword; if present, dispatches to gp-parse-for-range
which handles the four range shapes. C-style and while-like and
infinite are now in gp-parse-for-c-style — gp-parse-for is just a
dispatcher.
Send statement detection lives in the LHS-list branch of gp-parse-stmt:
after parsing a single LHS expression, '<-' triggers (:send LHS RHS).
Channel-recv (`<-ch`) was already parsed as unary `<-` in the expression
layer, so both directions cover.
This is the **chiselling-relevant iteration** for the scheduler sister
kit: the AST shapes Go-on-SX will eventually feed into the kit's
scheduler primitives (sched-spawn, sched-defer, chan-op) have landed.
Sister-plan diary updated with three design insights:
* :go / :defer both wrap a single expr — kit's sched-spawn should
accept a thunk uniformly across Erlang's spawn(M,F,A) and Go's
go fn().
* :send carries CHAN+VALUE symmetrically with the unary <- recv —
both reduce to (chan-op direction chan value) in the kit.
* `for v := range ch` uses the same :range-for shape as range-over-
slice; the scheduler kit's range dispatch is where chan-recv ⇄
iteration polymorphism lives.
parse 161/161, total 290/290.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the most-used control-flow forms:
if COND { ... } [else { ... } | else if ...]
for { ... } — infinite
for COND { ... } — while-like
for INIT; COND; POST { ... } — C-style
break / continue — keyword stmts (no labels yet)
x++ / x-- — Go statement inc-dec
AST shapes:
(list :if COND THEN ELSE) — ELSE nil / :if / :block
(list :for INIT COND POST BODY) — any of INIT/COND/POST may be nil
(list :break LABEL) (list :continue LABEL)
(list :inc-dec OP EXPR) — OP is "++" / "--"
**Closes the parser-mode caveat** logged when composite literals
landed. `gp-no-comp-lit` is a re-entrant counter on the parser state;
control-flow constructs increment it before parsing their condition
and decrement after, suppressing the postfix `{` → composite-lit
interpretation so that `if Foo { ... }` correctly reads `{ ... }` as
the body, not as `Foo{}` composite literal. Verified by the test:
(go-parse "if Foo {}") → (:if (:var "Foo") (:block ()) nil)
gp-parse-control-cond is the single helper that bracket-wraps the
flag bump so future control-flow forms (switch, select, range) can't
forget to engage suppression.
switch / select / defer / go / for-range / channel-send still deferred.
parse 152/152, total 281/281.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First slice of Phase 2 statements. Replaces the func-decl ':body'
sentinel with real (:block STMTS) parsing.
gp-parse-stmt dispatches on the leading token:
return [exprs] — (list :return EXPRS)
{ ... } — nested block (recurses into block-body)
lhs := exprs — (list :short-decl LHS-LIST EXPRS)
lhs = exprs — (list :assign LHS-LIST EXPRS)
lhs OP= expr — (list :assign-op OP LHS-LIST [EXPR])
expr — bare expression statement
var/const/type/func keywords — fall through to gp-parse-decl
LHS may be a comma-separated list. Compound-assign covers all 11 Go
forms (+= -= *= /= %= &= |= ^= <<= >>= &^=).
gp-parse-block-body iterates: skips semis, terminates on '}', and for
non-trivial tokens calls gp-parse-stmt. **Two progress guards** added
to avoid infinite loops on unsupported syntax:
* gp-block-body-loop force-advances one token if gp-parse-stmt
returns nil without consuming.
* gp-parse-composite-elems does the same when its expr parser
returns nil — fixes a hang on '`if true {`x := 1`}`' where the
parser was misreading `if true{...}` as a composite literal then
spinning on `:=` inside the brace body.
Existing func/method decl tests updated from the ':body' sentinel to
the new (:block STMTS) shape. Old `gp-skip-block!` left as dead code
(removed once control-flow stmts make the misinterpretation issue
moot).
Control-flow stmts (if/for/switch/select/defer/go/break/continue) and
channel send (`ch <- v`) deferred to subsequent iterations.
parse 141/141, total 270/270.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go func and method declarations:
func main() {}
func add(x, y int) int { return x + y }
func mix(x int, y string) {}
func divmod(a, b int) (int, int) {}
func sig(x int) int (no body)
func (p *Point) String() string { ... } (method, pointer recv)
func (s Stack) Len() int { ... } (method, value recv)
func nested() { if true { x := 1; { y := 2 } } } (nested braces)
New gp-parse-decl-param-group implements named-greedy disambiguation:
collects consecutive 'ident [, ident]*' then parses a type. Anonymous
mixed lists like 'func(int, string)' are a known limitation (parser
treats first ident as a name); flagged in plan.
gp-skip-block! brace-balances over the body; the AST stores ':body'
as a sentinel until statement parsing lands. Methods use the receiver
parameter shape directly.
AST:
(list :func-decl NAME PARAMS RESULTS BODY)
(list :method-decl RECV NAME PARAMS RESULTS BODY)
**All five `:field` binding-group consumers now exist** across the
parser: struct fields, var, const, func params, method receivers.
That's strong cross-deliverable validation of the ast-binding-group
proposal from Blockers — five different declaration contexts, one
shared shape.
This is the chisel-relevant insight for sister plan static-types-
bidirectional: an entry has been appended to its design diary
describing how `:field` will be the load-bearing input shape for
the bidirectional checker's `check Γ e T` judgment across these
contexts.
parse 132/132, total 261/261.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First slice of Phase 2 declarations:
package main → (list :package "main")
import "fmt" → (ast-import "fmt") [from kit]
var x int → var-decl + :field binding
var x = 5 → init only (type inferred)
var x int = 5 → both type and init
var x, y int = 1, 2 → multi-name shared type
const Pi = 3.14 → const-decl
const C int = 42 → typed const
type T int → named alias
type Point struct { x, y int } → named struct
New gp-parse-top dispatches on the leading keyword: routes
package/import/var/const/type to gp-parse-decl; everything else
still goes through gp-parse-expr. Existing expression tests are
unaffected (cur won't be a decl keyword at expression start).
var/const decls use the (:field NAMES TYPE) shape from the
ast-binding-group proposal — first concrete cross-deliverable use:
struct fields, var decls, const decls all envelope through the
same node. That's the smell test for whether the kit shape is
right; so far it's clean.
import uses the canonical ast-import from lib/guest/ast.sx — first
direct use of a kit constructor for a declaration shape.
Grouped/parenthesized decls (var (...), import (...), const (...),
type (...)) and func decls (with method receivers + named params)
deferred to subsequent iterations.
parse 124/124, total 253/253.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go composite literals:
T{} empty
T{1, 2} positional
T{X: 1, Y: 2} keyed
[]int{1, 2, 3} slice
[3]int{1, 2, 3} array
map[string]int{"a": 1} map
pkg.Point{1, 2} qualified
[]Point{Point{1,2}, Point{3,4}} nested
AST: (list :composite TYPE-OR-EXPR ELEMS). Each element is an
expression or (list :kv KEY VALUE).
Two parser entry points feed the same AST:
* gp-parse-primary picks up type-prefixed composites by seeing
a literal-type starter ([, map, struct) and parsing a type
first, then optionally a '{' body.
* The postfix loop picks up ident-prefixed composites — after
any base expression, '{' wraps it as a composite literal.
Known limitation flagged in plan: when statement parsing arrives,
the postfix '{' branch will misread `if cond { ... }` as a composite
literal. Standard fix: parser-mode flag suppressing composite-lit
disambiguation in control-flow expression positions. Added to plan.
Elided types in nested composites (`[][]int{{1,2},{3,4}}` with the
inner `{1,2}` typed implicitly) deferred.
parse 114/114, total 243/243.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go interface type expressions:
interface {} → empty
interface { Close() } → no-param method
interface { String() string } → with single return
interface { Read([]byte) (int, error) } → multi-return method
interface { Stringer } → embedded named iface
interface { io.Reader } → qualified embedded
interface { io.Reader; Close() error } → mixed
gp-parse-interface-elems walks elements tolerating ASI semis. Each
element is either:
(list :method NAME PARAMS RESULTS)
(list :embed TYPE)
Method params/results reuse gp-parse-func-type-params/results — the
shape is identical to a free-standing func type. Go 1.18+ type sets
(interface { ~int | ~float64 }) are deferred until the generics
sub-deliverable.
With this, the full Phase 2 **type expressions** sub-deliverable is
complete (pending only field tags, struct/iface embeds details,
variadic, named func params, generics — all flagged later).
parse 106/106, total 235/235.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go struct types to gp-parse-type:
struct {} → (list :ty-struct ())
struct { x int } → (list :ty-struct [(:field [x] (:ty-name int))])
struct { x int; y string } → multiple field rows
struct { x, y int } → shared-type row (NAMES is a list)
struct { inner struct { x int } } → nested struct types
gp-parse-struct-fields walks field rows tolerating ASI-inserted semis
(from newlines between fields). Each row collects 1+ names separated
by commas, then a single type that all the names share. Embedded
fields, field tags, and methods are deferred.
The :field shape (NAMES + TYPE) is a recurring multi-language pattern —
struct fields, func params, method receivers, var decls all map to it.
Logged in Blockers as a canonical-AST candidate
(ast-binding-group / ast-named-of-type); worth promoting once a second
consumer (parser of another statically-typed guest, or Go func decls)
exercises the same shape.
parse 98/98, total 227/227.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Go func-type parsing to gp-parse-type:
func() → (list :ty-func () ())
func() int → (list :ty-func () [int])
func(int, string) → (list :ty-func [int string] ())
func(int) string → (list :ty-func [int] [string])
func() (int, error) → (list :ty-func () [int error])
gp-parse-func-type-params handles the param list inside (...);
gp-parse-func-type-results dispatches between bare single-return,
multi-return parenthesised list, or no return.
Anonymous-only — named params (`func(a int, b string)`) require a
different shape and are mainly needed for func DECLARATIONS, not for
pure func-type expressions in type position. Variadic ('...T')
deferred.
Covers nested cases: func returning func, chan of func, func with
pointer/slice operands.
parse 90/90, total 219/219.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the bulk of Go's type-expression grammar:
[]T → (list :ty-slice T)
[N]T → (list :ty-array N T) — N is an expr
map[K]V → (list :ty-map K V)
chan T → (list :ty-chan :both T)
chan<- T → (list :ty-chan :send T)
<-chan T → (list :ty-chan :recv T)
gp-parse-type now dispatches on the head token: *, [, map, chan, <-,
or ident; each branch recurses for nested types. Channel direction
is encoded as :both / :send / :recv (Go-specific tag).
Coverage: nested types end-to-end — []*T, [][]int, map[string][]int,
chan map[K]V, *[]int — all via the v.(T) assertion carrier.
Logged a concrete kit-gap proposal in plans/go-on-sx.md Blockers for
canonical type-node shapes. The first six (:ty-name, :ty-sel, :ty-ptr,
:ty-slice, :ty-array, :ty-map) are universal across statically-typed
guests and worth promoting on the next consumer; channel/func shapes
stay guest-specific until a second user.
Phase 2 parse acceptance bar (80+ tests) crossed: parse 81/81, total
210/210. Func / struct / interface types and full decls + stmts still
keep Phase 2 open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Postfix '.' branch now peeks at the next token to disambiguate:
.ident → selector / member access (list :select OBJ "field")
.(TYPE) → type assertion (list :assert OBJ TYPE)
New gp-parse-type covers the minimum types needed for assertions:
name → (list :ty-name "int")
pkg.Name → (list :ty-sel "pkg" "Name")
*T / **T → (list :ty-ptr (list :ty-ptr ...))
Full type grammar — slice []T, array [N]T, map[K]V, chan, func,
struct, interface — is a separate Phase 2 sub-deliverable.
Type AST shapes are Go-specific tagged lists; the canonical AST kit
has no type-system primitives at all yet. Worth a richer kit
discussion once Phase 3 (bidirectional type checker) lands and the
sister plan static-types-bidirectional has a real surface to react to.
parse 70/70, total 199/199.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>