Spec M23 — dynamic limits: best-effort sizing with a tolerance, no hard caps

Owner's direction (2026-09-03): limits must be dynamic. mc computes a best-effort estimate of what the build needs, reserves that times 1 + tolerance, and grows if the estimate was short. The only knob is tolerance, a float in [0, 1] in mc.toml. A checker reports how good the estimate was and may, with consent, adjust the tolerance. No per-table caps anywhere in the self-hosted mc.

Problem #

Every compiler table is a static array sized by a MAX* constant; programs that outgrow one die with too many X, and the architect had to bump five constants by hand after M15.

Design (all in src/*.mc; stage0 keeps static tables and only compiles src/mc.mc) #

  1. Growable tables. Every MAX*-sized array in src/ becomes an arena block that doubles on demand (grow(ptr, count, cap, elem_size) in arena.mc, the pattern nodes_grow already uses). The MAX* constants disappear from src/ (they remain in stage0 only). Growth never changes output order (tables keep insertion order), so determinism is untouched.
  2. Best-effort estimate. Two sources, combined by taking the larger:

    • Static, before parsing: from the byte size of the entry file plus every include it can see (#include "x" and <name> resolved eagerly by a cheap pre-scan; the bundle sizes are known): tokens ~ bytes / 5, nodes ~ tokens * 3/5, strings ~ count of '"' / 2, functions ~ count of ") {", defines ~ count of "#define", symbols ~ functions + globals + strings, heap ~ sum of (count * record size) + section buffers ~ 2 x bytes. Coefficients live in one table and are calibrated against src/mc.mc and examples/api (documented with the measured values).
    • Remembered, from the previous build of the same project: mc build writes build/.mc-usage.toml (per table: used count; arena high-water) and the next build pre-sizes from it. Deterministic output does not depend on it (only capacities do).
  3. tolerance ([limits] tolerance = 0.25, default 0.25, float in [0, 1]): pre-reserve estimate * (1 + tolerance) for every table and for the arena (mmap at startup; the static heap[] is the fallback when mc runs without a TOML). tolerance = 0 reserves exactly the estimate; 1 doubles it. Values outside [0, 1] are refused with file:line:col. Floats are parsed by toml.mc as decimal text and stored as basis points (i64, 0..10000) — the language has no floating point and does not need it here.
  4. mc limits [DIR] (also mc build --limits): after the build, one line per table: estimate, reserved, used, growth events, and a verdict: ok (no growth), grew (the estimate was short — the build still succeeded), tight (used > 90% of reserved). Exit 0 / 3 (grew or tight) / 1 (build failed). Deterministic output for CI.
  5. mc build --fix-limits: with the developer's consent (the flag), if any table grew, raises tolerance in the project's mc.toml to the smallest value in [0, 1] that would have avoided growth (rounded up to 0.05), rewriting only the [limits] section byte for byte otherwise; if 1.0 is not enough, says so (the static estimate is the culprit) and refreshes the remembered usage instead. Never writes without the flag.
  6. Seed guard. make check gains check-limits: mc limits on src/mc.mc compares the seed's fixed MAX* constants (read from stage0/mc.h/*.c) with the actual usage and fails when any is above 90% — the early warning the architect lacked at M15.

Acceptance #

Edit this page