commit 1345a2a22f182156e9c40e5eb4c3cb63cd61ab35
parent 354384eae6863bd903d5ea4ed4511c54e11636b4
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 25 Apr 2026 06:13:26 -0700
Heap-allocate PRIM objects so the HEAP tag bits land correctly
The static :prim_sys_exit literal in the data section landed wherever
hex2 placed it -- offset depended on cumulative size of the preceding
code, so the label's address was 4- or 2-byte aligned in some builds
even though it needs to be 8-aligned for `&prim + 3` to tag as HEAP.
That made apply receive values like 0x6025c7 (tag bits 7) or 0x601d4d
(tag bits 5) and bail to ::not_proc.
aarch64 happened to align in the original layout, riscv64 and amd64
didn't. Adding any instrumentation to apply shifted the binary size
and flipped which arch was lucky -- that's how the layout-sensitivity
showed up.
Fix: register_primitives now allocates a 16-byte PRIM via alloc_hdr,
which the bump allocator keeps 8-aligned, then writes
&prim_sys_exit_entry into the entry slot and binds the symbol's
global to the resulting HEAP-tagged pointer. The static
:prim_sys_exit data is gone.
aarch64: PASS
amd64: PASS
riscv64: PASS
Diffstat:
1 file changed, 25 insertions(+), 11 deletions(-)
diff --git a/scheme1/scheme1.P1pp b/scheme1/scheme1.P1pp
@@ -697,18 +697,36 @@
# Primitives
# =========================================================================
#
-# Each primitive sits behind a 16-byte heap object literal in the data
-# section: [hdr_word, entry_word]. The tagged value is &obj + 3.
-# register_primitives interns the surface name and writes the tagged
-# pointer into the symbol's global slot.
+# PRIM objects live on the heap so the bump allocator's 8-byte alignment
+# is what makes (heap_ptr & 7 == 0) hold; that's what lets `+3` encode
+# the HEAP tag cleanly. (A static :prim_sys_exit emitted in the data
+# section was at the mercy of preceding code length and could land at
+# any 4-byte alignment, producing tag bits 5 or 7 instead of 3.)
+#
+# register_primitives allocates one 16-byte PRIM per builtin, writes
+# the entry-function address into the entry slot, interns the surface
+# name, and binds the symbol's global to the HEAP-tagged pointer.
+#
+# Frame: 16 bytes
+# +0 prim ptr (HEAP-tagged, spilled across the intern call)
+
+%fn(register_primitives, 16, {
+ # alloc_hdr(bytes=16, hdr_word=HDR.PRIM) -> HEAP-tagged a0
+ %li(a0, 16)
+ %li(a1, %HDR.PRIM)
+ %call(&alloc_hdr)
+ # Entry slot is at raw+8 = HEAP+5. la-prefix loads a 32-bit address
+ # into the low half of t0; the upper half of the 8-byte slot stays
+ # zero from the cons-zeroed heap, so an %st covers both halves.
+ %la(t0, &prim_sys_exit_entry)
+ %st(t0, a0, 5)
+ %st(a0, sp, 0)
-%fn(register_primitives, 0, {
%la(a0, &name_sys_exit)
%li(a1, 8)
%call(&intern)
%untag_sym(a0, a0) ; idx
- %la(a1, &prim_sys_exit)
- %addi(a1, a1, 3) ; tag HEAP
+ %ld(a1, sp, 0) ; HEAP-tagged prim ptr
%call(&sym_set_global)
})
@@ -725,10 +743,6 @@
# Read-only data
# =========================================================================
-# Primitive object literals (16 bytes each).
-:prim_sys_exit
-$(%HDR.PRIM) &prim_sys_exit_entry %(0)
-
# Surface names. Length is hard-coded at the call site; no NUL needed
# because intern takes (ptr, len). Aligned padding via "\0" bytes is
# fine -- M0 emits ASCII verbatim.