plan.md 8.3 KB

m2compiler-V3 — Actual Plan (locked 2026-09-19)

Modern Modula-2 syntax, Coco/R frontend, fresh QBE backend, Blaise-style phased process, self-hosting with byte-identical .ssa fixpoint.

1. Toolchain and dialect

  • Frontend: Coco/R (M2.atg → scanner/parser/driver), gm2 -fiso, CR -m -C regeneration, two-phase gm2 -fgen-module-list link (proven in V1/V2/Test2).
  • Backend: fresh QbeGen emitting QBE SSA text (.ssa), then qbe -o .s + cc link + execute. System qbe pinned in vendor/qbe-pin.txt.
  • Modern syntax base: PIM4/ISO core + Kowarsch/Redux cleanups — # only (no <>), NOT/AND/OR only (no ~/&), no B/C octal literals, 0x-only hex, unary minus at expression start. Evolved from V2 M2comp.atg (1290 lines), not pasted verbatim. A second, fenced legacy grammar (TopSpeed-V3-M2.atg, step 1.12) covers the historical dialect (_, <>, H/C/B) for corpus ingestion — never merged into the modern grammar (see docs/blaise-phases-map.md: our phase-10 sidecar).
  • Test convention (from Test2): global VAR ExitCode : INTEGER selects process exit code; otherwise return 0.

2. Directory tree (Blaise-adapted)

Blaise (graemeg/blaise) uses compiler/, runtime/, stdlib/, tools/migration-analyser/, vendor/qbe/, docs/, per-module target/. V3 port:

m2compiler-V3/
  README.md  build.sh  run_tests.sh
  compiler/src/      M2.atg (+ TopSpeed-V3-M2.atg legacy sidecar),
                     frames, FileIO, SymTab, QbeGen, generated
                     scanner/parser/driver (gitignored)
  compiler/tests/    accept / reject (.LST) / run (exit-code) tests
  runtime/syslib/    SYSTEM, Storage, SysIO, SysClock, Trap, Utf8
                     always-linked, self-hostable, NO gm2-only imports
  stdlib/            Strings, TextIO (+U*), WholeIO, Math, Files,
                     Conversions, RealIO, CharClass
                     opt-in via IMPORT, classic DEFINITION+OPAQUE,
                     Benjamin-R10 semantics, classic syntax only
  tools/redux-check/ modern-syntax enforcement helper
  vendor/qbe-pin.txt
  bootstrap/         stage1/ (gm2-built), stage2/, stage3/ (V3-built),
                     fixpoint.sh (rebuild 2x, diff -r *.ssa)
  docs/              plan.md (this file), blaise-phases-map.md,
                     summary_stepN.md (one file per step)

Build outputs (.o, .ssa, .s, binaries) are never committed.

