Proposed
The Z80 calling convention in MinZ returns all results in HL, including booleans
(LD HL, 1 for true, LD HL, 0 for false). This creates a critical problem
inside DJNZ iterator loops: HL is also the array pointer, so any CALL that
returns a boolean destroys the pointer.
Current filter(is_big).forEach(console_log) codegen:
LD A, (HL) ; Load element from array
LD C, A ; Element in C
CALL is_big ; Returns HL=1 (true) — POINTER GONE
LD D, L ; D = 1 (filter result)
LD A, D ; A = 1 (not the element!)
OR A
JP Z, filter_continue
CALL console_log ; Outputs 1, not the element
INC HL ; Increments 1, not the pointerThree problems: HL destroyed, element lost, 5 extra instructions for boolean routing.
Key insight: iterator predicates never store their boolean result — it's consumed
exactly once by OpJumpIfNot as a conditional jump. The HL round-trip is pure waste.
Introduce a flag-based boolean return convention scoped to iterator predicates
(filter, takeWhile). Instead of returning HL=0/1, predicate functions return
their result via CPU flags (CY or Z), leaving HL untouched.
| Comparison | Z80 CP natural flag |
Convention |
|---|---|---|
x > N |
CY=0 if A >= N+1 | CP N+1 then JR C, skip |
x >= N |
CY=0 if A >= N | CP N then JR C, skip |
x < N |
CY=1 if A < N | CP N then JR NC, skip |
x <= N |
CY=1 if A <= N | CP N+1 then JR NC, skip |
x == N |
Z=1 if A == N | CP N then JR NZ, skip |
x != N |
Z=0 if A != N | CP N then JR Z, skip |
Simple predicates (inlined — no CALL at all):
loop:
LD A, (HL) ; Load element
CP 68 ; filter: x > 67?
JR C, skip ; CY set = A < 68 = skip
ADD A, 5 ; map: x + 5
CALL print_u8 ; forEach
skip:
INC HL ; HL never touched by filter!
DJNZ loopComplex predicates (CALL with flag return):
loop:
LD A, (HL) ; Load element
LD C, A ; Save element
PUSH HL ; Save pointer (just in case)
CALL is_valid ; Sets CY flag, does NOT touch HL
POP HL ; Restore pointer
JR C, skip ; Direct flag test
LD A, C ; Restore element
CALL process ; Next chain op
skip:
INC HL
DJNZ loopThis is not a global ABI change. It applies only when:
- We're inside a DJNZ iterator loop body
- The operation is
filterortakeWhile - The predicate is either:
- A lambda with a simple comparison body (
|x| x > 5) — inline the comparison - A named function returning bool — generate a flag-return variant
- A lambda with a simple comparison body (
- New opcode
OpCallPredicate(or annotation onOpCall) that signals "result is in flags" - New opcode
OpJumpIfFlagwithFlagSensefield (CY/NC/Z/NZ) replacingOpJumpIfNot - Semantic layer marks filter/takeWhile predicates with
IsPredicateCall: true
For OpCallPredicate:
- Do NOT emit
LD HL, 0/1in the called function — emitRETafterCP - Do NOT emit
storeFromHL(inst.Dest)— result is in flags - Emit
PUSH HL / CALL / POP HLwrapper (pointer preservation)
For inlined lambda predicates:
- Detect
|x| x > Npattern → emitCP N+1+JR C, labeldirectly - No function call, no register allocation, no HL clobber
- Solves Bug 2 (HL clobber) for filter/takeWhile — highest-impact codegen bug
- Solves element-lost problem — A/C still hold the element after the test
- Massive T-state savings for filter chains:
- Current: ~45 T-states per filter (CALL + LD HL,0/1 + LD D,L + LD A,D + OR A + JP Z)
- Inlined: ~8 T-states (CP + JR) — 5.6x speedup per filter
- With CALL: ~28 T-states (PUSH HL + CALL + CP + RET + POP HL + JR C)
- Composable: Multiple
.filter()stages just chain CP + JR instructions - Natural Z80 idiom —
CP+ conditional jump is how Z80 programmers write comparisons
- Two boolean conventions (flag-based for iterators, value-based for everything else)
- Function reuse: a function used both as iterator predicate and normal boolean needs two variants (or a wrapper)
- More complex codegen path for iterator loop bodies
- Does not affect non-iterator code paths
- Error handling ABI (
@errorwith CY flag) already establishes precedent for flag-based returns - Lambda inlining is already partially implemented; this extends it with comparison awareness
Simple but expensive (+21 T/call), doesn't solve element-lost problem, doesn't reduce instruction count. Still needed as fallback for non-predicate calls (map, forEach).
Frees HL for calls but LD A, (IX+0) is 19 T vs 7 T for LD A, (HL).
+12 T per element access. Doesn't solve the boolean waste.
Too invasive — would break all non-iterator boolean usage. Booleans stored in memory/variables need value representation. Iterator-scoped is the right granularity.
- Iterator Implementation Status — Bug 2 analysis
- ADR-0006 — Related register pressure issue
- Z80 CPU manual: CP instruction flag behavior
- MinZ
@errorpattern — existing flag-based ABI precedent (CY flag)