Переглянути джерело

compiler: sets, increment 1 (CALC: type, constructor, typed const, compile-time in)

The ladder moves again: CALC.PAS went from ERR 102 at 7972 to ERR 41 at 8098.
The set typed constant at CALC.PAS:114 is now accepted, so the compiler reaches
the ENUMERATION at :120.

Layout - TP3's HYBRID one, from the book (coeur-tp-ocr 4.7.2), not P4's:
only the whole BYTES covering the base range are stored, element e is bit
(e MOD 8) of byte ((e DIV 8) - (base DIV 8)), and the high byte comes last.
`set of Char` is 32 bytes, `set of 12..17` is 2.  ParseType's TkSet arm records
`elem' as the origin (a multiple of 8) so membership and construction need
nothing more.

  - ParseType: `set of <ordinal>` - Char, byte, or a literal subrange.  A named
    or enumerated base is still ENoLib (`set of Attributes' waits for enums).
  - RdSetLiteral: `[e, a..b, ...]' and `[]'.
  - EmSetBlob: a set's bytes go IN THE CODE STREAM behind a JMP, exactly as
    EmPushStrAddr already does for a string literal, and the address of those
    bytes is the value.
  - Set typed constants: `Numbers : set of Char = ['0'..'9']' emits its bytes
    and registers the name as an addressable value.
  - Compile-time `in': a constant element against a set whose bytes are in the
    image, answered as TPSRC2 xsetin answers at run time (AH > 255 -> FALSE,
    else test the bit).
  - LoadAtom refuses a TSet as a number, so `writeln (Numbers)' cannot quietly
    load the set's first two bytes.

THE STORAGE DECISION the plan demanded is settled here: code-resident bytes,
addressed through DS.  The divergence from RESUME-TP3.md 3.14 is one line - the
original had separate code and data segments and so prefixed `seg cs'; a .COM
has ONE segment, so DS already covers the code area and no prefix is needed.
The bytes are ordinary RAM, so a typed constant stays assignable.  This is the
same scheme XPOS and the enum-indexed array of strings will use.

Still ENoLib, next increment: run-time `in', set VARIABLES, `+'/`*'/'='/'<>'
and assignment.  None is silently accepted.

