boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs

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:
Mdocs/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