Skip to content

syntax_type: a module participates in the type position (coop patch) - #26

Merged
schivei merged 1 commit into
mainfrom
syntax-type
Sep 5, 2026
Merged

syntax_type: a module participates in the type position (coop patch)#26
schivei merged 1 commit into
mainfrom
syntax-type

Conversation

@schivei

@schivei schivei commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What

void syntax_type(uptr fn) — the sibling of M41.5's syntax_param, one grammar position over.
It registers i64 f(i64 ty), consulted right after the core has read a type word and advanced
past 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 own type_new
or 0 for "not mine", and the core keeps ty. Handlers run in registration order, the first
non-zero answer wins, and nsyntype == 0 short-circuits the whole thing.

Why

The ngen port of teko needs T[]: an element type spelled by the core and a container
spelled by the module. type_new cannot buy it — the word that opens that type is i64, and
word_add refuses the core type words, so no keyed table can ever fire there. This is the same
shape 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) in src/parse.mc, which consumes the type word
(the next() each site used to make for itself) and then offers the position to the chain — so
each call site is one line and they cannot drift apart:

site source
p_type() whatever a module's own handler reads
a local (parse_var) i64[] xs;
a cast (parse_primary) (i64[]) p
a parameter (parse_params) i64 f(i64[] xs)
an extern (parse_extern) extern i64[] memcpy(i64[] d, i64[] s, i64 n);
a top-level declaration (parse_top) i64[] g; and i64[] f() { … }

The three guards

At the type word's own position (the word is copied out of the token before next() moves off
it) 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 from
    where 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 about
    this position;
  • syntax_type handler returned an invalid type: <word>< 0 or >= type_count(); that id
    goes straight into type_width / type_align / type_kind, so a bad one is a wrong frame
    layout later, not a diagnostic here.

Inertness

lib/user_type_nop.mc + lib/mc_type_nop.mc: a module whose only registration is
syntax_type and whose handler answers 0 for every type word of every declaration. Its
--dump-ast and its objects are byte-identical to the untaught compiler's over the whole tests/
corpus. check-obj stays 32/32 identical to the frozen C seed, and
scripts/check-inert.sh build/mc1.pre build/mc1 (pre = a mc1 built from origin/main) is
identical everywhere: 33 objects (tests/*.mc and src/mc.mc) plus byte-identical artefacts for
examples/api, lang, conc, desktop and kernel, through the taught compiler each side
builds.

Tests

  • lib/user_syntax_demo.mc teaches i64[]type_new("i64[]", 8, 8, TK_INT), a pointer-sized
    handle whose name is a lexeme the lexer can never form, which is exactly why the spelling is
    free. scripts/check-surface.sh compiles one source that uses it in all six source
    positions
    (a global, an extern, a return type, a parameter, a local and a cast), runs it
    (40 + 2 through memcpy, exit 42) and counts the nine type=i64[] nodes in --dump-ast.
  • sd_param now reads its type with p_type() instead of p_next(), which is what puts the
    parameter position on the same path. Because that handler claims every typed parameter, the
    core's own parse_params site is proved by a second, twelve-line module,
    lib/user_typearr.mc (syntax_type and nothing else): same source, same 42, same nine nodes.
  • The default compiler refuses both halves (name expected at top level, and
    variable name expected for i64[] xs; in a local) — asserted.
  • tests/err/077-type-consumed-zero.mc, 078-type-noadvance.mc, 079-type-badid.mc, each with
    its exact message, driven by the fixtures sd_teat / sd_tnop / sd_tbad (keyed on a u8 /
    u16 / u32 followed by [, a shape no ordinary source has).

Contract, not accident

The teko session asked whether type_new(w) and syntax_expr(w) on the same word coexist.
docs/reference/hooks.md § 3 now says it is a contract: tok_add is idempotent, the two
tables are consulted at disjoint grammar positions, and the one place they meet — the cast
(w) — resolves to the type because parse_primary tests type_of_token first.

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 two
seed rows). Arena tag T_SYNTYPE after T_SYNPARAM, T_COUNT 39 → 40, lim_names/lim_seeds
reconciled by name (the M42 lesson), and mc limits gains a syntax_type row. Three new
globals: the seed's MAXGLOBALS goes from 432/512 to 435/512 (84%). stage0/ untouched
(2848/3000).

Checks

make bundle re-run before bootstrapping (86 files, raw 1036368 → LZ 485658, blob 486738 B).
make check green end to end, RC 0, zero FAIL: test 32/32, check-lex/check-ast/
check-asm 139/139 (2 skipped), check-obj 32/32 identical to the frozen seed,
check-bundle, bootstrap at a fixed point (mc2.o == mc3.o, 1113264 B; the --dump-asm diff
between mc1 and mc2 is empty), check-surface 32/32 + the six new cases, test-exe
32/32, check-mc 15/15, check-standalone, check-parts, check-toml 10/10, check-build
53/53, check-stubs 9/9, check-limits 17/17 under 90%, check-minimal, test-linux 41/41
and test-linux-exe 44/44 musl + 44/44 gnu, test-linux-x86_64 38/38 and 41/41 + 41/41,
test-windows 42/42 and test-windows-x86_64 40/40 objects cross-compiled, check-examples,
check-lang, check-conc, check-desktop, check-float, check-wide, check-kernel,
check-avr, check-sandbox 55 ok, check-docs (197 symbols, 33 flags, 20 TOML keys, 10
directives, 51 samples, 321 links), site 87 pages + check-site (0 link problems).
make check-linux-host RC 0 over all four cells.

Goldens

Rewritten once, each only after its own criterion:

  • mc2.sha256 897b1887…5fcdf7d8a84d434a26ff6163149645b4390107543ae1e4b9c515ac84b001b1b868b918f
    (after the empty --dump-asm diff and cmp build/mc2.o build/mc3.o);
  • the Linux pair deleted and re-recorded by make check-linux-host
    mc2-linux-arm64.sha256 df620903bd5cd48a69c586d2583cdf04a7eae0cb92598b9a9a71e91b470053f6,
    mc2-linux-x86_64.sha256 14dc5fc814f1c82a59561b99b91313473032194aa976a75e003a649266d0c793;
  • the Windows pair cross-computed per tests/golden/README.md
    mc2-windows-arm64.sha256 7711f4866bda984f75029f0bcafa0c24b7b9ba729ae781e1956616604704263b
    (1135198 B), mc2-windows-x86_64.sha256
    d733d2aa7f07fda9fb551384a80084e8da2870c70b01a540fe7c5bde609f2b08 (1163986 B), both also
    written byte for byte by build/mc2.

Docs

docs/reference/hooks.md (§ 3 is now six word registrations + five hooks that claim none, the
syntax_type section 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 a
new § "The type position"), docs/reference/language.md § 2 ("A suffix on a type word the core
owns"). make check-docs green.

Patch, no release label.

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>
Copilot AI lite review requested due to automatic review settings September 5, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 single take_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/077079 cases, 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.

@schivei
schivei merged commit 8c31a0e into main Sep 5, 2026
15 checks passed
@schivei
schivei deleted the syntax-type branch September 5, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants