-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 12 native emitter rework
Part of the Amiga port design log.
Three gaps were fixed here, after which the native and viper emitter is functionally complete:
-
12a — register NLR for
try/except/with(ports/amiga/nlr68k.S). -
12b — big-endian prelude-index byte order behind the
nested-function / closure / generator crashes (
py/emitnative.c). -
12c — viper long-shift opcodes (
py/asm68k.h): the shift register-register encoders emitted word-size (LSL.W/LSR.W, andASRwas even encoded asASL) instead of long. Any viper shift past bit 15 was wrong.
The two strands the old notes predicted for viper — a register allocator
that spills across D2–D7, and "missing 16/32-bit pointer opcodes" — turned
out not to be the problem. The pointer opcodes exist and work; the
1-register-local cap (MAX_REGS_FOR_LOCAL_VARS = 1) is a performance
limit, not a correctness one (stack-based locals are correct). The single
shift-size bug was the actual cause of all the genuine viper failures.
The only micropython/ tests still failing are the viper ptr16/ptr32
boundary tests, which read/write multi-byte values in the machine's
native byte order — big-endian on m68k vs the little-endian .exp. Those
are platform differences (like struct1.py), not bugs.
The native emitter carries the exception-handler PC in REG_LOCAL_1
(D7) across the nlr_push call and reads it back from nlr_buf word
index NLR_BUF_IDX_LOCAL_1 (= 2, i.e. regs[0]; see py/emit68k.c).
Under the setjmp NLR fallback that word lives inside the jmp_buf that
setjmp overwrites, so every try/except/with inside a
@micropython.native body faulted the CPU (Invalid Memory Access).
Fixed with a register-based NLR:
| File | Role |
|---|---|
ports/amiga/nlr68k.S |
nlr_push (saves the callee-saved set into nlr_buf) + nlr_jump_tail (restores it and resumes with D0=1) |
ports/amiga/nlr68k_jump.c |
C nlr_jump — runs MP_NLR_JUMP_HEAD, tail-calls nlr_jump_tail
|
py/nlr.h |
__m68k__ arch case → MICROPY_NLR_M68K, MICROPY_NLR_NUM_REGS_M68K (13)
|
nlr_buf_t layout (machine words): prev, ret_val, then regs[13]
= D7 (at regs[0], the mandatory NLR_BUF_IDX_LOCAL_1 slot), D2–D6,
A2–A6, caller SP, return PC. The full AmigaOS callee-saved set is
D2–D7/A2–A6 (A6 is gcc's frame pointer, A4 a possible baserel base) — the
original "D2–D7/A2–A5" estimate was one register short. nlr_push and the
register restore are hand-written assembly because bebbo gcc 6.5 has no
naked attribute on m68k; only nlr_jump's bookkeeping stays in C. The
port also stops forcing MICROPY_NLR_SETJMP in mpconfigport.h so the
py/nlr.h auto-detect engages.
native_try, native_try_deep, native_with now pass under vamos and
on real Kickstart (Amiberry, standard); the setjmp baseline failed all
three. A full micropython/ re-run under vamos (2026-06-20) measured
+6 pass / -6 fail with zero regressions: the NLR also unblocked
viper_try, viper_with and viper_globals, which turned out to need
only the NLR and not the still-pending viper register/pointer work.
Regression test: tests/ports/amiga/test_native_nlr_smoke.py
(try/except/finally, with enter/exit ordering, locals surviving the
longjmp, nested re-raise, propagation out of the native frame, balanced
push/pop). The NLR asm touches no FPU registers, so the 68020fpu
variant is covered transitively.
Independent of the NLR. Any @micropython.native body that builds another
function object — a nested def, a closure, or a generator — faulted with
an Invalid Memory Access. native_const, native_closure, native_gen
all crashed this way.
Root cause: big-endian byte order. The native emitter stores
prelude_ptr_index in the first machine word of the function data via
mp_asm_base_data() (py/emitnative.c). That helper emits the word
little-endian (*c++ = val; val >>= 8) — fine for every other native
target (x86/x64/arm/xtensa/riscv are all little-endian), but the 68k is
the first big-endian native-emit target. At runtime
mp_obj_fun_native_get_prelude_ptr() reads the word back as a native
uintptr_t, so an index of 1 came back as 0x01000000 and the
subsequent child_table[prelude_ptr_index] indexed ~64 MB out of bounds.
That is the 0x01000000 / addr | 0x04000000 (= 0x01000000 << 2,
pointer-array scale) fault seen in the dumps. The generator
start_offset word, written the same way, had the same defect.
The earlier "A4 / mp_fun_table reload through ASM_CALL_IND" theory was
wrong — @micropython.native → native-function calls already worked
(a native function calling a top-level native function returns correctly);
the crash was specifically in creating the function object.
Fix. Byte-swap both words on N_68K (__builtin_bswap32) so they land
in the machine's native big-endian order. The bswap is correct for both
on-device JIT (host == target == big-endian) and any future 68k
cross-compile (host little-endian, target big-endian) because it always
produces big-endian bytes for the big-endian reader.
native_const, native_closure, native_gen now pass under vamos and on
Amiberry (standard), with zero native failures. Regression test:
tests/ports/amiga/test_native_nested_smoke.py (nested native def,
plain nested def, closure with nonlocal, generator, generator send,
nested def in a loop).
asm_68k_lsl_dreg_dreg / asm_68k_lsr_dreg_dreg / asm_68k_asr_dreg_dreg
in py/asm68k.h emitted the word-size register-shift opcodes
(LSL.W = 0xE168, LSR.W = 0xE068); asr was worse — 0xE160
decodes to ASL.W, the wrong direction as well as size. So any viper
shift whose result depended on bits above 15 was wrong: 1 << 30 → 0,
signed >> shifted the wrong way, and byte-assembly
(a<<24)|(b<<16)|(c<<8)|d lost its high bytes (the viper_subscr_multi
failure). Some tests that looked like pointer/register bugs were really
this — viper_ptr32_store/viper_ptr32_load use a >> in their
length/index maths.
Fix. Emit the long forms: LSL.L = 0xE1A8, LSR.L = 0xE0A8,
ASR.L = 0xE0A0. This flipped viper_binop_arith,
viper_binop_bitwise_uint, viper_misc2, viper_subscr_multi,
viper_ptr32_store, viper_ptr32_load. Regression test:
tests/ports/amiga/test_viper_shift_smoke.py.
After 12a–12c the full micropython/ suite is 85 pass / 18 skip / 5
fail; the 5 remaining are the viper ptr16/ptr32 boundary byte-order
platform differences described above. The native + viper emitter is
functionally complete.