Spec M24 -- Tier 4: primitives and hardware instructions taught from the surface

Owner's direction (2026-09-04): "capabilities become surface." A developer must be able to introduce NEW PRIMITIVES without touching mc -- new value types (fp8/fp16 for a GPU particle study, i128/u128, floats in general) and hardware-specific instructions (a legacy SSE/MMX op, a new AVX op on a specific CPU) usable on those types -- from the surface, in a module, teachable through mc build [compiler].modules.

This replaces the previous M24, which put f32/f64 keywords in the lexer, a decimal-to-binary routine in lex_number, thirteen mf_* machine tasks and a #machine directive inside src/. That spends ~450 core lines on ONE capability and leaves the next one (fp16, i128, an AVX op, an 8-bit word) exactly as unreachable as before. The rule for this milestone is the plan's: only mechanisms enter src/, each priced in lines; f32/f64 become the FIRST LIBRARY built on them. The C seed (stage0/) is frozen, only ever compiles src/mc.mc, and learns nothing.

What already exists #

Design #

The rule: a type id below TY_MAX is a core type and behaves exactly as it does today, byte for byte. A type id at or above TY_MAX was registered by a module, and every core decision about it is delegated -- width and alignment to the registry, representation/register file/spill/ABI to the machine, literals to a syntax_lit handler, instruction selection to a derived machine table. The core never learns what a float is. Eight mechanisms, 187 core lines, contract version 3 with no slot added and no signature changed (the bump is documentation: what a module may rely on).

M1 -- the type registry (~55 lines) #

i64  type_new(uptr name, i64 width, i64 align, i64 kind) -> ty      // ty >= TY_MAX
i64  type_width(i64 t)        // existing name, registry-aware above TY_MAX
uptr type_name(i64 t)         // ditto: --dump-ast prints `type=f64`, not `?`
i64  type_kind(i64 t)  i64 type_align(i64 t)  i64 type_count()
#define TK_INT 0 / TK_FLOAT 1 / TK_WIDE 2 / TK_OPAQUE 3

type_new registers the word through the SAME table type_alias uses, so type_of_token (src/parse.mc:677-686) needs one line and the word becomes valid in all seven type positions at once. type_alias's guard widens from base >= TY_MAX to base >= type_count(). A growable arena block under M23's rule (it scales with what a module teaches), seeded with the seven core widths. kind is what a machine dispatches on when it does not know the exact id; the core consumes only width and align. Seed: none -- an ordinary function called from user_init(), no keyword and no directive, so tok_init (src/lex.mc:246-293) is untouched, K_U8..K_EXTERN do not shift and check-lex keeps cross-checking the two lexers.

M2 -- the literal's type survives resolve (1 line) #

res_expr's N_INT arm sets TY_I64 unconditionally (src/gen_resolve.mc:409), discarding nd_type. Under the design below a taught literal is an N_INT whose nd_type is the module's, so without this the type is thrown away before the walker or any machine sees it. Honour nd_type when it is >= TY_MAX. This one line is load-bearing and was missed by every proposal.

M3 -- constant folding stops at a type the core did not define (~6 lines) #

fold_binary folds any two N_INTs regardless of type (src/parse.mc:958-969), fold_unary one (:941-957), fold_cast masks by 1/2/4 bytes (:971-983). With a float literal carried as its IEEE bit pattern, 1.5 + 2.5 would fold to an INTEGER add of two bit patterns and produce an infinity at compile time with no diagnostic; -1.5 would integer-negate a bit pattern. Three early returns on nd_type(...) >= TY_MAX. The core has no arithmetic for a type it did not define, so it leaves the node to the module's machine. Inert today: no node in the tree carries such a type.

M4 -- the depth type, as walker functions (~30 lines) #

i64 walk_depth_type(i64 d)    // the type of the value at depth d; TY_I64 by default
i64 walk_ret_type()           // the type the value ABOUT to land at this depth will have

