Conversation
The coop patch the teko/ngen consumer asked for. A module can already see the files it opens itself; what it cannot see is a #include the CORE resolved -- do_directive is internal and pushes without telling anybody -- so a lexical pre-scan was blind to exactly the files the source asks for. void on_source(uptr fn) -> void f(uptr name, uptr src, i64 len) The table is in src/hooks.mc with every other registry (arena tag T_ONSOURCE, the tags after it renumbered and lim_names/lim_seeds reconciled by name). The call site is one place, lex_push_mem, which every road goes through: the entry, a relative #include, a bundled or package one, and p_push_source. The lexer reaches the table through one function pointer, the shape M15 gave the bundle, because src/lexdump.mc includes the lexer without src/hooks.mc. The entry file is announced too: lex_init pushes it before user_init runs, so on_source replays the sources already open to the handler it has just registered, in push order. OF_SRC/OF_LEN keep each frame's whole buffer, so a replay hands over the source and not the bytes still unread. One guard, measured before it was written: a handler that pushes a source from inside the callback recursed until the process stack was gone (SIGSEGV, exit 139, no diagnostic). A re-entrancy flag makes the push itself the error -- `on_source handler pushed a source: <name>`, exit 1. Cost: 133 added lines in src/, 66 of them code; five globals (444 -> 449 of the seed's MAXGLOBALS). Inert with nothing registered: the pointer is 0 and there is not even a callp. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Collaborator
Author
|
Merging: CI 14/14 green on cbe0811, mergeable clean, patch (no release label). Copilot answered |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
on_source(&fn)— a module is told about every source the lexer pushes. Requested by theteko/ngen consumer, approved by the owner on 2026-09-06. A patch: no release label.
Why
A module can already see the files it opens itself (it calls
lex_includefrom its ownhandler). What it cannot see is a
#includethe core resolved:do_directiveis internaland pushes without telling anybody. ngen gets free declaration order from a lexical pre-scan, so
it was blind to exactly the files the source asks for.
lex_init#include "x.mc"do_directive→lex_include— internal to the core#include <name>p_push_sourcenameis whatlex_file()prints for that frame;src/lenare the whole buffer. The handleranswers nothing.
What was decided
The entry file IS announced.
lex_initpushes it beforeuser_init()runs (src/cli.mc,src/driver.mcandsrc/limits.mcare all in that order), so the natural call has alreadyhappened when a handler can first exist.
on_sourcecloses that by replaying — at registrationtime, to the newly registered handler alone, in push order — every source already open. The
rule is one sentence: a handler sees every source pushed after it registers, plus the ones
already open when it registers, and it holds for a registration made during parsing too. That
is what
OF_SRC/OF_LENare for (OF_SIZE32 → 48): a frame keeps the buffer it was pushedwith untouched while
cpwalks it, so a replay hands over the whole source and not the bytesstill unread. The doc says a module need not scan the entry by hand any more.
The table is in
src/hooks.mc, the call site cannot be.src/lexdump.mcincludes the lexerwith
arena.mcand nothing else, so the lexer must not name a symbol ofhooks.mc. It is M15'sbundle shape exactly:
lex_set_source_hook(&run_on_source), one function pointer, stored by thefirst registration. With nothing registered the pointer is 0 and there is not even a
callp.The guard was measured before it was written. A handler runs with the frame it is being told
about already on the stack. A push from inside the callback interleaves the announcement of one
source with the opening of the next, and a handler that pushes unconditionally recursed until the
process stack was gone — reproduced with
lib/user_srcpush.mc: SIGSEGV, exit 139, nodiagnostic. A frame-depth comparison after the call cannot see it, because the call never
returns; the guard is a re-entrancy flag tested at the head of
lex_push_mem, so the push itselfis the error, on the first one:
Arena tag
T_ONSOURCEinserted afterT_ONJUMP,T_COUNT40 → 41, the eleven tags after itrenumbered and
lim_names/lim_seedsreconciled by name (the M42 lesson: the 16 that belongsto
backendstravelled with it).mc limitsgains anon_sourcerow.Cost
133 added lines in
src/, 66 of them neither comment nor blank:lex.mc+60/31,hooks.mc+52/19,arena.mc+21/16 (twelve of those last being the renumbered tags and the twoseed rows). Five new globals — the seed's
MAXGLOBALSgoes from 444/512 to 449/512 (87%).Proofs
lib/user_syntax_demo.mc(+55) counts the sources and joins their names(
syntax_expr("srccount"),syntax_expr("srcnames"), the latter building anN_STRnode).scripts/check-surface.sh(+115) compiles one program that reads both back and prints them:the entry (replay), the source the demo module itself pushes at the end of
user_init, arelative
#includethe core resolved and a bundled#include <prelude>the core resolved —in push order. The default compiler refuses the same source (
onsrc.mc:6: unknown name).The guard asserted with its exact message through
lib/mc_srcpush.mc.Inert by construction:
lib/user_source_nop.mc+lib/mc_source_nop.mc— a module whoseonly registration is
on_sourceand whose handler does nothing — produce byte-identical--dump-astand objects over the wholetests/corpus. The four newlib/fixtures arenot in
tools/bundle.list(the M41 precedent for check-script-only modules).Gates
make bundlefirst (93 files, raw 1178425 → LZ 551503, blob 552665 B), then:make checkRC 0, zero FAIL (8m14s):budget2848/3000,test32/32,check-lex149/149(3 skipped),
check-ast150/150,check-asm150/150,check-obj32/32 identical to thefrozen seed,
check-bundle,bootstrapat a fixed point (mc2.o == mc3.o, 1268832 B; the--dump-asmdiff betweenmc1andmc2is empty),check-surface32/32 + the four newcases,
test-exe32/32,check-mc15/15,check-standalone,check-parts,check-toml10/10,check-build53/53,check-pkg85/85,check-stubs9/9,check-sysroots(13 rows),check-limits17/17 under 90%,check-minimal,test-linux41/41,test-linux-x86_6439/39,
test-linux-exe44/44 musl + 44/44 gnu,test-linux-x86_64-exe42/42 + 42/42,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(QEMU 11.0.1),
check-avr,test-sandbox55 ok / 0 failed / 1 skipped,check-docs(198 symbols, 36 flags, 27 TOML keys, 10 directives, 51 samples, 361 links),
site89 pagescheck-site(0 link problems) +check-site-linux11/11.make check-linux-hostRC 0 over all four cells (aarch64 musl 41/41 and gnu 42/42, x86_64musl 39/39 and gnu 40/40), each after its own
mc2l.o == mc3l.oand with the cross proof green.scripts/check-inert.sh build/mc1.pre build/mc1(pre = amc1built fromorigin/main):33 objects identical (
tests/*.mcandsrc/mc.mc) plus byte-identical artefacts forexamples/api,lang,conc,desktopandkernel—check-inert: everything identical.The five goldens, each after its own criterion
mc2.sha256after the empty--dump-asmdiff andcmp build/mc2.o build/mc3.o; the Linux pairdeleted and re-recorded by
make check-linux-host; the Windows pair cross-computed pertests/golden/README.md, andbuild/mc2writes both byte for byte asbuild/mc1does.Docs
docs/reference/hooks.md(§ 1's pipeline, § 3 is now six word registrations + six hooks thatclaim none, and an
on_sourcesection with the four roads, the entry-file rule and the guard),docs/reference/diagnostics.md(one row in § 5),docs/surface.md(the ten registrations and a§ "Every source the lexer pushes"),
CLAUDE.md§ State.stage0/untouched: 2848/3000,git diff origin/main -- stage0/empty.🤖 Generated with Claude Code