Nanz → Z80 via CY flag. Zero-cost error propagation.
fun safe_div(a: u8, b: u8) -> u8 {
if b == 0 { @error(1) }
return a / b
}
Compiles to:
safe_div: ; a=A, b=C (PFCCO)
LD A, C ; check b
AND A ; b == 0?
JR NZ, .ok ; no → do division
; @error(1):
SCF ; CY = 1 (error!)
RET ; return with CY set, A = 1
.ok:
; ... 8-bit division ...
RET ; A = result, CY = 0 (from division)
Cost: error path = 2 bytes (SCF + RET). Success path = 0 extra bytes.
fun compute(a: u8, b: u8) -> u8 {
var x: u8 = safe_div(a, b)
@propagate // if CY, return immediately
return x + 1
}
Compiles to:
compute:
CALL safe_div
RET C ; ← 1 BYTE! if CY=1, return with error
INC A ; x + 1
RET ; success
RET C is the Z80's native error propagation. One byte. One instruction.
CY flag and A register pass through the return chain untouched.
fun safe_sub(a: u8, b: u8) -> u8 {
if a < b { @error(3) }
return a - b
}
Compiles to:
safe_sub: ; a=A, b=C
CP C ; compare a - b
JR NC, .ok ; a >= b → safe
SCF ; CY = 1 (error)
RET ; A = 3 (error code)
.ok:
SUB C ; A = a - b
RET ; CY = 0 (from SUB with no borrow)
5 instructions total. The Z80's CP + JR NC pattern is the natural bounds check.
fun chain(a: u8, b: u8) -> u8 {
var x: u8 = safe_div(a, b)
@propagate
var y: u8 = safe_add(x, 5)
@propagate
return y
}
Compiles to:
chain:
CALL safe_div
RET C ; propagate div error
LD C, 5 ; second arg for safe_add
CALL safe_add
RET C ; propagate add error
RET ; success: A = result
6 instructions for a 2-call error chain. Each @propagate = 1 byte.
Compare with Rust's ? which generates match + discriminant check + error conversion.
fun array_get(idx: u8) -> u8 {
if idx >= data_len { @error(4) }
var p: ^u8 = &data
return p[idx]
}
Compiles to:
array_get: ; idx=A
LD HL, data_len
LD C, (HL) ; C = data_len
CP C ; idx >= data_len?
JR C, .ok ; idx < len → safe
SCF ; CY = 1 (bounds error)
RET ; A = 4
.ok:
LD HL, data ; base address
LD E, A / LD D, 0 ; offset
ADD HL, DE ; HL = &data[idx]
LD A, (HL) ; A = data[idx]
RET ; CY = 0 (success)
var x: u8 = safe_div(10, 0)
@check // if error: propagate + return
// x is valid here
Compiles to:
CALL safe_div
JR NC, .ok ; success → skip handler
RET ; error → return (propagate)
.ok:
; A = result, use it
| Pattern | Nanz/Z80 | Rust | C |
|---|---|---|---|
| Declare fallible func | 0 bytes | 0 bytes | 0 bytes |
| Return error | 2 bytes (SCF+RET) | ~4 bytes (mov+ret) | varies |
| Return success | 0 extra (OR A implicit) | 0 bytes | 0 bytes |
| Check at call site | 2 bytes (JR C) | ~4 bytes (cmp+jmp) | varies |
| Propagate error | 1 byte (RET C) | ~6 bytes (match+ret) | N/A |
| Chain 3 calls | 3 bytes (3× RET C) | ~18 bytes | manual |
RET C is the key insight. No other architecture has a 1-byte conditional error propagation instruction. The Z80 was designed for this pattern.
| Code | Meaning | Used by |
|---|---|---|
| 0 | No error (reserved) | CY=0 implies no error |
| 1 | Division by zero | safe_div |
| 2 | Overflow | safe_add |
| 3 | Underflow | safe_sub |
| 4 | Bounds error | array_get |
| 5-127 | Application-defined | User functions |
| 128-255 | System errors | CP/M BDOS, ROM |
With enum:
enum MathErr { DivByZero = 1, Overflow = 2, Underflow = 3 }
fun safe_div(a: u8, b: u8) -> u8 {
if b == 0 { @error(MathErr.DivByZero) }
return a / b
}
Same codegen. Enum just provides compile-time names.
| Component | What | LOC |
|---|---|---|
| Parser | @error(N), @check, @propagate in parseExpr |
25 |
| Z80 codegen | SCF/RET, JR NC/RET, RET C as intrinsics | 30 |
| Total | 55 |
No HIR changes. No MIR2 changes. No semantic analysis. No new types. Pure metafunction expansion to inline Z80 asm.
Future (Layer 2): ? in function name = compiler-enforced @check after call.