Skip to content

Commit cfdc305

Browse files
committed
feat(rd-18): Slice 8b Phase 5 — acceptance tier GREEN (3-part bar on real VICE)
- examples/slice8b fixture + committed 8-byte table.bin: PETSCII string const, embed() through the real source-relative reader, bracketed string init with char fill + char-store mutation, char-literal comparison; copy through by-ref const src + by-ref mutable dst staging arrays - Fixture corrections vs the plan listing (mechanical, oracles unchanged): <word>(i) cast form; hoisted inclusive-to bound; poke's compile-time- constant-address rule (E10045) → staging copy + const-address pokes - Fixes a REAL shipped codegen bug the fixture exposed: _cmpN/_shN generated labels restarted per function (global assembler symbols → "Symbol already defined" as soon as two functions compare); program- shared label allocator through generateInstr; eleven prior goldens stay byte-exact - Harness slice8b module (absolute outDir, asset staging), negatives suite 8/8 (E10200/01/02/05, E10116, E10124, E10127, format E90001), 205-line golden + landmarks, VICE runtime GREEN FIRST RUN ($0400 title, $C000 table, $C010 banner, $C020 flag) - Verification: passing (full chain, 17/17; harness 173 tests) Ref: codeops/features/blend65-ri/plans/rd-18-slice-8b-strings-embed/99-execution-plan.md Tasks: 5.1.1-5.2.4
1 parent 14d2788 commit cfdc305

12 files changed

Lines changed: 722 additions & 29 deletions

File tree

codeops/features/blend65-ri/plans/rd-18-slice-8b-strings-embed/00-ambiguity-register.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ re-check (AR-10 mechanics, RD-18 "canonicalize" clause); the raw no-profile enco
7171
`SemanticModel`/`ConstValue` provenance edits are `@blend65/core` changes; citation fixes
7272
(Ch 15 §6; EMB-1 vs the AR-11 tightening; ST-25..35/ST-40 ids).
7373