Reference: Resources/COMPIL4/ (Wirth's Pascal-P4) was added and read; its sets
are a FIXED 8-byte bitset over 0..63 with `in' as a runtime opcode, so it is a
cross-check on the algorithm, not the representation.  Noted in CALC-COMPILE.md.

Red proof: inverting the membership bit test leaves the compile matrix GREEN at
the same OK 183 4 and only execution sees FALSE TRUE FALSE FALSE where TRUE
FALSE TRUE TRUE belongs.  nonvacuity.sh gains that case (82 ok, 0 failed).

The audit harness caught a real gap in this change: EmSetBlob emits bytes and
was not name-audited, so audit_helpers.py went red on the UNMUTATED sources and
nonvacuity reported it twice as "NOT RESTORED".  It is renamed to the Em*
convention and listed as COMP beside PushStrAddr.

run_all.sh: ALL PASS - compile matrix 62/62, CALC ladder 1/1 at ERR 41 8098,
52 .COM linked and 52 executed under both qemu and Exec86.
Eric Streit 15 годин тому
батько
коміт
eed46b22b3

+ 87 - 44
CALC-COMPILE.md

@@ -95,35 +95,34 @@ Current rung (measured today, normalised position):
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| **7972** | `Numbers : set of Char = ['0'..'9']` — a **set** typed constant (`ParseType` `TkSet`, `ERR 102`) |
+| **8098** | `Attributes = (Constant,Formula,…)` — an **enumeration** type (`ERR 41`) |
 
 
 Rungs behind it (now passed):
 Rungs behind it (now passed):
 
 
 | was | blocker | done by |
 | was | blocker | done by |
 |---|---|---|
 |---|---|---|
-| 7791 | `FXMax : Char = 'G'` — typed constant (`DefConst`, `ENoSemi` 1) | Stage 4 (scalar) |
-| 7791 | `EofLine = ^M` — caret character constant (`ERR 41`) | Stage 3 |
+| 7791 | `FXMax : Char = 'G'` — scalar typed constant | Stage 4 (scalar) |
+| 7791 | `EofLine = ^M` — caret character constant | Stage 3 |
+| 7972 | `Numbers : set of Char = ['0'..'9']` — set typed constant | sets, increment 1 |
 
 
-Rungs ahead, unchanged in the census but they now sit *behind* 7972:
+Rungs ahead:
 
 
 | position | blocker |
 | position | blocker |
 |---|---|
 |---|---|
-| 8036 | `Attributes = (Constant,…)` — enumeration (`ERR 41`) |
-| 8060 | `SetOfAttri = Set of Attributes` (`ERR 102`, `ENoLib`) |
+| 8060+ | `SetOfAttri = Set of Attributes` — a set over an enum (`ERR` — needs enums) |
 | 8237 | `CellRec = record` (`ERR 102`, `ENoLib`) |
 | 8237 | `CellRec = record` (`ERR 102`, `ENoLib`) |
 
 
-Two corrections the measurements force, both worth stating plainly because the
-stage list below was written before them:
+Corrections the measurements force, worth stating plainly because the stage list
+below was written before them:
 
 
 - **The include (Stage 2) does not move the rung.** It is at normalised `9404`,
 - **The include (Stage 2) does not move the rung.** It is at normalised `9404`,
   past the stop, so Stages 3-4 had to happen before the include is ever reached.
   past the stop, so Stages 3-4 had to happen before the include is ever reached.
 - **`7791 → 8036` in one stage was optimistic.** `CALC.PAS:114`,
 - **`7791 → 8036` in one stage was optimistic.** `CALC.PAS:114`,
   `Numbers : set of Char = ['0'..'9']`, is a typed constant whose *type* is a
   `Numbers : set of Char = ['0'..'9']`, is a typed constant whose *type* is a
-  set, and it comes before the enumeration at 8036. So the compiler now stops at
-  **7972**, on sets, and the enumeration at 8036 cannot be reached until sets
-  exist — i.e. sets (Stage 6) must come before enums (Stage 5) in the real
-  order, not after. The measured ladder is the authority; the stage numbering
-  below is kept for reference but is no longer the dependency order.
+  set, and it comes before the enumeration at 8036. Sets had to go first, and
+  now they have — the compiler stops at **8098**, on the enumeration. **The
+  measured ladder is the authority; the stage numbering below is the original
+  plan, not the dependency order.**
 
 
 ---
 ---
 
 
@@ -138,12 +137,15 @@ stripped.
 |---|---|---|
 |---|---|---|
 | `{$I CALC.INC}` | `CALC.PAS:150` | **done (Stage 2)** — expanded before parsing; `t56`/`t57`/`t58` |
 | `{$I CALC.INC}` | `CALC.PAS:150` | **done (Stage 2)** — expanded before parsing; `t56`/`t57`/`t58` |
 | typed constants (`X : T = v`) — **scalar** | `CALC.PAS:111` | **done (Stage 4)** — declared class kept; `t59_typedconst` |
 | typed constants (`X : T = v`) — **scalar** | `CALC.PAS:111` | **done (Stage 4)** — declared class kept; `t59_typedconst` |
-| typed constants — **set/array** | `CALC.PAS:114,137`; `CALC.INC:777` | `ERR 102` — needs initialised storage |
+| typed constants — **set** | `CALC.PAS:114` | **done (sets, increment 1)** — code-resident bytes; `t61_setconst` |
+| typed constants — **array** | `CALC.PAS:137`; `CALC.INC:777` | `ERR 102` — needs initialised storage |
 | lexical `^M`, `#nn` | `CALC.PAS:113` | **done (Stage 3)** — `t60_charcaret` |
 | lexical `^M`, `#nn` | `CALC.PAS:113` | **done (Stage 3)** — `t60_charcaret` |
-| enum type `(Constant,Formula,…)` | `CALC.PAS:120` | `ERR 41` |
+| enum type `(Constant,Formula,…)` | `CALC.PAS:120` | `ERR 41` — the current rung |
 | subrange types `'A'..'G'`, `0..20`, `string[70]` | `CALC.PAS:117,119,130` | OK |
 | subrange types `'A'..'G'`, `0..20`, `string[70]` | `CALC.PAS:117,119,130` | OK |
-| `set of Char`, `set of Attributes` | `CALC.PAS:114,121,127` | `ERR 102` |
-| set constructor `[a..b, c]`, `in`, `+`, `*`, `=`/`<>`, assignment | 47 × `in`, 6 × `+ [`, 2 × `*` | not implemented |
+| `set of Char` / `set of <subrange>` | `CALC.PAS:114` | **type + sizing done** (hybrid bytes) |
+| `set of <enum>` | `CALC.PAS:121,127` | needs the enum base |
+| set constructor `[a..b, c]`, `in` (compile-time) | `t61_setconst` | **done** — folding only |
+| set `in` (run-time), `+`, `*`, `=`/`<>`, assignment, set *variables* | 47 × `in`, 6 × `+ [`, 2 × `*` | `ENoLib` — next sets increment |
 | `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | `ERR 102` |
 | `record` + field select `Sheet[FX,FY].CellStatus` | `CALC.PAS:126` | `ERR 102` |
 | `with` | 13 sites | `ERR 102` (via `record`) |
 | `with` | 13 sites | `ERR 102` (via `record`) |
 | 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | one range only |
 | 2-D array `array['A'..'G',1..FYMax] of CellRec` | `CALC.PAS:133` | one range only |
@@ -403,11 +405,17 @@ The ladder row now sits on **sets**.
   typed-constant red proof is the interesting one: dropping the class keeps the
   typed-constant red proof is the interesting one: dropping the class keeps the
   compile matrix green at the same `OK 65 4` and only execution sees `71` for
   compile matrix green at the same `OK 65 4` and only execution sees `71` for
   `'G'`.
   `'G'`.
-- **The ladder now stops at `ERR 102` at `7972`** — `Numbers : set of Char =
-  ['0'..'9']`, a set-typed constant. Structured typed constants need
-  initialised storage this compiler does not have, so they are refused with
-  `ENoLib` rather than accepted-and-ignored. The enumeration at 8036 is past
-  this rung, so **sets must land before enums**.
+- **Sets, increment 1, are landed** — the hybrid type, the constructor, set
+  typed constants, and compile-time `in`. Fixture `t61_setconst` (`.out` TRUE
+  FALSE TRUE TRUE), and the storage decision is recorded in Stage 6: a typed
+  constant's bytes are emitted in the code stream behind a `jmp` (as string
+  literals already are), addressable through `DS` because a `.COM` has one
+  segment — no `seg cs`, which is the single divergence from the original.
+  Still `ENoLib`: run-time `in`, set *variables*, `+`/`*`/`=`/`<>`, assignment.
+- **The ladder now stops at `ERR 41` at `8098`** — `Attributes =
+  (Constant,…)`, an **enumeration** type. So enums are the next rung and the
+  `SetOfAttri = Set of Attributes`/`CellStatus : set of Attributes` lines come
+  right behind them.
 - **One surprise on resuming:** the working tree was found holding the
 - **One surprise on resuming:** the working tree was found holding the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   nonvacuity script's *frame-rule* mutation of `shell/Compiler.mod` (the
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
   `locFree := 0FFFEH` / pre-decrement-base revert), i.e. a mutation from a
@@ -417,13 +425,11 @@ The ladder row now sits on **sets**.
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   byte-for-byte the same for INTEGER locals, so the compile matrix would have
   been green and only `t40_readln` (and later CALC) would have noticed.
   been green and only `t40_readln` (and later CALC) would have noticed.
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
 - **Verified baseline, run on this commit:** `./tests/run_all.sh` →
-  **OVERALL: ALL PASS**, compile matrix **61/61**, CALC ladder **1/1** at
-  `ERR 102 7972`, 51 `.COM` linked and 51 executed under both qemu and Exec86.
-- **`tests/nonvacuity.sh`:** the last full run before this commit was
-  **79 ok / 0 failed**; this commit adds two more compiler cases (the typed-const
-  class, and `^`/`#` as literals) and it was re-run after them — see the commit
-  message. It is not in the default `run_all.sh` and must be run **manually**
-  after touching any check.
+  **OVERALL: ALL PASS**, compile matrix **62/62**, CALC ladder **1/1** at
+  `ERR 41 8098`, 52 `.COM` linked and 52 executed under both qemu and Exec86.
+- **`tests/nonvacuity.sh`:** re-run after this commit's cases (the set
+  membership bit among them); see the commit message for the count. It is not in
+  the default `run_all.sh` and must be run **manually** after touching any check.
 - **Still present and deliberately untracked:** `Resources/TP3/CALC.COM` and
 - **Still present and deliberately untracked:** `Resources/TP3/CALC.COM` and
   `Resources/TP3/HELLO.COM`, the two reference binaries used for §3.1. They are
   `Resources/TP3/HELLO.COM`, the two reference binaries used for §3.1. They are
   derived from the proprietary `TURBO.COM`; do not stage them. The recipe and
   derived from the proprietary `TURBO.COM`; do not stage them. The recipe and
