commit 3b6a3c115744b6b268f83c3a7e122ab4155e78c2
parent f10eab299b822884ec68f4c6b3b8285859515ca9
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 23 Apr 2026 21:25:10 -0700
Add P1v2 program-entry model (aarch64 _start stub calling p1_main)
P1.M1pp now unconditionally invokes the backend %p1_entry() macro, which
on aarch64 emits :_start: read argc from [sp], compute argv=sp+8, call
p1_main, sys_exit its return value. Portable sources just define
:p1_main; hello.P1 is converted accordingly.
Diffstat:
4 files changed, 73 insertions(+), 23 deletions(-)
diff --git a/docs/P1.md b/docs/P1.md
@@ -458,6 +458,41 @@ naturally aligned addresses for `LD` and `ST`. Unaligned word accesses are
outside the portable contract. Byte accesses have no additional alignment
requirement.
+## Program Entry
+
+P1 v2 defines a portable program-entry model so that portable source
+does not need to know how the native loader delivers `argc` / `argv`.
+
+The target backend is responsible for emitting a per-arch `_start`
+stub that:
+
+1. Captures `argc` and a pointer to the first argv word from the
+ native entry stack or registers.
+2. Calls the portable label `p1_main` under the one-word direct-result
+ convention with:
+ - `a0` = `argc`
+ - `a1` = `argv`, a pointer to the first argv word. Subsequent slots
+ live at offsets `1*WORD`, `2*WORD`, ..., and the list is
+ terminated by a NULL word.
+3. On return from `p1_main`, performs `sys_exit` using the returned
+ value in `a0` as the exit status.
+
+Portable P1 source defines `p1_main` as an ordinary P1 function and
+reads `argc` / `argv` through the standard calling convention.
+
+At entry to `p1_main`, the native entry-stack layout has already been
+consumed by the backend stub. Portable source may not assume anything
+about the `sp` value inherited from `_start` except that it satisfies
+the call-boundary alignment rule and that the standard frame protocol
+(`ENTER` / `LEAVE`) works correctly from it.
+
+`p1_main` may return normally, or it may call `sys_exit` directly at
+any point.
+
+The portable entry model does not expose `envp` or auxiliary vectors.
+Targets that need them must provide target-specific extensions; those
+are outside the portable P1 surface.
+
## System
`SYSCALL` is part of the portable ISA surface.
diff --git a/p1/P1-aarch64.M1pp b/p1/P1-aarch64.M1pp
@@ -496,6 +496,21 @@
%aa64_mem(ST, x8, sp, 8)
%endm
+%macro p1_entry()
+# :_start stub emitted by the aarch64 backend per the P1v2 program-entry
+# model. Captures argc from [sp] into a0, computes argv=sp+8 into a1,
+# calls p1_main under the one-word direct-result convention, then issues
+# a native Linux sys_exit with p1_main's return value as the exit status.
+:_start
+%aa64_mem(LD, a0, sp, 0)
+%aa64_add_imm(a1, sp, 8)
+%aa64_lit32_prefix(br)
+&p1_main
+%aa64_blr(br)
+%aa64_movz(x8, 93)
+%(0xD4000001)
+%endm
+
%macro p1_syscall()
%aa64_mov_rr(x8, a0)
%aa64_mov_rr(save0, a1)
diff --git a/p1/P1.M1pp b/p1/P1.M1pp
@@ -226,3 +226,14 @@
%macro sys_waitid()
%p1_sys_waitid()
%endm
+
+# ---- Program entry --------------------------------------------------------
+#
+# The portable P1v2 program-entry model emits a backend-specific `:_start`
+# stub that captures argc/argv from the native entry state, calls the
+# portable label `p1_main` under the one-word direct-result convention
+# (a0=argc, a1=argv), and sys_exits p1_main's return value. The stub is
+# emitted unconditionally here so portable sources only need to define
+# `:p1_main` as an ordinary P1 function.
+
+%p1_entry()
diff --git a/tests/p1/hello.P1 b/tests/p1/hello.P1
@@ -1,34 +1,23 @@
-# tests/p1/hello.P1 — P1-language hello world.
+# tests/p1/hello.P1 — P1-language hello world using the portable entry model.
#
-# Build pipeline (driven by tests/p1/test.sh):
-# cat p1/P1-aarch64.M1pp p1/P1.M1pp tests/p1/hello.P1
-# -> m1pp expander -> .M1 (macro and builtin substitution)
-# -> m1pp/build.sh -> aarch64 ELF binary (lint, M0, hex2-0)
+# The backend-owned `:_start` stub is emitted automatically by P1.M1pp. It
+# captures argc/argv from the native entry state, calls `p1_main` under the
+# one-word direct-result convention (a0=argc, a1=argv), then sys_exits
+# p1_main's return value as the process exit status.
#
-# The binary writes "Hello, World!\n" (14 bytes) to stdout and exits 0.
-#
-# Notes on the syntax:
-# %li(rd) — emits an "ldr xN, [pc,#8]; b +12" prefix; the next 8 source
-# bytes are the literal pool slot. We supply them as either
-# a sized syscall-number macro (%sys_write -> $(64) = 8B hex)
-# or as a `%lo %hi` decimal pair (each %N is 4 LE bytes).
-# %la(rd) — emits an "ldr wN, [pc,#8]; b +8" prefix; the next 4 source
-# bytes are the literal pool slot. `&msg` resolves to a
-# 4-byte label address at M0 link time.
-# %syscall — wraps the P1v2 syscall ABI (number in a0, args in
-# a1..a3,t0,s0,s1) into the Linux/aarch64 register layout
-# and issues SVC #0.
+# p1_main is an ordinary P1 function written to the portable ISA. It's a
+# leaf — no P1 `%call` — so no %enter/%leave frame is needed. It ignores
+# argc/argv, writes "Hello, World!\n" to stdout via sys_write, and returns
+# 0 in a0 so the backend stub exits with status 0.
-:_start
+:p1_main
%li(a0) %sys_write()
%li(a1) %1 %0
%la(a2) &msg
%li(a3) %14 %0
%syscall()
-
- %li(a0) %sys_exit()
- %li(a1) %0 %0
- %syscall()
+ %li(a0) %0 %0
+ %ret()
:msg
"Hello, World!