A MAXDEPTH-sized array beside frame_off, reset in gen_func (src/gen_walk.mc:870), written from res_type(n) at the top of gen_expr (:660-679). Not a task slot: no signature moves, the contract stays additive, and a machine that never reads it emits byte-for-byte what it emits today. This is the mechanism the old spec's thirteen mf_* slots existed to work around -- MTASK_BIN(MOP_ADD, d, d2) with walk_depth_type(d) == f64 is fadd, MTASK_RET(d) returns in v0, and MTASK_CALL(d, na, sym) walks walk_depth_type(d + i) and runs the AAPCS64 NGRN/NSRN split -- the whole float ABI, for free.

Re-announcement is part of the mechanism, not a caveat. Five sites produce a value whose type differs from what the child announced, and each must set the entry after the task: gen_binary's comparison branch (:527), gen_logic's shortcut MTASK_CONST (:518), MUN_LNOT, gen_intrin's load, and gen_call -- where depth d is overwritten by argument 0 (:648-656), which is exactly why walk_ret_type() is mandatory rather than convenient. Without this, (a < b) + 1 on taught floats emits a float add on an integer 0/1, silently.

M5 -- the frame slot is sized by the type (2 lines) #

slot_new(8) is unconditional for a scalar local (src/gen_walk.mc:693) and for a parameter (:881); both become slot_new(type_width(ty)). Provably byte-identical for all seven core types, because slot_new rounds (size + 7) & ~7 (:321-324) -- widths 1..8 give the same offset. A 16-byte type gets 16. No asymmetric helper, no TY_MAX clause. The local-array path (:686-688) and gen_globals (:924-933) already read type_width.

M6 -- syntax_lit(&f) (~28 lines) #

void syntax_lit(uptr fn);     // i64 f() -> node index, or 0 = the core handles it

Consulted by parse_primary where it is about to build the N_INT/N_CHAR node (src/parse.mc:705-712); shaped like on_stmt (src/hooks.mc:116-139), short-circuited by if (nonlit == 0) so an untaught compiler does not even make the callp. The handler reads the raw lexeme through p_start() (:1737) and does its own decimal-to-binary conversion. This is the one grammar position Tier 3 genuinely cannot reach, and it is generic: it says "numeric literal", not "float". The decimal-to-binary routine therefore lives in the MODULE, not in lex_number -- which is what keeps --dump-tokens byte for byte what the frozen stage0/lex.c produces and check-lex meaningful over the whole tree. The old spec would have ended that.

The documented fallback stays: #token "." + syntax_infix(".", prec, &f) works for D.D, costs zero core lines, and is rejected as the mechanism because it reserves . program-wide (colliding head-on with examples/lang, whose lg_dot owns it, and syntax_infix refuses the second registration outright), cannot spell 1e-3 at all, and makes the handler guess the token span.

M7 -- intrinsic(name, nargs, ty, &f) (~55 lines) #

void intrinsic(uptr name, i64 nargs, i64 ty, uptr fn);   // void f(i64 d, i64 nargs)
i64  intrinsic_find(uptr name);

One row inserted between opc_find and func_find in the dispatch both res_call (src/gen_resolve.mc:366-394) and gen_call (src/gen_walk.mc:638-658) already run in that order, so a core intrinsic can never be shadowed and every existing diagnostic keeps its order. The handler receives its arguments already lowered to depths d..d+nargs-1 with walk_depth_type filled in.

This is the mechanism the owner's principle actually asks for and the only one with no zero-line alternative. Traced: #opcode refuses a non-constant argument, so a named instruction can only be applied to pinned registers inside a whole leaf function; syntax_expr can build any node the CORE already lowers but gen_expr ends in expression with no codegen (:678), so a module cannot introduce a node kind; gen_call ends in call to unknown function (src/gen_resolve.mc:389), so it cannot introduce a call. ldf64(p), f16_to_f32(h), sqrt_f64(x) and one AVX op are the same twelve module lines after this.

Paired with M7, contract v3 publishes val_reg(d, scratch), dst_reg(d) and dst_done(d, reg) -- present with identical signatures in both machines (src/machine_arm64.mc:115-128, src/machine_x86_64.mc:226-237) -- as the names a handler may call to find where the allocator put its operands. Zero lines; it is a decision about what is frozen, taken deliberately rather than by convention.

M8 -- deriving a machine (~10 lines) #