@@ -456,16 +462,18 @@ The ladder row now sits on **sets**.
 
 
 ### Next session
 ### Next session
 
 
-**The next rung is a SET.** The ladder stops at `ERR 102 7972` on
-`Numbers : set of Char = ['0'..'9']`, a set-typed constant, because this
-compiler has no initialised data to hold the value. So the honest next step is
-not "enums" (Stage 5) but **sets and the storage decision that makes a
-structured typed constant representable at all** — either the original's
-code-resident scheme (`seg cs`, program jumps over the values, `RESUME-TP3.md`
-§3.14) or an init block copied at startup. That decision also gates `XPOS`, the
-enum-indexed array of strings in `CALC.INC:767-778`, and arrays and records
-generally, so it is worth settling once, in writing, before the set layout is
-written. `CALC.PAS:114` is the fixture to move.
+**The next rung is an ENUM.** The ladder stops at `ERR 41 8098` on
+`Attributes = (Constant, Formula, Txt, OverWritten, Locked, Calculated)`: a
+type whose definition is a parenthesised list of identifiers, which `ParseType`
+has no arm for. Land the enumeration type next (Stage 5), then the set-over-enum
+lines at `CALC.PAS:121,127` follow immediately, and the rest of the set
+operations (run-time `in`, set variables, `+`/`*`/`=`/assignment) alongside.
+
+The storage decision that sets needed is **settled and recorded** (Stage 6): a
+typed constant's bytes live in the code stream behind a `jmp`, addressable
+through `DS`; no `seg cs`, because a `.COM` is one segment. That is the same
+decision the enum-indexed array of strings (`CALC.INC:767-778`) and `XPOS` will
+use, so it does not need re-litigating.
 
 
 **Stage 1 — the ratchet (§1). DONE.** `tests/calc_ladder.tsv` +
 **Stage 1 — the ratchet (§1). DONE.** `tests/calc_ladder.tsv` +
 `tests/calc_ladder.sh`, wired into `run_all.sh`; proved red against 7792 before
 `tests/calc_ladder.sh`, wired into `run_all.sh`; proved red against 7792 before
