commit 07de70b206e96bc65e95c530a6b487101f218640
parent e05d709edfb9f6f42bff02097a80ebb58282b4e6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 22 Apr 2026 07:19:54 -0700
P1.md: clarify leaf-function semantics (CALLable in, not out)
The prior rewording left ambiguous whether a leaf could be CALLed
at all. Split the leaf contract into the three operations it may
(or may not) perform internally: RET is fine, tail-branch via
li_br+B is fine and lets the target's RET skip the leaf in the
return chain, only inner CALL is forbidden. Being CALLed is always
fine — the restriction is on what the leaf does, not on who invokes
it.
Diffstat:
| M | docs/P1.md | | | 40 | ++++++++++++++++++++++++---------------- |
1 file changed, 24 insertions(+), 16 deletions(-)
diff --git a/docs/P1.md b/docs/P1.md
@@ -234,22 +234,30 @@ SYSCALL # num in r0, args r1-r6, ret in r0
Concrete rule: **a function that itself executes a `CALL` must wrap
its body in a matching `PROLOGUE`/`EPILOGUE` pair.** `PROLOGUE` is
what spills the incoming return address into the frame; `EPILOGUE`
- restores it so `RET` can find it. A leaf function (no `PROLOGUE`) is
- permitted — but it may only execute `RET`, never `CALL`. A bare
- `CALL` in a prologue-less function clobbers its own return address
- on arches where the native mechanism uses a register rather than a
- stack push, and the eventual `RET` branches to itself.
-
- The failure mode is platform-asymmetric: amd64's native `CALL`
- pushes onto the stack so a prologue-less `CALL ; RET` happens to
- work; aarch64 and riscv64 write the return address to a link
- register and hang silently. Don't write code that relies on the
- amd64-happens-to-work behavior.
-
- Tail-call substitute for the "leaf wants to dispatch to another
- function and inherit its return" pattern: `li_br &callee ; B`
- (unconditional branch, not `CALL`). The callee's `EPILOGUE` returns
- directly to the current function's caller.
+ restores it so `RET` can find it.
+
+ Leaf functions (no `PROLOGUE`) are permitted and may be called
+ normally: `CALL leaf` sets up the return address, the leaf's `RET`
+ uses it, control returns to the caller. The restriction is only on
+ what a leaf may itself do:
+
+ - **RET** — returns to whoever established the current return
+ address. Usually the direct `CALL`er; in the tail-branch case
+ below, whoever `CALL`ed the outermost caller in the chain.
+ - **Tail-branch** (`li_br &target ; B`) to another function — the
+ target's own `PROLOGUE`/`EPILOGUE` preserves the current return
+ address across the target's body, so the target's `RET` returns
+ directly to the leaf's caller, skipping the leaf in the return
+ chain.
+ - **`CALL`** — forbidden. The inner `CALL` clobbers the return
+ address slot (on arches where it's a register, not a stack
+ push), so the leaf's subsequent `RET` branches to itself.
+
+ The failure mode of a leaf `CALL` is platform-asymmetric: amd64's
+ native `CALL` pushes onto the stack so a prologue-less `CALL ; RET`
+ happens to work; aarch64 and riscv64 write the return address to a
+ link register and hang silently. Don't write code that relies on
+ the amd64-happens-to-work behavior.
`RET` pops / branches through the return address.
- `PROLOGUE` / `EPILOGUE` set up and tear down a frame with **k