3. Library spec (Benjamin, classic form)

  • Semantics follow Benjamin/Actual/m2bsk-master/r10/stdlib/*.if (+ blueprints/templates as semantic reference) and PDFs-master/.
  • Syntax is classic DEFINITION/IMPLEMENTATION + OPAQUE only. Explicitly OUT: INTERFACE MODULE, [ProtoX]/[TMIN] attributes, PROCEDURE [+] operator overloads, .bp/.gen.if templates.
  • runtime/syslib vs stdlib/ split: syslib is always linked and trivially compilable; stdlib is opt-in via IMPORT.

4. Phased roadmap

Step Goal Acceptance
1 pipeline minimal M2.atg + fresh QbeGen hello → .ssa → qbe → cc → run ✅ done (5/5 → grown since)
2 scalars scalar type system, SymTab errors 200–224 + 230/232/233 (+234 unicode) ✅ done (30/30: ints, bools, chars, REALs, control flow)
3 composites length-prefixed arrays/records/sets/pointers (§5) 🔄 3.1 arrays ✅, 3.2 showcase ✅, 3.3 sets ✅, 3.4 pointers next
4 modules/procs DEFINITION/IMPLEMENTATION, opaque TYPE T;, nested/display, separate compilation not started (syntax parses; lowering pending)
5 stdlib Strings/TextIO/WholeIO/Math/Files per Benjamin I/O + file tests
6 harden CASE/WITH/LOOP/EXIT/FOR-BY, conversions, traps full-suite green
7 syslib port rewrite FileIO on SysIO+Storage only; ban Environment, FileSysOp, TextIO, RawIO, WholeIO, IOChan, SysClock, ProgramArgs via grep gate compiler sources syslib-clean; Stage1 still gm2-built
8 fixpoint bootstrap/fixpoint.sh: Stage1→Stage2→Stage3, diff -r all .ssa byte-identical, suite green on all stages SELF-HOSTED

Full-language-first: the compiler itself may use the full language; self-hosting is the last phase. Hygiene from step 1: deterministic %tN/@LN counters, sorted symbol emission, no timestamps/paths in .ssa; restricted SYSTEM use (ADR/TSIZE/BYTE only).

5. Memory rule — length-prefixed arrays & strings (LOCKED)

  • Every ARRAY value: bytes 0..7 = LONGCARD element count (HIGH-LOW+1), QBE l; bytes 8.. = packed elements.
  • Applies to ALL arrays including fixed ARRAY [lo..hi] (bounds still checked statically AND stored at runtime). 8 bytes overhead/array.
  • Multi-dim: each level has its own header (recursive).
  • Records: flat blobs; scalar/set fields inline, array fields as 8-byte pointers to static descriptors, nested records inline (locked amendment 3.4: same type keeps one layout — inline headers would fork it from step-3.1's separate objects); whole-record assignment is deep (per-field copy).
  • SYSTEM.TSIZE on arrays includes the header.
  • Open formal ARRAY OF T: single l descriptor pointer; callee reads count via loadl. No hidden length parameter.
  • Whole-array := requires equal counts, else Trap.RangeFault (no silent truncation — safety-perimeter principle).
  • Index a[i]: base + 8 + (i-lo)*elemSize + runtime i < lo+count check.
  • HIGH(a)/LEN(a): static for fixed, header read for open.
  • String = ARRAY OF CHAR with this header; literals emitted as data $n = { l count, b ... }. No trailing NUL; C boundary via SysIO.ExportCStr copy.
  • SYSTEM.BYTE buffers (FileIO, SymTab.Name) conform automatically.

6. Unicode — UCHAR / UString (LOCKED)

Status: increments 1–2 landed (see docs/summary_unicode.md). Remaining: UString whole-string assignment to a fixed array; UString =/+/concat.

  • UCHAR: 32-bit codepoint 0..1114111 (QBE w). CHAR unchanged (8-bit, 0..255). Distinct types, no implicit mixing. ✅
  • UString = LONGCARD count (codepoints) + count × l codepoints (prior header rule, element = UCHAR, 4 bytes each). 🔄 (literals emit the descriptor; assignment/backing pending)
  • Source files are UTF-8. Identifiers stay ASCII. Coco/R captures literal bytes; LexUString action strict-RFC3629-decodes (no overlongs/surrogates/>10FFFF/truncation). ✅
  • Literals: U'..' / U".."; single codepoint U'a' → UCHAR, longer/empty → UString. 'a' ≠ U'a'. ✅
  • Invalid UTF-8 in literal → hard lex error 234. ✅ (reported in .LST + console; note the image is still written, gate pending)
  • Conversions: UCHR(x:CHAR|INTEGER):UCHAR (constructor); CHR8(u:UCHAR):CHAR traps unless u ≤ 255; ORD 8-bit; UORD(u):INTEGER. ⚠ deviation: UORD returns INTEGER, not LONGCARD (V3 has no LONGCARD name or 64→32-bit conversion; a codepoint fits in 32 bits). ⚠ UCHR also accepts an integer family operand.
  • runtime/syslib/Utf8 (Decode/Encode, MaxUTF8Length=4, MaxCodePoint=1114111, cf. Benjamin Utf8.def): used by the output libs. ⚠ the front-end decode lives in QbeGen.DeclUStr (stage 1 is gm2-built and does not know UCHAR, so it cannot import Utf8).
  • TextIO.UWriteChar/UWriteString/UWriteLn: encode to UTF-8, emit via the shim. Raw WriteChar stays byte-oriented. ✅

7. Interactive ATG order (done through §6)

  1. COMPILER M2 header + CHARACTERS/IGNORE/COMMENTS/TOKENS ✅ (0x, E-exponents, // decided interactively; ustring token implemented, see §6).
  2. Module / Definition / Implementation / Import (+ opaque hook) ✅
  3. CONST/TYPE/VAR/ProcHeading + error codes (200–234) ✅
  4. Types (scalars ✅, UCHAR ✅, composites 🔄 arrays+sets ✅, records/pointers next, proc types with step 4).
  5. Statements ✅ (all but WITH, which rides with records).
  6. Expressions ✅ (relations, IN, set ops; pointer ops with 3.4).
  7. Action wiring ✅ (driver reports file:line:col).

Each section: proposed snippet → user approve/tweak → append to compiler/src/M2.atg → CR -m -C syntax check before moving on. Per-section self-host question: "can this cover the compiler's own source idioms?".

8. Open points for build time

  • Exact U'..' vs U".." spelling confirmation during §1.
  • stdlib Phase-5 subset order: SYSTEM+Strings+TextIO first?
  • Trap exit-code conventions for run tests.