compiler-ac85 · git:20261027.97fb25f · 2026-10-27 · sha256 6e854ac86dd98173
compiler-ac85 git:20261027.97fb25fA
Immutable. This exact content never changes and is served at /api/v1/blob/6e854ac86dd98173.
---
name: compiler-ac85
description: >
Agent compiler for Intel 8085 (ac85): emit Zilog assembly for
z88dk-z80asm -m8085 from standard C90. Data model, SMALLC ABI, stack
frames, HL as ALU bus, BC/DE parking, 8085 extended ops, C90 shape →
ISA lowering. Use for /compiler-ac85, "agent compiler 8085",
"C compiler guide for 8085", "compile this C to 8085 assembly",
"8085 C lowering". Do not use when invoking zcc, 80cc, or sccz80 as
the compiler, or when editing src/80cc (those are compiler-80cc,
compiler-sccz80, tool-zcc).
---
# C compiler guide — 8085
Compile **ISO C90** to **Zilog** 8085 assembly that `z88dk-z80asm -m8085`
accepts. Load **`cpu-8085`** for the opcode map, flag effects, timings, and
stack-access sequences. This card is the **C** layer: types, ABI, register
assignment, and how C constructs map onto that ISA.
Optimise from the **8085 instruction set**, not from Z80 habit. Extended
ops (`ld de,sp+*`, `ld hl,(de)`, `ld (de),hl`, `sub hl,bc`, `rl de`,
`sra hl`, `jp k` / `jp nk`) are first-class on every 8085. There is no IX,
no `exx`, no native `djnz`, and no 2-byte `jr` (`18` is `rl de`). Assembler
`jr` becomes a 3-byte `jp`. **8085-only listings write `jp`.** Dual-CPU
library source may write `jr` (`cpu-8085`).
**Shape before statements.** Name the C90 shape in a function (walking
byte array, word swap, pre-dec-to-zero, struct cursor, …) and emit the
ISA sequence for that shape. Do not lower a naïve `a[i]` / `i++` form
when the shape is a pointer walk or an induction.
## C data model on 8085
Default **`char` is signed**. Little-endian. Integer promotions go to
16-bit `int`. `short` is 16-bit (same home as `int`). Struct members:
`offset += size` (no 2-byte ABI holes).
**Unsigned bitfields** (classic 8085 emit): pack **LSB-first** in the
declared unit (`unsigned` → 16-bit, `unsigned long` → 32-bit). Adjacent
fields share a unit; a field that does not fit starts a new unit.
Offset-0 width-*n* is a **mask only** (`and (1<<n)-1` on that unit — no
shift). Emit read-modify-write on that unit — not a C bitfield opcode
(none exists). A field that spans a byte boundary is a multi-byte RMW.
`f->rate++` is extract / add / mask / insert, not `inc` of the whole
unit. Consecutive writes to the same object: keep the unit live in HL
or DEHL; store once.
**Preserve-mask on RMW.** The bits you `and` back in must include
**every** bit a later field read tests — even bits this op does not
touch. “Unused” in the current field is not “don’t care”. Re-derive the
preserve mask from the full unit layout each RMW; do not reuse a
narrower mask from the extract step.
**`and` is 8-bit.** `and 0x01FF` assembles as `and 0xFF` (range warning)
and drops the high mask bit. A >8-bit mask is two ANDs
(`ld a,l; and lo; ld l,a; ld a,h; and hi; ld h,a`), never `and 0x1FF`.
**sccz80 unsigned-bitfield parity.** sccz80 packs unsigned bitfields
LSB-first into **byte** units: a field that does not fit the current
byte starts a new byte. Size is the sum of those allocations
(`mode:3+chan:5` = 1B, `rate:12` = 2B, `flag:1+level:7` = 1B,
`spare:4` = 1B → **5B**, not a 16-bit stride-4). Field bit position =
sum of prior widths. Hybrid agent code that must match sccz80 layout:
verify against that codegen, or **skip**. **Signed** bitfields, or any
foreign layout you have not measured: stop.
If the source typedefs 1/2/4-byte aliases (`BYTE`, `WORD`, `DWORD`,
`UINT`, `WCHAR`, …), use those widths. They are not extra register homes.
`QWORD` / `long long` is memory + helpers.
| Type | Bytes | In registers |
|------|------:|----------------|
| `char` | 1 | **L** on return (H dead); park live bytes in **C**, not A |
| `unsigned char` | 1 | same; zero-test is `or a`, not a signed compare |
| `short` / `int` / `enum` / near pointer | 2 | **HL** as ALU result; park in **BC** or **DE** |
| `long` / `unsigned long` | 4 | **DEHL** (DE high, HL low) |
| `long long` | 8 | Memory + `__i64_acc` / helpers; not a register home |
| `float` / `double` / `double_t` | maths mode | IEEE32 = 4, DEHL; MBF32 = 4; genmath is not IEEE64. If the source has `float` and no library is named, **ask** (TIMER work) or use IEEE32 for a toy |
| `_Float16` | 2 | `library-math16` |
Ingest C90 plus common source sugar: `//` comments (emit as `;` — Comments),
implicit-int `main()`,
`for (T i = 0; …)` and mixed declarations (hoist to the enclosing block;
do not treat as C99), `TIMER_*` macros as **labels** at those sites,
`register` as a BC/DE hint only. `Assert` / `assertEqual` bind as ordinary
calls.
**Storage duration follows the C, not the ISA.** BSS/data only for objects
that already have static storage duration in the source:
| Source | Storage |
|--------|---------|
| Block-scope **without** `static` (automatics, including `-DSTATIC` identifiers the source did **not** wrap) | **Stack only.** Never BSS, never a compiler scratch cell |
| The **`static` keyword** (block- or file-scope) | BSS/data as declared |
| File-scope without `static` | Still C static storage duration (external BSS/data). Do not move it to the stack |
| Temps, spills, induction variables, register homes | Stack, BC, DE, or C — **not** BSS |
Do not insert `static` the source did not write. `-DSTATIC` only affects
identifiers the source spelled `STATIC` (that macro becomes `static`). Do
not promote every local. Do not apply it to automatics of a recursive
function the source left automatic.
Refuse: VLA, designated initializers, nested functions. Mixed declarations
and `for`-init declarations: hoist, then emit C90.
`stdint.h` only if the source already uses it. Do not mix newlib `FILE*`
cores into a classic 8085 image (`library-classic`). Do not rewrite
1-based arrays to 0-based.
`sizeof` is a compile-time constant. String concatenation of literals is
one object.
## Aliasing and lifetime — the correctness floor
Correct C outranks every size/cycle win; these rules are the floor.
**An address-taken local is memory everywhere.** Once the C takes `&x` —
passed to a `call`, stored through a pointer, handed out as `&arr[i]` —
code that never names `x` can write it. After that call or indirect
store, **reload** `x` from its slot; do not keep a value obtained earlier
and do not fold a constant belief across the event
(`unsigned a=0; bump(&a); return a+x;` must reload `a`). The escape is
function-wide: an escape in an earlier statement aliases a later one.
A local whose address is never taken **keeps** a constant across a
`call`. Reload only an address-taken or `volatile` object.
**A write through a pointer names no symbol.** `*p = v` (or a `call`
whose callee can store) can change **anything** reachable, so no value
derived from memory survives unchanged across it: `t1 = a + x; *p = …;
t2 = a + x;` recomputes `t2`. `*p = a + x` records nothing about `p`
itself — the store writes what `p` points at. Contrast the two spellings:
`a += v` writes `a`'s slot; `*p = v` writes through `p`.
**`volatile` is a contract.** Every access is performed, in order, and a
volatile store is **never a read-modify-write** (a store must not read the
old slot), never fused, never elided, never reordered against another
access.
**A parameter's slot lives in the caller's frame.** Reloading a **stepped**
pointer parameter after a `call` revives the **original** address — the
stepped value was never written back to that slot. Either keep the stepped
pointer in its own stack slot and reload that, or step in registers only
across call-free stretches.
**A value survives an op only if its register is not in that op's effect
set.** A `call` clobbers **A F BC DE HL**; a library helper preserves only
what its listing documents. Spill first when a home must cross its
clobber — a stale belief miscompiles, it does not deoptimise. A home may
span an intra-function `jp` to a label whose path never reads it; a
`call` / `ret` may read any pair (args / DEHL results), so homes die there.
**A store-back is dead only when no later reader depends on the previous
writer having stored.** Skip a home's store-back only if every later read
of that slot either writes its own value first or never happens; a reader
that only reads ("a channel") still needs the store. When in doubt, store.
## ABI (classic 8085 C)
One convention: **SMALLC** — arguments pushed **left to right**, **caller
cleans**. Fastcall / callee only when the **prototype** says so. Do not
invent a private register-only ABI.
C `foo` → asm `_foo`. `char` arguments occupy **2** stack bytes.
A `signed char` pushed as an `int` or a variadic argument is
**sign-extended** into that word (`(signed char)-3` → `ld hl,-3` /
`push hl`). Zero-extension prints 253. `unsigned char` zero-extends.
A constant byte whose only uses are those pushes has no slot.
Return address is 2 bytes (`params-offset` 2). No saved IX.
```
high addresses
first-pushed argument
…
last-pushed argument
return address ; [sp+0] on entry
callee locals / pushes ; grow down
low (SP)
```
`f(a, b)` pushes `a` then `b`; entry **`[sp+2]=b, [sp+4]=a`**. Arg1 is
at the **higher** address (pushed first). Do not read arg1 at `[sp+2]`.
A `long` argument is pushed **high word first, then low** (low at the
lower address). `f(long a)` → entry **`[sp+2]=lo, [sp+4]=hi`**. The
DEHL **return** home (DE high, HL low) is not the stack order. `g(int x,
long a)` → `[sp+2]=a.lo, [sp+4]=a.hi, [sp+6]=x`. Carry a fetched 32-bit
parameter in **DEHL past the prologue** — do not materialise its slot and
reload it at first use. Spill it only when a second 32-bit value needs
DEHL.
`__stdc`: reverse stacked args. `__z88dk_fastcall`: last scalar already in
HL (or DEHL if 32-bit). `__z88dk_callee`: callee pops (use `pop af` **only**
to discard a word).
| Return | Location |
|--------|----------|
| `char` / width-1 | **L only; H dead.** Caller extends. Do not `ld h,0` in the callee |
| 16-bit | HL |
| 32-bit int / IEEE32 float | DEHL |
| `long long` | hidden pointer / `__i64_acc` |
**Variadic SMALLC** (`printf` and any `__smallc` varargs): `ld a,N` (or
`xor a`) immediately before `call`. N = argument **words** on the stack.
A stuffed `&__i64_acc` is not part of N. Non-variadic calls must not assume
A is live.
**An argument is a terminal consumer** — loaded once, pushed once, read by
the callee. Materialise it at the push site (`ld hl,(slot)` / `ld hl,_g` /
`push hl`); do not load it into a parking pair and then transfer before
pushing. A global read whose only use is that same argument list needs no
frame slot.
**Libc:** do not lower headers. Bind the **preprocessed** prototype to a
library `PUBLIC`: `__z88dk_callee` → `call _foo_callee` (callee pops);
plain SMALLC → `call _foo` (caller pops). C may see `__builtin_memset`;
asm uses `_memset` / `memset`. Never `EXTERN __builtin_memset`. Combined
`/` and `%` of the same operands: one helper (`quot` and `rem`), not two
divides.
If the source defines `ld_16` / `ld_32` / `st_16` / `st_32` as
`*(WORD *)` / `*(DWORD *)`, emit native little-endian word traffic
(`ld hl,(de)`), not a portable byte-shift body. Unaligned LE loads are
legal on this CPU.
Two image shapes, same ABI:
| Shape | Use |
|-------|-----|
| Relocatable module | `SECTION` + `PUBLIC _name`; CRT supplies `_main` / start |
| Complete image | Tiny `start` plus your functions; still SMALLC |
```asm
SECTION code_compiler
PUBLIC _foo
EXTERN _memset
_foo:
ret
```
Zilog, lowercase, four-space indent. No Intel names; the Intel→Zilog map is **`cpu-8085`**.
Blank line after a PC break: **Listing layout**. Prefer **saccharine** (below).
## Registers — what the ISA actually gives C
The 8085 is not a bank of interchangeable GPRs. Treat it as a **bus** plus
two parking pairs.
```text
Bus (scratch, not homes)
A 8-bit ALU, ld a,(de) / ld (de),a / ld a,(bc)
HL 16-bit ALU, (hl), addresses, add hl,rp, sub hl,bc
Parking (C locals that must survive the bus)
C 8-bit local — never A (ALU) and never L inside a live HL
BC count, stride, 8-bit (bc) cursor
DE word cursor, 2nd operand, stack cursor, 32-bit high half
stack everything else
```
**Cursor law (this ISA):**
| What you walk | Home | Why |
|---------------|------|-----|
| Word array / `int *` / pointer-to-struct | **DE** | only `ld hl,(de)` / `ld (de),hl` |
| Byte array, any-register load/store | **HL** | `ld r,(hl)` / `ld (hl),r` |
| Byte array, A-only | DE or BC | `ld a,(de)` / `ld (de),a` / `(bc)` |
| Two byte strings at once | **HL** and **DE** | `ld a,(de)` vs `cp (hl)` / `ld (hl),a`. Park one cursor on the stack if HL is needed as ALU |
| Two word arrays at once | **DE** + **stack** | No second `(de)` bus, no IX. Load one array through DE; the other cursor lives on the stack (`ex (sp),hl` or a slot) |
| Stride or trip count | **BC** | `add hl,bc` keeps the cursor in HL |
There is no `ld hl,(bc)` and no `ld (bc),hl`. Do not put a word cursor in
BC. `ex de,hl` is a native 4-cycle swap. Use it when it wins. There is no
alternate bank: a second live 32-bit value lives on the **stack**.
**After any op that uses HL as the bus, the previous HL is gone** unless
it was parked in BC, DE, or a stack slot.
### Assignment heuristic
1. Hottest 16-bit values (loop index, walking pointer) in **BC** or **DE**
across a window with no `call`. Word cursor prefers DE; stride prefers BC.
2. Keep a stack cursor in **DE** and use `ld hl,(de)` / `ld (de),hl` /
`ld a,(de)` rather than re-materialising SP every access.
3. One live `long` or IEEE32: **DEHL**. A second: stack slots.
Two walking **word** pointers: DE plus a stack slot — not IY/`exx`.
4. 8-bit live values in **C** (or B if C is taken). A is the 8-bit ALU.
Park an 8-bit accumulator in C while a float kernel uses DEHL/A.
5. `volatile`: memory at every sequence point — no BC/DE home that skips
a reload or store.
6. Do not park a 16-bit value in **AF**. `pop af` forces F bit 3 to 0
(`$FFFF` → `$FFF7`); never use AF for a return address.
Hot-function homes go in the function header **Uses** line (Comments).
**Hard residency (call-free loops).** `lo` / `hi` / induction / seed /
cursor homes live in **BC or DE** for the whole loop. Reloading those
homes from `ld de,sp+*` every iteration is a miscompile when the loop
has **no** `call`. Four `ld de,sp+*` per binary-search probe = failed
residency. Park to the stack **only** across a `call`, or when a third
live word has no free pair.
A park created **before** a long loop changes SP for the whole kernel.
Every later `ld de,sp+N` uses the **post-park** SP. **Every exit
including `ret` must pop that park** before the return address is
consumed. After the loop, re-derive homes from the restored SP — do not
keep a pre-loop offset.
Any `ld de,sp+*` / `ld hl,(de)` / `ex de,hl` **destroys** the pair it
names. Park a live home that shares that pair first. Load the value you
must keep **last**. After `ex de,hl`, the old HL is gone.
**Offset invariant:** `slot now at sp+(N + 2*active_pushes)`. After
every `push`, every later `ld de,sp+N` increases by 2. After every
`pop`, it decreases by 2. Recompute for the **current** depth. A `pop`
inside a swap/RMW block invalidates the next store’s offset — re-derive
that store from the post-pop SP. `ld de,sp+*` resolves SP at execution: a second
`ld de,sp+N` of the same object after a `push`/`pop` uses the new N, not
the first. A 4-byte slot is two words (low at N, high at N+2 at that
depth); if a `push` sits between the two reads, the high-word offset is
N+4. Open-code both halves. Do not reuse the first N. Worked sequence:
**Sequences**.
Any `call` to a library helper (`l_mult`, `l_div`, `l_mult_ulong`, …)
**clobbers BC** (and A F DE HL). Never hold a live home in BC across a
`call`; keep the counter on the frame.
## Frames — stack only
No frame pointer. Automatics live on the stack. BSS/data only where C
gives static storage duration (`static` keyword, or file-scope). Do not
invent BSS for temps, and do not rewrite a `static`/file-scope object to
the stack “for 8085 style.”
Prefer **`cpu-8085`** sequences:
| Need | Sequence | Why it wins |
|------|----------|-------------|
| Pointer to a slot, HL free | `ld de,sp+n` | 2 bytes, 10 cycles, no flags |
| Word load/store | `ld hl,(de)` / `ld (de),hl` | Native (de) word traffic |
| Byte load/store | `ld a,(de)` / `ld (de),a` | Only legal `(de)` byte forms |
| HL = SP+n, DE free | `ld de,sp+n` / `ex de,hl` | Cheaper than `ld hl,nn` / `add hl,sp` when n is 0…255 |
| Offset > 255 | `ld hl,nn` / `add hl,sp` | `*` on `ld de,sp+*` / `ld de,hl+*` is **unsigned 8-bit** |
If DE is a live home, `push de` around the SP op and add 2 to the offset.
`&local` is **rematerialisable**: a pure function of the slot offset and
the current SP depth. Never give the address itself a slot or a BSS cell —
rebuild it with `ld de,sp+n` / `ex de,hl` when needed (Offset invariant).
Function headers (purpose, inputs with this map, outputs, registers): **Comments**.
```asm
; void f(int a, int b) /* SMALLC: push a, then b; caller cleans */
; entry: [sp+0]=ret, [sp+2]=b, [sp+4]=a
; locals: push, or ld hl,-n / add hl,sp / ld sp,hl
; exit: restore SP; ret
```
`[sp+n]` is reachable only through a register pointer
(`ld de,sp+n` / `ld hl,(de)` / `ld (de),a`). `ld a,(sp+n)` /
`ld (sp+n),a` is a syntax error on `-m8085`.
**`ld hl,N; add hl,sp; ld sp,hl` clobbers HL.** That idiom pops args or
unwinds the frame and **destroys the SMALLC return home**. Park the
return first (`ld bc,hl` or `ex de,hl`), unwind, then restore
`ld hl,bc` before `ret`. `add hl,sp` writes V,C only — a Z already in F
survives, so `ld a,l; or a` **before** the unwind can still `jp z` after
it.
**After every `call` + caller-clean, re-derive every `ld de,sp+N` from
the restored SP.** Call 2 of a recursive pair does not reuse call 1’s
offsets. Callee-cleaned helpers (`memset_callee`, `memcpy_callee`,
`l_f32_add` / `sub` / `mul` / `div`) pop the return address and then
the stacked operand. Enter them with `call`. A tail `jp` returns into
that operand. Push exactly the declared word count; do not also pop.
## Comments
Carry the C into the listing. The `.asm` must still show intent.
**C comments.** Every `/* … */` and `//` appears as `;` at the matching site. Do not drop them when hoisting mixed declarations or rewriting a shape. Keep the C wording. If that wording is too thin to identify the following instructions, **expand** the comment with the C the block implements (the statement, the named shape, or a one-line restatement of the function).
**Function header (required).** Immediately before each `_name:` (and before `start` in a complete image), even when the C function has no comment:
```asm
; _foo — walk p for n bytes and return the count
; C: int foo(int n, char *p)
; Inputs: n at [sp+4], p at [sp+2]; [sp+0]=ret; SMALLC, caller cleans
; Outputs: HL = count
; Uses: DE = p; BC = n; A HL scratch
```
- **Purpose** — from the C comment on the function if present; otherwise what the function computes.
- **C:** the C90 signature (hoisted types, not C99 `for`-init).
- **Inputs:** each parameter: type, name, stack slot or incoming register (`__z88dk_fastcall` last scalar in HL / DEHL). Include the SMALLC map.
- **Outputs:** ABI return home (`L` / `HL` / `DEHL` / void). Out-parameters: address + what is stored.
- **Uses:** live homes (which C object in BC, DE, C, DEHL, which stack slots). A and HL as bus are scratch unless a home. If the body `call`s, **A F BC DE HL** die across that call.
- **Slots (required on any function that `call`s, and on every recursive function):** a depth diagram of `[sp+N]` after **every** `push` / `pop` / `call` / arg-clean. After `call`, only reload from those documented slots. Deep recursion (N-queens / `qsort_rec`) without a slot map is a miscompile.
**Attached C on non-obvious blocks.** After a shape rewrite, one `;` line with the C that block implements (`while (p < end)`, `acc = (acc << 1) ^ K`, `y[i] = a*x[i] + y[i]`). Do not narrate every opcode.
Zilog `;` only. Same-line or above the block. Do not invent commentary that is not the required header, a carried C comment, or an expansion of the C being implemented.
## Listing layout
One **blank line** after any instruction that does not fall through: `jp`, `jp cc` (`z`/`nz`/`c`/`nc`/`k`/`nk`/`m`/`p`/`pe`/`po`), `jp (hl)`, `ret`, `ret cc`. Assembler `jr` is a 3-byte `jp` here — same blank. **New 8085-only emit writes `jp`, not `jr`.**
**Not** after `call` / `call cc`: the program counter returns. Do not insert a blank between `call` and the next instruction.
**One label scheme per function.** Either all `.local` or all unique
global names — **never both**. Mixing `.loop:` with bare `bit1:` makes
`jp loop` bind a different symbol than `jp p,loop` (extra CRC bit,
wrong checksum).
**Existing libsrc:** match that file’s whitespace (`cpu-8085`). Do not restyle a math32/math16 core to this layout while editing it.
## Saccharine
**Saccharine** is zero-cost z80asm sugar: the listing is one mnemonic; the bytes and flags are exactly the two (or one) chip ops you would have written. **Write it.** Full family: **`cpu-8085`** §10. Normal `-m8085`; strict / `-no-synth` rejects it.
| Write | Instead of |
|-------|------------|
| `ld bc,de` / `ld de,hl` / `ld hl,bc` | `ld b,d` / `ld c,e` (pairs **bc/de/hl** only) |
| `ld a,(hl+)` / `ld b,(hl+)` / `ld (hl+),a` | `ld a,(hl)` / `inc hl` |
| `ld a,(de+)` / `ld (de+),a` / `ld (de-),a` | `ld a,(de)` / `inc de` |
```asm
ld a,(de+) ; *p++
ld (hl+),a ; *q++ = a
ld bc,hl ; park
```
**Not saccharine:** `ld (de),l`, `ld b,(de+)`, `ld (de+),n` (`ex de,hl`); `sub hl,de` (helper `call`). Chip `(de)` byte traffic is still **A only**.
```asm
sub hl,bc
jp c,less
; unsigned >=
...
ret
less:
...
call l_mult_ulong
ld a,e ; next insn; no blank after call
...
ret
```
## Helpers (library vs inline)
When a C op is not a few native/extended insns, **consider** the 8085 catalogs below (integer helpers, integer math, IEEE32, half float). **Hot path: inline** a short body instead of calling (`*10` shift-add, `sub hl,bc` compare, `ld de,sp+*`, `rl de`, `sra hl`, `<<8` byte move). One-shot or bulky work (general mul/div, 32-bit mul, float) **`call`** the catalog name and `EXTERN` it. Every `call` clobbers **A F BC DE HL**. **The cost of a helper is the `call`, not the operation:** a `call` forces the result through a slot, kills every resident home, and ends call-free residency. In a hot loop a longer inline body that keeps values resident beats a shorter helper behind a `call`; one-shot work is the opposite — `call` it.
| Catalog | Use |
|---------|-----|
| `libsrc/l/sccz80/8085.lst` | 16/32-bit sccz80 runtime (`l_mult`, `l_div`, `l_div_u`, `l_mult_ulong`, `l_long_*`). Includes `8080.lst` for the rest |
| `libsrc/l/util/8085.lst` | 32-bit shifts `l_lsl_dehl` / `l_asr_dehl`; small ASCII `l_small_utoa` / `l_small_atoul` / `l_small_htoul` / `l_small_otoul` |
| `libsrc/math/float/math32/` (`asm/8085/`) | IEEE32 cores: `f32_fsadd`, `f32_fsmul`, `f32_fsdiv` (**restoring**). Public `m32_fsinv_fastcall` is the same restoring bridge. NR reciprocal sources in `asm/<cpu>/hist/` are not linked. `invsqrt` stays NR. Policy: **`library-math32`** |
| `libsrc/math/float/math16/` (`asm/8085/`) | Half: `asm_f16_add` / `mul` / `div` (restoring). Public `asm_f16_inv` is the restoring bridge; `hist/` NR is not linked. `invsqrt` stays NR. Policy: **`library-math16`** |
**Open-code; do not call** on 8085: `l_eq`/`l_ne`/`l_lt`/`l_le`/`l_gt`/`l_ge`/`l_ult`/`l_ule`/`l_ugt`/`l_uge` (`sub hl,bc` + K/C/Z); `l_rlde` (native `rl de`); `l_gint*sp` (`ld de,sp+*` / `ld hl,(de)`); `l_pint_*` (`ld (de),hl`); 16-bit `l_asr` / `l_asr_u` when the count is 1 or a small constant (`sra hl` / logical `>>`). Arithmetic `long >> k` with **k > 1** is `l_asr_dehl`, not an unrolled Z80 slide (Primitives). A one-bit shift, and a bit-serial loop, stay inline. Do not bind `l_setix` / `l_setiy` / f48.
**4-byte / far loads — open-code.** Two `ld de,sp+*` / `ld hl,(de)` at
the current depth (Offset invariant). Walking a DWORD cursor in DE:
two `ld hl,(de)` + `inc de`×2. **Last resort:** `call l_glong` (HL =
pointer, DEHL out) or `l_glong2sp` (same fetch, push onto the stack)
when the pointer is already in HL and a helper is cheaper than parking
for two `ld hl,(de)`. `EXTERN` from `libsrc/l/sccz80/8080.lst` (pulled in by
`8085.lst`).
16×16→16 is `l_mult`. 16×16→32 is **`l_mult_ulong`** (DEHL = DE×HL), not `l_mult`. Combined `/` and `%`: one `l_div` / `l_div_u` / `l_long_div*`.
## C → 8085 primitives
| C | Emit |
|---|------|
| Automatic (no `static`) | Stack slot; cursor in DE. **Not** BSS |
| `static` keyword / file-scope | BSS/data as declared. Do not invent extra BSS |
| `if (x)` / `while (x)` (16) | Test that **writes Z** (`ld a,h` / `or l`) — leftover K is not a truth test |
| Zero-test a **BC/DE home** | `if (!n)` with n in BC | Test **in place**: `ld a,b` / `or c` (or `ld a,d` / `or e`). Do not ferry the pair through a scratch register or `dec` first |
| `if (p)` pointer / `== NULL` | `ld a,h` / `or l` |
| `if (c)` byte | `ld a,c` (or `(hl)`) / `or a` |
| signed `c < 0` / `c >= 0` (8-bit) | `ld a,c` / `rla` / `jp c` (or `or a` / `jp m`). Do **not** `cp 0` — C after `cp 0` is never set, so `jp c` never takes |
| `a && b` / `a \|\| b` | Short-circuit; skip the second arm. Side-effecting operands **must not** run when skipped. Each arm writes Z. Cheap int test before a float/call. **As a value** (`int x = a && b`): each leg jumps to a join that writes 0 or 1 (boolean-of-equality). A leftover Z is only the branch form |
| `x + y` (16) | `add hl,de` or `add hl,bc` |
| word already in BC or DE `+ K` | `ld hl,K` / `add hl,bc` (or `de`). Do not copy the pair to HL and then add a second pair. `word - K` while BC is live: `ld de,-K` / `add hl,de` |
| `x == -1` (word) | `ld a,h` / `and l` / `inc a` / `jp z` |
| `x++ == K` / `x++ != K` | Compare the value **after** the step with `K+1` (`x--` with `K-1`). Do not keep the old value in BC across the store. Only `==` and `!=` |
| `x - y` / `==` / `!=` (16) | y in BC; `sub hl,bc`; **Z** for `==` / `!=`. **Memory operands:** subtrahend first into BC, minuend second into HL (`cpu-8085` §4). Two `ld hl,(mem)` as A then B computes `B − A` |
| boolean-of-equality `==` → 0/1 | **branch to set 1 / set 0**. `sub hl,bc` / `jp z,eq1` / `ld hl,0` / `jp done` / `eq1: ld hl,1`. Do **not** `ld h,0; ld l,a` on a leftover A |
| signed `<` / `>=` (16) | `sub hl,bc` then **immediately** `jp k` / `jp nk` |
| signed `<=` / `>` (16) | same `sub hl,bc`: `<=` is K **or** Z; `>` is NK and NZ |
| unsigned `<` / `>=` (16) | `sub hl,bc` then **immediately** `jp c` / `jp nc` — **C**, not K |
| unsigned `<=` / `>` (16) | same: `<=` is C **or** Z; `>` is NC and NZ |
| unsigned `HL >= N` (N const, **BC live**) | `ld a,l` / `sub lo` / `ld a,h` / `sbc hi` / `jp nc`. N<256: `sbc 0`. Do **not** steal BC for `ld bc,N` / `sub hl,bc`. Do **not** emit sccz80 `rla/ccf/rra/sbc 128` |
| `i < n` via `n−i` | HL=n, BC=i; `sub hl,bc`; **C means n<i** (borrow). Continue only on no-borrow AND NZ: `jp c,done; jp z,done; jp body`. Do not `jp c,body` |
| `*p` / `*p =` (word) | pointer in DE: `ld hl,(de)` / `ld (de),hl` |
| `*p` / `*p =` (byte) | pointer in HL: `ld a,(hl)` / `ld (hl),a` (or `(de)` if A-only) |
| `p->field` | Pointer in HL: `ld de,hl+off` (off unsigned 8-bit) then `(de)`. Pointer already in DE: `ld hl,off` / `add hl,de` (or `ex de,hl` first). Off > 255: `ld hl,nn` / `add hl,de` |
| `p++` (byte / word ptr) | `inc de` / `inc de` twice (or `ld hl,2` / `add hl,de`). **`inc`/`dec` on a pair are ±1 byte, not ±element** |
| `*p++ = byte` | **`ld (de+),a`** (saccharine). Word: `ld (de),hl` then `inc de` twice |
| `v[i]++` byte RMW, value unused | `&v[i]` in **HL**: **`inc (hl)`** — one op, no load/store back. Only when the C never reads the updated value. `inc (hl)` / `dec (hl)` write **Z**; a following `== 0` uses that Z — do not reload and `or a`. If the old value is used (`tmp = v[i]++`), read it first (`ld a,(hl)` / then the RMW). Word RMW keeps the address in DE: `ld hl,(de)` / `inc hl` / `ld (de),hl` |
| `s << 1` (16) | `add hl,hl` |
| `s << n` n const 2…7 | repeated `add hl,hl` |
| `u << 8` / `u >> 8` unsigned 16 | **byte move**: `ld h,l` / `ld l,0`; `ld l,h` / `ld h,0`. Not eight `add hl,hl` |
| `u >> 12` unsigned 16 | `>>8` then two `sra hl` + clear H7, or `ld a,h` / four logical `>>` |
| signed `s >> 1` | `sra hl` (**Z unchanged**) |
| unsigned `u >> 1` | `sra hl`; force H bit 7 clear (**Z unchanged**) |
| 8-bit logical `>> n` | **`rra`×n is not a logical shift** (`rra` is rotate-through-carry). n≤4: `rlca`×n then `and ((1<<(8-n))-1)`; or `and a` **before each** `rra`. 16-bit logical: `sra hl` with H=0, or `sra hl` + `ld a,$7f; and h` |
| variable `u << n` / `u >> n` (n live, 1…15) | n in B: `add hl,hl` or logical `>>`; `dec b` / `jp nz`. n==0 is a no-op |
| variable `byte << n` / `>> n` | n in B: `add a,a` or `and a` / `rra` (logical — `and a` each step). Signed byte `>>`: sign-extend to HL first, then `sra hl`, then take L |
| `(v << n) \| (v >> (16-n))` | 16-bit rotate: park v; `<< n`; OR with logical `>> (16-n)` |
| `long << 1` | `add hl,hl` (low, C=carry out) then `rl de` (high through that C). The low-half carry **must** enter the high half. Do not `add hl,hl` on each half independently. When that long is the **current top of the stack** and nothing sits above it: `pop hl` / `pop de` / `add hl,hl` / `rl e` / `rl d` / `push de` / `push hl`, then recount every later `ld de,sp+N` |
| `2*item-1` on `long` | `long << 1` as above, then `-1` with **borrow** into the high word (`ld a,l; or a` / `dec hl` is not a 32-bit −1). Low `dec hl`; if it underflows, `dec de` |
| `unsigned long >> 1` | `or a` / `rra` through A across D,E,H,L — not `sra hl` on both halves. C after the last `rra` is the old bit 0. **Each** `rra` is preceded by a clear-C on the first byte only (`xor a` / `or a` on D); later bytes consume the previous C. A **bit-serial loop** keeps the long in **DE** (high) and **BC** (low) and shifts those bytes in place. The trip count is in **A** (`dec a` / `jp nz`) — B holds data, so it is not the counter. Do not ferry the long through HL on every bit |
| arithmetic `long >> k`, k > 1, constant | `call l_asr_dehl` (DEHL in, A = k). Not an unrolled `sra`/`rr` body (Helpers) |
| `int * int` | shift-add for small constants; else `call l_mult` (HL = DE×HL) |
| `x * 2` / `* 3` / `* 5` / `* 8` / `* 10` / `* 25` | `*8`=`add hl,hl`×3; `*10`=`*8+*2`; `*25`=`*16+*8+*1`. Do not `l_mult` these in a hot loop |
| `(unsigned long)a * (unsigned long)b` of two 16-bit values | **16×16→32** (`l_mult_ulong`), then keep DEHL. `l_mult` (16×16→16) is a miscompile |
| signed `/` `%` | `call l_div` unless power-of-two |
| unsigned `/` `%` | `call l_div_u` unless power-of-two |
| both `/` and `%` of same ops | **one** helper; take quot and rem |
| `x % 2` | `ld a,l` / `and 1` — not `l_div` |
| `x % (1<<n)` unsigned | mask `(1<<n)-1`. **`and` is 8-bit**: 512 → `ld a,h; and 0x01; ld h,a` (L unchanged), never `and 0x01FF` |
| `x / (1<<n)` unsigned | n logical `>>` (sra + clear high bits, or drop bytes) |
| `x / 2` (non-neg) | `sra hl` |
| signed `v / (1<<n)` | C rounds **toward zero**. `sra hl` rounds toward −∞. If v<0, add `(1<<n)-1` then `sra` n times. `%` = v − quot×(1<<n) |
| Stack word whose consumer is **BC** | DE dead: `ld de,sp+N` / `ld hl,(de)` / `ld bc,hl`. HL and BC then hold the same word — do not reload it into HL. Not a byte walk into BC |
| Word store, pointer already in **HL**, value is the top stack word | `pop de` / `ex de,hl` / `ld (de),hl`. `pop hl` destroys the pointer. There is no `ld (hl),de` |
| `long` AND / OR / XOR with a zero-extended byte | Do not build a 4-byte zero. AND masks L, then clears H and DE. OR and XOR change only L. Read a slotted byte with one `(hl)`. ADD/SUB of a **sign-filled** or **left-shifted** operand does not fold — the carry chain needs the full value |
| LE `*(WORD *)p` | pointer in DE: `ld hl,(de)` — 8085 is little-endian, unaligned is legal |
| LE `*(DWORD *)p` | Pointer in DE: `ld hl,(de)` (low) / park / `inc de`×2 / `ld hl,(de)` (high) / `ex de,hl` / restore low. Pointer in HL: `ex de,hl` then the same. Stack slot: two `ld de,sp+N` at the **current** depth (Offset invariant). `l_glong` last resort (Helpers). Not a byte-shift chain |
| `*p++ = (BYTE)val; val >>= 8` | `ld (de),a` with A=L; `inc de`; then **logical** byte slide L←H←E←D, D=0 (unsigned). Not `sra hl` |
| Range `c >= 'A' && c <= 'Z'` | `ld a,c` / `cp 'A'` / `jp c` / `cp 'Z'+1` / `jp nc` — 8-bit, unsigned |
| `float /` | A literal constant divisor is `* (float32)(1/literal)`; the left operand of `/` stays the left factor. A runtime divisor stays restoring `l_f32_div` (`library-math32`), not inv×mul and not `hist/` inv. The reciprocal multiply rounds differently from restoring `/`, and it is not a reassociation |
| `1.0 / x` | restoring `/` (`m32_fsdiv` or public `m32_fsinv_fastcall` — same bridge). Do not call `asm/<cpu>/hist/f32_fsinv.asm` |
| Counted 16-bit loop | `cpu-8085` §5. Flat `ld bc,N-1` / `jp nk`, or nested `dec bc` / `inc b` / `inc c`. Both are valid. Do not mix them. **Not** C `n--` → `jp nk` |
| Counted loop that **may run 0 times** | Test the counter **before** decrement (`ld a,h; or l; jp z,done` then body then `dec hl`). `dec; jp z` runs n−1 times and underflows when n=0 |
| 8-bit counted loop | `dec b` / `jp nz` (opcode `10` is `sra hl`). A down-count whose seed is 1..255, tested for zero **after** its own decrement, is this form — not a 16-bit home |
| C `n == 0` / post-dec to zero | A test that **writes Z** |
| `switch` | compare chain (tiny dense enum) or address table via HL; no `jp (ix)` |
| `goto` | `jp` (cost assembler `jr` as 3-byte `jp`) |
| `?:` | compare then two tails; signed test uses K, unsigned uses C |
| `if (d<0) d=-d` / `abs` 16-bit | `sub hl,bc` then `jp k` / `cpl` L and H / `inc hl`. **`neg` is Z80 (A only) — not 8085** |
| `return` 16-bit | HL; never `pop af` for the return address |
| `return` `char` | L; H dead. Constant `1…255`: `ld l,N`. Zero: `xor a` / `ld l,a` |
| `return` `long` | DEHL |
| `printf` / varargs | `ld a,N` then `call` |
**K is two different flags in two different ops.** After `dec bc` (and
other 16-bit `dec`), K sets on the rollover from 0 to `$FFFF`, not when
the result is 0. After
`sub hl,bc`, **K encodes signed ordering** (`jp k` = signed HL < BC)
and **C encodes unsigned borrow** (`jp c` = unsigned HL < BC). Pick by
the C type, not by habit. `lo<=hi` with a possibly negative `hi` is
**signed** (K or Z). Use K/C **immediately**. Do not mix them. Do not
copy Z80 `dec bc; jp nz`. A signed compare whose **both** sides stay in
0…32767 for the whole live range may use **C**. A rising `int i = 0; i++`
is not that proof (it can pass 32767). A down-count that starts
non-negative and decrements only when non-zero may. Polarities: **Sequences**.
**`(de)` stores:** chip ops are `ld (de),a` and `ld (de),hl` (loads
`ld a,(de)` / `ld hl,(de)`). There is no `ld (de),l` / `ld (de),n` as a
chip op. **Do** write saccharine `ld a,(de+)` / `ld (de-),a`. **Do not**
write `ld (de),l` (paid `ex de,hl`). Same rule: **`cpu-8085`**.
## Recognise these C90 shapes
Rewrite the C to the ISA shape, then allocate. Storage still follows the
C (`static` / file-scope vs automatic) — a hot loop does not justify BSS.
### Arrays, strides, induction
| Shape | C | 8085 |
|-------|---|------|
| Walking **byte** array | `a[k]`; `k += stride` | Cursor **HL** = `&a[k]`, stride **BC**. `ld a,(hl)` / `ld (hl),r` / `add hl,bc`. Bound vs `&a[N]`: unsigned `sub hl,bc` / `jp c` |
| `!byte` as 0/1 | `n -= !a[k]` | `ld a,(hl)` / `or a` / skip `dec` if non-zero. Do not sign-extend `unsigned char` |
| Square induction | `i_sq += i+i+1` | Keep `i_sq` live; `add hl,bc` twice + `inc hl`. Do not recompute `i*i` |
| Walking **word** array | `dst[i] = src[i]` linear | Cursor **DE**, `ld hl,(de)` / `ld (de),hl` / `inc de` twice. Index `i` is not a home if the walk is linear |
| Overlapping word shift-down | `while (i<r) a[i]=a[i+1]` | Two cursors. Source starts one word above dest. Copy **forward** so each read happens before a later trip overwrites that word. Count in BC, tested before the body. A saved head is stored at `&a[r]` from `r`, not from the walk cursor. One cursor that writes two bytes below its read must finish the trip **+2** from where it started. Pair `inc`/`dec` are ±1 byte; a net +1 still matches the first two trips while every high byte is 0 |
| `a[i]` random word | `a[k-i]` | `k-i` in HL; `add hl,hl`; `ld de,a` / `add hl,de` / `ex de,hl` / `ld hl,(de)` |
| Word swap | `t=a[i]; a[i]=a[j]; a[j]=t` | Two addresses: one in DE, the other parked (BC as a **word** addr is wrong; park the second on the stack or rebuild). `ld hl,(de)` / `ex (sp),hl` / `ld (de),hl` |
| 2D `int a[R][C]` | `A[i][j]` | `i*C+j` then `add hl,hl`. `*50` = `*2 + *16 + *32` (`add hl,hl` then `add hl,hl`×4 + orig) |
| `p + k` / `s[k]` | byte pointer + const | `ld de,hl+off` if off is u8, else `add hl,de` |
| `&arr[i]` then fields | `b = &arr[i]; b->x` | Form `b` **once** in DE; `ld de,hl+off` per field. Do not redo `i*sizeof` for every member |
| `p = &g; p->field` | base is one symbol, not stepped | Absolute `ld hl,(_g+off)`. A later `p++` blocks the fold |
| `*out++ = byte` | packed output | **`ld (de+),a`** |
| 1-based array | `E[1]…E[n]` | Keep the unused `[0]` hole; do not rewrite indexes to 0-based |
| Power-of-two window | `buf[off % 512]` / `off / 512` | `% 512` is `ld a,h; and 0x01; ld h,a` (never `and 0x01FF`). `/ 512` is `>> 9`. Keep the window base in DE |
| Multiply by power-of-two field | `(x-2) * (1<<k) + base` | Shift `x-2` by `k`, then add. Not `l_long_mult` when the scale is 1…128 and a power of two |
| Histogram RMW | `bins[(x >> k) & mask]++` | Index **once**: logical `>>`, `and mask`, `add hl,hl`, add base. **DE** = cell address (home for the whole RMW). **HL** = live unit (`ld hl,(de)` / `inc hl` / `ld (de),hl`). Do not recompute the address for the store. Seed/index stay in BC/DE if the loop is call-free; `l_mult` for the LCG kills those homes |
| Dual-array saxpy / dot | `y[i]=a*x[i]+y[i]`; `s+=x[i]*y[i]` | Two word cursors: **DE** + stack. `short` stride = 2. Mul clobbers parking — reload both cursors. Cast to `unsigned` before mul if the C does |
| 5-point stencil | `out[i][j]=in[i][j]+N+S+E+W` | W compile-time: `i*W` is shift-add (40=`*32+*8`). Keep a **row** word cursor; N/S = ±`W` words (`ld bc,±2*W` / `add hl,bc`); E/W = ±2 bytes. No `l_mult` on the hot path. Double-buffer: swap two pointers (DE + stack) |
| Flattened 2D | `((int *)m)[i]` | Linear word walk after the cast. Param `T m[R][C]` decays to `T (*)[C]`; row stride `C*sizeof(T)` |
| Open-address probe | `idx = (idx + 1) & (N-1)` | N = `1<<k`: `inc hl` / `ld a,l` / `and` low mask / if N>256 also mask H. Do not `l_div` |
| Coprime wrap | `idx += STEP; if (idx >= N) idx -= N` | Unsigned `>=` (**C**); then `sub hl,bc` with N in BC. Not `% N` |
| djb2 / `h*33+c` | `h = (h<<5)+h+k[i]` | Park h in DE; `add hl,hl`×5; `add hl,de`; add the byte (`ld d,0` / `ld e,c` / `add hl,de`). Mask `& 0xffff` is free on 16-bit |
| 16-bit LCG | `seed = seed * A + C` | `l_mult` or shift-add for A; `add hl,bc`; wrap is free. File-scope seed is BSS |
### Loops and conditions
| Shape | C | 8085 |
|-------|---|------|
| `for (i=lo; i<hi; ++i)` unsigned | `i < SIZE` | Condition is **unsigned** `<` → **C**, not K. Do not use a signed `jp k` |
| Nested `for` inner reinit | `for (r) for (k) for (i)` | Each inner counter **and** bound is re-derived at the start of that inner body. A spent inner BC/DE is not the next inner start. Outer `k` that indexes `regs[k]` must be rebuilt every outer step |
| `for (k=n; k>0; k-=s)` | subtract const | Keep `k` in BC/DE; subtract; test **Z** or unsigned `>` via C |
| `for (d=min; d<=max; d+=2)` | step 2 | `inc de` / `inc de` (or `inc l` twice if H is known 0 and no wrap) |
| `for (; n>0; n-=W)` | chunked remainder | `n` in HL; `ld bc,W` / `sub hl,bc`; last iter `if (todo<W) m=todo` |
| `for (i=0; i<n && f; ++i)` | cheap then expensive | Test `i<n` first (Z or C); only then the float/call. Hoist invariants (`limit*limit`) |
| `while (1)` + `break` | infinite + exit | `jp` to header; `jp` out. No `djnz` |
| `if (--i == 0)` | pre-dec to zero | 16-bit `dec` does **not** write Z. `dec hl` then `ld a,h` / `or l` / `jp z`. **Not** `jp k` (K is −1) |
| Assign in predicate | `while (!((k = a[0])==0))` | Load once into the home; test Z; keep `k` |
| `a ? b : -b` / `cond ? x : y` | signed select | `% 2` is `and 1`; negate is `sub hl,hl` / `sub hl,bc` (or `cpl` / `inc` on 8-bit) |
| `max(a,b)` | `a>b?a:b` | signed: `sub hl,bc` / `jp k` take BC |
| `switch (enum)` | small dense 0…n | `ld a,l` / `cp` chain. Larger: table of addresses, `jp (hl)` |
| `goto L` | labelled loop | `jp L`. Counted `j+=1; if (j<n) goto L` is an 8-bit `inc c` / `ld a,c` / `cp n` / `jp c` (unsigned) |
| `do { … } while (n)` fill | counted body | Count in B/C; `dec b` / `jp nz`. Body often a byte store through HL |
| `do { … } while (0)` | statement macro | Not a loop — emit the body once |
| `if (1)` / `if (0)` / constant `?:` | compile-time condition | Emit the taken arm only — no branch, no park for the untaken side |
| `while (p < end)` pointers | byte/word walk vs sentinel | **Unsigned** `<` on the addresses: **C**. End in BC, p in HL/DE |
| Nested run-length | inner `while` equal bytes, cap 255 | Outer in-cursor **HL**, out-cursor **DE**, run in **C**. Inner: `ld a,(hl)` / `cp v` / `inc hl` / `inc c` / stop on Z of `inc c` (wrap 255→0) or mismatch |
| Binary search | `mid=(lo+hi)>>1`; `lo=mid+1` / `hi=mid-1` | **Mandatory homes (call-free):** `lo` **BC**, `hi` **DE** (or reverse), `mid` in HL once. Reload lo/hi from the stack **only after `call`**. Non-neg mid: `add hl,de` / `sra hl`. `lo<=hi` is signed K **or** Z (`hi` may be −1). Indexed load: `add hl,hl` + table base → DE / `ld hl,(de)` — park `hi` first if that load needs DE. Masked `tab[mid]&m`: AND per byte in A (`and` is 8-bit). Four `ld de,sp+*` per probe = failed residency |
| Insertion shift-up | `while (j>=0 && v[j]>key) v[j+1]=v[j]` | Short-circuit: signed `j<0` (S/K) **before** the load. Word copy: DE at `&v[j]`, `ld hl,(de)` / `inc de`×2 / `ld (de),hl`, then step DE back 4 |
| `while (*p)` / `p-s` | strlen | HL at s; `xor a` / `cp (hl)` / `jp z` done / **`inc hl` only after a non-NUL** (or `cp (hl+)` only when you consume the byte). Do not `inc` on the NUL. Length = HL − start (`ex de,hl` / start in BC / `sub hl,bc`) |
| `while (*a && *a==*b)` | strcmp | DE and HL; `ld a,(de)` / `cp (hl)` / `jp nz`; `or a` / `jp z` equal; then **`inc de` / `inc hl`** (or `ld a,(de+)` only on the continue path). Return `(unsigned char)*a - (unsigned char)*b` in HL |
| `while ((*d++=*s++))` | strcpy | DE=src, HL=dst; `ld a,(de+)` / `ld (hl+),a` / `or a` / `jp nz` |
| Range ladder | `if (c<32)… else if (c<48)…` | Keep the byte in **A**. Successive `cp` / `jp nc` — do not reload. Lexer class: ws / alpha / digit / other as 8-bit unsigned ranges (`'_'` is a `cp`) |
| Clamp / saturate | `if (v>hi) v=hi; if (v<lo) v=lo` | Signed: `sub hl,bc` then K; assign the bound. Same variable on both arms — one home |
| Side-effect `&&` / `\|\|` | `a < b && probe(c) > d` | Jump over `probe` when `a<b` is false. Flattening to arithmetic is a miscompile |
| `&&` / `\|\|` as a value | `int x = a && b` | Each leg jumps to a 0/1 join (boolean-of-equality). Do not leave Z as the result |
| `x++ == K` | post-inc equality | Compare the stepped value with `K+1` (`x--` with `K-1`). Only `==` / `!=` |
| `x == -1` | word all-ones | `ld a,h` / `and l` / `inc a` |
### Widths, bits, 32-bit
| Shape | C | 8085 |
|-------|---|------|
| `(k+1)>>1` | non-neg k | `inc hl` / `sra hl` |
| `byte <<= 1` then `\|= 1` | pack bits | `add a,a` / `or 1`. Park the acc in **C** across calls |
| **Variable** `byte <<= n` | live count | `n` in B: `add a,a` / `dec b` / `jp nz`. `w%8` is `and 7` when w≥0. Load the byte into A only after `n` is in B (the count expression uses A). Count 0 is a no-op (do not shift by an uninitialised imm) |
| Variable **promoted** byte shift | `(int)uc << k` (16-bit source) | Narrow to a **byte loop** only when the shift is the byte-width consumer. `<<` narrows freely (low output byte depends on the low input byte); `>>` narrows only with a proven sign- or zero-extension from a byte |
| `t ^= t >> c` / `t << c` | 32-bit xorshift | One long in DEHL; `>>` is **logical** (`unsigned long`) via `rra` through A or `l_long_asr_u`. `t & 0x7fff` → HL low 15, D=0 |
| LCG 32 | `(last*A+C)%M` | `static` state stays BSS; `l_long_mult` / `l_long_div_u` (or combined). Calls clobber parking |
| `uint32_t d += uint16 * K` | widen then madd | Zero-extend word to DEHL (`ld de,0`); const× by shift-add or one `l_long_mult`; then **one** divmod if both quot and rem are used |
| `i*2-1` | odd from index | `add hl,hl` / `dec hl` |
| `(uint16_t)long` / `(uint32_t)word` | narrow / widen | Truncate = HL; zero-extend = `ld de,0` |
| 32-bit add/sub | two words | Low: `add hl,bc` (sets C). High: `adc` **through A** on D/E. No `adc hl,de` chip op. Multi-word borrow: `sbc a` through A, not `sub hl,bc` on the high half. A long ± a value that fits in 16 bits: `add hl,bc` then `jp nc` / `inc de` (borrow: `jp c` / `dec de`). A full `adc` of a zero high word is the longer form |
| `g += h` long globals, stored back | result only feeds the store | Two pointers, four bytes through A. Two LHLX/SHLX plus `add hl,bc` is larger and slower (no `adc hl`: 29 B / ~142 T vs 25 B / ~120 T). Keep word loads when the sum must stay in DEHL |
| `12L * N` | long product | Width is **long**. 16-bit `l_mult` is a miscompile if the product can exceed 65535. Hot `*2` / `*10` / `*25` stay shift-add. A one-shot long times an awkward constant (not `2^a` or `2^a±1`) is the 9-byte `l_long_mult` — expanding `*6` / `*10` / `*24` on a long is larger than that call |
| Packed 12-bit LE | `i += i/2`; odd nibble | `i/2` is `sra hl`. `i & 1` is `ld a,l` / `and 1`. Pack: `<< 4` is `add a,a`×4; 12-bit mask is `ld a,h` / `and 0x0F`. Two window bytes may sit on a **sector/page boundary** — do not assume one `ld hl,(de)` spans them |
| WORD array in a window | `*(WORD *)(win + i*2 % W)` | `i*2` is `add hl,hl`; then `% W` if W is `1<<n`; `ld hl,(de)` |
| DWORD array + mask | `*(DWORD *)p & 0x0FFFFFFF` | LE DWORD load; `ld a,d` / `and 0x0F` / `ld d,a` |
| Nibble insert | `(*p & 0x0F) \| (val << 4)` | Load byte, mask in A, `or`; store `ld (hl),a`. Adjacent byte is a second access |
| Time / bitfield or | `y<<25 \| m<<21 \| d<<16` | Build in DEHL with `add hl,hl`/`rl de`. Do not call `l_long_mult` for `1<<k` |
| UTF-8 / UTF-16 unit | `uc << 6 \| (tb & 0x3F)` | 32-bit shift-or; continuation test `tb & 0xC0` is `and 0xC0` / `cp 0x80` |
| Allocated-bit in `size_t` | `x & (1<<(sizeof(size_t)*8-1))` | MSB of a 16-bit size is **H bit 7**. Test `ld a,h` / `or a` / `jp m` (or `rla` / `jp c`). **No** Z80 `bit 7,h` |
| Overflow guard | `a > SIZE_MAX - b` | Unsigned 16-bit: `ld hl,MAX` / `sub hl,bc` / `jp c` then compare `a` |
| Bit-serial feedback, MSB out | `acc ^= *p++;` then 8× `(acc & msb) ? (acc<<1)^K : (acc<<1)` | **CRC-16 map (fixed):** acc **HL**, byte cursor **DE**, end **BC**. Do not push crc per byte. XOR poly `0x1021` as `ld a,l; xor 0x21; ld l,a` / `ld a,h; xor 0x10; ld h,a`. Quality bar vs 80cc-sp: **156 B** for `_crc16_ccitt`. 8-bit: acc in **A** (park in C across the pointer inc), `add a,a` / `jp nc` / `xor K`. **Unroll all 8 bit-steps** (no `dec b` inner loop). Pointer vs `end`: unsigned `sub hl,bc` with end in BC |
| Bit-serial feedback, LSB out | `acc ^= *p++;` then 8× `(acc & 1) ? (acc>>1)^K : (acc>>1)` | Acc in **DEHL**. XOR the byte into L. Logical `>>1` (Sequences). If C (old bit 0) XOR poly **`0xEDB88320` in D,E,H,L order**: `xor 0xED` → D, `0xB8` → E, `0x83` → H, `0x20` → L. Final `^ ~0UL` is `cpl` on D, E, H, L |
| 16-bit rotate | `(a<<5)\|(a>>11)` | Park in BC; `add hl,hl`×5; OR with logical `B>>3` into L. General: `(v<<n)\|(v>>(16-n))` |
| Boolean mix | `(b&c)\|((~b)&d)` | 16-bit: `cpl` both bytes of b; AND/OR per byte in A. Four live words: extras on the stack |
| Q8.8 mul | `(u16*u16)>>8` → u16 | 16×16→**32** then **byte slide** `>>8` (L←H←E←D, D=0). Not `l_mult` (16×16→16), not four `l_mult` partials — `l_mult_ulong` is in `8085.lst`. Not `sra hl` |
| Sign-extend `char`→`int` | `int x = sc;` | `ld a,l` / `add a,a` / `sbc a,a` / `ld h,a` |
| Zero-extend `unsigned char`→`int` | `int x = uc;` | `ld h,0`. Promotes to **signed** `int` (the sum can go negative) |
| Widen into the pair the consumer wants | `(int)u8 + y` | `ld l,a` / `ld h,0` **immediately before** `add hl,de`. Do **not** widen into DE then copy to HL (`ld e,a; ld d,0; ld hl,de` is a pipeline defect). A zero-extended byte is widened where it will be used |
| `(uint8_t)(w << n)` narrow | truncate of a left shift | **Compute only the bytes the result keeps.** A left shift's low output byte depends only on the low input byte (`ld a,l` / `add a,a`×n on the byte); a full-word shift into a truncation is waste. A **right** shift narrows only when the source is provably zero- or sign-extended from a byte — the shifted-out high bits feed the low byte. A byte op on a widened byte is `ld a,l` while that word is still in HL. Reload the original byte only after the word was spilled. `x & 0xff` / `x \| 0` / `x ^ 0` on a byte is a copy |
| Sign-extend `int`→`long` | `(long)i` / `(unsigned long)(long)i` | HL as-is; `ld a,h` / `add a,a` / `sbc a,a` / `ld d,a` / `ld e,a` |
| Zero-extend via `(unsigned)` | `(unsigned long)(unsigned)i` | HL truncated; `ld de,0` — not sign-extend |
| Narrowing store | `(signed char)(v>>k)` / `(unsigned char)` | **One** byte `ld (de),a` or `ld (hl),a`. Must not write the neighbour |
| Unsigned bitfield RMW | `r->rate++`; `r->flag=1` | LSB-first in the unit. Extract = shift + mask; insert = `and ~mask` / `or`. Keep the unit live across a burst of field writes |
| `~b` 16-bit | `~b` in a mix | `ld a,l` / `cpl` / `ld l,a`; same for H. 8-bit: `cpl` A |
### Structs, pointers, calls
| Shape | C | 8085 |
|-------|---|------|
| Packed struct | `{char c; double p}` | `p` at offset `sizeof(c)` (no ABI hole). `ld de,hl+1` then float/word load |
| Large-struct field | offset can exceed 255 | Offset += size (no holes). `ld hl,nn` / `add hl,de`, not `ld de,hl+*`. Cursor in DE; `ld hl,(de)` for WORD, two loads for DWORD |
| Recursive tree | `node->left==NULL` | SMALLC push / `call` / caller pop. Word at DE, `ld hl,(de)` / `or`. `2*item` is `add hl,hl`. Locals on **stack** even if other TUs used `-DSTATIC` |
| Intrusive circular list | `n->prev=p; n->next=p->next; p->next->prev=n; p->next=n` | `next`/`prev` are **word** cursors in DE. Four stores. Self-end: `end->next = end`. Do not linear-search an array |
| Nested chase | `p->next->value` | Load `next` into DE first, then field. One live struct pointer at a time |
| Sorted insert | `item <= key` on unsigned ticks | **Unsigned** compare. 16-bit: `sub hl,bc` **C**/Z. 32-bit: high then low through A. Equality sentinel first if the C tests it |
| Width-1 count / status | `uint8_t n++;` / `int8_t ok` | **8-bit**: `inc c` / `dec c`, not 16-bit `inc hl`. Return **L only; H dead.** Signed 8-bit compare in A (`cp` / `jp m` / `or a`) |
| Owner back-pointer | `void *owner` | Stored pointer (2 bytes). Load DE; do not synthesise `offsetof` unless the source does |
| `* const` pointer param | `T * const p` | Pointer value is const, not the object. Still a stack/DE home |
| `volatile` members | list/queue/lock fields | Reload after every sequence point and after `call`. Parking in BC/DE is a miscompile across a yield or ISR |
| Array of lists | `lists[prio]` | Small unsigned index: `add hl,hl` (pointer scale) + base. Empty walk is 8-bit `dec c` / `jp nz` if the count is 8-bit |
| Circular byte/item queue | `w += size`; wrap at `tail` | Byte cursor HL or DE; add item size. Unsigned `>= tail` via **C**. Copy payload with `_memcpy` or a counted `ld a,(de)` / `ld (hl),a` / `inc` loop |
| Address-ordered free list | split/coalesce blocks | Word pointers. Compare addresses **unsigned** (C). `size_t` is 16-bit |
| `malloc` / `free` / `memcpy` / `strlen` / `strcpy` / `strcmp` / `qsort` | hosted libc **call** | Bind the preprocessed `PUBLIC`. If the source **writes** the loop (`while (*p)`), emit the byte-walk — do not replace it with a library call. `strlen` of a string literal is its length; do not call |
| Function pointer | `(*fp)(args)` / `qsort(..., cmp)` | SMALLC push args left-to-right, then a **fake return address**, then `jp (hl)`. Callee `ret` pops only that address; SP is then at the last-pushed arg — `pop bc` once per arg word. Recompute frame offsets after the trampoline. Worked listing: **Sequences**. No IX vtable |
| Pointer table dispatch | `ops[k & 7](a, b)` | `k&7`; `add hl,hl` (pointer scale); base + DE; `ld hl,(de)`; push args; `jp (hl)` / trampoline. Same stride as `int ops[]`. Fetch `ld de,table` **clobbers DE** — park a register-homed index first |
| Opaque callback loop | `acc = f(acc, i)` | **Every** home dies across `call`. Acc, i, n, f on the stack; reload each iter |
| Dense `switch` VM | `op = prog[pc++]; switch(op)` | Table of addresses in `rodata_compiler` (`defw op_…`) + `jp (hl)`, explicit default for out-of-range. Operand stack `stk[++sp]`: word cursor DE, `sp` in BC (`inc bc` / `dec bc` with **byte** scale — `inc` is +1). The fetch (`ld de,table; add hl,de; ld a,(hl)`) **clobbers DE**: park the register-homed index around the fetch, or keep it on the frame. State live roles at the top of the routine; never reuse BC as both stack-index and ALU |
| Stationary `p->a` / `p->b` | many fields, p does not move | **No IX.** Park p in BC (`ex de,hl` / `ld bc,hl`). Each field: `ld hl,bc` / `ld de,hl+off` / `ld hl,(de)`. `push de` / `pop de` only when BC is already a live home. **Max 1 reload of p per iteration.** Do not reload p from BSS per field. A `ld de,sp+*` for the counter must not steal DE=p — park p first |
| Array of structs walk | `s += a[i].x+a[i].y; a[i].z = s` | Element cursor **DE**, stride `sizeof` in **BC**. `struct pt { int x,y,z; }` → **stride 6**: fields +0,+2,+4 only; `ld hl,6` / `add hl,de` once per element. Checksum fold `& 0xffff` is free on 16-bit add. Copy-paste: **Sequences** |
| Singly-linked chase | `while (p) { s+=p->val; p=p->next; }` | p in DE. Load `val` first, load `next` last, `or` for NULL. Write pass: `ld hl,(de)` / `inc hl` / `ld (de),hl` then chase |
| Deep recursion | N-queens / qsort_rec | Automatics **stack-only** (col, row, lo, hi, i). File-scope `board[]` is BSS. Header **Slots** after every call (Comments). `if (d<0) d=-d`: `sub hl,bc` then `jp k` / `cpl; cpl; inc hl` with HL=d only. After call 1’s arg-clean, re-read call 2’s args from the **restored** frame. Worked `_safe` / `_place`: **Sequences**. A stub `_place` that does not recurse is not this shape — do not claim ticks |
| Frame-resident array | `int loc[16]; … loc[k]` | Allocate on SP (`ld hl,-n` / `add hl,sp` / `ld sp,hl`). Base `ld de,sp+*`. Dynamic k: `add hl,hl` / `add hl,de` / `ex de,hl` / `ld hl,(de)`. **No** `add hl,ix` |
| Address-taken local | `f(&v)` / `f(&t[i])` | Real stack slot. Reload v and the array after the call. Cannot keep v in BC across `call` |
| Private local struct, fixed offsets | `struct pt a; a.x=…; a.y=…` | A local struct whose address never escapes can be **field-split**: each scalar field in its own home (BC / DE / C / stack), no struct address emitted. Refuse if a field address is taken, the object is a parameter or `volatile`, a field is dynamically indexed, or a union is written via one member and read via another |
| `T m[R][C]` parameter | decays to `T (*)[C]` | Row is pointer + `i * C * sizeof(T)`. Inner j walks a row cursor |
| Task / `void (*)(void *)` | push param, call through HL | SMALLC: push `void *`, `jp (hl)`. A yield **clobbers** A F BC DE HL; reload from TCB/stack |
| `*(const T *)p` | comparator load | Arg pointer in DE: `ld hl,(de)` |
| Out-parameter | `void f(..., T *out)` | Address in DE; store DEHL with `ld (de),hl` / high half via `inc de`×2 or a second cursor |
| Array parameter `E[]` | decays to pointer | Pointer, not a copy. 1-based stores through that pointer stay 1-based |
| `enum` / small `Boolean` | `int` on the stack | 2 bytes stacked; values often fit in A for `cp` |
| `register` | storage-class hint | BC/DE only. Ignore if it parks a live value in HL/A |
| Path / class tests | `IsUpper` / `IsDigit` / `c=='/'` | 8-bit `cp` ranges in A |
| Critical section / context save | `di` around a sequence; save regs to a TCB | 8085: `di`/`ei`. **`ei` enables after the next instruction**, so `ei` / `ret` returns without an interrupt. Save IFF with **`rim`/`sim`**, not `ld a,i`. Save **AF BC DE HL** and SP via `ld (de),hl` into the TCB. **No** `exx`, IX, IY, `reti`. If existing port asm uses those, keep the C shapes and replace the asm |
### Float kernels
One live float in **DEHL**; every other **automatic** float on the stack.
File-scope / `static` floats stay in BSS/data. Never park an automatic
float in BSS to “free” DEHL.
`sin` / `cos` / `atan` / `log` / `exp` / `sqrt` / `pow` / `fsdiv` clobber
**A F BC DE HL**. Reload cursors after the call. Bind `*_fastcall` when
the prototype says so. An IEEE32 whose only reader is that helper is
`push de` / `push hl`. Do not `ld bc,hl` in front of the push.
Enter `l_f32_add` / `sub` / `mul` / `div` with `call` (Frames).
**The outer operator keeps its C operands.** `k * (a + b)` and
`k - (a + b)` push **k**, then evaluate the parenthesized right into
DEHL. Pushing the parenthesis and loading `k` into DEHL emits the
swapped operator. One commutative mul may still exchange its factors
so a power of two is the scanned operand (next paragraph). Do not
exchange `-` or `/`.
**8085 math32 mul scans the DEHL factor.** `m32_fsmul` unpacks the
incoming DEHL (C right) first and parks it as `y`. The stacked left
is unpacked second and becomes `x` in LDE, the multiplicand.
`m32_mulu_32h_24x24` skips a 24×8 pass when a mantissa byte of `y`
is zero, and leading-zero-scans the bytes it does not skip. A power
of two (`1`, `2`, `0.5`, and the same values negative) has zero
mantissa bytes: leave it in DEHL, even when C wrote it on the left.
Exchanging those two operands does not change a finite product.
Do not reassociate a chain (Optimise). Two runtime factors, neither
known to have a zero byte: push the C left operand.
| Shape | C | 8085 |
|-------|---|------|
| Integer then convert | `1.0/(n*(n+1)/2+…)` | Integer triangular first. `n*(n+1)` is even. If that product fits in signed 16, `sra hl`. If it sets bit 15 and still fits in unsigned 16 (consecutive positives, `n` = 181…255), a signed `/2` (`sra hl`, or the toward-zero bias then `sra`) returns a negative quotient — use unsigned `>> 1` (Primitives). Or keep a running `t += step; ++step` from a seed whose product fits in signed 16 (square induction) and never form the bit-15 product. Then convert; restoring `/` (`m32_fsdiv` or the public inv bridge). Do not call `hist/f32_fsinv.asm` |
| `1.0/sqrt(r)` | unless the source writes `invsqrt` | `sqrt` then restoring `/` |
| Horner / iterate | several live floats | Hoist invariants. Spill the second complex pair; do not pretend `exx` exists |
| `pow(2, integer)` assigned to integer | `1 << n` | `ld hl,1` / `add hl,hl` `n` times (or 32-bit if width is long). Do not call `pow` for that |
| Compare `<=` float | library compare | Flags after the call are the helper’s, not K from `sub hl,bc` |
| `fabs(a-b) < eps` | tolerance | Library sub; if negative, negate; compare to eps. Do not `==` on IEEE32 vs MBF32 |
| `static T v[N]` inside a function | local with `static` | BSS/data. Automatic `T v[N]` is stack |
### Sequences (use these, not Z80 `(ix+d)` / `exx`)
**Unsigned long `>> 1`**, C ← old bit 0:
```asm
xor a ; C = 0 (logical). `or a` on D also works if D is tested
ld a,d
rra
ld d,a
ld a,e
rra
ld e,a
ld a,h
rra
ld h,a
ld a,l
rra
ld l,a ; C = old L bit 0
```
**Shift then conditional XOR** (one bit, after mixing a data byte): `add a,a` or `add hl,hl`, then `jp nc` skip, else XOR a constant through A.
**Q8.8** `(uint16)((uint32)a * b >> 8)`: `l_mult_ulong` into DEHL, then byte slide L←H, H←E, E←D, D←0; result in HL. Hot path may inline that mul.
**Sign-extend L to HL:**
```asm
ld a,l
add a,a
sbc a,a
ld h,a
```
**Stationary struct** (p in DE, field at +4). Park p in BC; `ld de,hl+*` leaves HL = p:
```asm
ex de,hl ; HL = p
ld bc,hl ; park p
ld de,hl+4 ; DE = p+4, HL still p
ld hl,(de) ; field
```
Repeated fields: p stays in BC; each access is `ld hl,bc` / `ld de,hl+*` /
`ld hl,(de)`. If BC is already a live home, `push de` around the field
access and add 2 to later `ld de,sp+N`.
**Dense switch** (opcode in A, 0…n): `add a,a` / `ld h,0` / `ld l,a` / add table base / `ld e,(hl+)` / `ld d,(hl)` / `ex de,hl` / `jp (hl)`. Tiny n: `cp` chain.
**8-bit logical `>> n` — `rra`×n is not it.** `rra` is rotate-through-carry.
`c=0x91` then four bare `rra` yields `0x29`, not `0x09`.
```asm
; n≤4: rotate left into the low bits, then mask
rlca
rlca
rlca
rlca
and 0x0F ; (1<<(8-n))-1 for n=4
; or clear C before every rra
and a
rra
and a
rra
; 16-bit logical >>n: H=0 then sra hl; or sra hl + ld a,$7f / and h
```
**`sub hl,bc` polarity.** C sets when **HL < BC** (unsigned borrow). K sets
when signed HL < BC. For `i < n` tested as `n−i` (HL=n, BC=i):
```asm
sub hl,bc ; C => n < i; Z => n == i
jp c,done
jp z,done
; body: n−i strictly positive
```
`jp c,body` here is inverted (runs when `n<i`). Check the C comparison,
not the comment.
**Memory-operand 16-bit subtract** — subtrahend first, minuend second
(`cpu-8085` §4). Two `ld hl,(mem)` as A then B computes `B − A`.
**Read a 4-byte stack slot** — open-code both words; recount after every
`push`/`pop` (Offset invariant). Slot at `sp+8`/`sp+10` (low/high):
```asm
ld de,sp+8
ld hl,(de) ; low
push hl ; SP − 2; high is now at sp+12
ld de,sp+12 ; not sp+10 — the push added 2
ld hl,(de) ; high
ex de,hl ; DE = high
pop hl ; HL = low → DEHL
```
Reusing the first N after the `push` reads the low word twice. Last
resort when the pointer is already in HL: `call l_glong` / `l_glong2sp`
(Helpers).
**Boolean-of-equality** (`Assert` / `int eq = (a==b)`):
```asm
sub hl,bc
jp z,eq1
ld hl,0
jp eqdone
eq1:
ld hl,1
eqdone:
```
Do not `ld h,0; ld l,a` on a leftover A.
**CRC-16 CCITT (poly `0x1021`, MSB out) — fixed map.** Acc **HL**, cursor
**DE**, end **BC**. Do not push crc per byte. Size bar **156 B**.
```asm
; crc ^= *data++ << 8; then 8× (acc<<1) ^ 0x1021 if old msb
ld a,(de+)
xor h
ld h,a
ld a,h
or a
jp p,nox
add hl,hl
ld a,l
xor 0x21
ld l,a
ld a,h
xor 0x10
ld h,a
jp nxt
nox:
add hl,hl
nxt:
```
End test: park crc, `ld hl,end; sub hl,bc` with cursor moved to BC, or
keep cursor in DE and end in BC via `ex` — **unsigned** C/Z. One label
scheme for the 8 bits.
**CRC-32 poly `0xEDB88320` xor order** (after logical `>>1`, C = old bit 0):
```asm
jp nc,nox
ld a,d
xor 0xED
ld d,a
ld a,e
xor 0xB8
ld e,a
ld a,h
xor 0x83
ld h,a
ld a,l
xor 0x20
ld l,a
nox:
```
**Array-of-struct stride 6** (`struct pt { int x, y, z; }`; `s += p->x +
p->y + p->z; p->z = s`):
```asm
; DE = &arr[i], HL = s (s parked if DE is needed as cursor)
ld hl,(de) ; x at +0
inc de
inc de
ld bc,hl
ld hl,(de) ; y at +2
add hl,bc
inc de
inc de
ld bc,hl
ld hl,(de) ; z at +4
add hl,bc ; s (& 0xffff free)
ld (de),hl ; z = s
inc de
inc de ; DE += 6 from start of z, i.e. next element
```
Prefer `ld hl,6 / add hl,de` from a parked copy of the element base if
the field stores need DE restored.
**Indirect SMALLC call** `(*fp)(a, b)`:
```asm
ld hl,(a_slot)
push hl ; arg1 (left first)
ld hl,(b_slot)
push hl ; arg2
ld hl,fp_ret
push hl ; fake return — callee ret pops only this
ld hl,(fp)
jp (hl)
fp_ret:
pop bc ; discard arg2
pop bc ; discard arg1
; HL = return. Recompute every ld de,sp+N from this SP.
```
**Counted loop that must accept n=0:**
```asm
loop:
ld a,h
or l
jp z,done ; test before decrement
; body
dec hl
jp loop
```
`dec hl; jp z,done` runs n−1 times and wraps when n=0.
### N-queens — slot map after every call
`safe(int col, int row)` / `place(int col)`. Automatics stack-only.
`board[]` / `solutions` BSS. One global label scheme. After `call`,
reload **only** from the documented slots.
```asm
; _safe — can a queen sit at (col, row)?
; C: int safe(int col, int row)
; Inputs: [sp+0]=ret, [sp+2]=row, [sp+4]=col
; Outputs: HL = 0/1
; Uses: BC = i; HL/DE bus; board[] BSS
; Slots: entry [0]=ret [2]=row [4]=col
; after push i: [0]=i [2]=ret [4]=row [6]=col
_safe:
ld bc,0 ; i = 0
sloop:
ld de,sp+4
ld hl,(de) ; col
sub hl,bc ; col - i (unsigned bound; i>=0)
jp z,sok
jp c,sok
push bc ; park i. Slots: [0]=i [2]=ret [4]=row [6]=col
ld hl,bc
add hl,hl
ld de,_board
add hl,de
ex de,hl
ld hl,(de) ; ri
ld bc,hl
ld de,sp+4
ld hl,(de) ; row
sub hl,bc ; row - ri
jp z,sno_i
jp k,sneg
jp spos
sneg:
ld a,l
cpl
ld l,a
ld a,h
cpl
ld h,a
inc hl ; d = -(row-ri)
spos:
push hl ; park d. Slots: [0]=d [2]=i [4]=ret [6]=row [8]=col
ld de,sp+8
ld hl,(de) ; col
push hl ; park col. Slots: [0]=col [2]=d [4]=i …
ld de,sp+4
ld hl,(de) ; i
ld bc,hl
pop hl ; col. Slots: [0]=d [2]=i …
sub hl,bc ; col - i
ld bc,hl
ld de,sp+0
ld hl,(de) ; d
sub hl,bc ; d - (col-i); Z => diagonal
jp z,sno_d
pop hl ; d
pop hl ; i → HL
inc hl
ld bc,hl
jp sloop
sno_i:
pop bc ; i
ld hl,0
ret
sno_d:
pop hl ; d
pop hl ; i
ld hl,0
ret
sok:
ld hl,1
ret
; _place — recurse columns
; C: void place(int col)
; Inputs: [sp+0]=ret, [sp+2]=col
; Outputs: void
; Uses: row on stack; board[] / solutions BSS
; Slots: after `push row`: [0]=row [2]=ret [4]=col
; after call _safe + clean 4: same as after push row
; after call _place + clean 2: same as after push row
_place:
ld hl,0
push hl ; row. Slots: [0]=row [2]=ret [4]=col
ld de,sp+4
ld hl,(de) ; col
ld bc,8
sub hl,bc
jp z,pinc
prow:
ld de,sp+0
ld hl,(de) ; row
ld a,l
cp 8
jp z,pret
ld de,sp+4
ld hl,(de) ; col
push hl ; arg1. Slots: [0]=col [2]=row [4]=ret [6]=col
ld de,sp+2
ld hl,(de) ; row
push hl ; arg2. Slots: [0]=row [2]=col [4]=row [6]=ret [8]=col
call _safe
ld a,l
or a ; Z = (safe==0); add hl,sp keeps Z (V,C only)
ld hl,4
add hl,sp
ld sp,hl ; Slots restored: [0]=row [2]=ret [4]=col
jp z,pnext
ld de,sp+4
ld hl,(de) ; col — re-derived after call
add hl,hl
ld de,_board
add hl,de
ex de,hl ; DE = &board[col]
push de ; park addr. Slots: [0]=addr [2]=row [4]=ret [6]=col
ld de,sp+2
ld hl,(de) ; row
pop de ; &board[col]
ld (de),hl ; board[col] = row
ld de,sp+4
ld hl,(de) ; col
inc hl
push hl ; arg1 col+1. Slots: [0]=col+1 [2]=row [4]=ret [6]=col
call _place
ld hl,2
add hl,sp
ld sp,hl ; Slots restored: [0]=row [2]=ret [4]=col
pnext:
ld de,sp+0
ld hl,(de)
inc hl
ld (de),hl
jp prow
pinc:
ld hl,(_solutions)
inc hl
ld (_solutions),hl
pop hl ; row — park cleanup before ret
ret
pret:
pop hl ; row
ret
```
Every exit pops `row` before `ret`. After `call _place`, `col` is
re-read from `[sp+4]` of the **restored** frame, not from a pre-call
offset.
## Optimise from the ISA
Correct C90 + this ABI first. Then **cycles**, then **bytes**. Timings and
flag side effects: **`cpu-8085`**.
1. **Name the shape**, then emit it (pointer walk, dual cursors, bit-serial
shift-xor, Q8.8, bitfield RMW, combined divmod, variable shift). Index
arithmetic is the fallback.
2. **Use the extra ops.** `ld de,sp+n` beats `ld hl,nn`/`add hl,sp` for
unsigned 8-bit offsets. `sub hl,bc` is the 16-bit subtract, signed
compare (K), and unsigned compare (C). `rl de` with `add hl,hl` is the
32-bit shift. `sra hl` is signed `>>`.
3. **HL is the ALU, not a local.** Every deref and most arithmetic destroy
it. Park first. Word cursor in DE; stride in BC.
4. **Calls kill parking.** Helpers and unknown C functions clobber
**A F BC DE HL**. Reload from slots. Do not call `l_gint*sp` —
open-code `ld de,sp+*` (4-byte: two loads at the current depth;
Offset invariant). `l_glong` / `l_glong2sp` last resort (Helpers).
5. **Inline vs helper.** Hot path: inline a short body. Otherwise `call`
from **Helpers**. Measure with **`tool-ticks`** (`-m8085` before the
binary) when unsure.
6. **Do not emit Z80-only forms.** No IX, `(ix+d)`, `exx`, native `djnz`,
native 2-byte `jr`, `sbc hl,de`. 8085-only listings **write `jp`**
(assembler `jr` is still a 3-byte `jp`, cond 10/7). Opcode `10` is
`sra hl`; opcode `18` is `rl de`.
7. **No copt** on this output (hand-written asm). Drop copy-backs yourself
(`ld a,e` then `ld e,a`; prefer `ex de,hl` / `ld bc,hl` over push/pop
transfers).
8. **Saccharine.** Write `ld bc,de`, `ld a,(hl+)`, `ld (de+),a` — same
bytes as the two-insn form (`cpu-8085` §10). Do not write paid
synthetics (`ld (de),l`, `call __z80asm__*`).
9. **Listing check.** Assemble `-m8085 -l`. If the hot path shows
`call __z80asm__*`, rewrite to a native, extended, or saccharine form.
10. **Float op order is ABI when matching another compiler bit-for-bit.**
IEEE32 `+ - * /` are not associative; printed digits flip if the
expression tree is reassociated. Reorder only integer and
exact-constant transforms. Keep named float kernels as real
functions when the reference compiler does. Exchanging the two
operands of one commutative mul, so the scanned factor is in DEHL,
is not a reassociation (Float kernels).
### Do not emit
| Form | Why |
|------|-----|
| IX, IY, `(ix+d)` | No index registers |
| `exx`, AF′/BC′/DE′/HL′ | No alternate bank |
| Native `djnz` | Opcode `10` = `sra hl` |
| Native 2-byte `jr` | Opcode `18` = `rl de` |
| `sbc hl,de` / `sbc hl,bc` as a chip op | Not on 8085; may become a helper `call` |
| `sub hl,de` as a chip op | `sub hl,bc` is **HL−BC only** |
| `adc hl,de` as a chip op | 32-bit carry is `adc a` through the high bytes |
| `ld (de),r` for r ≠ A, or `ld (de),n` as a chip op | Illegal. Saccharine `ld (de+),a` is fine; `ld (de),l` is a paid `ex` |
| `ld bc,sp+*` / `ld hl,sp+*` as a chip op | Only `ld de,sp+*` (sp-relative) is 8085; `ld bc,sp+n` is not |
| `ld bc,(de)` / `ld (de),bc` | Illegal. Word through DE is `ld hl,(de)` / `ld (de),hl` |
| `srl` / `srl r` / `srl hl` | Z80 CB. Logical `>>` is `sra hl` + clear H7, or `and a`/`rra` through A |
| `bit n,r` / `bit n,(hl)` | Z80 CB. Test with `and` mask, `or a` / `jp m`, or `rla` / `jp c` |
| `rr a` / `rl a` / `rrc a` | 8085 forms are `rra` / `rla` / `rrca` / `rlca` |
| `rra r` / `rra d` / `rra e` (r ≠ A) | `rra` is **A only**. Multi-byte `>>` is `ld a,r` / `rra` / `ld r,a` |
| `neg` | Z80 `ED 44`. 8-bit negate is `cpl` / `inc a`; 16-bit is `cpl` both / `inc hl` |
| `cp (bc)` / `cp (de)` / `and (de)` / `add a,(bc)` | ALU memory operand is `(hl)` only |
| Word cursor in BC | No `ld hl,(bc)` |
| Z80 `bit n,r` / `ld a,i` / `exx` / IX / IY | Not on 8085 — even if accompanying port asm uses them. IFF is `rim`/`sim`; critical is `di`/`ei` |
| `jp k` after `dec rp` for `== 0` | K sets on rollover 0→`$FFFF`, not at 0 (`cpu-8085` §5) |
| `jp k` for unsigned `<` | Unsigned order is **C** after `sub hl,bc` |
| `pop af` as return address or 16-bit temp | F bit 3 hardwired 0 |
| BSS for an automatic, temp, or spill | Static storage only if C wrote `static` or the object is file-scope |
| Inserting `static` / `-DSTATIC` on locals the source left automatic | Would alias frames (recursion) and change the program |
| `-fframe-pointer` | No IX |
| Intel mnemonics | Zilog in this tree |
| `__sdcccall(1)` objects | Not this ABI |
| `l_mult` for 16×16→32 (Q8.8, widening mul) | Product exceeds 16 bits — use a long mul, then shift |
| Eight `add hl,hl` for `<< 8` | Byte move `ld h,l` / `ld l,0` |
| `sra hl` for signed `/ (1<<n)` of a negative | C toward zero needs the bias `(1<<n)-1` first |
| `(iy+d)` / `add iy,de` for `p->field` | No IY. Park p in DE / stack |
| Evaluating both arms of `&&` / `\|\|` | Side effects and checksum probes diverge |
| `ld de,(nn)` | Not an 8085 instruction. A global word is `ld hl,(nn)`, which destroys HL |
| Copy `x` into DE across `x << n` so a later `+ x` reuses it | The slot reload is `ld de,sp+*` / `ld hl,(de)`. The copy burns a home |
| Moving a loop-carried init down to its first use | The move puts the def after a branch that skips it. Emit the init where the C wrote it |
## Workflow
0. **8085-support skip probe.** Before emit, try `zcc +test -clib=8085` on
the C (or the bench’s classic TIMER line). **Skip** — do not invent
libc — if the link fails on `qsort`, `_heap`, `pow`, `malloc`, or
float helpers (`dmul`, `cpcmath.inc`). Always skip unless a proven
8085 classic TIMER/`+test` path exists:
**n-body, spectral-norm, fasta, binary-trees, sorting, dhrystone,
coremark, mandelbrot, whetstone, paranoia**. `coremark10` /
`sprintf` / `sscanf` / `gamer_benchmark` have no in-tree +test 8085
recipe. Hybrid links need `PUBLIC _name` on every agent function.
**zcc-multi float:** `-compiler=multi` does not forward `--math32` /
`--math-mbf32` to the per-variant compiles, so those benches build as
f48/genmath and fail. Do not score them against a multi “winner”.
Detail: **`methodology-measure`**.
1. Read the C as C90. Note widths, signedness, `static`, attributes, float,
and every comment.
2. Name the shapes per function (tables above).
3. Lay out objects: `static` keyword and file-scope → BSS/data; every
automatic → stack. No extra BSS.
4. Plan residency (word cursor DE, stride BC, byte acc C, one long DEHL;
**Hard residency** for call-free loops). Write the function header
(Comments) from that plan — **Slots** if the body `call`s.
5. Lower with extended ops; spill across `call`. Carry and expand comments.
Blank line after `jp` / `ret` (not after `call`). Saccharine. Inline hot helpers.
6. **Assemble gate:** `z88dk-z80asm -m8085 -l` must be **clean** (no
error, no `call __z80asm__*` on the hot path) **before any ticks
claim**. A ticks number without this gate is invalid. Rewrite helper
calls on the hot path. If printf conversions render wrong (`%7ld`
mangled, `%lu` as a literal `u`), check `CLIB_OPT_PRINTF` before
touching asm — CLI `-pragma-define:CLIB_OPT_PRINTF=…` overrides
source `#pragma printf`. `%lu` is unsupported in the +test 8085
classic default.
7. If the source uses TIMER macros, emit `TIMER_START` / `TIMER_STOP` as
**labels at those source points**, not around CRT. `%12.4e` prints
IEEE32 `0.0` as blank — dump each float half as `%ld` (raw words)
before treating formatted output, or a ticks delta, as evidence.
8. **Checksum before ticks.** Host-compute the `Assert` value. A hang at
`rim` (opcode `0x20`) in `+test` is Assert → longjmp → SYSCALL, not
an infinite kernel. A stub that returns a constant, or whole-program
ticks ≪ multi by >10× **and** Assert fail, is not a result.
Comparing this output to another compiler, and feeding lessons back into
**this** skill, is **`methodology-measure`** (with `compiler-80cc` /
`compiler-sccz80` / `tool-ticks` as needed). Do not load those to emit.
**Metric taxonomy** (do not mix; **`methodology-measure`**):
| Name | What it is |
|------|------------|
| multi TSV `ticks selected` / `bytes selected` | Static per-function sum of the `-compiler=multi` winner (80cc-sp or sccz80). Not whole-image |
| TIMER | `z88dk-ticks -m8085 … -start TIMER_START -end TIMER_STOP` (CPU flag **before** the binary) |
| whole-program | `z88dk-ticks -m8085 bin` `Ticks:` line (CRT + harness + printf) |
## Related
- ISA, flags, timings, stack sequences: `cpu-8085`
- Assemble: `tool-z80asm`
- Float algorithms: `library-math32`, `library-math16` (div and public inv = restoring; NR inv bodies unlinked; `invsqrt` stays NR)
- Integer helpers: `libsrc/l/sccz80/8085.lst`, `libsrc/l/util/8085.lst`
- Classic vs newlib: `library-classic`
- Optional quality loop vs other compilers: `methodology-measure`