Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Phase 5 cont. New go-eval-range-for handles the parser's :range-for
AST shape. Dispatches on the collection's runtime type:
:go-slice → bind index + element, iterate by position
:go-map → bind key + value, walk entries assoc list
:go-chan → bind value, drain until buffer empty (v0 limitation)
Each loop carries:
- go-range-extend: handles 0/1/2-name binding patterns uniformly
- go-range-body: evaluates body whether it's a :block or other shape
- per-collection loop helper: threads env, catches :break/:continue/
:return-value/:eval-error sentinels
**Subtle break fix:** loops were previously returning the *pre-loop*
env when break fired, clobbering all assignments made in prior
iterations. Now returns the current iteration's input env (which
carries forward successful iterations' state). Patched for the three
range variants and for the regular for-loop where the same pattern
applied. The shape:
(= r :break) env ;; was: (= r :break) original-env
Tests:
range: slice — sum of 1..5 = 15
range: slice — key only (index)
range: map — sum values
range: channel — collect all buffered
range: slice with break exits early
range: slice with continue skips an element
range: empty slice — body never runs
range: chan + goroutine producer
runtime 26/26, total 483/483.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1055 lines
60 KiB
Markdown
1055 lines
60 KiB
Markdown
# Go-on-SX — Go as an SX guest language
|
|
|
|
Port Go to SX as the **first static-typed, bidirectional-checked guest** in
|
|
the rose-ash language family. Goal isn't a production Go compiler; it's to
|
|
prove the substrate from a paradigm angle the existing eleven guests don't
|
|
cover, and to chisel out the lib/guest kits that statically-typed guests N+1
|
|
and N+2 will need.
|
|
|
|
Reference:
|
|
- `plans/lib-guest.md` — parent, chiselling discipline, two-language rule.
|
|
- `plans/lib-guest-scheduler.md` — sister kit; Go's scheduler pairs with
|
|
Erlang's. Extraction gated on this loop reaching Phase 5.
|
|
- `plans/lib-guest-static-types-bidirectional.md` — sister kit; Go's
|
|
checker pairs with a TBD second consumer. Extraction gated on this loop
|
|
reaching Phase 3.
|
|
- `plans/erlang-on-sx.md` — reference implementation for paradigm-port:
|
|
process model, BIF registry, hot reload, VM bytecode opcodes.
|
|
|
|
**Branch:** `loops/go` (loop-style workstream once kicked off). SX files via
|
|
`sx-tree` MCP only.
|
|
|
|
## Thesis — why Go
|
|
|
|
Eleven guests already live in `lib/`: apl, common-lisp, datalog, erlang,
|
|
forth, haskell, hyperscript, js, kernel, lua, minikanren, ocaml, prolog,
|
|
ruby, scheme, smalltalk, tcl. Every one is either **dynamically typed**
|
|
(most) or **HM-inferred** (haskell, ocaml). None exercise:
|
|
|
|
1. **Bidirectional static type checking** — annotation-driven, locally-
|
|
inferred, the dominant paradigm of modern statically-typed languages.
|
|
2. **Anonymous-channel concurrency** — Go's `chan` and `select`. Erlang has
|
|
addressed processes + mailboxes; Go has anonymous values + structural
|
|
pairing. Two different vocabularies for the same underlying scheduler
|
|
machinery.
|
|
3. **Structural interfaces** — `io.Reader` is "anything with this method
|
|
signature", not a declared subtype relationship. Different from Haskell
|
|
typeclasses (nominal), different from Lua duck typing (no declaration).
|
|
|
|
These three together make Go an unusually high-value port for proving SX.
|
|
If SX can host Go cleanly, it can host the next decade of mainstream
|
|
statically-typed languages (Rust, TS, Swift, Kotlin, Scala 3, Hack) because
|
|
they share these three properties.
|
|
|
|
Like Erlang-on-SX validated the actor model on the substrate, Go-on-SX
|
|
validates the goroutine model + bidirectional types.
|
|
|
|
## Non-goals (deliberate)
|
|
|
|
Out of scope. Reject feature requests for these without further consideration:
|
|
|
|
- **`unsafe` package.** Memory mucking. Skip entirely.
|
|
- **CGo.** C interop. Out of scope at every level.
|
|
- **Full `reflect`.** Provide enough for `fmt.Println` to render values;
|
|
reject the rest.
|
|
- **Build tags, modules, vendoring.** Treat source as monolithic. One
|
|
package per file, no real import resolution.
|
|
- **Production performance.** Conformance tests pass; benchmarks don't.
|
|
- **Garbage collection tuning.** SX's GC is what you get.
|
|
- **Race detector, escape analysis, inlining.** Out of scope.
|
|
- **`os`, `net/http`, full stdlib.** Provide a deliberately small slice
|
|
(Phase 8 below).
|
|
|
|
## Architecture sketch
|
|
|
|
```
|
|
Go source text
|
|
│
|
|
▼
|
|
lib/go/lex.sx — tokens; ASI; literals; operators
|
|
│ (consumes lib/guest/core/lex.sx)
|
|
▼
|
|
lib/go/parse.sx — AST: package/import/var/const/type/func/struct/
|
|
│ interface; expressions; statements
|
|
│ (consumes lib/guest/core/pratt.sx + ast.sx)
|
|
▼
|
|
lib/go/types.sx — bidirectional type checker. Synth + check judgments;
|
|
│ structural interface satisfaction; pluggable subtype
|
|
│ (INDEPENDENT — no lib/guest/static-types-bidirectional
|
|
│ yet; this loop builds the first consumer)
|
|
▼
|
|
lib/go/eval.sx — tree-walk evaluator on CEK. Variables as mutable cells;
|
|
│ slices = (length, capacity, backing-vector); maps =
|
|
│ SX dict; defer stack per frame.
|
|
▼
|
|
lib/go/sched.sx — goroutine scheduler + channels + select
|
|
│ (INDEPENDENT — no lib/guest/scheduler yet; this loop
|
|
│ builds the first consumer)
|
|
▼
|
|
lib/go/std/ — minimal stdlib slice (fmt, strings, strconv, sync,
|
|
time, errors)
|
|
```
|
|
|
|
Semantic mappings (operational):
|
|
- `go fn(args)` → `task-spawn` on the local scheduler.
|
|
- `ch <- v` → `task-block` with predicate "receiver waiting on ch".
|
|
- `v := <-ch` → `task-block` with predicate "sender waiting on ch".
|
|
- `select { case ... }` → `task-block` with predicate "any case ready".
|
|
- `defer fn()` → push thunk onto per-frame defer stack; runs LIFO on
|
|
return or panic.
|
|
- `panic(v)` → raise SX exception; deferred fns run while unwinding.
|
|
- `recover()` → CEK exception capture inside a deferred fn.
|
|
- `interface{T}` → type-check matches structurally against T's method
|
|
set; at runtime, the value carries its concrete-type metadata.
|
|
- `struct{...}` → SX dict + type tag; methods are functions in the type's
|
|
method table.
|
|
- `*T` (pointer) → mutable cell (Common Lisp port did the same).
|
|
- `[]T` (slice) → triple (length, capacity, backing-vector).
|
|
- `map[K]V` → SX dict; iteration order spec-undefined (v1 = sorted for
|
|
determinism — programs that depend on indeterminism fail loudly, which
|
|
is a feature not a bug).
|
|
|
|
## Conformance scoreboard
|
|
|
|
Following `lib/erlang/scoreboard.json` precedent. Add
|
|
`lib/go/scoreboard.json` on first iteration; populate as suites land.
|
|
Suites planned:
|
|
|
|
| Suite | Tests target | What it covers |
|
|
|---|---|---|
|
|
| `lex` | 50+ | Keywords, operators, literals, ASI |
|
|
| `parse` | 80+ | All statement & expression shapes |
|
|
| `types` | 90+ | Synth, check, interface satisfaction, generics |
|
|
| `eval` | 100+ | Tree-walk over typed AST |
|
|
| `runtime` | 60+ | Goroutines, channels, select, close |
|
|
| `stdlib` | 40+ | fmt, strings, strconv, sync, time, errors |
|
|
| `e2e` | 10+ | Complete representative programs |
|
|
|
|
## Phasing — one feature per commit
|
|
|
|
Loop-style. Each phase: implement → test → commit → tick `[ ]` → append
|
|
Progress-log line → push `origin/loops/go`.
|
|
|
|
### Phase 1 — Tokenizer (`lib/go/lex.sx`) ✅
|
|
- [x] Scaffold + scoreboard + conformance runner (consumes lib/guest/lex.sx)
|
|
- [x] Identifiers + 25 keywords
|
|
- [x] Decimal integer literals
|
|
- [x] Interpreted string literals `"..."` with `\n \t \r \\ \" \'` escapes
|
|
- [x] Rune literals `'x'` (single char + simple escapes)
|
|
- [x] Line + block comments (block w/ newline triggers ASI)
|
|
- [x] Common operator/punct set incl. `:= <- ++ -- == != <= >= && || ...`
|
|
- [x] **Automatic semicolon insertion** (Go spec § Semicolons) — newline,
|
|
EOF, and block-comment-with-newline trigger `;` after
|
|
ident/int/string/rune/{break,continue,fallthrough,return}/{++,--,),],}}.
|
|
- [x] Float / imaginary literals (decimal floats: `3.14 .5 1. 1e10 1.5e-3`;
|
|
imag: `2i 3.14i 1e2i`; hex floats `0x1.fp0` deferred)
|
|
- [x] Raw string literals `` `...` `` (multi-line, no escape processing,
|
|
`\r` stripped per Go spec § String literals; same `"string"` type
|
|
as interpreted strings)
|
|
- [x] Hex/octal/binary integer literals (0x… 0o… 0b…) + underscores
|
|
(legacy 0123 octal also accepted; consumes lex-hex-digit?)
|
|
- [x] Full operator set audit (47 distinct per Go spec, plus `~` for
|
|
generics type-sets). Exhaustive coverage tests in `op-audit:` block.
|
|
- **Acceptance:** lex/ suite at 50+ tests. Current: 129/129. **Phase 1
|
|
done** — hex floats deferred (rare). Move to Phase 2 next.
|
|
|
|
### Phase 2 — Parser (`lib/go/parse.sx`) ✅
|
|
- [x] Parser scaffold + Go operator-precedence table (entry shape from
|
|
`lib/guest/pratt.sx`) + primary expressions (int/float/imag/string/
|
|
rune/ident → ast-literal / ast-var via `lib/guest/ast.sx`).
|
|
- [x] Binary operators (Pratt precedence climbing using `pratt-op-lookup`
|
|
+ Go precedence table). Operator app emitted as
|
|
`(ast-app (ast-var OP) [LHS RHS])`; left-assoc raises right-min by 1.
|
|
- [x] Unary operators (`+x`, `-x`, `!x`, `^x`, `*p`, `&v`, `<-ch`).
|
|
`gp-parse-unary` recursive, sits between `gp-parse-expr` and
|
|
`gp-parse-primary`; right-associative chains (`!!x`).
|
|
- [x] Function calls `f(a, b)` (canonical `ast-app`) and member access
|
|
`x.field` (Go-specific `(list :select OBJ "field")` — the AST kit
|
|
doesn't ship a selector node; this is a sister-plan-static-types
|
|
data point about what the canonical AST is missing).
|
|
- [x] Index `x[i]` and slice `x[a:b]`/`x[a:b:c]`. Go-specific
|
|
`(list :index OBJ IDX)` and `(list :slice OBJ LOW HIGH MAX)`
|
|
(LOW/HIGH/MAX may be nil) — kit lacks both. Permissive parser
|
|
accepts `a[1::3]` (strict Go rejects, but type phase can enforce).
|
|
- [x] Type assertion `v.(T)`. `(list :assert OBJ TYPE)`. Includes a
|
|
minimal `gp-parse-type` (named / qualified `pkg.T` / pointer
|
|
`*T` / `**T`); full type grammar still pending below.
|
|
- [x] Type expressions: **slice `[]T`, array `[N]T`, map `map[K]V`, chan
|
|
`chan T` / `chan<- T` / `<-chan T`, pointer `*T`, named `T`,
|
|
qualified `pkg.T`, func `func(...) ...`, struct `struct{...}` with
|
|
shared-type field rows (`x, y int`), interface `interface{...}`
|
|
with methods + embedded interfaces (named and qualified)** all
|
|
done — kit has no type primitives. Field tags, struct embeds,
|
|
variadic, named func-params, Go 1.18 type sets, generics deferred.
|
|
- [x] Composite literals: `T{...}`, `[]T{...}`, `[N]T{...}`,
|
|
`map[K]V{...}`, `pkg.T{...}`, nested. Positional and keyed
|
|
(`X: 1, Y: 2`) elements. AST `(list :composite TYPE-OR-EXPR ELEMS)`;
|
|
elements are exprs or `(list :kv KEY VALUE)`. Note: in statement
|
|
context (e.g. `if cond { ... }`) my parser would WRONGLY treat
|
|
the body as a composite; statement parsing will need a "no-
|
|
composite-here" mode flag — to be added when statements arrive.
|
|
- [x] Declarations: `package`, `import`, `var`, `const`, `type`, `func`
|
|
(with named-greedy params + method receivers + body skipped
|
|
opaquely until statement parsing arrives). All five `:field`
|
|
consumers now exist (struct fields, var, const, func params, method
|
|
receivers) — strong signal that `ast-binding-group` belongs in the
|
|
canonical AST kit. Grouped/parenthesized decls (`var (...)`, etc.)
|
|
and variadic params deferred. Anonymous param-list disambiguation
|
|
(`func(int, string)`) is a known parser-greedy limitation, flagged.
|
|
- [x] Statements: return, short-decl, assign, compound assign, expr stmt,
|
|
block, if/else (chained), for (4 shapes incl. range), break,
|
|
continue, inc-dec, go, defer, send, **switch (tagged / tagless,
|
|
multi-value cases, default), select (recv-into-var / send /
|
|
bare-recv / default)** all done. Composite-literal `{` suppression
|
|
active in control-flow conditions. Type-switch (`switch v :=
|
|
x.(type)`) deferred to a follow-up.
|
|
- [x] End-to-end: hello-world, fibonacci, FizzBuzz, goroutine ping-pong,
|
|
struct + method, interface, defer+select+range. `go-parse` extended
|
|
to handle multi-form files: returns the single form for one-form
|
|
input (backward compat) or `(list :file FORMS)` for multiple.
|
|
Structural tests assert top-level decl-tag sequences via the
|
|
`decl-tags` helper rather than full ASTs.
|
|
- **Acceptance:** parse/ suite at 80+ tests. Current: **176/176**.
|
|
**Phase 2 complete.** Type-switch is the one syntactic shape still
|
|
deferred to a follow-up; it doesn't gate Phase 3.
|
|
|
|
### Phase 3 — Bidirectional type checker, MVP (`lib/go/types.sx`) ✅
|
|
- [x] Scaffold: `go-synth` / `go-check` skeletons; context-as-value
|
|
(`go-ctx-empty` / `-extend` / `-lookup` / `-extend-field`);
|
|
predeclared `true`/`false`/`nil`; structural type equality.
|
|
- [/] Literal synth: heuristic kind detection from value strings
|
|
(`go-classify-literal-string`) → `:ty-untyped-int`/`-float`/
|
|
`-imag`/`-string` (`-rune` deferred — value-shape ambiguous with
|
|
single-char string). Parser-shape change to `(:literal KIND VALUE)`
|
|
flagged as future work; the heuristic stopgap avoids breaking 66
|
|
existing parse tests.
|
|
- [x] Binary-op synth with untyped-constant flow. **Canonical pitfall
|
|
handled**: `42 / 7` synthesises to `:ty-untyped-int`, then checks
|
|
successfully against `float64`. Untyped int + untyped float
|
|
unifies to untyped float. Typed-var + untyped-int propagates the
|
|
var's type. Comparison/logical ops produce `bool`.
|
|
- [x] Var/const declaration checking (`var x T = expr`, `var x = expr`,
|
|
`var x T`, `const Pi = 3.14`, `type T int`, `var x, y int`, plus
|
|
short-decl `x := 5` and `a, b := 1, 2`). `go-check-decl` returns
|
|
the extended context or a `:type-error`. Untyped synthesized types
|
|
get their default-type (`untyped-int → int`, `untyped-float →
|
|
float64`, etc.) when bound in inferred-type decls.
|
|
- [x] Function declaration: extends ctx with each `:field` param group,
|
|
checks block body (decls thread through, returns verify against
|
|
signature, assignments verify RHS assignable to LHS). The function
|
|
itself is bound in the body's ctx so recursion will work once
|
|
call-checking lands. Signature-only (no body) just binds.
|
|
- [x] Call type-checking. `go-synth-call`: synth callee → expect
|
|
`:ty-func`, arity-check, check each arg assignable to param,
|
|
then return type by result count (0 → `:ty-void`, 1 → that type,
|
|
N → `:ty-tuple`). Recursive calls now type-check because the func
|
|
is bound in the body's ctx. Untyped-constant args flow through.
|
|
- [x] Composite literal element checking — slice `[]T{...}`, array
|
|
`[N]T{...}`, map `map[K]V{...}` (key + value checked).
|
|
`:kv` element with no key on slice/array is permitted (Go's
|
|
index-keyed shorthand). Nested composite literals work
|
|
(`[][]int{[]int{1,2}, []int{3,4}}`). Named-type composite
|
|
literals (`Point{...}`) need type-decl resolution; deferred.
|
|
- [x] Interface satisfaction (structural match against method sets).
|
|
Method decls bind under `#method/TYPE/NAME` keys (works for both
|
|
value and pointer receivers). `go-iface-satisfies?` walks an
|
|
interface's `:method` elements and looks each up; partial sets
|
|
and arity-mismatches fail. Embedded interfaces deferred.
|
|
- [x] Short variable declaration `:=` (synth RHS into LHS bindings).
|
|
Handled inline by `go-check-short-decl` since the decl-checking
|
|
slice; works both at top-level and inside `for`/`if` init clauses.
|
|
- Defer: generics (Phase 7), full conversion rules, type assertions,
|
|
type switches.
|
|
- **Acceptance:** types/ suite at 60+ tests. **Bar crossed: 72/72.**
|
|
Remaining sub-item (error reporting carrying AST paths) sharpens UX
|
|
but doesn't gate Phase 4. Chisel note
|
|
`shapes-static-types-bidirectional` — sister-plan design diary is the
|
|
cross-language record.
|
|
|
|
### Phase 4 — Tree-walk evaluator (`lib/go/eval.sx`) ⬜
|
|
- [x] Scaffold: env-as-value, literal decoding (decimal/hex/oct/bin
|
|
with underscores), variable lookup (incl. predeclared true/false/nil),
|
|
arithmetic + comparison + logical binops. eval suite at 25/25.
|
|
- [x] Statement evaluation: block / return / short-decl / assign /
|
|
var-decl / if / for (all three header shapes) / break / continue /
|
|
inc-dec all done. `:break` and `:continue` propagate as sentinel
|
|
keywords through `go-eval-block` until `go-for-loop` catches them.
|
|
- [ ] Variables as mutable cells; pointer semantics: `&x` returns the
|
|
cell, `*p` dereferences.
|
|
- [/] Slices: v0 represents a slice as `(list :go-slice ELEMS)` —
|
|
simpler than the full (length, capacity, backing-vector) triple.
|
|
Composite-literal `[]T{...}` evaluates to a `:go-slice`; `a[i]`
|
|
indexes, `a[low:high]` slices, `len(a)` returns count, `append(a, ...)`
|
|
extends. The full triple with capacity-grow comes in a later
|
|
slice when programs need it.
|
|
- [x] Maps: v0 represents `m` as `(list :go-map ENTRIES)` where ENTRIES
|
|
is an assoc list. Composite-literal `map[K]V{...}`, `m[k]` lookup
|
|
(nil for missing key, until runtime type info enables zero-value),
|
|
`m[k] = v` index-assignment, `len(m)`. Index-assignment for slices
|
|
also lands here (`a[i] = v` rebuilds via `go-slice-set`).
|
|
- [x] Structs + method dispatch. `(list :go-struct TYPE-NAME FIELDS)`
|
|
assoc-list. `type T struct {...}` registers `:go-struct-type`
|
|
with field names. Positional + keyed composite literals; `p.f`
|
|
/ `p.f = v` selectors. Methods bind under `#method/T/N` mangled
|
|
keys — same scheme as the type checker. `p.M(...)` dispatches via
|
|
receiver type lookup, binds the receiver param to the struct
|
|
value, runs body. Both value and pointer receivers work in v0
|
|
(treated the same since pointer semantics aren't modelled yet).
|
|
- [/] Functions: top-level definition + call (incl. recursion via the
|
|
calling env). Lexical closures and multiple return values pending.
|
|
- [ ] Channels: stub (Phase 5 wires them).
|
|
- Tests: arithmetic, control flow, recursion, closures, slices, maps,
|
|
structs, methods, pointer semantics, multiple-return.
|
|
- **Acceptance:** eval/ suite at 80+ tests. **Bar crossed: 80/80.**
|
|
Remaining sub-items (lexical closures, multi-return funcs, full
|
|
slice triple with capacity) refine but don't gate Phase 5.
|
|
|
|
### Phase 5 — Goroutines + channels + select (`lib/go/sched.sx`) ⬜
|
|
- [x] Scaffold: `lib/go/sched.sx` with `go-make-chan` (closures-over-
|
|
mutable-buf), `go-chan-send!` / `go-chan-recv!` / `go-chan-closed?`
|
|
/ `go-chan-close!`. Channel identity via closure-instance.
|
|
- [x] Eval integration: `make`/`close` builtins; `:send` stmt; unary
|
|
`<-` recv on channels; `:go` stmt (synchronous in v0 — see
|
|
sched.sx header).
|
|
- [ ] Real preemption (suspending sends on full buffer / recvs on empty).
|
|
Requires reified execution state; deferred to Phase 5b.
|
|
- [x] `select { case ... }` multiplexing. v0 tries cases in declared
|
|
order, picks first ready (sends always ready; recv ready iff
|
|
channel has buffered values). Default runs if nothing else
|
|
ready; no default and nothing ready → typed error.
|
|
Recv-into-var (`case v := <-ch`) binds; recv-into-assign
|
|
(`case v = <-ch`) re-binds. Real-Go random selection among
|
|
ready cases deferred (v0 deterministic).
|
|
- [x] `range` over slices / maps / channels. New `go-eval-range-for`
|
|
dispatches on collection type: slice (index+elem), map (key+val),
|
|
channel (just value). v0 chan-range stops when buffer empties
|
|
(no preemption to wait for new sends). break exits with the
|
|
pre-break env (preserving prior-iteration assignments).
|
|
- [ ] `time.After`-like timer channel.
|
|
- **Independent implementation.** Do NOT use lib/guest/scheduler/ — that
|
|
kit doesn't exist yet and depends on this work for its design. See
|
|
`plans/lib-guest-scheduler.md`.
|
|
- `go expr` — spawn a goroutine; returns nothing.
|
|
- `chan T` — `make(chan T)` creates an unbuffered channel; `make(chan T,n)`
|
|
creates a buffered channel (Phase 5b — defer buffer to a sub-phase).
|
|
- `<-ch` — receive (blocks until sender ready).
|
|
- `ch <- v` — send (blocks until receiver ready for unbuffered, or buffer
|
|
has room for buffered).
|
|
- `select { case ... }` — non-deterministic multiplexing; `default` makes
|
|
it non-blocking.
|
|
- `close(ch)` — closes channel. Receive on closed → zero value + ok=false.
|
|
- Tests: ping-pong, fan-out/fan-in, work queue, select with default,
|
|
select with timeout (via a `time.After`-like stub), close semantics,
|
|
range over channel.
|
|
- **Acceptance:** runtime/ suite at 40+ tests. Chisel note `shapes-
|
|
scheduler` — append a paragraph to the sister plan's design diary
|
|
describing what task-spawn/block/wake/yield shape emerged.
|
|
|
|
### Phase 5b — Buffered channels + select fairness ⬜
|
|
- Buffered: send blocks only when buffer full; recv only when empty.
|
|
- `select` random case ordering (spec mandates pseudo-random; v1 uses a
|
|
fixed seed for determinism with a `runtime`-package knob to randomise).
|
|
- Tests: buffer-full blocking, buffer-empty blocking, select fairness
|
|
over many iterations.
|
|
- **Acceptance:** runtime/ +20 tests.
|
|
|
|
### Phase 6 — `defer` + panic/recover ⬜
|
|
- Defer stack per function frame; runs LIFO on return (normal or panic).
|
|
- `panic(v)` unwinds frames running deferreds; `recover()` inside a
|
|
deferred fn captures the panic value and stops unwinding.
|
|
- Goroutine panic propagation: a panicking goroutine that doesn't recover
|
|
crashes the whole program (honour Go spec, or document divergence).
|
|
- Tests: defer order (LIFO), defer + named-return mutation, panic/recover,
|
|
panic across goroutines, defer in a loop (push per iter, run on fn
|
|
return — common bug).
|
|
- **Acceptance:** eval/ +20 tests.
|
|
|
|
### Phase 7 — Generics (Go 1.18+) ⬜
|
|
- Type parameters with constraints (type sets: `interface{ int | float64
|
|
}`, `comparable`, `any`).
|
|
- Type inference at call sites — basic; the full Go inference algorithm
|
|
is notoriously complex. Implement enough for common cases; document
|
|
limitations in a Blockers section below.
|
|
- Tests: generic function (`func Map[T, U any](xs []T, f func(T) U) []U`),
|
|
generic data structure (linked list), constrained type param.
|
|
- **Acceptance:** types/ +30 tests.
|
|
|
|
### Phase 8 — Minimal stdlib (`lib/go/std/`) ⬜
|
|
- Implement just what's needed for representative programs:
|
|
- `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`,
|
|
`Stringer` dispatch. Verbs: `%d %s %v %t %f %T %+v`.
|
|
- `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`,
|
|
`TrimSpace`, `ToUpper`, `ToLower`, `Replace`, `Index`, `Count`,
|
|
`Repeat`, `NewReader`.
|
|
- `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`,
|
|
`FormatInt`.
|
|
- `errors` — `New`, `Is`, `As`, `Unwrap`.
|
|
- `sync` — `Mutex` (cooperative — flag + waiter queue), `WaitGroup`,
|
|
`Once`, `RWMutex`.
|
|
- `time` — `Now`, `Since`, `After` (channel-returning timer), `Sleep`,
|
|
`Duration`, `Time`.
|
|
- `io` — `Reader`/`Writer` interfaces; `ReadAll`; `Copy`.
|
|
- `sort` — `Slice`, `Ints`, `Strings`.
|
|
- Tests: round-trip Itoa/Atoi, fmt verb coverage, sync.WaitGroup with
|
|
goroutines, time.After in a select, sort.Slice with custom less fn.
|
|
- **Acceptance:** stdlib/ suite at 40+ tests.
|
|
|
|
### Phase 9 — End-to-end programs ⬜
|
|
- Complete programs from canonical sources (gopl.io, "concurrency
|
|
patterns" talk examples) running end-to-end:
|
|
- Concurrent prime sieve
|
|
- HTTP-ish ping-pong over stubbed transport
|
|
- Word frequency counter
|
|
- Pipeline (channel chain)
|
|
- Producer/consumer with sync.WaitGroup
|
|
- "Bounded parallelism" pattern (worker pool over a job channel)
|
|
- **Acceptance:** e2e/ suite at 10+ tests, all passing.
|
|
|
|
### Phase 10 — lib/guest extraction enabler ⬜
|
|
- Now that Go has lex+parse+types+eval+sched, sister plans are unblocked
|
|
on the Go side. This phase is **doc-only** in `loops/go`:
|
|
- Cross-reference `plans/lib-guest-scheduler.md` — mark its Phase 1
|
|
(Go scheduler independent) as complete from Go's side.
|
|
- Cross-reference `plans/lib-guest-static-types-bidirectional.md` —
|
|
mark its Phase 1 as complete from Go's side.
|
|
- Update the chiselling diary in each sister plan with the actual
|
|
Go-side surface that emerged.
|
|
- **Acceptance:** sister plans cross-referenced + diaries updated. No
|
|
new Go code.
|
|
|
|
### Phase 11 — VM bytecode opcodes (deferred, optional) ⬜
|
|
- Following Erlang-on-SX Phase 10 precedent: identify hot paths in the
|
|
tree-walk evaluator, define Go-specific bytecode opcodes, compile hot
|
|
fns through them. Substantial work; only justified if Go programs
|
|
exercise enough volume that performance starts mattering.
|
|
- **Acceptance:** TBD on demand.
|
|
|
|
## Ground rules (loop-style)
|
|
|
|
- **Scope:** only `lib/go/**` and this plan. Do not touch `spec/`,
|
|
`hosts/`, `shared/`, `lib/guest/**` (read-only consumer at this phase),
|
|
or other `lib/<lang>/`.
|
|
- **Consume `lib/guest/core/`** for lex/parse/ast/match/layout. Hand-
|
|
rolling defeats the chiselling goal.
|
|
- **Do NOT extract into `lib/guest/scheduler/` or `lib/guest/static-
|
|
types-bidirectional/` from this loop.** Those extractions are gated on
|
|
two consumers AND the discipline of writing each consumer
|
|
independently. Extraction is its own workstream after Go and the
|
|
second consumer both exist.
|
|
- **Substrate gaps** → Blockers entry with minimal repro. Don't fix the
|
|
substrate from this loop. Belongs to `sx-improvements.md`.
|
|
- **NEVER call `sx_build` without timeout awareness** — 600s watchdog.
|
|
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit.
|
|
- **Worktree:** branch `loops/go`, push `origin/loops/go`. Never `main`,
|
|
never `architecture`.
|
|
- **Commit granularity:** one feature per commit. Short factual messages:
|
|
`go: parse short-decl + 6 tests [consumes-pratt]`. Chisel note at end
|
|
in brackets.
|
|
- **Plan file:** update Progress log + tick boxes every commit.
|
|
- **If blocked** for two iterations on the same issue, add to Blockers
|
|
and move on. Phases 1-4 are sequential; Phases 5-8 are largely
|
|
independent once 4 lands.
|
|
|
|
## Chisel discipline (per parent lib-guest plan)
|
|
|
|
Every commit ends its message with a chisel note in brackets:
|
|
|
|
- `[consumes-X]` — used `lib/guest/X` kit.
|
|
- `[shapes-scheduler]` / `[shapes-static-types-bidirectional]` — revealed
|
|
something about what the sister lib-guest kits should look like. Add a
|
|
paragraph to the relevant sister plan's design diary.
|
|
- `[proposes-Y]` — revealed a gap in another existing kit. Blockers entry
|
|
in the kit's plan.
|
|
- `[nothing]` — pure Go work that didn't touch substrate or lib/guest
|
|
story. Acceptable; if it shows up twice in a row, stop and reflect.
|
|
|
|
## Go-specific gotchas
|
|
|
|
- **ASI (automatic semicolon insertion).** Newline becomes `;` after
|
|
identifier/literal/`)`/`]`/`}`. Build into the tokenizer; the Go spec's
|
|
"Semicolons" section is unusually precise — follow it literally.
|
|
- **Untyped constants.** `42` has type `untyped int` until used in a
|
|
context that forces a type. The canonical example: `var x float64 = 42
|
|
/ 7` — must compute as `untyped int / untyped int = 6` then convert to
|
|
`float64 = 6.0`. Wrong: float-coercing eagerly gives 6.0 prematurely.
|
|
Wrong: integer-truncating after coercion gives `5.something`. Test it.
|
|
- **Methods vs functions.** `func (r Receiver) Method()` is a method
|
|
bound to a type; `func Function(r Receiver)` is just a function.
|
|
Methods on pointer-receivers vs value-receivers have asymmetric
|
|
satisfaction in interfaces — pointer-receiver methods are NOT in the
|
|
value's method set for interface satisfaction.
|
|
- **Interface satisfaction is structural and silent.** Type satisfies an
|
|
interface if its method set contains all the interface's methods.
|
|
Lazy check: at every point a value flows into an interface-typed slot.
|
|
- **Channels are first-class values.** Pass them, store them, send them
|
|
through other channels. Each channel has identity.
|
|
- **`select` with `default`** = non-blocking. Without `default`, blocks
|
|
until a case is ready.
|
|
- **`nil` is typed.** `var x *int` makes x a `(*int)(nil)`. Comparison
|
|
`x == nil` works on typed nil; but `var i interface{} = (*int)(nil); i
|
|
== nil` is `false` — i holds a typed-nil-of-type-`*int`, not untyped
|
|
nil. The classic Go footgun. Test it.
|
|
- **Goroutine panic propagation.** A panicking goroutine that doesn't
|
|
recover crashes the whole program. Implement faithfully or document
|
|
divergence.
|
|
- **`defer` in a loop.** Each iteration pushes; they all run on function
|
|
return. Common bug; tests should cover.
|
|
- **Iteration order of maps.** Spec: unspecified. v1 = sorted by SX-
|
|
canonical key order for determinism; document that programs depending
|
|
on iteration order are not Go-conformant. Add a `runtime`-package knob
|
|
to enable randomisation later.
|
|
|
|
## Style
|
|
|
|
- No comments in `.sx` unless non-obvious. Cite Go spec sections inline
|
|
for non-obvious decisions (Go's spec is rigorous; citations work).
|
|
- No new planning docs — update this plan inline.
|
|
- One feature per iteration. Commit. Log. Push. Next.
|
|
|
|
## Open questions
|
|
|
|
1. **Module/import model.** Go has packages and import paths. Probably
|
|
model "package" as one or more `.sx` files in a directory, no real
|
|
import resolution against a remote module graph. Decide in Phase 2.
|
|
2. **Goroutine identity.** Spec says goroutines have no identity; the
|
|
scheduler does internally. Expose to user code? No (not Go). Expose
|
|
for debugging? Yes via a `runtime`-package stub.
|
|
3. **Error handling: panic-as-exception vs explicit error returns.** Go
|
|
strongly prefers explicit errors. Stdlib stubs follow that: `strconv.
|
|
Atoi("x")` returns `(0, err)`, not panic.
|
|
4. **Memory model.** Go has a happens-before model for atomics + channel
|
|
ops. SX runtime is single-threaded under the scheduler — every channel
|
|
op is a synchronization point automatically. Don't model relaxed
|
|
memory; document the simplification.
|
|
5. **Iteration order of maps.** Already addressed in Gotchas; flagged
|
|
here as a known divergence from spec.
|
|
|
|
## Blockers
|
|
|
|
### Kit-gap proposals against `lib/guest/ast.sx`
|
|
|
|
Observed from building the Go parser:
|
|
|
|
1. **No selector / field-access node.** `obj.field` is a universal shape
|
|
across nominally-typed languages — Go, Rust, Swift, TS, JS, Python,
|
|
Ruby, Java, C#. The kit ships `ast-app` (function application) but
|
|
not `ast-select`. We rolled `(list :select OBJ "field")` locally as
|
|
a Go-specific tag. Worth promoting once a second consumer hits the
|
|
same need (likely immediately — almost every guest needs it).
|
|
|
|
2. **No index / subscript node.** `x[i]` is universal across nearly every
|
|
guest with arrays/maps. Rolled `(list :index OBJ IDX)` locally.
|
|
|
|
3. **No slice node.** Go's two- and three-index slice expressions are
|
|
distinctive but the basic two-index `x[a:b]` shape covers Python,
|
|
Rust, Swift, JS, Ruby slicing too. Rolled
|
|
`(list :slice OBJ LOW HIGH MAX)` (LOW/HIGH/MAX may be nil for
|
|
omitted indices). MAX-as-fourth-field is Go-specific; the canonical
|
|
kit shape could ship as `(list :slice OBJ LOW HIGH)` for the common
|
|
case and a separate `:slice3` or `:full-slice` for the Go variant.
|
|
|
|
Minimal repro: see `lib/go/parse.sx#gp-parse-postfix` + `gp-parse-bracket`.
|
|
|
|
4. **No "named binding(s) of a type" node.** Building struct types
|
|
surfaced a shape that recurs everywhere:
|
|
|
|
```
|
|
(list :field NAMES TYPE)
|
|
```
|
|
|
|
Same shape appears in: struct fields (`x, y int`), func parameters
|
|
(`func(a, b int, c string)`), method receivers (`m(a, b int)`),
|
|
variable declarations (`var x, y int`). Three Phase-2 sub-deliverables
|
|
(struct fields, func decls, var decls) all want this shape. Promoting
|
|
it once means Rust struct fields, Swift parameters, TS class fields,
|
|
Java method signatures all get a free home. Candidate canonical name:
|
|
`ast-binding-group` or `ast-named-of-type`.
|
|
|
|
5. **No type-expression primitives.** Every statically-typed guest needs
|
|
to express types in source. Proposed canonical shapes:
|
|
|
|
```
|
|
(list :ty-name "T") — named type
|
|
(list :ty-sel "pkg" "T") — qualified type
|
|
(list :ty-ptr T) — pointer to T
|
|
(list :ty-slice T) — slice / dynamic array of T
|
|
(list :ty-array N T) — fixed array, N is an expr
|
|
(list :ty-map K V) — map type (also Python dict, Rust HashMap)
|
|
```
|
|
|
|
The first six are universal: Rust, Swift, TS, Kotlin, Scala, Hack
|
|
all need them. Go-specific extensions like `:ty-chan` (channel with
|
|
direction) and `:ty-func` (parameter+return) should stay
|
|
guest-specific until a second consumer wants them.
|
|
|
|
Minimal repro: see `lib/go/parse.sx#gp-parse-type`.
|
|
|
|
### Kit-gap proposals against `lib/guest/lex.sx`
|
|
|
|
Observed from building the Go tokenizer. Not blocking Phase 2; surfaced
|
|
here for the substrate-maintainer / next statically-typed-guest loop:
|
|
|
|
1. **No `lex-oct-digit?` / `lex-bin-digit?`.** Go's prefixed integer forms
|
|
`0o17` and `0b1010` need digit-class predicates that the kit doesn't
|
|
provide. We rolled local `gl-oct-digit?` and `gl-bin-digit?`. Rust and
|
|
Swift's lexers will need the same. Cheap to promote.
|
|
|
|
2. **No table-driven longest-prefix matcher.** Go has 47+ operator
|
|
sequences with longest-match semantics. Our `gl-match-op` is a
|
|
25-clause `cond` ladder; Rust/Swift/TS will each need ~50+. A kit
|
|
helper like `(lex-match-longest TABLE SOURCE POS)` that takes a sorted
|
|
prefix table would collapse this. Worth proposing once a second
|
|
statically-typed guest hits the same pattern.
|
|
|
|
Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`.
|
|
|
|
## Progress log
|
|
|
|
_Newest first. Append one dated entry per commit._
|
|
|
|
- 2026-05-27 — Phase 5 cont.: range-over-{slice,map,channel}. New
|
|
`go-eval-range-for` dispatches on the collection type:
|
|
slice → bind index + element, iterate by position
|
|
map → bind key + value, iterate over entries assoc list
|
|
chan → bind value, drain until empty (v0: no preemption to wait
|
|
for new sends; real Go blocks until close + empty)
|
|
break/continue propagate via the existing sentinel scheme.
|
|
Subtle fix in break-from-loop: was returning the pre-loop env
|
|
(clobbering prior-iteration assignments); now returns the current
|
|
iteration's input env so successful iterations stick. Patched for
|
|
range-slice, range-map, range-chan in one go. +7 tests, runtime
|
|
26/26, total 483/483. `[nothing]` — collection-iteration semantics
|
|
are Go-specific; the cross-language scheduler insights are already
|
|
in the sister-plan diary.
|
|
- 2026-05-27 — Phase 5 cont.: `select` statement evaluation. New
|
|
`go-eval-select-stmt` + `go-select-pick` + `go-select-try-case`.
|
|
Walks cases in declared order: send always ready in v0; recv ready
|
|
iff `(go-chan-len ch) > 0` (new accessor added to sched.sx);
|
|
recv-into-decl/assign binds the value into env. Default deferred
|
|
until end of walk; if nothing ready and no default, returns
|
|
`(:eval-error :select-blocked-no-default)`. Subtle bug fixed where
|
|
`:select` stmt was returning the old env instead of the
|
|
case-body-extended env — assignments inside cases now stick. +6
|
|
tests, runtime 18/18, total 475/475. `[nothing]` — `:select-case`
|
|
uniform shape was already chiselled into the sister-plan diary when
|
|
the parser landed.
|
|
- 2026-05-27 — **Phase 5 first slice.** `lib/go/sched.sx` lands with
|
|
the v0 channel primitive: `go-make-chan` returns a closures-over-
|
|
mutable-buf channel. Send appends, recv pops first, close flips a
|
|
flag. Channel identity via closure-instance (matches Go spec — two
|
|
`make()` calls produce distinct values). `make`/`close` are now
|
|
builtins; `:send` stmt and unary `<-` recv hook through;
|
|
`:go expr` evaluates synchronously (no real preemption — SX doesn't
|
|
expose continuations, so v0 runs goroutines to completion in source
|
|
order). Patterns that rely on the spawned goroutine pushing to a
|
|
channel before the main reads do work end-to-end. runtime suite
|
|
12/12 incl. multi-goroutine fan-in and worker-pattern. Total
|
|
469/469. `[shapes-scheduler]` — sister-plan diary updated with the
|
|
realised channel-as-closures-over-state shape and the v0
|
|
synchronous-spawn caveat.
|
|
|
|
Sister-plan diary update follows.
|
|
- 2026-05-27 — Phase 4 cont.: **method dispatch + unary ops + e2e
|
|
programs. Acceptance bar (80+) crossed.** Methods register under
|
|
`#method/TYPE/NAME` (same scheme the type checker uses). When `p.M(...)`
|
|
is called, `go-eval-method-call` looks up the receiver type's method,
|
|
binds the receiver param to the struct value, runs the body. Both
|
|
value and pointer receivers work in v0 (treated the same — no
|
|
pointer semantics yet). Unary `-x` / `+x` / `!x` in `go-eval`.
|
|
E2E programs evaluating end-to-end now include: counter-via-method
|
|
(factorial), linear search returning index or -1, recursive
|
|
fibonacci(10) = 55, and the counter-bump-N-times pattern. +14 tests,
|
|
eval 80/80, total 457/457. `[nothing]`.
|
|
- 2026-05-27 — Phase 4 cont.: structs + selector access +
|
|
selector-assignment. `(:go-struct TYPE-NAME FIELDS)` value, with
|
|
FIELDS an assoc list. `type T struct {...}` is now significant at
|
|
eval-time too: registers `(:go-struct-type FIELD-NAMES)` in env so
|
|
positional composite literals like `Point{1, 2}` can map positions
|
|
to field names. Keyed literals `Point{x: 5, y: 10}` also work.
|
|
`go-eval-select` does field lookup; LHS `:select` in
|
|
`go-eval-assign-pairs` does field update. **`add(Point{1,2},
|
|
Point{3,4}) → Point{4,6}` works end-to-end** — functions receiving
|
|
and returning structs round-trip through the evaluator. +8 tests,
|
|
eval 66/66, total 443/443. Method-dispatch (looking up methods by
|
|
receiver type) pending; needs threading the type checker's
|
|
`#method/T/N` scheme into eval. `[nothing]`.
|
|
- 2026-05-27 — Phase 4 cont.: maps + index-assignment. Maps represented
|
|
as `(list :go-map ENTRIES)` where ENTRIES is an assoc list. New
|
|
helpers `go-map-get` / `go-map-set` / `go-slice-set`. Composite-lit
|
|
for `map[K]V{...}` evaluates via `go-extract-map-entries`. `m[k]`
|
|
index lookup added to `go-eval-index`; `len(m)` extended in
|
|
`go-eval-builtin`. **Index-assignment** for both slices and maps
|
|
added to `go-eval-assign-pairs`: only var-rooted LHS for v0
|
|
(`a[0] = 99`, `m["k"] = v`), enough for canonical programs.
|
|
**Word-count via `counts[words[i]] = counts[words[i]] + 1` works
|
|
end-to-end.** +8 tests, eval 58/58, total 435/435. `[nothing]`.
|
|
- 2026-05-27 — Phase 4 cont.: slice values + index/slice exprs + the
|
|
`len`/`append`/`print` builtins. Slice representation is
|
|
`(list :go-slice ELEMS)` for v0 (deferring the full length/cap/
|
|
backing-vector triple). `go-eval-composite` handles `[]T{...}` /
|
|
`[N]T{...}` literals (maps next). `go-eval-index` returns the i-th
|
|
element with bounds-check. `go-eval-slice` handles two-index slicing
|
|
with omitted low/high. New `go-env-builtins` starter env binds the
|
|
three builtins as `(:go-builtin NAME)` values; `go-eval-call`
|
|
recognises them and dispatches to `go-eval-builtin`. **Summing a
|
|
slice via `for i := 0; i < len(a); i++ { sum = sum + a[i] }` works
|
|
end-to-end.** +10 tests, eval 50/50, total 427/427. `[nothing]`.
|
|
- 2026-05-27 — Phase 4 cont.: for-loops, break, continue, inc-dec.
|
|
`go-eval-for` handles all three for-header shapes (infinite, while-
|
|
like, C-style) including init+post stmts and missing-cond defaulting
|
|
to true. `:break` and `:continue` propagate as keyword sentinels
|
|
through `go-eval-block` (alongside the existing `:return-value`
|
|
sentinel) until `go-for-loop` catches them — break exits, continue
|
|
runs post and re-loops. Inc-dec `x++`/`x--` updates env via the
|
|
same shadowing model as assign. **Iterative `fact(5) = 120` and the
|
|
classic for-loop sum-to-9 (= 45) both evaluate.** +7 tests, eval
|
|
40/40, total 417/417. `[nothing]`.
|
|
- 2026-05-27 — Phase 4 cont.: statements + function application.
|
|
`go-eval-stmt` handles `:return` (propagates a `:return-value`
|
|
sentinel up through blocks), `:var-decl`, `:short-decl`, `:assign`
|
|
(immutable-env shadowing), `:block`, `:if`/`:else`, and `:func-decl`
|
|
(binds a `:go-fn` value). `go-eval-call` extends the caller's env
|
|
with params → arg values, runs the body block, unwraps the return.
|
|
**Recursive `fib(5) = 5` evaluates correctly** — recursion works
|
|
because top-level funcs are bound in the calling env before any
|
|
recursive call happens; the func value carries no captured env in
|
|
v0 (dynamic-scope-ish), so true lexical closures wait for a later
|
|
slice. +8 tests, eval 33/33, total 410/410. `[nothing]` — pure eval
|
|
composition.
|
|
- 2026-05-27 — **Phase 3 ticked; Phase 4 scaffold.** Short-decl `:=`
|
|
marked done (was already covered by go-check-short-decl from the
|
|
decl-checking iteration). New `lib/go/eval.sx`: env-as-value (same
|
|
shape as ctx but bound to runtime values), literal decoding for
|
|
decimal/hex/oct/bin int literals (with underscores), variable lookup,
|
|
predeclared `true`/`false`/`nil`, and the full set of arithmetic /
|
|
comparison / logical binops via `go-eval-binop`. Hex/oct/bin parsing
|
|
via `go-hex-digit-value` (explicit char-equality dispatch since SX's
|
|
nth-on-string returns single-char strings, not numeric codes —
|
|
cleaner than the char-arithmetic the kernel ports use). eval suite
|
|
25/25, total 402/402. `[nothing]` — pure Go eval mechanics, the
|
|
cross-language insights are about type-checking which is in the
|
|
sister-plan diary.
|
|
- 2026-05-27 — Phase 3 cont.: **interface satisfaction** — the headline
|
|
Go-distinguishing typing feature this loop set out to validate.
|
|
Method decls record under `#method/TYPE-NAME/METHOD-NAME` keys in
|
|
ctx (value-receiver and pointer-receiver both key the base type).
|
|
`go-iface-satisfies? CTX TY-NAME IFACE-TYPE` walks the interface's
|
|
`:method` elements and verifies each one is present with a matching
|
|
(PARAMS, RESULTS) signature. Embedded interfaces in iface elements
|
|
are silently skipped in v0 (recursive interface resolution comes
|
|
later). Partial method sets and arity mismatches correctly return
|
|
false. types 72/72, total 377/377. `[shapes-static-types-
|
|
bidirectional]` — sister-plan diary updated with the structural-
|
|
satisfaction primitive the kit should ship.
|
|
|
|
Sister-plan diary update follows.
|
|
- 2026-05-27 — Phase 3 cont.: composite-literal element checking.
|
|
`go-synth-composite` dispatches on the literal's type expression:
|
|
`:ty-slice` and `:ty-array` check each element assignable to the
|
|
element type; `:ty-map` checks each `:kv` pair (key against K, value
|
|
against V) and rejects non-`:kv` map elements. The literal's
|
|
synthesised type is the type-expression itself, so nested composites
|
|
fall out by recursion: `[][]int{[]int{1,2}, []int{3,4}}` checks each
|
|
inner `[]int{...}` as a value of type `[]int`. Named-type literals
|
|
(`Point{1,2}`, `pkg.T{...}`) need type-decl-driven field resolution;
|
|
deferred. **Phase 3 acceptance bar (60+ tests) crossed: 65/65, total
|
|
370/370.** `[nothing]` — composite-literal semantics are mostly Go-
|
|
specific. Remaining Phase 3 items (interface satisfaction; AST-path
|
|
error context) sharpen the surface but don't gate Phase 4 (evaluator).
|
|
- 2026-05-27 — Phase 3 cont.: call type-checking. `go-synth-call`
|
|
synthesises the callee's type, asserts it's a `:ty-func`, arity-
|
|
checks args, then `go-check-args-against` runs each arg through
|
|
`go-check` against the corresponding param type (untyped-constant
|
|
flow works). Result: `:ty-void` for 0-result funcs, the result type
|
|
for 1-result, `(list :ty-tuple TYPES)` for multi-return. The
|
|
`:app` dispatch in `go-synth` now routes via `go-is-binop-call?`
|
|
(operator name + 2 args + op in the binop tables) — binops short-
|
|
circuit; everything else goes through the call path. **Recursive
|
|
functions now type-check** because the func is bound in its own
|
|
body's ctx by `go-check-func-decl`. +8 tests, types 55/55, total
|
|
360/360. `[nothing]` — Go-side composition on top of established
|
|
primitives; no new kit-relevant shapes (call semantics are uniform
|
|
across statically-typed guests).
|
|
- 2026-05-27 — Phase 3 cont.: function-declaration checking +
|
|
statement-level dispatch. `go-check-func-decl` binds the function in
|
|
the outer ctx (so the body can see itself), extends the body's ctx
|
|
with each `:field` param group via `go-ctx-extend-field` (the
|
|
binding-group shape's third consumer in the type checker — now
|
|
five total across parser+typer combined), then runs `go-check-block`
|
|
through every statement. `go-check-stmt` dispatches on `:return`,
|
|
`:assign`, `:var-decl`/`:const-decl`/`:short-decl`/`:type-decl`,
|
|
`:block`, falling back to `go-synth` for expression statements.
|
|
Return-list and assign-pair count mismatches are typed errors. +7
|
|
tests, types 47/47, total 352/352. `[nothing]` — pure Go-side
|
|
composition; the kit-relevant insights are already in the sister-
|
|
plan diary.
|
|
- 2026-05-27 — Phase 3 cont.: declaration checking — `var`/`const`/`type`
|
|
+ short-decl `:=`. `go-check-decl` returns the extended context (or a
|
|
`:type-error`). New helpers: `go-default-type` (untyped-int → int,
|
|
untyped-float → float64, etc.), `go-check-exprs-against`,
|
|
`go-bind-names-to-synth`. Annotated decls check each init expression
|
|
is assignable to the declared type; inferred decls bind names to the
|
|
default-typed synthesis of the init. **`var x float64 = 42 / 7` and
|
|
`const C int = 42` both bind x to float64 / C to int correctly via
|
|
the assignability relation from the previous commit.** +12 tests,
|
|
types 40/40, total 345/345. `[nothing]` — the kit-relevant insights
|
|
(synth/check + assignable predicate) already in the diary; this is
|
|
pure Go-side composition on top.
|
|
- 2026-05-27 — Phase 3 cont.: literal synth + binop synth + assignability.
|
|
Heuristic `go-classify-literal-string` decodes the parser's untagged
|
|
literal values back into `:int`/`:float`/`:imag`/`:string` kinds
|
|
(rune defers); these become `:ty-untyped-*` types. `go-synth-binop`
|
|
handles arithmetic / bitwise / comparison / logical operators with
|
|
untyped-constant unification: untyped int + untyped float → untyped
|
|
float; untyped + typed → typed. **Canonical Go pitfall now handled**:
|
|
`42 / 7` synthesises to `:ty-untyped-int`, then `go-check` against
|
|
`float64` returns `:ok` via `go-type-assignable?`. +16 tests, types
|
|
28/28, total 333/333. `[shapes-static-types-bidirectional]` — sister
|
|
plan diary updated with the assignable-relation insight (kit's
|
|
`check` should accept a `subtype?`/`assignable?` predicate parameter).
|
|
- 2026-05-27 — **Phase 3 scaffold.** First `lib/go/types.sx` cut: context
|
|
as an association list (`go-ctx-empty` + `-extend` + `-lookup`), a
|
|
load-bearing `go-ctx-extend-field` that consumes the `:field` binding-
|
|
group shape (validating the Phase 2 cross-deliverable observation),
|
|
predeclared `true`/`false`/`nil`, `go-synth` for identifier lookup,
|
|
`go-check` deferring to synth + structural equality. types suite
|
|
12/12, total 317/317. Literal kinds (untyped int/float/string/rune)
|
|
+ binop synth + var-decl checking next. `[shapes-static-types-
|
|
bidirectional]` — sister-plan diary updated with the synth/check
|
|
Go-side surface as it emerges.
|
|
- 2026-05-27 — **Phase 2 complete.** End-to-end multi-form file parsing.
|
|
`go-parse` now returns single forms for backward compat (~169 tests
|
|
unchanged) or `(list :file FORMS)` for multi-form input. Tests cover
|
|
hello-world, fibonacci, FizzBuzz, goroutine ping-pong, struct+method,
|
|
interface+method, and defer+select+range — each asserted via top-
|
|
level `decl-tags`. Type-switch is the one syntactic shape still
|
|
deferred. +7 tests, parse 176/176, total 305/305. Next: Phase 3
|
|
(bidirectional type checker). `[nothing]` — pure Go parser
|
|
composition; the cross-language insights are already in the sister-
|
|
plan diaries from earlier Phase 2 commits.
|
|
- 2026-05-27 — Phase 2 cont.: `switch` and `select` statements.
|
|
Tagged + tagless switch, multi-value cases, `default`, and select
|
|
with recv-into-var / send / bare-recv / default cases. New
|
|
`gp-parse-case-body` reads stmts until the next `case`/`default`/`}`
|
|
without consuming the terminator. AST shapes:
|
|
`(list :switch TAG CASES)`,
|
|
`(list :case VALUES BODY)`,
|
|
`(list :select CASES)`,
|
|
`(list :select-case COMM-STMT BODY)`,
|
|
`(list :default BODY)`. With this, **Phase 2 statement coverage
|
|
is complete** — type-switch is the one remaining shape (deferred).
|
|
+8 tests, parse 169/169, total 298/298. `[shapes-scheduler]` —
|
|
sister-plan diary updated with the `:select-case` uniform shape
|
|
insight (single kit primitive covers all four Go case kinds; default
|
|
vs no-default determines blocking semantics; cross-references to
|
|
Erlang's `receive ... after`).
|
|
|
|
Sister-plan diary update follows.
|
|
- 2026-05-27 — Phase 2 cont.: concurrency + iteration statements.
|
|
`go EXPR`, `defer EXPR`, channel send `ch <- v`, and the four
|
|
`for ... range` shapes (no-kv / k-only / k,v / assign-form). New
|
|
`gp-for-find-range` pre-scans the for-header to dispatch between
|
|
range and C-style/while forms cleanly. Send-stmt detection added to
|
|
the LHS-list branch (after lhs, `<-` → send). +9 tests, parse
|
|
161/161, total 290/290. `[shapes-scheduler]` — Go's concurrency-
|
|
primitive AST shapes (`:go`, `:defer`, `:send`, `:range-for`) all
|
|
landed; sister-plan diary updated with the corresponding kit-API
|
|
insights (uniform spawn-thunk shape, channel-recv ⇄ iteration
|
|
polymorphism at the range-coll dispatch).
|
|
|
|
Sister-plan diary update follows.
|
|
- 2026-05-27 — Phase 2 cont.: control-flow statements. `if cond { } [else
|
|
{ }]` with chained else-if, `for { }` (infinite), `for cond { }`
|
|
(while-like), `for init; cond; post { }` (C-style), `break`,
|
|
`continue`, plus `x++` / `x--` inc-dec statements. **Closed the
|
|
parser-mode caveat** flagged when composite literals landed:
|
|
`gp-no-comp-lit` is a re-entrant counter that suppresses the postfix
|
|
`{...}` → composite-lit interpretation inside control-flow condition
|
|
positions, matching Go spec § Composite literals. `gp-parse-control-
|
|
cond` wraps the increment/decrement so callers can't forget. +11
|
|
tests, parse 152/152, total 281/281. `[nothing]` — pure Go parser
|
|
shape work; the bidirectional-checker-relevant shapes (cond/body) are
|
|
already covered by the earlier `:field` insight.
|
|
- 2026-05-27 — Phase 2 cont.: statements. First slice covers
|
|
`return [exprs]`, short-decl `lhs := exprs`, assignment `lhs = exprs`,
|
|
compound assignment (`+= -= *= /= %= &= |= ^= <<= >>= &^=`), bare
|
|
expression statements, and nested blocks `{ ... }`. New `gp-parse-stmt`
|
|
dispatches on the leading token; `gp-parse-block-body` replaces the
|
|
func-decl `:body` sentinel with real `(:block STMTS)`. Existing
|
|
func/method tests updated to the new body shape. **Progress guards**
|
|
added to `gp-block-body-loop` and `gp-parse-composite-elems` —
|
|
unsupported syntax (`if`, `for`, etc.) now advances one token instead
|
|
of spinning. `gp-skip-block!` left as dead code; will be deleted once
|
|
control-flow stmts land. +9 tests, parse 141/141, total 270/270.
|
|
`[nothing]` — pure Go parser work; the cross-language statement
|
|
shapes will become a chiselling target once a second statically-typed
|
|
guest hits them.
|
|
- 2026-05-27 — Phase 2 cont.: func declarations. `func f() {}`,
|
|
`func add(x, y int) int { ... }`, multi-group params, multi-return,
|
|
signature-only (no body), pointer-receiver and value-receiver methods,
|
|
nested-brace body. New `gp-parse-decl-param-group` uses a named-greedy
|
|
algorithm: collects consecutive `ident [, ident]*` then parses a
|
|
type. `gp-skip-block!` brace-balances over the body opaquely; the AST
|
|
stores `:body` as a sentinel pending statement parsing. With this,
|
|
**all five `:field` binding-group consumers now exist** (struct
|
|
fields, var, const, func params, method receivers) — strong cross-
|
|
deliverable validation of the `ast-binding-group` kit proposal.
|
|
Anonymous-param-list disambiguation (`func(int, string)`) is a known
|
|
greedy-parser limitation flagged in plan. +8 tests, parse 132/132,
|
|
total 261/261. `[shapes-static-types-bidirectional]` — the consistent
|
|
use of `:field` across decls is what the sister kit's bidirectional
|
|
checker will use to propagate types from declarations to bindings.
|
|
|
|
Sister-plan diary update follows.
|
|
- 2026-05-27 — Phase 2 cont.: declarations — `package N`, `import "p"`,
|
|
`var name [TYPE] [= EXPRS]`, `const name [TYPE] [= EXPRS]`,
|
|
`type NAME TYPE`. New `gp-parse-top` dispatcher routes the five
|
|
decl keywords to `gp-parse-decl` while preserving expression parsing
|
|
for everything else. `var` and `const` reuse the `:field` binding-
|
|
group shape from Blockers — **first cross-deliverable use of the
|
|
proposed kit shape**: struct fields, func params, and now var/const
|
|
decls all share the same `(list :field NAMES TYPE)` envelope. `import`
|
|
uses canonical `ast-import` directly. Grouped forms (`var (...)`)
|
|
and `func` decls deferred. +10 tests, parse 124/124, total 253/253.
|
|
`[consumes-ast]` — first concrete use of `ast-import` from the kit;
|
|
also validates the `:field` shape across three contexts.
|
|
- 2026-05-27 — Phase 2 cont.: composite literals. `T{}`, `T{1, 2}`,
|
|
`T{X: 1, Y: 2}`, `[]T{...}`, `[N]T{...}`, `map[K]V{...}`,
|
|
`pkg.T{...}`, nested composites. AST shape
|
|
`(list :composite TYPE-OR-EXPR ELEMS)`; each element is an expression
|
|
or `(list :kv KEY VALUE)`. Two parser entry points: type-prefixed
|
|
(`gp-parse-primary` adds `[`/`map`/`struct` branches) and
|
|
ident-prefixed (postfix loop adds `{` branch). **Known limitation
|
|
flagged in plan:** when statement parsing arrives, the postfix `{`
|
|
branch will misread `if cond { ... }` as composite literal — needs a
|
|
"no-composite-here" parser-mode flag. +8 tests, parse 114/114, total
|
|
243/243. `[nothing]` — pure Go parser shape work.
|
|
- 2026-05-27 — Phase 2 cont.: interface-type expressions. `interface {}`,
|
|
`interface { Close() }`, `interface { String() string }`,
|
|
`interface { Read([]byte) (int, error) }`, plus embedded interfaces
|
|
(`Stringer`, `io.Reader`). AST shape:
|
|
`(list :ty-interface ELEMS)` where each element is either
|
|
`(list :method NAME PARAMS RESULTS)` or `(list :embed TYPE)`. Method
|
|
params reuse `gp-parse-func-type-params` — same anonymous-only shape
|
|
as func types. Go 1.18+ type sets (`~int | ~float64`) deferred to
|
|
generics work. With this, all Phase-2 **type expressions** are
|
|
complete. +8 tests, parse 106/106, total 235/235. `[nothing]` — pure
|
|
Go parser; the field-binding-group kit-gap proposal from the previous
|
|
commit covers the cross-language angle.
|
|
- 2026-05-27 — Phase 2 cont.: struct-type expressions. `struct {}`,
|
|
`struct { x int }`, `struct { x int; y string }`, `struct { x, y int }`
|
|
(shared type), nested struct fields. `gp-parse-struct-fields` walks
|
|
field rows tolerating ASI semis; each row is a name list + type. AST:
|
|
`(list :ty-struct FIELDS)` with each field `(list :field NAMES TYPE)`.
|
|
Embedded fields, tags, and methods deferred. +8 tests, parse 98/98,
|
|
total 227/227. `[proposes-ast]` — the `:field` shape (NAMES + TYPE)
|
|
recurs in func params, method receivers, var decls; flagged in
|
|
Blockers as a unified `ast-binding-group` candidate for the kit.
|
|
- 2026-05-27 — Phase 2 cont.: func-type expressions. `func()`,
|
|
`func() int`, `func(int, string)`, `func(int) string`,
|
|
`func() (int, error)`. AST shape `(list :ty-func PARAMS RESULTS)`
|
|
where both are lists of type nodes. Results parsing reuses param
|
|
parser for the multi-return `(T, T, ...)` case. Anonymous-only
|
|
params for now — named params (`func(a int, b string)`) need a
|
|
different shape and are required mainly for func DECLARATIONS not
|
|
pure func-type expressions. Variadic deferred. Covers nested
|
|
func-as-return and chan-of-func. +9 tests, parse 90/90, total 219/219.
|
|
`[nothing]` — pure Go parser; type AST proposals already in Blockers.
|
|
- 2026-05-27 — Phase 2 cont.: type expressions — slice `[]T`, array
|
|
`[N]T`, map `map[K]V`, chan in all three directions (`chan T`,
|
|
`chan<- T`, `<-chan T`). `gp-parse-type` now dispatches on
|
|
`*`/`[`/`map`/`chan`/`<-`/ident; each branch recurses for nested
|
|
types. Channel direction is `:both`/`:send`/`:recv`. AST stays
|
|
Go-specific tagged lists — kit has no type primitives at all.
|
|
Covers nested types end-to-end (slice-of-pointer, slice-of-slice,
|
|
map-with-slice-value, chan-of-map, pointer-to-slice). **Parse
|
|
acceptance bar (80+) crossed: +11 tests, parse 81/81, total 210/210.**
|
|
Func / struct / interface types and generics still pending in Phase 2.
|
|
`[proposes-ast]` — surfaces concrete type-node proposals (slice / array
|
|
/ map are universal across statically-typed guests; channel direction
|
|
is Go-specific). Logged in Blockers.
|
|
- 2026-05-27 — Phase 2 cont.: type assertion `v.(T)` postfix form.
|
|
Postfix `.` branch now disambiguates between `.field` (selector) and
|
|
`.(...)` (type assertion) by peeking at the next token. New
|
|
`gp-parse-type` handles the minimum needed: named (`int`, `MyType`),
|
|
qualified (`pkg.T`), pointer (`*T`, `**T`). AST shapes are
|
|
Go-specific tagged lists — kit has no notion of types at all yet
|
|
(this is a meta-gap: full bidirectional types arrive in Phase 3, but
|
|
even the parser needs a type substrate). Covers chained,
|
|
call-result, after-selector, and binary-precedence interactions. +9
|
|
tests, parse 70/70, total 199/199. `[nothing]`.
|
|
- 2026-05-27 — Phase 2 cont.: index `x[i]` and slice `x[a:b]` /
|
|
`x[a:b:c]` postfix forms. New `gp-parse-bracket` + `gp-parse-bracket-expr`
|
|
branch off the same postfix loop as calls/selectors. AST: Go-specific
|
|
`(list :index OBJ IDX)` and `(list :slice OBJ LOW HIGH MAX)` —
|
|
LOW/HIGH/MAX may be nil for omitted indices. Two more kit gaps logged
|
|
(no `:index`, no `:slice` in canonical AST). Permissive on `a[1::3]`.
|
|
Covers: literal idx, var idx, expr idx, string idx, chained `a[0][1]`,
|
|
mixed `a[0].field`, full slice with three indices. +12 tests, parse
|
|
61/61, total 190/190. `[proposes-ast]`.
|
|
- 2026-05-27 — Phase 2 cont.: postfix forms — function calls `f(a, b)`
|
|
via canonical `ast-app`, and member access `x.field` via Go-specific
|
|
`(list :select OBJ "field")`. The AST kit has no selector node;
|
|
logged in Blockers as `[proposes-ast]` — every nominally-typed guest
|
|
will hit the same gap, worth promoting on the next consumer. Postfix
|
|
loop sits between unary and primary so calls bind tighter than unary
|
|
(`-f(x)` = `-(f(x))`). Covers nested calls, chained selectors,
|
|
methods `obj.m(x)`, mixed precedence. +12 tests, parse 49/49, total
|
|
178/178. `[consumes-ast proposes-ast]`.
|
|
- 2026-05-27 — Phase 2 cont.: unary prefix operators (`+`, `-`, `!`, `^`,
|
|
`*`, `&`, `<-`). `gp-parse-unary` is recursive (`!!x`) and sits between
|
|
`gp-parse-expr` and `gp-parse-primary` so unary always binds tighter
|
|
than any binary. Symbols `+ - * & ^` are shared with binary; the
|
|
positional split (expression-start vs mid-expression) disambiguates
|
|
cleanly without lookback. Unary nodes are single-arg `ast-app`. +11
|
|
tests, parse 37/37, total 166/166. `[nothing]` — pure Go parser work.
|
|
- 2026-05-27 — Phase 2 cont.: binary operators via Pratt precedence
|
|
climbing. `gp-pratt-loop` consumes `pratt-op-lookup` against
|
|
`go-precedence-table`; left-assoc bumps right-min by 1, right-assoc
|
|
keeps prec. Binary op nodes are `(ast-app (ast-var OP) [LHS RHS])` —
|
|
uses the canonical `ast-app` shape rather than inventing a Go-specific
|
|
binary node. Covers: equal-prec left-to-right, `*` tighter than `+`,
|
|
`&&` tighter than `||`, comparison tighter than `&&`, long chains.
|
|
+9 tests, parse 26/26, total 155/155. `[consumes-pratt]`.
|
|
- 2026-05-27 — Phase 2 first slice: `lib/go/parse.sx` parser scaffold.
|
|
Defines `go-precedence-table` using `lib/guest/pratt.sx` entry shape
|
|
`(NAME PREC ASSOC)` — five Go precedence levels, all left-associative
|
|
per Go spec § Operator precedence. `go-parse` tokenises via
|
|
`go-tokenize`, then `gp-parse-primary` reads one literal / identifier
|
|
and emits a canonical AST node via `lib/guest/ast.sx`'s `ast-literal`
|
|
/ `ast-var`. parse 17/17, lex still 129/129, total 146/146.
|
|
`[consumes-pratt consumes-ast]`.
|
|
- 2026-05-27 — **Phase 1 complete.** Operator-set audit: added missing
|
|
`~` (Go 1.18+ generics type-set), exhaustive op coverage tests grouped
|
|
by category. Two kit gaps observed and logged in Blockers:
|
|
`lex-oct-digit?`/`lex-bin-digit?` predicates + `lex-match-longest`
|
|
table-driven prefix matcher — both useful for future statically-typed
|
|
guests. +6 tests, lex 129/129. `[proposes-lex]`. Phase 2 (parser) next.
|
|
- 2026-05-27 — Phase 1 cont.: raw string literals (backtick-delimited).
|
|
Multi-line, no escape processing, `\r` stripped per Go spec § String
|
|
literals. Same `"string"` token type as interpreted strings — parsers
|
|
/ type checkers don't need to distinguish. +9 tests, lex 123/123.
|
|
`[nothing]` — pure Go work; raw strings don't touch the substrate or
|
|
lib/guest story.
|
|
- 2026-05-27 — Phase 1 cont.: decimal float + imaginary literals.
|
|
`3.14`, `.5`, `1.`, `1e10`, `1.5e-3`, `2i`, `3.14i`. `gl-finish-number!`
|
|
handles exponent + `i` suffix; `gl-read-number!` returns the type
|
|
string (int/float/imag). ASI trigger list extended to float/imag.
|
|
Greedy-grammar pin: `1.method` lexes as `float ident`. Hex floats
|
|
(`0x1.fp0`) deferred. +22 tests, lex 114/114. `[consumes-lex]`.
|
|
- 2026-05-27 — Phase 1 cont.: prefixed integer literals (`0x..`, `0X..`,
|
|
`0b..`, `0B..`, `0o..`, `0O..`, legacy `0123`) + underscore separators
|
|
in any digit run. Dispatch in `gl-read-number!`; consumes
|
|
`lex-hex-digit?` from the kit. +14 tests, lex 92/92. `[consumes-lex]`.
|
|
- 2026-05-26 — Phase 1 first slice: `lib/go/lex.sx` tokenizer consuming
|
|
`lib/guest/lex.sx` predicates. 25 keywords, ident/int/string/rune lits,
|
|
line+block comments, common operators, automatic semicolon insertion per
|
|
Go spec § Semicolons (newline / EOF / block-comment-with-newline triggers).
|
|
Scoreboard + conformance.sh wired. 78/78 tests. `[consumes-lex]`.
|
|
- 2026-05-26 — Plan rewritten to integrate the lib/guest framework
|
|
(chiselling discipline, sister plans for scheduler + bidirectional
|
|
types, type-checker phase added, conformance scoreboard model adopted).
|
|
Original 2026-04-26 draft preserved in git history. Loop not yet
|
|
kicked off; Phase 1 (tokenizer) is the first iteration when this loop
|
|
spins up.
|