74+
**Runtime corrections (2026-07-17, exec_plan Phase 5 — mechanical, no decided outcome changed):**
75+
the 03-04 fixture listing was not legal blend65 on three counts, corrected during execution with
76+
the oracles (observables table, golden landmarks, proof points) preserved byte-for-byte:
77+
(1) `word(i)``<word>(i)` (the language's cast form; `word(i)` parses as an undeclared call);
78+
(2) `for (… = 0 to len)` → a hoisted `last = len - 1` bound (`to` is INCLUSIVE — slice7's
79+
`0 to 4` covers five elements — and the for-bound parses as a primary expression);
80+
(3) `poke(dst + <word>(i), …)` violates poke's compile-time-constant-address rule (E10045) —
81+
the copy became by-ref-array staging (`copyBytes(src: const byte[], dst: byte[], len)`) with
82+
unrolled const-address pokes relaying the staged bytes to the observable ranges, which also
83+
exercises the mutable by-ref array parameter path. Additionally the fixture exposed a REAL
84+
shipped codegen bug: `_cmpN`/`_shN` generated labels are global assembler symbols but their
85+
counter reset per function, so any program with two comparison-bearing functions failed ACME
86+
("Symbol already defined") — fixed with a program-shared label allocator threaded through
87+
`generateInstr` (`translate.ts`/`instr-program.ts`); all eleven prior goldens stay byte-exact
88+
(any prior hitting the bug could never have assembled).
89+
7490
**Deferral consequences (stated at deferral):** AR-4 leaves the four encoding intrinsics +
7591
`encode()` unparsed-as-intrinsics (they fail as undeclared identifiers); AR-11 leaves format-aware
7692
embed behind a loud E90001; AR-5 defers full encoding-table fidelity (graphics chars, shifted

codeops/features/blend65-ri/plans/rd-18-slice-8b-strings-embed/03-04-acceptance.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
77
## The fixture — `examples/slice8b/main.blend` (+ `table.bin`)
88

9+
> **Runtime corrections (mechanical, 2026-07-17, exec_plan):** the original listing used
10+
> `word(i)` (the language's cast form is `<word>(expr)``word(i)` parses as a call to an
11+
> undeclared identifier) and `for (… = 0 to len)` (`to` is INCLUSIVE — slice7's `0 to 4`
12+
> covers five elements — so `to len` would copy len+1 bytes); the for-bound is a PRIMARY
13+
> expression, so the fix hoists `len - 1` into a local. Observables unchanged. The fixture
14+
> comments also drop their register citations per the documentation ban.
15+
916
```blend65
1017
module Main;
1118
@@ -14,8 +21,9 @@ const TABLE: byte[] = embed("table.bin"); // 8 committed bytes: $01 02 04 08 10
1421
let banner: byte[8] = ["HI"; '.']; // bracketed string + char fill (AR-8 form 2)
1522
1623
function copyBytes(src: const byte[], len: byte, dst: word): void {
17-
for (let i: byte = 0 to len) {
18-
poke(dst + word(i), src[i]);
24+
let last: byte = len - 1;
25+
for (let i: byte = 0 to last) {
26+
poke(dst + <word>(i), src[i]);
1927
}
2028
}
2129

codeops/features/blend65-ri/plans/rd-18-slice-8b-strings-embed/99-execution-plan.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
> **Document**: 99-execution-plan.md
44
> **Parent**: [Index](00-index.md)
5-
> **Last Updated**: 2026-07-17 20:12
6-
> **Progress**: 43/58 tasks (74%)
5+
> **Last Updated**: 2026-07-17 21:10
6+
> **Progress**: 52/58 tasks (90%)
77
> **CodeOps Skills Version**: 3.8.0
88
99
## Overview
@@ -172,18 +172,18 @@ implement → green → impl tests → full verify). Designs live in the 03-docs
172172

173173
**Reference**: 03-04 · AR-14 · ST-36..40
174174

175-
- [ ] 5.1.1 Create `examples/slice8b/main.blend` + committed `table.bin` (verbatim per 03-04); harness `slice8b.ts` (`SLICE8B_MAIN_SRC`, `buildSlice8b` with ABSOLUTE outDir, asset staging)
176-
- [ ] 5.1.2 Write `slice8b.spec.test.ts` (ST-36 assemble-clean + ST-37 VICE observables) — `packages/test-harness/src/`
177-
- [ ] 5.1.3 Write `slice8b-negatives.spec.test.ts` (ST-40, one per code) — `packages/test-harness/src/`
178-
- [ ] 5.1.4 Write `golden-slice8b.spec.test.ts` (ST-38 landmarks + ST-39 prior-goldens sweep) — `packages/test-harness/src/`
179-
- [ ] 5.1.5 Run assemble/negatives/golden suites — confirm expected red (no golden yet) and assemble-clean status
175+
- [x] 5.1.1 Create `examples/slice8b/main.blend` + committed `table.bin` (verbatim per 03-04); harness `slice8b.ts` (`SLICE8B_MAIN_SRC`, `buildSlice8b` with ABSOLUTE outDir, asset staging) ✅ (completed: 2026-07-17 20:52; impl 20:24 — **runtime corrections (mechanical)**: `word(i)``<word>(i)` (the cast form; `word(i)` parses as an undeclared call) and `0 to len` → hoisted `last = len - 1` bound (`to` is INCLUSIVE per slice7's 0-to-4-over-5-elements pin; the for-bound is a primary expression); corrections recorded in 03-04)
176+
- [x] 5.1.2 Write `slice8b.spec.test.ts` (ST-36 assemble-clean + ST-37 VICE observables) — `packages/test-harness/src/` ✅ (completed: 2026-07-17 20:52; impl 20:24)
177+
- [x] 5.1.3 Write `slice8b-negatives.spec.test.ts` (ST-40, one per code) — `packages/test-harness/src/` ✅ (completed: 2026-07-17 20:52; impl 20:24 — 8 codes incl. the existence-irrelevant E10205 probe)
178+
- [x] 5.1.4 Write `golden-slice8b.spec.test.ts` (ST-38 landmarks + ST-39 prior-goldens sweep) — `packages/test-harness/src/` ✅ (completed: 2026-07-17 20:52; impl 20:24 — ST-39's eleven-priors sweep = the existing per-slice golden suites running green in the same tier, noted in the doc comment)
179+
- [x] 5.1.5 Run assemble/negatives/golden suites — confirm expected red (no golden yet) and assemble-clean status ✅ (completed: 2026-07-17 20:52 — negatives 8/8 green immediately; assemble red exposed TWO real issues, both fixed under 5.2.1: the fixture's poke const-address violation (E10045; redesigned to by-ref staging + const-address pokes, oracles unchanged) and the shipped cross-function `_cmp`/`_sh` label collision (program-shared allocator; priors byte-exact))
180180

181181
### Step 5.2: Bar
182182

183-
- [ ] 5.2.1 Fix any assemble/negative failures; mint `test/golden/slice8b.asm.golden` once ASM is correct-by-inspection against 03-04 landmarks
184-
- [ ] 5.2.2 Run the VICE runtime suite on real VICE 3.10 — ST-37 observables GREEN (local tier)
185-
- [ ] 5.2.3 Verify the eleven prior goldens byte-exact (ST-39) + CI-tier suites green
186-
- [ ] 5.2.4 Full verify
183+
- [x] 5.2.1 Fix any assemble/negative failures; mint `test/golden/slice8b.asm.golden` once ASM is correct-by-inspection against 03-04 landmarks ✅ (completed: 2026-07-17 20:52 — 205-line golden; landmarks verified (PETSCII title row, verbatim table row, JSR __init + JSR _main, program-unique _cmp labels))
184+
- [x] 5.2.2 Run the VICE runtime suite on real VICE 3.10 — ST-37 observables GREEN (local tier) ✅ (completed: 2026-07-17 20:52 — GREEN FIRST RUN: $0400-09 title, $C000-07 table, $C010-17 banner, $C020 flag)
185+
- [x] 5.2.3 Verify the eleven prior goldens byte-exact (ST-39) + CI-tier suites green ✅ (completed: 2026-07-17 20:52 — 11/11 priors byte-exact under the label-allocator fix; harness package 173/173)
186+
- [x] 5.2.4 Full verify ✅ (completed: 2026-07-17 21:10 — 17/17 turbo tasks from the repo root; harness 48 files/173 tests incl. the 8b three-part bar; boundary tier green)
187187

188188
**Verify**: (same command)
189189

examples/slice8b/main.blend

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module Main;
2+
3+
const TITLE: byte[] = "HELLO C64!"; // 10 bytes, PETSCII-encoded
4+
const TABLE: byte[] = embed("table.bin"); // 8 committed bytes: $01 02 04 08 10 20 40 80
5+
let banner: byte[8] = ["HI"; '.']; // bracketed string + char-literal fill
6+
let title2: byte[10]; // staging: the copied screen text
7+
let table2: byte[8]; // staging: the copied embed bytes
8+
9+
function copyBytes(src: const byte[], dst: byte[], len: byte): void {
10+
let last: byte = len - 1;
11+
for (let i: byte = 0 to last) {
12+
dst[i] = src[i];
13+
}
14+
}
15+
16+
function main(): void {
17+
copyBytes(TITLE, title2, length(TITLE)); // by-ref const src, by-ref dst, length() fold
18+
copyBytes(TABLE, table2, length(TABLE)); // embedded bytes through the same copy path
19+
banner[0] = 'B'; // mutable string mutation via a char literal
20+
21+
poke($0400, title2[0]); // screen RAM: the copied title
22+
poke($0401, title2[1]);
23+
poke($0402, title2[2]);
24+
poke($0403, title2[3]);
25+
poke($0404, title2[4]);
26+
poke($0405, title2[5]);
27+
poke($0406, title2[6]);
28+
poke($0407, title2[7]);
29+
poke($0408, title2[8]);
30+
poke($0409, title2[9]);
31+
32+
poke($C000, table2[0]); // the copied embed bytes
33+
poke($C001, table2[1]);
34+
poke($C002, table2[2]);
35+
poke($C003, table2[3]);
36+
poke($C004, table2[4]);
37+
poke($C005, table2[5]);
38+
poke($C006, table2[6]);
39+
poke($C007, table2[7]);
40+
41+
poke($C010, banner[0]); // the mutated banner
42+
poke($C011, banner[1]);
43+
poke($C012, banner[2]);
44+
poke($C013, banner[3]);
45+
poke($C014, banner[4]);
46+
poke($C015, banner[5]);
47+
poke($C016, banner[6]);
48+
poke($C017, banner[7]);
49+
50+
if (TITLE[0] == 'H') { // char-literal comparison against encoded data
51+
poke($C020, 1);
52+
}
53+
}

examples/slice8b/table.bin

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
 @�

packages/codegen/src/instr/instr-program.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export function generateInstr(
7777
): InstrProgram {
7878
const plan = ilProgram.allocationPlan;
7979
const streams: InstrStream[] = [];
80+
// One comparison-label sequence for the whole program — the `_cmpN`
81+
// labels are global assembler symbols, so per-function restarts would
82+
// collide as soon as two functions both compare.
83+
const sharedOpts: TranslateOptions = { ...opts, cmpLabels: { next: 0 } };
8084

8185
if (ilProgram.initCode.length > 0) {
8286
// The module-initializer stream, shaped like a void function so the
@@ -90,7 +94,7 @@ export function generateInstr(
9094
tempCount: ilProgram.initTempCount,
9195
isInterrupt: false,
9296
};
93-
const initStream = translateFunction(initFn, plan, cpuVariant, bag, opts);
97+
const initStream = translateFunction(initFn, plan, cpuVariant, bag, sharedOpts);
9498
validateStream(initStream, cpuVariant, bag);
9599
streams.push(initStream);
96100
}
@@ -101,7 +105,7 @@ export function generateInstr(
101105
if (!hasBody) {
102106
continue;
103107
}
104-
const stream = translateFunction(fn, plan, cpuVariant, bag, opts);
108+
const stream = translateFunction(fn, plan, cpuVariant, bag, sharedOpts);
105109
// Post-translation validation: every emitted opcode+mode must be CPU-legal
106110
// for the variant; an illegal pair is an E90001 codegen bug.
107111
validateStream(stream, cpuVariant, bag);

packages/codegen/src/instr/translate.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ export interface TranslateOptions {
7070
readonly zpArgBlockSize?: number;
7171
/** The target platform id, for the E10044 message. */
7272
readonly platformId?: string;
73+
/**
74+
* A program-shared comparison-label allocator. The `_cmpN` labels are
75+
* global assembler symbols, so every function of one program must draw
76+
* from a single sequence — two functions both starting at `_cmp0` would
77+
* collide at assembly. Absent (single-function translation in tests),
78+
* a translator-local counter serves.
79+
*/
80+
readonly cmpLabels?: { next: number };
7381
}
7482

7583
/**
@@ -177,9 +185,23 @@ class FunctionTranslator {
177185
private leadSpan: SourceSpan | undefined = undefined;
178186
/** Instruction index whose `store` has been folded into a word ALU (skip it). */
179187
private skipIndex = -1;
180-
/** Per-function generated-label counter (`_cmpN`). Reserved for comparisons. */
188+
/**
189+
* Fallback generated-label counter (`_cmpN`) for translator-local use
190+
* when no program-shared allocator is supplied.
191+
*/
181192
private cmpCounter = 0;
182193

194+
/** The next program-unique generated-label number (shared across families). */
195+
private nextLabelNumber(): number {
196+
const shared = this.opts?.cmpLabels;
197+
return shared !== undefined ? shared.next++ : this.cmpCounter++;
198+
}
199+
200+
/** The next program-unique comparison label. */
201+
private nextCmpLabel(): string {
202+
return `_cmp${this.nextLabelNumber()}`;
203+
}
204+
183205
constructor(
184206
private readonly fn: ILFunction,
185207
private readonly plan: AllocationPlan,
@@ -713,8 +735,8 @@ class FunctionTranslator {
713735
}
714736
const count = this.rightSource(right, 0);
715737
this.emit("LDX", count.mode, count.operand);
716-
const loopL = `_sh${this.cmpCounter++}`;
717-
const doneL = `_sh${this.cmpCounter++}`;
738+
const loopL = `_sh${this.nextLabelNumber()}`;
739+
const doneL = `_sh${this.nextLabelNumber()}`;
718740
this.emit("BEQ", "Relative", labelRef(doneL)); // a zero count shifts nothing
719741
this.out.push(label(loopL));
720742
this.emitByteShiftStep(op, signedShr);
@@ -741,8 +763,8 @@ class FunctionTranslator {
741763
}
742764
const count = this.rightSource(right, 0);
743765
this.emit("LDX", count.mode, count.operand);
744-
const loopL = `_sh${this.cmpCounter++}`;
745-
const doneL = `_sh${this.cmpCounter++}`;
766+
const loopL = `_sh${this.nextLabelNumber()}`;
767+
const doneL = `_sh${this.nextLabelNumber()}`;
746768
this.emit("BEQ", "Relative", labelRef(doneL));
747769
this.out.push(label(loopL));
748770
this.emitWordShiftStep(op, signedShr, home);
@@ -1042,7 +1064,7 @@ class FunctionTranslator {
10421064
} else {
10431065
// Carry-based (BCC/BCS): `LDA` preserves the carry flag, so the compact
10441066
// form is correct — the branch tests the carry `CMP` set.
1045-
const done = `_cmp${this.cmpCounter++}`;
1067+
const done = this.nextCmpLabel();
10461068
this.emit("LDA", "Immediate", imm8(0x01));
10471069
this.emit(branch, "Relative", labelRef(done));
10481070
this.emit("LDA", "Immediate", imm8(0x00));
@@ -1053,8 +1075,8 @@ class FunctionTranslator {
10531075

10541076
/** Branch-taken ⇒ 1: materialise the 0/1 from a flag-fresh branch opcode. */
10551077
private materialiseOnBranch(branch: Opcode): void {
1056-
const trueL = `_cmp${this.cmpCounter++}`;
1057-
const endL = `_cmp${this.cmpCounter++}`;
1078+
const trueL = this.nextCmpLabel();
1079+
const endL = this.nextCmpLabel();
10581080
this.emit(branch, "Relative", labelRef(trueL));
10591081
this.emit("LDA", "Immediate", imm8(0x00));
10601082
this.emit("JMP", "Absolute", labelRef(endL));
@@ -1079,7 +1101,7 @@ class FunctionTranslator {
10791101
this.emit("SEC", "Implied", none());
10801102
const r = this.rightSource(rhs, 0);
10811103
this.emit("SBC", r.mode, r.operand);
1082-
const skipL = `_cmp${this.cmpCounter++}`;
1104+
const skipL = this.nextCmpLabel();
10831105
this.emit("BVC", "Relative", labelRef(skipL));
10841106
this.emit("EOR", "Immediate", imm8(0x80));
10851107
this.out.push(label(skipL));
@@ -1089,7 +1111,7 @@ class FunctionTranslator {
10891111

10901112
/** 16-bit equality: low bytes decide fast, high bytes break the tie (Z-based). */
10911113
private wordEquality(op: "eq" | "ne", dest: ILOperand, lhs: ILOperand, rhs: ILOperand): void {
1092-
const diffL = `_cmp${this.cmpCounter++}`;
1114+
const diffL = this.nextCmpLabel();
10931115
this.wordLeftByteIntoA(lhs, 0);
10941116
const rLo = this.rightSource(rhs, 0);
10951117
this.emit("CMP", rLo.mode, rLo.operand);
@@ -1115,9 +1137,9 @@ class FunctionTranslator {
11151137
rhs: ILOperand,
11161138
): void {
11171139
const wantLess = op === "lt" || op === "gt"; // after the caller's swap
1118-
const falseL = `_cmp${this.cmpCounter++}`;
1119-
const trueL = `_cmp${this.cmpCounter++}`;
1120-
const endL = `_cmp${this.cmpCounter++}`;
1140+
const falseL = this.nextCmpLabel();
1141+
const trueL = this.nextCmpLabel();
1142+
const endL = this.nextCmpLabel();
11211143

11221144
this.wordLeftByteIntoA(lhs, 1);
11231145
const rHi = this.rightSource(rhs, 1);
@@ -1161,7 +1183,7 @@ class FunctionTranslator {
11611183
this.wordLeftByteIntoA(lhs, 1);
11621184
const rHi = this.rightSource(rhs, 1);
11631185
this.emit("SBC", rHi.mode, rHi.operand);
1164-
const skipL = `_cmp${this.cmpCounter++}`;
1186+
const skipL = this.nextCmpLabel();
11651187
this.emit("BVC", "Relative", labelRef(skipL));
11661188
this.emit("EOR", "Immediate", imm8(0x80));
11671189
this.out.push(label(skipL));
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* CI golden test for the Slice 8b fixture: emits the fixture's ACME source
3+
* via `emitAsm` and `assertGolden`s it against the committed
4+
* `test/golden/slice8b.asm.golden`, proving the data surface's emission
5+
* stays byte-exact — the string const bakes its PETSCII bytes and the
6+
* embedded file bakes its bytes verbatim under their data labels, the
7+
* banner initialises through the init stream, and the terminating shim
8+
* enters `main` with `JSR`. Runs in CI (no emulator, no ACME — `emitAsm`
9+
* stops before the assembler).
10+
*
11+
* The ten prior slice goldens plus the gate golden stay byte-exact through
12+
* their own suites in the same run — nothing here may change them.
13+
*
14+
* Regenerate after an intentional codegen change (inspect the diff first):
15+
* UPDATE_GOLDEN=1 yarn workspace @blend65/test-harness test golden-slice8b
16+
*/
17+
18+
import { describe, expect, it } from "vitest";
19+
import { fileURLToPath } from "node:url";
20+
import { dirname, join } from "node:path";
21+
import { assertGolden } from "./golden.js";
22+
import { emitAsmSlice8b } from "./testing/slice8b.js";
23+
24+
const GOLDEN = join(
25+
dirname(fileURLToPath(import.meta.url)),
26+
"..",
27+
"test",
28+
"golden",
29+
"slice8b.asm.golden",
30+
);
31+
32+
describe("Golden: Slice 8b fixture .asm snapshot", () => {
33+
it("emits the Slice 8b ACME source and matches the committed golden", () => {
34+
const result = emitAsmSlice8b();
35+
expect(result.hasErrors).toBe(false);
36+
expect(result.text).toBeDefined();
37+
assertGolden(result.text!, GOLDEN);
38+
});
39+
40+
it("carries the data-surface landmarks in the emitted source", () => {
41+
const text = emitAsmSlice8b().text!;
42+
43+
// The string const bakes its PETSCII bytes under its data label.
44+
expect(text).toContain("__data_Main_TITLE:");
45+
expect(text).toContain("!byte $48, $45, $4C, $4C, $4F, $20, $43, $36, $34, $21");
46+
47+
// The embedded file bakes its bytes verbatim under its data label.
48+
expect(text).toContain("__data_Main_TABLE:");
49+
expect(text).toContain("!byte $01, $02, $04, $08, $10, $20, $40, $80");
50+
51+
// The banner is a mutable module variable initialised through the init
52+
// stream (expanded string + char fill), not baked const data.
53+
expect(text).toContain("__var_Main_banner");
54+
expect(text).toContain("JSR __init");
55+
56+
// The fixture's main terminates — the terminating shim calls it.
57+
expect(text).toContain("JSR _main");
58+
});
59+
});

0 commit comments

Comments
 (0)