commit d19a402ecc3df0f912aecf4ec2a8d400f26c5f05
parent 4eba962ec4bb1f6874f10d0ebd5f62371b1a1b7a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 24 Apr 2026 08:09:38 -0700
Expand post with missing m1pp/P1 features and P1 source examples
Adds bullets for %struct/%enum, local labels, and %str in the m1pp
section; a program-entry paragraph in the P1 section; and a new
'Three programs' section walking through argc_exit, hello, and double
with their source inlined.
Diffstat:
| M | post.md | | | 80 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 80 insertions(+), 0 deletions(-)
diff --git a/post.md b/post.md
@@ -154,6 +154,15 @@ Features:
- **Conditional expansion**: `%select(cond, then, else)` evaluates `cond` as
an integer and emits exactly one of the two branches. Non-zero is true.
- **Token concat**: `a ## b` pastes two tokens into one identifier.
+- **Struct / enum shorthands**: `%struct NAME { f1 f2 }` synthesizes
+ `%NAME.f1` → 0, `%NAME.f2` → 8, `%NAME.SIZE` → 16. `%enum` does the same
+ with stride 1 and a `COUNT` terminator. Saves hand-counting offsets for
+ records and small tag sets.
+- **Local labels**: inside a macro body, `:@loop` and `&@loop` pick up a
+ fresh per-expansion suffix, so a macro can define its own labels without
+ colliding with itself at a second call site.
+- **Stringify**: `%str(foo)` turns a word token into the string literal
+ `"foo"`.
Comments (`#` and `;`) and M0 `DEFINE`s pass through untouched. That's it — no
`%ifdef`, no string manipulation, no floating point. Enough to encode
@@ -250,12 +259,83 @@ Calling convention:
passes a writable result buffer in `a0`, real args slide over by one,
callee returns that same pointer in `a0`.
+Program entry: portable source writes `:p1_main` as an ordinary P1 function
+with `argc` in `a0` and `argv` in `a1`, and returns an exit status in `a0`.
+The backend emits a small per-arch `:_start` stub that captures the native
+entry state, calls `p1_main`, and hands its return value to `sys_exit`. The
+portable side never sees the raw entry stack.
+
P1 ships as a pair of files you catm before any P1 source:
- `P1A.M1pp` — architecture-specific backend implementing the portable
interface.
- `P1pp.M1pp` — the portable interface that P1 programs program against.
+## Three programs
+
+The shortest useful P1 program is a bare return. `p1_main` gets `argc` in
+`a0` on entry, `a0` is also the one-word return register, and the backend
+stub hands the return value to `sys_exit` — so this returns `argc` as the
+exit status:
+
+```
+:p1_main
+ %ret()
+
+:ELF_end
+```
+
+Hello world is a single `sys_write` from a leaf function:
+
+```
+:p1_main
+ %li(a0) %sys_write()
+ %li(a1) %1 %0
+ %la(a2) &msg
+ %li(a3) %14 %0
+ %syscall()
+ %li(a0) %0 %0
+ %ret()
+
+:msg
+"Hello, World!
+"
+
+:ELF_end
+```
+
+A few things to notice. `%li(a0)` and `%la(a2)` don't take the immediate
+as a macro argument — the backend emits a load-from-literal-pool prefix
+and the bytes that follow in source become the literal. Here
+`%sys_write()` expands to the 8-byte Linux syscall number for write
+(the aarch64 backend's choice); `&msg` is a 4-byte label reference
+(addresses fit in 32 bits in the stage0 image layout). The two `%N %0`
+pairs are M0 4-byte decimal immediates padded to 8 bytes.
+
+A function call, with a helper that doubles its argument:
+
+```
+:double
+ %shli(a0, a0, 1)
+ %ret()
+
+:p1_main
+ %enter(0)
+ %la_br() &double
+ %call()
+ %leave()
+ %ret()
+
+:ELF_end
+```
+
+`LA_BR` loads the hidden branch-target register; the next control-flow
+op consumes it. `double` is a leaf and needs no frame. `p1_main` is not
+— it calls `double`, so it opens a standard frame with `%enter(0)` to
+preserve the hidden return-address state across the call, and closes it
+with `%leave()` before returning. Run with `./double a b c` and the exit
+status is `8` (argc=4, doubled).
+
## What it cost
Concrete sizes for what's landed today: