Skip to content

Phase 12 native emitter rework

Simon Dick edited this page Jun 20, 2026 · 1 revision

Phase 12 — 68k 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, and ASR was even encoded as ASL) 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.

12a — try/except/with in native mode ✅

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.

12b — nested-function / closure / generator crashes ✅

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).

12c — viper long shifts ✅

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.

Clone this wiki locally