uptr machine_tab(uptr name);  void machine_slot(uptr tab, i64 task, uptr fn);

machine_task writes the global m_arm64 by name (src/machine_arm64.mc:702) while docs/reference/hooks.md:145-146 tells a module author to derive a table with it -- following the published recipe corrupts arm64's own table (M39's G9). A module can already derive with ld64/ st64 (the Win64 machine does), so this is contract and safety, not capability, and the doc bug is fixed with it. Delegation needs no new names: the module copies the table first, so the built-in implementation of any slot is a pointer it holds and can callp -- which is what keeps a float module from reimplementing integer codegen without freezing a64_bin/a64_const as surface.

M9 -- --dump-machine (~40 lines, step B, observability not mechanism) #

Per registered machine, one line per task: task name, bundled or the module's symbol. This is the audit the old § 4 promised, kept; the #machine directive it was attached to is dropped.

Why #machine is dropped (a reversal, recorded rather than silently omitted): it is a fourth encoding-template language after #opcode and x86_desc; a task is not an instruction -- MTASK_BIN(MOP_ADD, d, d2) on a spilled depth is a load, an op and a store (src/machine_arm64.mc:113-126), so a one-word template is false for every deep expression; and it would be a directive the frozen stage0/lex.c dir_names[] cannot parse, so it could never appear in src/*.mc anyway. machine_slot + #opcode + intrinsic + --dump-machine reach the same place with no new syntax.

Step 1 -- <float>, the first library #

All outside src/, bundled (tools/bundle.list +4). lib/user_default.mc does not register it: the stock mc has no floats, which makes "objects identical to the seed" a structural fact rather than a tested coincidence, and float programs are built by a taught compiler the way examples/api and examples/lang are.

