summary_set-constants-2026-10-18.md 9.8 KB

Session summary — SET CONST + builtin CONST folding 2026-10-18

Branch resolve-check. Suite 235/235; fixpoint OK (3,854,595 bytes); corpus 389/612 compile-OK, 0 crashes.

Tags: v3-set-constants, v3-const-builtin-fold, v3-const-conv-fold, v3-proc-addr, v3-proc-val, v3-real64, v3-shift-import, v3-foreign-module, v3-m2-conformance, v3-strict-decl-order, v3-convert, v3-gap-examples.

Conformance note

This project follows Modula-2 (declaration before use; the only forward reference is a POINTER's base type), not gm2's leniencies. A v3-array-index-fwd change (forward array-index types) was reverted for that reason: record7/complextypes use a type before its declaration and are correctly rejected again (corpus 400 → 398). Every remaining case, classified (standard / gm2-leniency / V3-bug / limit), is catalogued with a reproducer in docs/gaps/.

What this session did

Landed the two features recorded in docs/analysis_gaps.md §9 by the previous corpus sweep — set constants (static masks) and builtin calls in CONST initialisers — then went further on CONST initialisers, procedure values, SYSTEM sized reals, builtin imports, and ISO foreign modules. (A forward-type-reference change was added and then reverted for conformance; see the note at the top.) Corpus 382 → 388 (+16 files, 0 crashes).

CONST Digits = CharSet{"0".."9"};          (* char range *)
      Signs  = CharSet{'+', '-'};
      NumCh  = Signs + Digits;             (* union of two set constants *)
      Warm   = CS{Red, Blue};              (* enum base *)

New suite tests: t_setconst (run), t_bad_setconst (reject), t_constbuiltin (run).

Why it was reverted before, and the fix

A set literal is emitted as a runtime temp (NewSetTemp/SetZero/ SetBit), so a set CONST had no static value; accepting the declaration produced a 230 rather than a miscompile (see the previous sweep's finding). Sets are also addresses of memory words, so the constant must be a real data symbol.

The fix computes the bit mask at parse time — where the element values are known and validated — and has Lower re-emit it as a pooled static descriptor:

  • Lower.SetConstMaskOf (exported) folds a set expression to (mask, words, lo): set literals (NkSetLit/NkBraceLit), char / integer / named-CONST elements, a..b ranges, and +/-/*// over two foldable set operands (union / difference / intersection / symdiff), plus + - * DIV MOD on element ordinals.
  • ConstDecl (grammar) calls it for ClSet; on success it records the mask (Lower.NoteSetMask) and lets the constant be an aggregate; on failure it stays a clear 230.
  • Lower.LowerDecls looks the mask up by name and calls the new QbeGen.PoolSet, which registers data $setN = { w mask0, ... } and returns its address. FlushSets emits the pool in EndModule.
  • The grammar's Design and Lower's LowerDesign now treat a ClSet KindConst as an aggregate: its value is the descriptor address (so c1 := Digits, c IN Digits work).

Bound: masks are folded up to 16 words (512 bits); a wider SET CONST stays a 230 (MaxCFoldW). The corpus set constants are ≤ 8 words.

Builtin CONST folding

CAP/ABS/ODD set the runtime sentinel "@", so a CONST such as A = CAP(VAL(CHAR, 65)) (testcap2) was a 230. They now fold over an immediate operand in the grammar (Fact): CAP uppercases a CHAR, ABS takes the magnitude, ODD yields 0/1. This only feeds the ConstDecl IsImm check (Lower re-lowers from the AST, so runtime code is unchanged).

MIN/MAX of a set type already folded via TypeBounds. A BOOLEAN CONST other than TRUE/FALSE was also rejected at the designator — the grammar's Design class list was missing ClBool (Lower already had it); added. Corpus 387 → 389.

CONST conversions

VAL between representations used to leave a runtime temp, so CONST r = VAL(REAL, MAX(INTEGER)) (constcast) and CONST a = VAL(LONGCARD, MAX(LONGINT)) (maxlongint) were 230. The grammar now folds an immediate operand for the representation-changing conversions: INT/LONGINT→REAL (and FLOAT, via NormReal), INT↔LONGINT (identity on an immediate), and identical-class LONGINT→LONGINT. Corpus 389 → 391.

Forward array-index type (REVERTED)

TYPE a = ARRAY idx OF T; idx = [-2..2] (and ARRAY sub OF … inside a record/pointer, record7/complextypes) was briefly accepted by deferring the array bounds (SymTab.DeferArrayIdx/FixupArrayIdxs, tFwd). Reverted: this is use-before-declaration, which strict Modula-2 forbids (only a POINTER's base type may forward-reference). V3 rejects record7/complextypes again. Corpus 391 → 393 is undone (see the conformance note above).

Procedure values

ADR(foo) returned 0 (Lower's ADR handled only vars/params), while VAL(ADDRESS, foo) gave the code address, so procadr compiled but failed at run time. Lower's ADR now emits the code address for a procedure (LowerProcAddr). The grammar's VAL conversion was also missing procedure-value classes: added PROC↔PROC, PROC↔PTR. Then Assignable rejected p := foo (a procedure value into a PROC variable, mapped onto ADDRESS): added PROC↔PTR assignability. procconv now passes and runs. Corpus 393 → 396; suite tests t_procaddr, t_procval.

SYSTEM sized reals

REAL32/REAL64 (SYSTEM) were undeclared; mapped onto V3's REAL (dReal), so MIN(REAL64)/MAX(REAL64) fold. Corpus 396 → 397; suite test t_real64.

Builtin imports; SHIFT fault

FROM SYSTEM IMPORT SHIFT (and ROTATE/CAST/…) failed: builtins are reserved tokens, so ImpName rejected them. The import list now accepts the builtins (accepted and ignored — the builtin applies regardless).

That exposed a real miscompile: SHIFT by an out-of-range amount (negative, or ≥ the span) addressed a wild word and loadw'd from it (bits were discarded only at the store). The word index is now clamped to 0 when the destination bit is out of range, so the load is safe and the store is still a no-op. Corpus 397 → 399 (shift, shift2); suite test t_shiftneg.

ISO foreign modules

DEFINITION/IMPLEMENTATION MODULE FOR "C" M and EXPORT UNQUALIFIED (ISO foreign-module syntax) are now parsed; v.def → 0 errors. Corpus 399 → 400; suite session test CForeignProg.

Conformance: strict declaration order (Track A, first cut)

TypeIdent no longer invents a forward placeholder for an undeclared type: a new allowFwdType flag is set only while parsing a POINTER's base type (Modula-2's single forward-reference exception), so module-level forward type/VAR references, forward types inside a TYPE block, and forward SET OF <type> are now 201. The compiler's own sources (and runtime/stdlib) do not rely on it — fixpoint stays green. t_fwdtype/t_fwdset became reject tests; t_ptrfwd locks the legal exception. Corpus 398 → 388: the 10 files dropped (arrayptr, arrayptr6, constructor1, constructor3, param3, record14.def–record16.def, tinyset6, tinyset7) were gm2 leniency. Forward references to module-level variables/procedures from a body (the two-phase parse) remain — a separate, larger item.

Track B: CONVERT

CONVERT(T, x) (ISO SYSTEM, a VAL sibling) is now accepted — the grammar's VAL production takes ( "VAL" | "CONVERT" ), so it reuses VAL's conversion rules; procconv2 passes and runs. Corpus 388 → 389; suite test t_convert, docs/gaps/gap07 retired.

Remaining genuine gaps (Track B)

  • gap13 builtins as identifiers — a real V3 bug, but the builtins are Coco/R keyword tokens; fixing it means de-reserving them (a parser refactor), not a one-liner.
  • gap08 set↔word CAST/VAL (BITSET as a machine word).
  • gap15 enumeration literals with an imported type (ISO import/export through re-exports).
  • gap16/gap17 set-span limits.

Not fixed (recorded)

  • setconst3.mod still fails: its VAR mem: ARRAY [0..CARDINAL( addressLines)] names a CONST declared after the VAR (a forward CONST reference), which is a separate gap. Its SET CONST itself now parses.
  • Forward type references in a CONST conversion (subrange4/7) and as a VAR type before its TYPE block (subrange11, shift3/4); procconv2 (CONVERT); packed (CAST set↔int); aochar/ arrayofbyte/bytearray (a scalar actual into ARRAY OF BYTE); testparam2/3 (VAR actual of a different int/real type); tinyproc2/tinyproc4 (bare procedure value as a statement); variant-record empty arms / ELSE (varient2/3/8, tsize/2); builtin/fileio (a builtin name as a procedure name, __BUILTIN__); the imp* qualified-import family (partly a corpus harness transitive-.def gap).

Files

compiler/src/Lower.{def,mod} (evaluator + PoolSet call + ADR(proc)), compiler/src/QbeGen.{def,mod} (PoolSet/FlushSets/pool state; SetShift clamp), compiler/src/SymTab.{def,mod} (proc-value assignability; sized reals), compiler/src/M2.atg (ConstDecl, Design, CAP/ABS/ODD folds, VAL conversion folds, proc-value conversions, builtin imports, foreign modules), compiler/tests/t_setconst.mod, t_bad_setconst.mod, t_constbuiltin.mod, t_constreal.mod, t_constlong.mod, t_procaddr.mod, t_procval.mod, t_real64.mod, t_shiftneg.mod, t_ptrfwd.mod, t_fwdtype.mod, t_fwdset.mod, c_foreign.def, c_foreign.mod, c_foreign_prog.mod, compiler/run_tests.sh.

Resume

cd .../m2compiler-V3
git checkout resolve-check
cd compiler && ./build.sh && ./run_tests.sh   # 235/235
cd .. && TMP="$PWD/.scratch/fixpoint" ./bootstrap/fixpoint.sh
M2FLAGS="" ./.scratch/corpusx.sh              # 389 OK, 0 crash