@@ -562,11 +570,41 @@ Locked,Calculated)`. 1 byte if fewer than 256 values, else 2 (`RESUME-TP3.md`
 §2). Ordinal values 0..n; this feeds `Succ`/`Pred` and `set of T`.
 §2). Ordinal values 0..n; this feeds `Succ`/`Pred` and `set of T`.
 Ladder: 8036 → 8060.
 Ladder: 8036 → 8060.
 
 
-**Stage 6 — sets.** Type sizing (1 bit per element, ≤ 32 bytes, hybrid bounds
-per `RESUME-TP3.md` §2), constructor `[a..b, c]`, `in` (bit test — 47 sites,
-the single most-used operator in CALC), `+` union, `*` intersection,
-`=`/`<>` against `[]`, and assignment. `set of Char` is a 32-byte value; decide
-inline vs a runtime helper. Ladder: 8060 → 8237.
+**Stage 6 — sets. Increment 1 DONE; the rest is the next milestone.**
+
+Done:
+- **Type sizing** — TP3's hybrid layout (coeur-tp-ocr 4.7.2): only the whole
+  bytes covering the base range are stored, element `e` is bit `(e MOD 8)` of
+  byte `((e DIV 8) - (base DIV 8))`, high byte last. `set of Char` is 32 bytes,
+  `set of 12..17` is 2. `elem` carries the origin (a multiple of 8).
+- **The constructor** `[a..b, c]`, and `[]`.
+- **Set typed constants** — `Numbers : set of Char = ['0'..'9']`.
+- **Compile-time `in`** — a constant element against a set whose bytes are in
+  the image, answered as TPSRC2 `xsetin` does (AH > 255 → FALSE, else test the
+  bit). Fixture `t61_setconst`, `.out` TRUE FALSE TRUE TRUE.
+- **The storage decision the plan demanded**, and it is the original's own:
+  a typed constant's bytes are emitted **in the code stream behind a `jmp`**,
+  exactly as string literals already are, and the name becomes addressable.
+  The **divergence** from `RESUME-TP3.md` §3.14 is one line: the original had
+  separate code and data segments and therefore prefixed `seg cs`; a `.COM` has
+  one segment, so `DS` already covers the code area and no prefix is needed.
+  The bytes are ordinary RAM, so a typed constant stays assignable, as TP3's
+  static typed constants are.
+
+Still to do (all currently `ENoLib`, none of them silently accepted):
+- **Run-time `in`** — a variable element or a set variable. The bit arithmetic
+  is the same, but it must be emitted, not folded.
+- **Set variables** — a 32-byte local/global (`DefVar` still refuses `size > 2`
+  for a non-string) and their layout in the frame.
+- **`+` union, `*` intersection, `=`/`<>`, assignment, `[]` in context.**
+- **`set of <enum>`** — needs the enum base first (the current rung).
+
+Reference note: `Resources/COMPIL4/` (Wirth's Pascal-P4, `P.PAS` + `P41..P48`)
+was added as a cross-check. It is a **different** design — a fixed
+`setsize = 8`-byte bitset over `0..sethigh (63)`, with `in` as a built-in
+runtime opcode — so it informs the *algorithms* but not the representation.
+TP3's hybrid bytes remain the ground truth here, per the repo rule.
+Ladder: 8060 → 8237 (now `8098` → the enum).
 
 
 **Stage 7 — records.** Non-compacted field layout (`RESUME-TP3.md` §2), field
 **Stage 7 — records.** Non-compacted field layout (`RESUME-TP3.md` §2), field
 selection as address arithmetic, `with` (13 sites), records as array elements.
 selection as address arithmetic, `with` (13 sites), records as array elements.
@@ -731,5 +769,10 @@ as the include, because the include is what consumes the slack.
   §3.11 parameters (the SEG:OFF push), §3.14 typed constants, §4 options.
   §3.11 parameters (the SEG:OFF push), §3.14 typed constants, §4 options.
 - `TP3-COMPILER.md` — what the compiler does today and why.
 - `TP3-COMPILER.md` — what the compiler does today and why.
 - `Resources/turbopascal3source/TP3/TPSRC1-10` — the ground truth.
 - `Resources/turbopascal3source/TP3/TPSRC1-10` — the ground truth.
+- `Resources/coeur-tp-ocr/coeur-tp-03-d.txt` §4.7 — sets explained: the hybrid
+  byte layout this port implements (4.7.2) and the memory order (4.7.3).
+- `Resources/COMPIL4/` — Wirth's Pascal-P4 (`P.PAS`, `P41..P48`), added as a
+  cross-check. Its sets are a *fixed* 8-byte bitset over 0..63 with `in` as a
+  runtime opcode, so it informs the algorithm, not the representation.
 - `Resources/TP3/CALC.PAS`, `CALC.INC`, `CALC.HLP` — the target.
 - `Resources/TP3/CALC.PAS`, `CALC.INC`, `CALC.HLP` — the target.
 - `shell/tests/probe/README.md` — read before touching any emitter.
 - `shell/tests/probe/README.md` — read before touching any emitter.

+ 10 - 9
SUMMARY.md

@@ -2036,15 +2036,16 @@ reals → string builtins → files → CRT → recursion → layout), plus the
 census of what `CALC.PAS` actually needs and the reference measurements from
 census of what `CALC.PAS` actually needs and the reference measurements from
 the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
 the original `TURBO.COM`. Two tracks run in parallel: the ladder (front-end,
 verifiable on small fixtures) and the layout (§3, the only thing that stops
 verifiable on small fixtures) and the layout (§3, the only thing that stops
-the compiler *emitting* a 16,560-byte program). Stages 1-4 are landed: the
-ratchet (`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by
-`run_all.sh`; CALC.PAS now stops at `ERR 102` at pos 7972), the `{$I}` include
-(`ExpandIncludes`, resolved before parsing; fixtures `t56_include`,
-`t57_include_missing`, `t58_include_nest`), the lexical character constants
-`^X`/`#nn` (`t60_charcaret`), and the scalar typed constant
-(`t59_typedconst`). The next rung is a set, so sets and the initialised-storage
-decision come next. Items 1-4 below are the string/robustness work and are
-independent of both.
+the compiler *emitting* a 16,560-byte program). The ratchet and front-end
+features through **sets (increment 1)** are landed: the ladder
+(`tests/calc_ladder.sh` + `tests/fixtures/calc_ladder.tsv`, run by `run_all.sh`;
+CALC.PAS now stops at `ERR 41` at pos 8098, an enumeration), the `{$I}` include,
+the lexical `^X`/`#nn` character constants, scalar typed constants, and the
+hybrid set type + constructor + set typed constants + compile-time `in`
+(fixtures `t56_include`, `t57_include_missing`, `t58_include_nest`,
+`t60_charcaret`, `t59_typedconst`, `t61_setconst`). The next rung is an
+enumeration. Items 1-4 below are the string/robustness work and are independent
+of both.
 
 
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
 1. The rest of the string language. `v-TP3-STRVAR`/`v-TP3-STRREAD-ASSIGN` cover
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`
    string variables, `s := 'literal'`, `writeln (s)`, `readln (s)`/`read (s)`

+ 257 - 18
shell/Compiler.mod

@@ -182,12 +182,17 @@ TYPE
          kind : CARDINAL ;          (* 0 const, 1 var, 2 value in AX,
          kind : CARDINAL ;          (* 0 const, 1 var, 2 value in AX,
                                        3 string literal (cls = TString),
                                        3 string literal (cls = TString),
                                        4 value on the stack (SaveLeft),
                                        4 value on the stack (SaveLeft),
-                                       5 string element, address in DI *)
+                                       5 string element, address in DI,
+                                       7 set value (cls = TSet) *)
          imm  : LONGINT ;
          imm  : LONGINT ;
          idx  : CARDINAL ;
          idx  : CARDINAL ;
          boff : CARDINAL ;          (* constant fold-in for subscripts *)
          boff : CARDINAL ;          (* constant fold-in for subscripts *)
          chr  : BOOLEAN ;           (* single-quoted literal, e.g. 'a' *)
          chr  : BOOLEAN ;           (* single-quoted literal, e.g. 'a' *)
          strx : CARDINAL ;          (* string literal: index into strPool *)
          strx : CARDINAL ;          (* string literal: index into strPool *)
+         setoff  : CARDINAL ;       (* SET value: image offset of the bytes *)
+         setbase : CARDINAL ;       (* ... the first byte's first element,
+                                       a multiple of 8 (TP3's hybrid origin) *)
+         setsize : CARDINAL ;       (* ... and how many bytes there are *)
       END ;
       END ;
 
 
    DirRec = RECORD  rng, chk : BOOLEAN  END ;
    DirRec = RECORD  rng, chk : BOOLEAN  END ;
@@ -2025,6 +2030,15 @@ BEGIN
       r.kind := 2 ;
       r.kind := 2 ;
       RETURN
       RETURN
    END ;
    END ;
+   IF r.cls = TSet THEN
+      (* A set is bytes, not a 16-bit value.  It is legal as the right operand
+         of `in' (and later as a whole-value operand of `+'/`*'/'='/assignment),
+         all of which handle it BEFORE LoadAtom - but never as a number, which
+         would otherwise load its first two bytes as if it were an integer. *)
+      Err (ENoLib) ;
+      r.kind := 2 ;
+      RETURN
+   END ;
    IF r.kind = 5 THEN
    IF r.kind = 5 THEN
       (* A string element with a variable index: ParseSub already computed its
       (* A string element with a variable index: ParseSub already computed its
          address into DI (LEA DI,[base] / ADD DI,AX), so the byte is at [DI].
          address into DI (LEA DI,[base] / ADD DI,AX), so the byte is at [DI].
@@ -2149,6 +2163,14 @@ BEGIN
       Err (EUnknown) ;
       Err (EUnknown) ;
       RETURN
       RETURN
    END ;
    END ;
+   IF r.cls = TSet THEN
+      (* a set value: carry where its bytes are so ParseCmp can fold `in' and
+         (later) compare.  A set VARIABLE is still refused at its declaration,
+         so today this is a typed constant or an inline literal. *)
+      r.setoff := symtab [r.idx].off ;
+      r.setbase := symtab [r.idx].elem ;
+      r.setsize := symtab [r.idx].size
+   END ;
    ParseSub (r)
    ParseSub (r)
 END ParseVar ;
 END ParseVar ;
 
 
