commit f500bfdc04a79ef0ef3c4b10958be34fa3a3c327
parent afb4cccb5119f51e1af693d59e161745019087ce
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 15 May 2026 10:24:37 -0700
docs: describe LocalId storage policy
Diffstat:
| A | doc/LOCALS.md | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 127 insertions(+), 0 deletions(-)
diff --git a/doc/LOCALS.md b/doc/LOCALS.md
@@ -0,0 +1,127 @@
+# LocalId Design
+
+## Goal
+
+Make source locals independent from concrete frame slots.
+
+Today many frontend paths treat a local as a `FrameSlot`. That makes every
+local memory-backed even when CG is recording to `opt_cgtarget`, where
+`CGTarget.virtual_regs` already means CG can mint unbounded virtual registers
+and leave physical allocation to opt.
+
+The new model introduces `LocalId` as the source-local identity. A `LocalId` is
+the mutable lvalue for a local variable. Its storage policy is chosen once at
+allocation time from the active `CGTarget`; it does not transition later.
+
+## Storage Policy
+
+Storage policy is a function of `CGTarget.virtual_regs`:
+
+- `virtual_regs != 0`: eligible locals are virtual-register locals.
+- `virtual_regs == 0`: locals are frame-slot locals.
+
+This keeps `-O0` single-pass. Direct machine CG never needs source-local
+liveness, local register pressure handling, or control-flow join repair. It
+continues to use frame slots for locals and the existing expression vstack
+spill path for temporary register pressure.
+
+The opt path records mutable local values in virtual registers. Register
+pressure and frame placement are handled later by opt/lowering/regalloc, where
+CFG and liveness information exist.
+
+## LocalId Shape
+
+Internally, a local needs roughly:
+
+```c
+typedef enum LocalStorageKind {
+ LOCAL_STORAGE_FRAME,
+ LOCAL_STORAGE_VREG,
+} LocalStorageKind;
+
+typedef struct Local {
+ CfreeCgTypeId type;
+ CfreeSym name;
+ uint32_t flags;
+ LocalStorageKind storage;
+ FrameSlot slot; /* valid for LOCAL_STORAGE_FRAME */
+ Reg vreg; /* valid for LOCAL_STORAGE_VREG */
+} Local;
+```
+
+`LocalId` indexes this table. It is not a `FrameSlot`, and `OPK_LOCAL` should
+remain the concrete frame-memory operand kind. If a local is frame-backed,
+pushing it produces the same lvalue shape that `cfree_cg_push_local` produces
+today. If a local is virtual-register-backed, pushing it produces a mutable
+register local lvalue.
+
+## Lvalue Semantics
+
+`LocalId` is the source lvalue. The backing store decides how load, store, and
+address operations lower:
+
+- frame local load/store: operate on the frame slot.
+- virtual-register local load: read the current virtual register value.
+- virtual-register local store: copy/define the local's virtual register value.
+- address of a frame local: address the frame slot.
+- address of a virtual-register local: unsupported unless the frontend has
+ already selected frame storage for that local.
+
+Virtual-register locals are mutable pseudo-locals, not SSA values at the public
+CG layer. If opt later wants SSA, it must build SSA from this mutable local
+stream. `LocalId` should not force direct CG to implement phi insertion.
+
+## Addressable Locals
+
+A local that can require an address must be frame-backed. Since `-O0` must stay
+single-pass and this design has no storage transition, the frontend/API needs a
+declaration-time way to request addressable storage.
+
+Use the existing slot-style attributes as the semantic source:
+
+- `CFREE_CG_SLOT_ADDRESS_TAKEN`: force frame storage.
+- aggregate, VLA, `alloca`-like, volatile, ABI-required memory objects, and
+ compiler temporaries that need an address: force frame storage.
+- scalar locals without addressable requirements may use virtual-register
+ storage when `CGTarget.virtual_regs` is set.
+
+This may be conservative. Correctness is more important than promoting every
+possible scalar. Later analysis can mark more locals register-eligible before
+creating them, but CG itself should not need to discover that mid-stream.
+
+## Public/API Direction
+
+The API should grow local handles distinct from slot handles:
+
+```c
+CfreeCgLocal cfree_cg_local(CfreeCg*, CfreeCgTypeId type,
+ CfreeCgSlotAttrs attrs);
+void cfree_cg_push_local_id(CfreeCg*, CfreeCgLocal local);
+```
+
+Compatibility can keep `cfree_cg_local_slot` and `cfree_cg_push_local` as the
+explicit frame-slot path. C frontend migration should move source automatic
+variables to `LocalId`; explicit stack objects and address-required temporaries
+can keep using slots.
+
+## Non-Goals
+
+- No `REG -> FRAME` local transition in direct CG.
+- No O0 local spilling for register-backed source locals.
+- No phi or join repair in direct CG.
+- No change to `OPK_LOCAL` meaning; it remains concrete frame memory.
+- No guarantee that every scalar local becomes a virtual register. Addressable
+ or otherwise memory-required locals stay frame-backed.
+
+## Implementation Order
+
+1. Add the internal `LocalId` table and public/internal handle plumbing.
+2. Route local allocation through the storage policy above.
+3. Teach push/load/store/address paths to handle frame-backed and vreg-backed
+ locals.
+4. Update the C parser adapter so normal automatic variables use `LocalId`
+ while explicit frame objects remain slots.
+5. Keep O0 behavior equivalent by verifying `virtual_regs == 0` still allocates
+ frame-backed locals only.
+6. Add opt-path tests that scalar locals record as virtual-register locals and
+ address-required locals still record frame slots.