syntax_type: a module participates in the type position (coop patch) - #26
Conversation
The sibling of M41.5's syntax_param, and it comes from the same consumer: `T[]`, an element type spelled by the CORE and a container spelled by the module. `type_new` cannot buy it -- the word that opens the type is `i64`, and `word_add` refuses the core type words, so no keyed table can ever fire there. `void syntax_type(uptr fn)` registers `i64 f(i64 ty)`, consulted right after the core has read a type word and advanced past it, at all six sites that read one: `p_type()`, a local, a cast, a parameter, an `extern` and a top-level declaration. The handler receives the id the core read, may consume a suffix it owns (`[]`, `?`, `*`) and answers another type id, or 0 for "not mine". Registration order, first non-zero wins, and `nsyntype == 0` short-circuits the whole thing. One helper, `take_type(ty)` in src/parse.mc, consumes the type word and runs the chain, so the six call sites are one line each and cannot drift apart. Three guards at the type word's own position, run once after the whole chain (the post-M41.5 rule, which is why the broken fixtures register LAST): consumed tokens and returned 0, consumed no tokens, returned an invalid type. Arena tag T_SYNTYPE after T_SYNPARAM, T_COUNT 39 -> 40, lim_names/lim_seeds reconciled BY NAME (the M42 lesson). Cost: 126 added lines in src/, 61 of them neither comment nor blank (parse.mc +59/28, hooks.mc +47/19, arena.mc +20/14). Three new globals: the seed's MAXGLOBALS goes from 432/512 to 435/512 (84%). Proofs: lib/user_syntax_demo.mc teaches `i64[]` and check-surface uses it in all six source positions (exit 42, nine `type=i64[]` nodes); lib/user_typearr.mc proves the CORE's parse_params site, which the demo's syntax_param handler would otherwise hide; tests/err/077-079 assert the three guards with their exact message; lib/user_type_nop.mc is the inertness fixture (identical trees and objects over the whole tests/ corpus). check-inert against origin/main is identical everywhere. The five goldens rewritten once, each after its own criterion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The implementation is consistently threaded through all core type-reading sites with validated guard diagnostics, and the PR includes docs + focused negative tests + updated goldens to match the new behavior.
Pull request overview
Adds a new Tier-3 parser hook, syntax_type, that lets a module “participate in the type position” by consuming a suffix immediately after a core type word (e.g., i64[]) and returning an alternate type id. This closes the remaining module-unreachable grammar position needed for container/generic-style type spellings (not addressable via type_new because the leading type word is core-owned and blocked by word_add).
Changes:
- Implement
syntax_type(&f)registration and parsing support via a singletake_type(ty)helper used by all six core type-reading sites. - Extend arena table tags/limits tracking for the new hook and update demo/fixture modules to exercise and prove the hook (including the three guard diagnostics).
- Update docs, add new
tests/err/077–079cases, and refresh all five golden SHA256 files.
File summaries
| File | Description |
|---|---|
| src/hooks.mc | Adds the syntax_type registry and run_syntax_type(ty) dispatcher. |
| src/parse.mc | Introduces take_type(ty) with the three guard diagnostics; routes all type-reading sites through it. |
| src/arena.mc | Adds T_SYNTYPE, updates T_COUNT, lim_names, and lim_seeds alignment for limits reporting. |
| lib/user_syntax_demo.mc | Updates sd_param to use p_type() and adds syntax_type demo + three broken fixtures for guard testing. |
| lib/user_typearr.mc | Minimal module proving the core parse_params site via syntax_type only (i64[]). |
| lib/mc_typearr.mc | Small compiler wrapper that includes user_typearr for surface checks. |
| lib/user_type_nop.mc | Inertness fixture: syntax_type registered but always declines (returns 0). |
| lib/mc_type_nop.mc | Compiler wrapper for user_type_nop inertness checks. |
| tests/err/077-type-consumed-zero.mc | New error-case asserting “consumed tokens and returned 0” guard for syntax_type. |
| tests/err/078-type-noadvance.mc | New error-case asserting “consumed no tokens” guard for syntax_type. |
| tests/err/079-type-badid.mc | New error-case asserting “returned an invalid type” guard for syntax_type. |
| docs/surface.md | Documents syntax_type as the 9th registration and explains the “type position” contract and guards. |
| docs/reference/hooks.md | Adds the syntax_type API docs, its six call sites, and guard messages. |
| docs/reference/language.md | Documents “A suffix on a type word the core owns” and points to syntax_type. |
| docs/reference/diagnostics.md | Adds the three syntax_type diagnostic rows. |
| tests/golden/mc2.sha256 | Updates the macOS golden hash for build/mc2.o. |
| tests/golden/mc2-linux-arm64.sha256 | Updates the Linux arm64 golden hash for build/mc2l.o. |
| tests/golden/mc2-linux-x86_64.sha256 | Updates the Linux x86_64 golden hash for build/mc2l.o. |
| tests/golden/mc2-windows-arm64.sha256 | Updates the Windows arm64 golden hash for build/mc2w.obj. |
| tests/golden/mc2-windows-x86_64.sha256 | Updates the Windows x86_64 golden hash for build/mc2w.obj. |
Review details
- Files reviewed: 22/23 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
What
void syntax_type(uptr fn)— the sibling of M41.5'ssyntax_param, one grammar position over.It registers
i64 f(i64 ty), consulted right after the core has read a type word and advancedpast it. The handler receives the id the core read, may consume a suffix it owns (
[],?,*, whatever it declared) and answers another type id — typically one of its owntype_new—or 0 for "not mine", and the core keeps
ty. Handlers run in registration order, the firstnon-zero answer wins, and
nsyntype == 0short-circuits the whole thing.Why
The
ngenport of teko needsT[]: an element type spelled by the core and a containerspelled by the module.
type_newcannot buy it — the word that opens that type isi64, andword_addrefuses the core type words, so no keyed table can ever fire there. This is the sameshape of gap M41.5 closed for the parameter list, and it is the last position on the declaration
path a module could not reach.
The six positions
All six go through one helper,
take_type(ty)insrc/parse.mc, which consumes the type word(the
next()each site used to make for itself) and then offers the position to the chain — soeach call site is one line and they cannot drift apart:
p_type()parse_var)i64[] xs;parse_primary)(i64[]) pparse_params)i64 f(i64[] xs)extern(parse_extern)extern i64[] memcpy(i64[] d, i64[] s, i64 n);parse_top)i64[] g;andi64[] f() { … }The three guards
At the type word's own position (the word is copied out of the token before
next()moves offit) and run once after the whole chain, not per handler — the post-M41.5 review's rule, which
is why the deliberately broken fixtures are registered LAST:
syntax_type handler consumed tokens and returned 0: <word>— declining is only sound fromwhere the handler was called; otherwise the core reads the rest of the declaration from the
middle of a type, with no diagnostic anywhere;
syntax_type handler consumed no tokens: <word>— a type with no suffix read cannot be aboutthis position;
syntax_type handler returned an invalid type: <word>—< 0or>= type_count(); that idgoes straight into
type_width/type_align/type_kind, so a bad one is a wrong framelayout later, not a diagnostic here.
Inertness
lib/user_type_nop.mc+lib/mc_type_nop.mc: a module whose only registration issyntax_typeand whose handler answers 0 for every type word of every declaration. Its--dump-astand its objects are byte-identical to the untaught compiler's over the wholetests/corpus.
check-objstays 32/32 identical to the frozen C seed, andscripts/check-inert.sh build/mc1.pre build/mc1(pre = amc1built fromorigin/main) isidentical everywhere: 33 objects (
tests/*.mcandsrc/mc.mc) plus byte-identical artefacts forexamples/api,lang,conc,desktopandkernel, through the taught compiler each sidebuilds.
Tests
lib/user_syntax_demo.mcteachesi64[]—type_new("i64[]", 8, 8, TK_INT), a pointer-sizedhandle whose name is a lexeme the lexer can never form, which is exactly why the spelling is
free.
scripts/check-surface.shcompiles one source that uses it in all six sourcepositions (a global, an
extern, a return type, a parameter, a local and a cast), runs it(
40 + 2throughmemcpy, exit 42) and counts the ninetype=i64[]nodes in--dump-ast.sd_paramnow reads its type withp_type()instead ofp_next(), which is what puts theparameter position on the same path. Because that handler claims every typed parameter, the
core's own
parse_paramssite is proved by a second, twelve-line module,lib/user_typearr.mc(syntax_typeand nothing else): same source, same 42, same nine nodes.name expected at top level, andvariable name expectedfori64[] xs;in a local) — asserted.tests/err/077-type-consumed-zero.mc,078-type-noadvance.mc,079-type-badid.mc, each withits exact message, driven by the fixtures
sd_teat/sd_tnop/sd_tbad(keyed on au8/u16/u32followed by[, a shape no ordinary source has).Contract, not accident
The teko session asked whether
type_new(w)andsyntax_expr(w)on the same word coexist.docs/reference/hooks.md§ 3 now says it is a contract:tok_addis idempotent, the twotables are consulted at disjoint grammar positions, and the one place they meet — the cast
(w)— resolves to the type becauseparse_primaryteststype_of_tokenfirst.Cost
126 added lines in
src/, 61 of them neither comment nor blank (parse.mc+59/28,hooks.mc+47/19,arena.mc+20/14, twelve of those last being the renumbered tags and the twoseed rows). Arena tag
T_SYNTYPEafterT_SYNPARAM,T_COUNT39 → 40,lim_names/lim_seedsreconciled by name (the M42 lesson), and
mc limitsgains asyntax_typerow. Three newglobals: the seed's
MAXGLOBALSgoes from 432/512 to 435/512 (84%).stage0/untouched(2848/3000).
Checks
make bundlere-run before bootstrapping (86 files, raw 1036368 → LZ 485658, blob 486738 B).make checkgreen end to end, RC 0, zero FAIL:test32/32,check-lex/check-ast/check-asm139/139 (2 skipped),check-obj32/32 identical to the frozen seed,check-bundle,bootstrapat a fixed point (mc2.o == mc3.o, 1113264 B; the--dump-asmdiffbetween
mc1andmc2is empty),check-surface32/32 + the six new cases,test-exe32/32,
check-mc15/15,check-standalone,check-parts,check-toml10/10,check-build53/53,
check-stubs9/9,check-limits17/17 under 90%,check-minimal,test-linux41/41and
test-linux-exe44/44 musl + 44/44 gnu,test-linux-x86_6438/38 and 41/41 + 41/41,test-windows42/42 andtest-windows-x86_6440/40 objects cross-compiled,check-examples,check-lang,check-conc,check-desktop,check-float,check-wide,check-kernel,check-avr,check-sandbox55 ok,check-docs(197 symbols, 33 flags, 20 TOML keys, 10directives, 51 samples, 321 links),
site87 pages +check-site(0 link problems).make check-linux-hostRC 0 over all four cells.Goldens
Rewritten once, each only after its own criterion:
mc2.sha256897b1887…5fcdf7d→8a84d434a26ff6163149645b4390107543ae1e4b9c515ac84b001b1b868b918f(after the empty
--dump-asmdiff andcmp build/mc2.o build/mc3.o);make check-linux-host—mc2-linux-arm64.sha256df620903bd5cd48a69c586d2583cdf04a7eae0cb92598b9a9a71e91b470053f6,mc2-linux-x86_64.sha25614dc5fc814f1c82a59561b99b91313473032194aa976a75e003a649266d0c793;tests/golden/README.md—mc2-windows-arm64.sha2567711f4866bda984f75029f0bcafa0c24b7b9ba729ae781e1956616604704263b(1135198 B),
mc2-windows-x86_64.sha256d733d2aa7f07fda9fb551384a80084e8da2870c70b01a540fe7c5bde609f2b08(1163986 B), both alsowritten byte for byte by
build/mc2.Docs
docs/reference/hooks.md(§ 3 is now six word registrations + five hooks that claim none, thesyntax_typesection with the six sites and the three guards, and the coexistence contract),docs/reference/diagnostics.md(three rows),docs/surface.md(the nine registrations, and anew § "The type position"),
docs/reference/language.md§ 2 ("A suffix on a type word the coreowns").
make check-docsgreen.Patch, no release label.