@@ -2445,6 +2467,176 @@ BEGIN
    END
    END
 END EmPushStrAddr ;
 END EmPushStrAddr ;
 
 
+(* ---------------------------------------------------------------- *)
+(*  sets - type sizing, literals, compile-time membership            *)
+(* ---------------------------------------------------------------- *)
+
+PROCEDURE SetRangeBits (VAR bytes : ARRAY OF CHAR ; size, base : CARDINAL ;
+                        lo, hi : LONGINT) ;
+(* Set the bits for every element in [lo, hi], using TP3's hybrid rule
+   (coeur-tp-ocr 4.7.2): element e is bit (e MOD 8) of byte
+   ((e DIV 8) - (base DIV 8)), and is IGNORED unless that byte is in
+   [0, size).  A value that lands outside the used bytes is truncated away, as
+   TP3 does; one inside them but outside the DECLARED range still lands, which
+   is also TP3's behaviour. *)
+VAR rel, first, last, bidx, bit, m, o : CARDINAL ;
+BEGIN
+   IF (hi < 0) OR (lo > 255) THEN
+      RETURN
+   END ;
+   IF lo < 0 THEN first := 0 ELSE first := VAL (CARDINAL, lo) END ;
+   IF hi > 255 THEN last := 255 ELSE last := VAL (CARDINAL, hi) END ;
+   rel := first ;
+   WHILE rel <= last DO
+      IF rel >= base THEN
+         bidx := (rel - base) DIV 8 ;
+         IF bidx < size THEN
+            bit := (rel - base) MOD 8 ;
+            m := 1 ;
+            WHILE bit > 0 DO
+               m := m * 2 ; DEC (bit)
+            END ;
+            o := ORD (bytes [bidx]) ;
+            IF (o DIV m) MOD 2 = 0 THEN
+               bytes [bidx] := CHR (o + m)
+            END
+         END
+      END ;
+      INC (rel)
+   END
+END SetRangeBits ;
+
+PROCEDURE RdSetLiteral (base, size : CARDINAL ; VAR bytes : ARRAY OF CHAR) : BOOLEAN ;
+(* Read a set constructor `[e, a..b, ...]' and fill `bytes' (already size
+   long).  `[]' is the empty set. *)
+VAR v, hi : LONGINT ; subcls : CARDINAL ; strf : BOOLEAN ; k : CARDINAL ;
+BEGIN
+   k := 0 ;
+   WHILE k < size DO
+      bytes [k] := CHR (0) ;
+      INC (k)
+   END ;
+   ExpectDelim ('[', ENoSemi) ;
+   IF MatchDelim (']') THEN
+      RETURN OK ()                              (* [] - the empty set *)
+   END ;
+   LOOP
+      RdConst (v, subcls, strf) ;
+      IF strf THEN
+         Err (ETypeErr) ;
+         RETURN FALSE
+      END ;
+      IF MatchRange () THEN
+         RdConst (hi, subcls, strf) ;
+         IF strf THEN
+            Err (ETypeErr) ;
+            RETURN FALSE
+         END
+      ELSE
+         hi := v
+      END ;
+      SetRangeBits (bytes, size, base, v, hi) ;
+      IF MatchDelim (',') THEN
+         (* another element *)
+      ELSE
+         EXIT
+      END
+   END ;
+   ExpectDelim (']', ENoSemi) ;
+   RETURN OK ()
+END RdSetLiteral ;
+
+PROCEDURE EmSetBlob (VAR bytes : ARRAY OF CHAR ; size : CARDINAL) : CARDINAL ;
+(* Emit a set's bytes into the code stream behind a JMP over them - the same
+   trick EmPushStrAddr uses for a string literal - and return the image offset
+   of the first byte.  Nothing goes in the data segment: a .COM's DS already
+   covers the code segment, which is why this port needs no `seg cs' where the
+   original did (see CALC-COMPILE.md's typed-constant storage decision). *)
+VAR here, k : CARDINAL ;
+BEGIN
+   IF size + 2 <= 127 THEN
+      Ebyte (0EBH) ; Ebyte (VAL (BYTE, size))        (* JMP rel8 over it *)
+   ELSE
+      Ebyte (0E9H) ; Eword (size)                    (* JMP rel16 over it *)
+   END ;
+   here := pc ;
+   k := 0 ;
+   WHILE k < size DO
+      Ebyte (VAL (BYTE, ORD (bytes [k]))) ;
+      INC (k)
+   END ;
+   RETURN here
+END EmSetBlob ;
+
+PROCEDURE FoldSetIn (left, right : ERes ; VAR res : ERes) ;
+(* `e IN s' where e is a compile-time ordinal and s a set whose bytes are
+   already in the image: answer at compile time, exactly as TPSRC2's xsetin
+   answers at run time (AH > 255 -> FALSE, else test the bit).  A run-time
+   element or a run-time set is still ENoLib - that is the next milestone, and
+   it must stay LOUD rather than silently answer wrong. *)
+VAR off, base, size, rel, bidx, bit, m, o : CARDINAL ; isIn : BOOLEAN ;
+BEGIN
+   res.kind := 0 ;
+   res.imm := 0 ;
+   res.cls := TBool ;
+   res.chr := FALSE ;
+   IF (left.kind # 0) OR (right.cls # TSet) THEN
+      Err (ENoLib) ;
+      RETURN
+   END ;
+   isIn := FALSE ;
+   IF (left.imm >= 0) AND (left.imm <= 255) THEN
+      off := right.setoff ; base := right.setbase ; size := right.setsize ;
+      rel := VAL (CARDINAL, left.imm) ;
+      IF rel >= base THEN
+         bidx := (rel - base) DIV 8 ;
+         IF bidx < size THEN
+            bit := (rel - base) MOD 8 ;
+            m := 1 ;
+            WHILE bit > 0 DO
+               m := m * 2 ; DEC (bit)
+            END ;
+            o := VAL (CARDINAL, cbuf [off + bidx]) ;
+            isIn := (o DIV m) MOD 2 # 0
+         END
+      END
+   END ;
+   res.imm := ORD (isIn)
+END FoldSetIn ;
+
+PROCEDURE RdSetBase (VAR baseCls : CARDINAL ; VAR lo, hi : LONGINT) ;
+(* The base type of `set of ...': an ordinal range.  `Char' and `byte' are
+   0..255; a literal subrange `a..b' is read directly.  A named or enumerated
+   base is ENoLib for now - `set of Attributes' waits for enums. *)
+VAR idx, subcls : CARDINAL ; strf : BOOLEAN ;
+BEGIN
+   baseCls := TScalar ;
+   Skip () ;
+   IF Alpha (CurCh ()) THEN
+      GetWord () ;
+      IF Search (wrd, idx) AND (symtab [idx].tag = KType) THEN
+         IF (symtab [idx].cls = TChar)
+            OR ((symtab [idx].cls = TScalar) AND (symtab [idx].size = 1)) THEN
+            lo := 0 ; hi := 255 ; baseCls := symtab [idx].cls
+         ELSE
+            Err (ENoLib)
+         END
+      ELSE
+         Err (EUnknown)
+      END
+   ELSE
+      RdConst (lo, subcls, strf) ;
+      IF NOT MatchRange () THEN
+         Err (ESimpType) ;
+         RETURN
+      END ;
+      RdConst (hi, subcls, strf) ;
+      IF (lo < 0) OR (hi > 255) OR (lo > hi) THEN
+         Err (EConstRange)
+      END
+   END
+END RdSetBase ;
+
 PROCEDURE NewScratchStr () : CARDINAL ;
 PROCEDURE NewScratchStr () : CARDINAL ;
 (* Allocate a fresh 256-byte global STRING and return its symbol index.
 (* Allocate a fresh 256-byte global STRING and return its symbol index.
 
 
@@ -2612,6 +2804,15 @@ BEGIN
          END
          END
       END ;
       END ;
       IF op = 0 THEN
       IF op = 0 THEN
+         IF KwAhead ("IN") THEN
+            (* `e IN s' is at the comparison level in TP3 (TPSRC9 cmpin, 116).
+               Only the compile-time case is implemented; see FoldSetIn. *)
+            GetWord () ;
+            left := r ;
+            ParseAdd (right) ;
+            FoldSetIn (left, right, r) ;
+            RETURN
+         END ;
          RETURN
          RETURN
       END ;
       END ;
       left := r ;
       left := r ;
@@ -2772,10 +2973,12 @@ BEGIN
 END ParseNeg ;
 END ParseNeg ;
 
 
 PROCEDURE ParseAtom (VAR r : ERes) ;
 PROCEDURE ParseAtom (VAR r : ERes) ;
-(* const | variable | func(params) | '(' expr ')' *)
+(* const | variable | func(params) | '(' expr ')' | '[' set ']' *)
 VAR idx : CARDINAL ;
 VAR idx : CARDINAL ;
     strf : BOOLEAN ;
     strf : BOOLEAN ;
     quoted : BOOLEAN ;
     quoted : BOOLEAN ;
+    ssize, sbase : CARDINAL ;
+    sbuf : ARRAY [0..31] OF CHAR ;
 BEGIN
 BEGIN
    r.chr := FALSE ;            (* default: not a quoted char literal *)
    r.chr := FALSE ;            (* default: not a quoted char literal *)
    Skip () ;
    Skip () ;
@@ -2785,6 +2988,22 @@ BEGIN
       ExpectDelim (')', ENoSemi) ;
       ExpectDelim (')', ENoSemi) ;
       RETURN
       RETURN
    END ;
    END ;
+   IF CurCh () = '[' THEN
+      (* An inline set literal.  Full TP3 takes its type from context; here it
+         defaults to `set of Char' (0..255), which is what CALC writes
+         (`Ch in ['a'..'z']'), and its bytes are materialised in the code
+         stream.  A typed/larger-base literal is a later milestone. *)
+      ssize := 32 ;
+      sbase := 0 ;
+      IF RdSetLiteral (sbase, ssize, sbuf) THEN
+         r.kind := 7 ;
+         r.cls := TSet ;
+         r.setoff := EmSetBlob (sbuf, ssize) ;
+         r.setbase := sbase ;
+         r.setsize := ssize
+      END ;
+      RETURN
+   END ;
    IF (CurCh () = '$') OR (Digit (CurCh ())) OR (ORD (CurCh ()) = AposC)
    IF (CurCh () = '$') OR (Digit (CurCh ())) OR (ORD (CurCh ()) = AposC)
       OR (CurCh () = '^') OR (CurCh () = '#') THEN
       OR (CurCh () = '^') OR (CurCh () = '#') THEN
       (* remember that this was a quoted literal BEFORE RdConst consumes it:
       (* remember that this was a quoted literal BEFORE RdConst consumes it:
@@ -3741,12 +3960,23 @@ BEGIN
          size := W16 (hi) + 1
          size := W16 (hi) + 1
       END
       END
    ELSIF tok = TkSet THEN
    ELSIF tok = TkSet THEN
-      Err (ENoLib) ;
-      (* Unreachable, and it would be wrong even if it were reached: a
-         speculative `MatchKey (tok) AND (tok = TkOf)` consumes the token it
-         rejects.  See the note in the IF handler. *)
-      IF MatchKey (tok) AND (tok = TkOf) THEN
-         ParseType (cls, s2, e2)
+      (* set of <ordinal>.  TP3's hybrid layout (coeur-tp-ocr 4.7.2): only the
+         whole BYTES covering the base range are stored, so `set of Char' is 32
+         bytes and `set of 12..17' is 2.  `elem' carries the origin - the first
+         element of the first byte, a multiple of 8 - which is all the
+         membership and constructor arithmetic needs. *)
+      PeekKw (tok) ;
+      IF tok = TkOf THEN
+         DropB (MatchKey (tok)) ;
+         RdSetBase (subcls, lo, hi) ;
+         IF OK () THEN
+            cls := TSet ;
+            elem := (VAL (CARDINAL, lo) DIV 8) * 8 ;
+            size := (VAL (CARDINAL, hi) DIV 8)
+                    - (VAL (CARDINAL, lo) DIV 8) + 1
+         END
+      ELSE
+         Err (ENoSemi)
       END
       END
    ELSIF tok = TkRecord THEN
    ELSIF tok = TkRecord THEN
       Err (ENoLib) ;
       Err (ENoLib) ;
@@ -3925,10 +4155,11 @@ END DefVar ;
 PROCEDURE DefConst () ;
 PROCEDURE DefConst () ;
 VAR nm : ARRAY [0..MaxName] OF CHAR ;
 VAR nm : ARRAY [0..MaxName] OF CHAR ;
     v : LONGINT ;
     v : LONGINT ;
-    cls, typCls, typSize, typElem : CARDINAL ;
+    cls, typCls, typSize, typElem, soff : CARDINAL ;
     isStr : BOOLEAN ;
     isStr : BOOLEAN ;
     tok : CARDINAL ;
     tok : CARDINAL ;
-    typed : BOOLEAN ;
+    typed, setTyped : BOOLEAN ;
+    sbuf : ARRAY [0..31] OF CHAR ;
 BEGIN
 BEGIN
    LOOP
    LOOP
       PeekKw (tok) ;
       PeekKw (tok) ;
@@ -3950,26 +4181,34 @@ BEGIN
          recorded as the symbol's class - `FXMax : Char = 'G'' is a TChar
          recorded as the symbol's class - `FXMax : Char = 'G'' is a TChar
          constant holding 71, and `writeln (FXMax)' prints G rather than 71.
          constant holding 71, and `writeln (FXMax)' prints G rather than 71.
 
 
-         A typed constant of a structured type (set, array, record) is a value
-         with a layout, and this compiler has no initialised data yet, so those
-         are refused loudly (ENoLib) rather than accepted with the initialiser
-         thrown away - see CALC-COMPILE.md Stage 4: `Numbers : set of Char =
-         ['0'..'9']' and `XPOS : array['A'..'G'] of Integer = (...)' wait for
-         the milestones that lay that storage out. *)
+         A SET typed constant (`Numbers : set of Char = ['0'..'9']') is a value
+         with a layout, so it cannot fold: its bytes are emitted code-resident
+         and the name becomes an addressable value.  See the storage decision
+         in CALC-COMPILE.md.  An ARRAY or RECORD typed constant is still
+         refused loudly - those layouts come later. *)
       typed := FALSE ;
       typed := FALSE ;
+      setTyped := FALSE ;
       Skip () ;
       Skip () ;
       IF CurCh () = ':' THEN
       IF CurCh () = ':' THEN
          DropCh (GetCh ()) ;
          DropCh (GetCh ()) ;
          ParseType (typCls, typSize, typElem) ;
          ParseType (typCls, typSize, typElem) ;
          IF OK () THEN
          IF OK () THEN
-            IF typSize > 2 THEN
+            IF typCls = TSet THEN
+               setTyped := TRUE
+            ELSIF typSize > 2 THEN
                Err (ENoLib)
                Err (ENoLib)
             ELSE
             ELSE
                typed := TRUE
                typed := TRUE
             END
             END
          END
          END
       END ;
       END ;
-      IF OK () THEN
+      IF OK () AND setTyped THEN
+         ExpectDelim ('=', ENoSemi) ;
+         IF RdSetLiteral (typElem, typSize, sbuf) THEN
+            soff := EmSetBlob (sbuf, typSize) ;
+            DropC (NewSym (nm, KVar, TSet, typSize, typElem, soff, 0, FALSE))
+         END
+      ELSIF OK () THEN
          ExpectDelim ('=', ENoSemi) ;
          ExpectDelim ('=', ENoSemi) ;
          RdConst (v, cls, isStr) ;
          RdConst (v, cls, isStr) ;
          IF isStr THEN
          IF isStr THEN

+ 7 - 0
shell/tests/audit_helpers.py

@@ -125,6 +125,13 @@ NON_ONE_LINE = {
         # name grammar cannot describe has to be written down, or the report is
         # name grammar cannot describe has to be written down, or the report is
         # green and the helper unchecked.
         # green and the helper unchecked.
         "PushStrAddr": COMP,
         "PushStrAddr": COMP,
+        # SetBlob is PushStrAddr's sibling for sets: a set cannot fold to a
+        # word, so its bytes travel inline - a JMP over them, then the bytes -
+        # and the address of those bytes is the value.  Like PushStrAddr the
+        # emitted sequence depends on the operand (its length), so there is no
+        # single sequence to disassemble; the LAYOUT it produces is pinned by
+        # t61_setconst's .out instead.
+        "SetBlob": COMP,
     },
     },
 }
 }
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,
 # The Vx family is NOT here: those four are audited by audit_vx_helpers,

+ 7 - 7
shell/tests/fixtures/calc_ladder.tsv

@@ -25,10 +25,10 @@
 # positions shift — which is exactly why this must be a measured number, not a
 # positions shift — which is exactly why this must be a measured number, not a
 # remembered one.
 # remembered one.
 #
 #
-# Current rung (2026-10-19): Numbers : set of Char = ['0'..'9'] — a typed
-# constant whose type is a SET, so ParseType reaches its TkSet arm and reports
-# ENoLib (102).  This moved from 7791 (FXMax : Char = 'G', a scalar typed
-# constant) when scalar typed constants and the ^M / #nn character constants
-# landed.  The enumeration at 8036 is PAST this rung and cannot be reached
-# until sets exist — see CALC-COMPILE.md Stage 4.
-CALC.PAS	ERR	102	7972
+# Current rung (2026-10-19): Attributes = (Constant, Formula, ...) — an
+# ENUMERATION type.  ParseType has no arm for the '(' that begins an enumerated
+# list, so the enumerator word is an unknown identifier: EUnknown (41).  The
+# rung moved from 7972 (the set typed constant `Numbers : set of Char =
+# ['0'..'9']') when sets landed: the hybrid set type, the constructor, the set
+# typed constant, and compile-time `in'.
+CALC.PAS	ERR	41	8098

+ 14 - 0
shell/tests/fixtures/expected.tsv

@@ -457,3 +457,17 @@ t59_typedconst	OK	65	4
 #                   the text "13"/"27"; the last two lines check the values
 #                   the text "13"/"27"; the last two lines check the values
 #                   (EofLine = ^M, Esc = 27).
 #                   (EofLine = ^M, Esc = 27).
 t60_charcaret	OK	75	4
 t60_charcaret	OK	75	4
+#   t61_setconst           `Numbers : set of Char = ['0'..'9']' - CALC.PAS:114.
+#                   TP3's HYBRID set layout (coeur-tp-ocr 4.7.2): only the whole
+#                   bytes covering the base range are stored, element e is bit
+#                   (e MOD 8) of byte ((e DIV 8) - (base DIV 8)), high byte
+#                   last.  A set cannot fold to a word, so its bytes go in the
+#                   code stream behind a JMP (as string literals do) and the
+#                   name becomes addressable; no `seg cs' is needed because a
+#                   .COM's DS covers the code segment.  `in' with a compile-time
+#                   element is answered at COMPILE time (TPSRC2 xsetin: AH > 255
+#                   -> FALSE, else test the bit).  The .out is TRUE FALSE TRUE
+#                   TRUE - the constant, an inline literal, and a two-range
+#                   literal.  data=4 is the @@T temporaries; the set's 32 bytes
+#                   are in the code stream, not the data area.
+t61_setconst	OK	183	4

+ 4 - 0
shell/tests/fixtures/t61_setconst.out

@@ -0,0 +1,4 @@
+TRUE
+FALSE
+TRUE
+TRUE

+ 33 - 0
shell/tests/fixtures/t61_setconst.pas

@@ -0,0 +1,33 @@
+program t61_setconst;
+
+{ t61 -- sets, first increment: the hybrid TYPE, the set CONSTRUCTOR, a set
+  TYPED CONSTANT, and compile-time membership.
+
+  CALC.PAS:114 is
+
+      Numbers : set of Char = ['0'..'9'];
+
+  The layout is TP3's hybrid one (coeur-tp-ocr 4.7.2): only the whole BYTES
+  covering the base range are stored, element e is bit (e MOD 8) of byte
+  ((e DIV 8) - (base DIV 8)), and the high byte comes last.  For `set of Char'
+  that is 32 bytes; `set of 12..17' is 2.
+
+  A set typed constant is not foldable to a word, so its 32 bytes are emitted
+  in the code stream behind a JMP (the same trick string literals use) and the
+  name becomes an addressable value.  No `seg cs' is needed: a .COM's DS
+  already covers the code segment - that is the divergence from the original,
+  which had separate segments (see CALC-COMPILE.md).
+
+  `in' with a COMPILE-TIME element and a set already in the image is answered
+  at compile time, exactly as TPSRC2 xsetin answers at run time (AH > 255 ->
+  FALSE, else test the bit).  A run-time element or set is still ENoLib and
+  stays loud; that is the next increment, not a silent wrong answer. }
+
+const
+   Numbers : set of Char = ['0'..'9'];
+begin
+   writeln ('5' in Numbers);          { in the constant }
+   writeln ('X' in Numbers);          { not in it }
+   writeln ('0' in ['0'..'9']);       { an inline literal, same answer }
+   writeln ('z' in ['a'..'z', 'A'..'Z'])
+end.

+ 28 - 0
shell/tests/nonvacuity.sh

@@ -1508,6 +1508,34 @@ else
 fi
 fi
 cp "$SAVED_C" Compiler.mod
 cp "$SAVED_C" Compiler.mod
 
 
+# --- the set membership bit -----------------------------------------------
+# Compile-time `in' reads a byte out of the emitted set and tests one bit.  Flip
+# the test and every answer is wrong while the sizes are untouched, so the
+# compile matrix stays GREEN (same OK 183 4) and only execution sees it - the
+# same shape as the typed-constant class case above, and the reason t61 has a
+# .out rather than just a row.
+cp "$SAVED_C" Compiler.mod
+if python3 - <<'PYX'
+p = 'Compiler.mod'
+s = open(p).read()
+old = 'isIn := (o DIV m) MOD 2 # 0'
+assert s.count(old) == 1, 'the set membership bit test found %d times -- update this mutation' % s.count(old)
+open(p, 'w').write(s.replace(old, 'isIn := (o DIV m) MOD 2 = 0'))
+PYX
+then
+    if rebuild_compiler; then
+        expect_red "t61 red when the set membership bit test is inverted" \
+            "t61_setconst" python3 tests/run_com_exec.py t61_setconst
+    else
+        echo "  FAIL: the compiler would not rebuild with the set bit test inverted"
+        fail=$((fail + 1))
+    fi
+else
+    echo "  BROKEN CASE: the set-bit mutation did not apply"
+    fail=$((fail + 1))
+fi
+cp "$SAVED_C" Compiler.mod
+
 if rebuild_compiler; then
 if rebuild_compiler; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
     if python3 tests/run_com_exec.py >/dev/null 2>&1; then
         echo "  ok: every executed fixture passes on the restored sources"
         echo "  ok: every executed fixture passes on the restored sources"