filelineswhat
lib/float.mc~340type_new("f64", 8, 8, TK_FLOAT), f32, f64raw; the syntax_lit handler with correctly-rounded decimal-to-binary in .mc; ldf*/stf* and sqrt_f64/fabs/fmin/fmax via intrinsic; putf64; user_init
lib/machine_arm64_float.mc~280machine_tab("arm64") copied, ~14 slots replaced, the rest delegating through the copied pointers
lib/machine_x86_64_float.mc~320the same over x86_64 and x86_64-win (SSE2)
lib/mc_float.mc~20#include <mc/core> + the three + user_init()
tests/float/*, scripts/check-float.sh~420the proof

The literal is an ordinary N_INT whose nd_type is the module's and whose val is the IEEE-754 bit pattern. That single decision removes four would-be mechanisms: parse_initlist and parse_global accept it unchanged, so f64 tbl[] = {1.5, 2.5}; parses; glob_place writes it at type_width bytes into __data with no float-aware code in any object writer; and MTASK_CONST(d, val) reaches the machine, which materialises movz/movk into a scratch x and one fmov d, x -- no literal pool, no relocation, no new task. M2 and M3 are what make it sound.

The ABI, per host -- all of it inside the module's machine, none of it in src/:

targetint argsfloat argsfloat returnfloat depthsnotes
macOS/arm64, linux/aarch64x0..x7 (NGRN)v0..v7 (NSRN)v0v16..v23never d8..d15 (callee-saved); M38 stack-arg path reused as written
linux/x86_64 (SysV)rdi rsi rdx rcx r8 r9xmm0..xmm7xmm0xmm8..xmm13xmm14/15 spill scratch
windows/x86_64 (Win64)rcx rdx r8 r9xmm0..xmm3, slot-sharedxmm0xmm0..xmm5xmm6..xmm15 are callee-saved, so depths spill from 6
windows/aarch64as AAPCS64as AAPCS64v0v16..v23prologue slot only

% on floats: bin_op maps K_MOD to MOP_UMOD for any non-i64 type (src/gen_walk.mc:472-483, :528); the module's MTASK_BIN refuses it with no float remainder. That is a die in the module, not a core typing rule -- res_binary already inherits the left operand's type and already yields TY_I64 for comparisons and &&/|| (src/gen_resolve.mc:398-404), which is the whole rule a float family needs.

putf64(x, digits) is a fixed-precision formatter (half-up rounding from a table of f64raw bit patterns, fcvtzs, digit loop, nan/inf by bit pattern, hex fallback past 2^63). No Ryu, no shortest-representation claim; said plainly in the guide.

ldf64/stf64 are intrinsic registrations (M7), one instruction inline. The zero-core-line alternative -- a second registered type f64raw reached through a cast pair -- is documented in lib/float.mc's header as the fallback, with its cost named: one extra GPR round-trip per element access, which a particle study will feel.

Generality -- the acceptance of the principle #

fp16: feasible, zero further core lines. type_new("f16", 2, 2, TK_FLOAT). Width 2 drives glob_place (buf_u16), array bounds (src/parse.mc:1047, :2011) and, through M5, a 2-byte frame slot; MTASK_LOCAL_LOAD/GLOBAL_LOAD already carry ty, so ldr h/str h need no task. Where the hardware converts (fcvt s, h; vcvtph2ps with F16C) that is one intrinsic; where it does not, the same registration lowers to a call into a softfloat routine the module pushes as a second source (p_push_source, the sd_rt pattern). The module states what it built for -- there is no CPU-feature model in mc and there should not be one.

i128: feasible memory-resident, zero further core lines. type_new("i128", 16, 16, TK_WIDE). M5 gives the 16-byte local and parameter slot; M4 tells the machine the depth is wide. The value lives in ONE depth backed by a 16-byte slot the machine asks slot_new(16) for -- a value spanning two depths would collide with gen_binary's depth + 1 (src/gen_walk.mc:525) and gen_call's depth + i, i.e. it would change the walker's own arithmetic. Carry survives the spill on both hosts (ldr/str do not touch NZCV, mov does not touch x86 flags): adds/adc, subs/sbc, mul/umulh, cmp/sbcs, whose result the walker already types TY_I64. Two known holes, both with zero-line answers: MTASK_CONST and N_INT's val are one i64 each, so a 128-bit literal goes through a module-private global plus a load; and glob_place writes width 1/2/4 and otherwise buf_u64 (:377-383), so a 16-byte global initializer must be emitted as an N_BLOB, not as one element. Register pairs would need MTASK_DEPTH_SPAN (~20 lines, a real contract change) and are deferred -- nothing has yet shown the memory form too slow.

One AVX instruction named by encoding, on x86-64: feasible with M7. type_new("v8f32", 32, 32, TK_OPAQUE) gives 32-byte globals and slots; intrinsic("vaddps", 2, ty_v256, &do_vaddps) gives the handler two depths, and it calls val_reg/ dst_reg for the registers the allocator chose, builds the VEX bytes and adds its own opcode, which its derived machine encodes, sizes and dumps. Measured limits, on the record: VEX2 with a bare [reg] operand is exactly 4 bytes (vmovups ymm0,[rdi] = C5 FC 10 07) and is reachable through #opcode today in a fixed-register leaf function; VEX3 (vfmadd231ps, 5 bytes), any displacement and any immediate are not, because gen_word refuses > 32 bits and both MTASK_WORD slots write buf_u32. A module's OWN machine has no such limit (x86_put already emits 1..10 bytes), so M7 reaches them; only emit()/#opcode from a source file stay 4-byte-bound. emitb(v, n) + MTASK_BYTES (~40 lines, one appended slot) is deferred to the milestone that names a VEX3 instruction from a source file. Also record the trap: on x86 the #opcode word is a little-endian byte string, so C5 FC 10 07 is written emit(0x0710FCC5). Frame alignment is 16 (src/gen_walk.mc:892), so a 32-byte-aligned spill is unreachable and the machine uses vmovups -- which it would use anyway.

The narrow word (M39 G10, M40): a separate axis. Not unlocked. M1 makes the width of a type a registry entry and M5 removes the two slot_new(8)s -- 3 of M39's ~400 lines. The other ~397 are the data model and are untouched: N_INT is typed TY_I64 by the parser and by res_expr; callp returns TY_I64 unconditionally (src/gen_resolve.mc:375); ld64/st64 are the only 8-byte accessors and every #define offset in lib/*.mc, examples/*/lib/* and the compiler's own records is a multiple of 8; MAXDEPTH 64 spill slots at 8 bytes is 512 bytes of a 2 KiB SRAM before the first local; and tests/024-arena.mc's 4096-byte heap does not fit at all. The wide side (an i64 as four AVR registers) IS the i128 mechanism above, in a machine, with no core change. The pointer side would need type_width to become machine-declared, which makes array-size diagnostics (src/parse.mc:1047, :2011) depend on --machine= -- a source whose meaning varies with a flag, a decision M40 must take deliberately. docs/specs/M39.md:195 and :253-265 stand: out of reach, and this spec must not be written as if it followed.

Out of scope #

#machine in every form. Any float in stage0/ or in src/mc.mc. Float literals, NaN semantics, putf64, ordered predicates, %-on-float -- all module. A CPU-feature model. Correctly-rounded shortest float printing. a[i], struct, typed pointers, aggregates, members, generics, user conversions: type_new gives PRIMITIVES, and a module that wants structure is back to examples/lang's lowering-to-uptr, which is fine and must be said plainly. Deferred with a price: emitb/MTASK_BYTES (~40, one slot), MTASK_DEPTH_SPAN (~20, one slot), type_set_width for M40 (a mechanism with no caller is dead core).

Files and estimated deltas #

filedeltawhat
src/hooks.mc+95M1 registry, M6 syntax_lit, M7 intrinsic, M8 two accessors; word_is_taught +1
src/ast.mc+12/-6type_width/type_name fall through to the registry
src/parse.mc+12one line in type_of_token, M3's three guards, M6's dispatch
src/gen_resolve.mc+14M2's one line; M7's lookup and result type
src/gen_walk.mc+36/-2M4 array, readers, re-announcements; M5's two lines; M7's branch
src/main.mc, src/driver.mc+45--dump-machine
src/machine_*.mc+0untouched; three names become published contract
lib/float.mc, lib/machine_{arm64,x86_64}_float.mc, lib/mc_float.mc+960step 1
lib/f16.mc, lib/i128.mc, examples/avx/+~700generality proofs
tests/float/, scripts/check-float.sh, tools/bundle.list+420the corpus
docs--machine.md v3 (§ 3 and § 4 rewritten), hooks.md (+9 symbols, :145-146 fixed), language.md § 2/§ 11, cli.md (16 flags), new docs/guide/96-a-new-primitive.md

Core total 187 lines in step A+B (132 inert + 55), plus 40 for --dump-machine. stage0/ untouched, 2846/3000.

Acceptance #

Two gated steps, as M21 (decision 7.5) and M17 were.

Step A -- the inert half (M1-M6, M8; 132 lines). The gate is that nothing moves: check-obj 32/32 byte-identical to the frozen build/mc0, check-asm 74/74, check-ast 74/74, check-lex 74/74, bootstrap at a fixed point (cmp build/mc2.o build/mc3.o) with an empty --dump-asm diff between mc1 and mc2, golden rewritten once and only after both, check-limits 17/17 under 90%, test-linux/test-linux-x86_64/test-windows-x86_64 unchanged. Plus the M17-step-A proof, which is the only one that settles it: a copy of build/mc1 from before the change and the one after produce byte-identical objects for all 32 tests/*.mc, for src/mc.mc, and -- through the taught compilers each builds -- for examples/api/main.mc, examples/lang/main.lx, examples/conc/main.lx and examples/desktop/main.mc.

Step B -- intrinsic and --dump-machine (55 + 40). Golden rewritten once, after the empty asm diff; check-obj still 32/32, because no test in the corpus registers anything.

scripts/check-surface.sh gains one case per mechanism: a type_new colliding with a core keyword (cannot redefine core keyword); a taught type reaching a cast, a parameter and a 16-byte slot; a probe machine that asserts walk_depth_type at every task over the whole of src/mc.mc (the "prove it over 996 functions" style the ABI assertions already use); a module registering syntax_lit and returning 0 for every literal, whose --dump-ast from main on is byte-identical to mc1's (the M21.5 inertness shape); an intrinsic that cannot shadow ld64; the fold guards through +, -, ~, ! and a cast; and a re-registered arm64 being what --dump-machine reports after machine_use("arm64").

Step 1, <float>: tests/float/ covering arithmetic and precedence; 1.5, 2.0, 1e-3, 0.25f, -0.0; (i64) f truncation toward zero on both signs; (f64) i and (f64) u; f32<-> f64; a NaN pair where all six ordered predicates are false and != is true; a call with 6 integer and 6 float arguments forcing both the spill and the stack-argument path; an array of f64; putf64(3.5, 3) -> 3.500; and sqrt(2.0) through extern f64 sqrt(f64) -- the case that is flatly unreachable today, because MTASK_CALL cannot be told an argument is a double. Bit-exact: each stores its result with stf64, reads it back with ld64 and prints 16 hex digits against a literal in the test header, produced once by python3 and recorded, so the suite has no python3 dependency. The same source file is the oracle on all five legs -- macOS/arm64, linux/aarch64, linux/x86_64, windows/aarch64, windows/x86_64 -- via make check-float plus the existing cross-compile scripts, self-skipping without Docker/ld.lld.

llvm-mc sweep, mandatory for any machine that lands in lib/: every distinct instruction the float machines emit while compiling the whole corpus re-assembles byte-identically under llvm-mc -triple=aarch64-... and -triple=x86_64-linux-musl -- the standard M17 step B (948) and M20 (967) were held to, and the only credible check on a hand-written encoder or on a MTASK_INS_SIZE/MTASK_ENCODE disagreement.

Generality, proved by three modules whose git diff src/ is empty: f16 (a global array of 8 occupying 16 bytes, checked in the object; an f32->f16->f32 round trip against a recorded reference where round-to-nearest-even bites; the software fallback exercised by building x86-64 without F16C); i128 (--dump-asm showing a 16-byte slot and an adds/adc pair, a value passed to a two-register callee, mc limits showing the frame grow by 16); and examples/avx (one instruction named by encoding applied to two values the allocator placed, re-assembling under llvm-mc, with // skip-<arch>: on the other legs and a README stating which VEX forms are reachable).

A deliberately wrong machine_slot(tab, MTASK_BIN, &wrong_fadd) changes the program's answer and --dump-machine shows the changed origin -- the observable-override proof the old § 4 asked of #machine, delivered without the directive.

make check green end to end, RC 0, with check-float added.

Risks #

  1. fold_* is a silent corrupter. M3 is six lines that do nothing today and everything on the first float program; land it in the same commit as anything that can produce a typed N_INT, with its own negative test. Same class as the post-M11 8-byte-relocation bug.
  2. A stale depth type is wrong code, not a diagnostic. M4's re-announcement list is the mechanism; the probe machine over all of src/mc.mc is the only check that can cover it.
  3. . is a scarce resource. syntax_infix refuses a second registration outright (operator already taught), so a compiler taught both lx and a .-based float literal fails at user_init -- the good failure, but a failure. M6 avoids it; the fallback must document the chaining precedent (lg_more(), conc.mc wrapping lg_block).
  4. Machine composition is last-wins and lossy. machine(name, tab) replaces the whole table, so two modules teaching two type families collide silently. type_new composes (distinct ids) and the intrinsic registry composes (distinct names); the machine does not. A stated limit, with the chain-of-wrappers protocol named as a design of its own.
  5. MAXMACHINES 8, 3 used, <float> shadowing three names. Either reuse the slot when a registration shadows an existing name (~6 lines) or raise to 16 with the reason written down -- but do not let too many machines be discovered by a user stacking two taught modules.
  6. check-lex.sh:37 iterates lib/*.mc and has NO seed-skip escape (check-asm.sh:28 and check-ast.sh:38 do, used once at lib/sys_windows_host.mc:4). The frozen seed cannot lex 1.5, so no file under lib/ may contain a float literal -- lib/float.mc uses bit patterns, and anything that needs literals lives in tests/float/ or is pushed as a second source. Add the escape to check-lex.sh (~6 lines) as the fallback.
  7. res_type answers TY_VOID for nodes built after gen_resolve (src/gen_resolve.mc:225-235, deliberate, for gen_opcode). Taught modules build nodes, so this becomes far easier to hit and surfaces as value of type void. Worth a diagnostic that names the situation.
  8. Seed capacity, not seed lines. stage0/ is untouched but must still compile a src/mc.mc carrying all of this (~10 functions, ~25 strings). M17 step B already raised HEAP_SIZE to 64 MiB and left funcs at 996/2048, heap at 46%. make check-limits is the gate; if a row goes tight, the answer is the seventeenth-row discipline, not a second seed change.
  9. type_new reserves the word program-wide (word_add), so loading <float> breaks a project with a variable called f32. Correct and consistent with type_alias; keeping <float> out of lib/user_default.mc means nobody gets it by accident.
  10. Scope. Eight mechanisms plus a library plus three proofs plus five CI legs is large. The review question for every proposed core line is: would fp16, i128, an AVX op and an 8-bit word each need this, or only floats? If only floats, it belongs in <float>.

Decisions (architect, 2026-09-04 -- every recommendation below is adopted) #

  1. intrinsic (M7): in, or deferred? The two verdicts split. One holds that a f64raw cast pair plus #opcode leaf functions already reach it at zero core lines; the other that a named hardware instruction on values the allocator placed has no route at all. Recommend: in, at step B. The cast trick reaches reinterpretation only, and the leaf-function trick is fixed-register by construction -- examples/conc/lib/atomic.mc's own header records that an ldaxr/stlxr loop cannot be expressed and that it passes on Apple silicon, i.e. a silent portability trap. Without M7 the AVX half of the owner's principle is not delivered.
  2. val_reg/dst_reg/dst_done published (contract v3). Recommend: yes, and keep the published set to exactly those three; delegation to built-in task implementations goes through the copied table pointer, so no a64_* name is frozen.
  3. --dump-machine (M9). Recommend: keep, at step B, priced separately as observability. It is what makes a derived machine reviewable and is the cheapest observable-override test. Cut first if the budget binds.
  4. machine_tab/machine_slot (M8) or a documentation fix alone? M39's D7 offers the 0-line option. Recommend: take the 10 lines, and fix hooks.md:145-146 with them -- a published recipe that corrupts a live table is worse than either.
  5. MAXMACHINES. Recommend: reuse the slot on a shadowing registration (~6 lines) rather than raising the constant; it keeps the "does not scale with the program" argument true.
  6. Where does <float> live? Recommend lib/ + bundle, out of user_default, with risk 6's rule (no float literal under lib/) and the check-lex escape added as insurance.
  7. emitb/MTASK_BYTES and MTASK_DEPTH_SPAN. Recommend: both deferred, each with its price written here, to the milestone that actually needs it (a VEX3 instruction named from a source file; a register-pair wide value shown to be needed).
  8. type_set_width(TY_UPTR, w). Recommend: not built. A mechanism with no caller is dead core, and making type_width machine-dependent makes a source's meaning depend on --machine=. It lands with M40, which owns that decision.
  9. Two gated steps or one commit? Recommend two: step A is inert and verifiable alone, and if step 1 slips, examples/kernel and examples/lang still get M8 and M4 immediately.

Adopted, one line each: D1 intrinsic in, at step B; D2 val_reg/dst_reg/dst_done are the published set, nothing else of a machine's internals; D3 --dump-machine kept, #machine dropped for the three reasons recorded under M9; D4 machine_tab/machine_slot built and hooks.md corrected with them; D5 a shadowing machine() registration reuses the slot; D6 <float> lives in lib/ and the bundle, outside lib/user_default.mc, with the check-lex seed-skip escape added; D7 emitb/MTASK_BYTES and MTASK_DEPTH_SPAN deferred at the prices above; D8 type_set_width not built -- the narrow word is M40's decision; D9 two gated steps, each with make check green and the golden rewritten once. The architect's additions: (a) the three generality modules (f16, i128, examples/avx) are ACCEPTANCE, not stretch goals -- the principle is only proved when a module the core has never heard of works; (b) tests/float/ is the same source on all five legs; (c) the review question of risk 10 is applied to every core line in the PR description, line by line; (d) M40 is written as its own spec before any narrow-word code.

Edit this page