kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

CG stack API: parser-driven codegen without a shadow stack

Goal. Make the C parser drive KitCg directly, in the same single-pass shape it has today, without maintaining a duplicate pcg value stack. The codegen stack stays the liveness authority: a value/place is live exactly while it is represented by a stack slot, and popping that slot is the cheap single-pass death signal the native direct target and temporary reclaim already use.

This is not a plan to replace the stack API with free-floating value handles. It is a plan to (a) make stack slots rich enough for a language frontend to use them as its expression stack, (b) move all effective-address folding behind the CG API, (c) make codegen suppression a CG mode that preserves stack shape, frontend slot facts, and constant facts, and (d) delete the pcg adapter layer so the parser drives the CG API directly.

This is a clean-break rewrite, no backcompat. The implementation is phased only where subsystems are genuinely separable; each phase is a one-time cutover with the old path removed for that subsystem. There is no dual-stack compatibility mode and no byte-identity gate.


1. Decisions (resolved)

# Decision Choice
D1 Keep the stack API, add stack-owned inline frontend slot facts yes
D2 Free KitCgValue/KitCgPlace handles independent of stack lifetime rejected — loses the "on stack == live" invariant
D3 Where the type/flags stack lives during suppression CG unevaluated mode — KitCg maintains stack shape, inline lang facts, and constant facts while target emission is off
D4 Effective-address folding Automatic behind the CG API. The parser builds places naively (deref, field_at, elem); CG decides whether to fold into the fused [base + index*scale + offset] operand or materialize. No deferral or folding logic in the parser.
D5 Success gate Correctness + determinism (full suites pass, output deterministic). Emitted bytes MAY change vs today; byte-identity is not a gate.
D6 Fused store_void / store_keep as CG ops in scope
D7 The cg_adapter.c / pcg_* layer deleted entirely. The parser drives kit_cg_* directly and applies C facts via kit_cg_retag_top/set_top_flags. C-semantic operations survive as plain helper functions (op-enum maps, the conversion lattice, atomic/inline-asm/inc-dec shaping) — not a stack-mirroring layer, and not under a pcg_ prefix.
D8 Call-arg stack-mark API (old §4.4) dropped — C already passes args on the CG stack (kit_cg_call), so a mark API adds nothing
D9 KitCgSlotInfo + new ops are public API forcedlang/ builds only against include/kit/cg.h (no src/cg includes)
D10 Lang facts storage inline in ApiSValue; final node size is 64 bytes. Pack kind/res/pinned/lvalue into a uint16_t flags word and make lang_flags uint16_t, giving room for const void* lang_type while preserving the ApiSValue <= 64 invariant (src/cg/value.c:14).
D11 C integer-constant-expression evaluation moves onto a CG constant-value model, with parser-owned C legality tracking. The parser evaluates an ICE by parsing it once through the normal path under unevaluated mode, checking the syntactic/semantic ICE guard, and reading the folded value from CG. The parser's duplicate cexpr_* grammar and cint_* arithmetic engine are deleted in the final ICE phase, not during the stack/place cutover.
D12 Constant payload shape stack-parallel constant payload, not OPK_IMM alone. kit_cg_top_const_int today only answers for <=64-bit immediate operands; the new model carries known integer bits + width + type independently of whether the emitted operand would be an immediate/local/runtime call.
D13 __int128 constant policy support useful 128-bit integer bit-pattern folding in CG, but do not require 128-bit div/rem. CG already has i128/u128 types and emitted i128 div/rem lower through runtime helpers; those are dynamic code, not compile-time constants. Move the existing parser CConstInt add/sub/mul/bitwise/shift/compare/cast behavior into CG so current static i128 initializer coverage stays supported; i128 div/rem returns "unknown constant" until a true compile-time word-int division implementation is added.

2. Current state (what we are removing)

The C frontend currently runs two stacks in lockstep:

PcgSlot duplicates four things; only one is genuinely C-only:

PcgSlot field Status
const Type* (C type) C-only; opaque to CG; must live somewhere
cg_id (cached CG id) redundant — the CG node already has type
flags: LVALUE / MODIFIABLE / BITFIELD / NULL_PTR_CONST / REGISTER LVALUE≈node lvalue, BITFIELD≈node bit-field; MODIFIABLE/NULL_PTR_CONST/REGISTER are C-only
PcgLvAux offset/scale/base_kind/bit_* redundant with the CG place operand (OPK_INDIRECT base+index*scale+ofs) + node bit-field — kept only to defer the deref and avoid re-crossing C-layout

CG already has a complete PLACE/VALUE addressing model (include/kit/cg.h:668): push_local / deref(offset) / field(index) / elem / elem_scaled / addr / load / store, with offset/index/scale folded into the OPK_INDIRECT operand (kit_cg_field, src/cg/control.c:1157, folds the field offset into the displacement and preserves any index/scale). toy and wasm use this model directly. The C frontend bypasses kit_cg_field/kit_cg_elem and reimplements the same folding in PcgLvAux (pcg_materialize_lv_to_ptr, pcg_lv_to_memop_place, cg_adapter.c:585/647) purely to (1) defer the deref so *p/p->f stay a pointer rvalue, and (2) avoid re-deriving an offset it already knows. D4 deletes this: the folding moves behind the CG API.

The sqlite layer profile makes the cost visible: ~1.43 M kit_cg_* calls per sqlite3.c, CG API ~19% of compile ticks, of which the stack/memory family is ~49%. The hot expression seam is paid twice — once per CG stack edit, once per shadow-stack edit.


3. Invariants (load-bearing)

  1. Stack lifetime stays authoritative. A transient operand is live iff a live ApiSValue stack entry references it. api_push/api_pop remain the lifetime hooks. Inline lang facts and the constant payload never own lifetime.
  2. No free expression handles. Every API returns either a local/symbol/label handle with existing lifetime rules, or a short-lived stack depth — never a separately-live expression value.
  3. The frontend can inspect and retag slots. Read top/top2/depth-N lang type
    • flags; retag the result slot after an op.
  4. CG does not depend on C frontend types. src/cg must not include lang/c headers or dereference the lang payload. The lang payload is an opaque const void* copied/dropped with the slot.
  5. Backend boundaries stay intact. CgTarget / NDT / NativeTarget / MC receive no C frontend state. The new place ops lower through the existing CgTarget contract — no backend changes.
  6. The hot ApiSValue node stays <= 64 bytes. Inline lang facts are allowed only because the packed layout keeps the node at exactly 64 bytes (D10).
  7. No global state. Inline lang facts live on each ApiSValue; the constant payload and unevaluated counter hang off KitCg.
  8. No separate lang side allocation. Non-C frontends leave lang_type=NULL and lang_flags=0; they pay the 64-byte node size but no extra side array or enable/disable machinery.
  9. Constant facts are stack-owned CG facts, not backend facts. The constant payload shadows the CG stack in lockstep, carries known/unknown integer bits plus width/type, and has no CgTarget, local, temp, or native-backend lifetime. Frontends read it only through public CG APIs; they never inspect ApiSValue to decide whether a value is constant.
  10. Constant tracking is always-on. The constant payload is a CG semantic fact, not a C frontend feature and not an optimization-level feature. It is maintained for every KitCg stack slot in normal emitting mode and in unevaluated mode; there is no kit_cg_const_enable and no "constants off" mode.

4. The new architecture

4.1 One stack + inline opaque lang facts

ApiSValue carries the frontend facts directly. There is no KitCg.lang_side, no enable call, and no per-frontend side allocation. lang_type is an opaque pointer copied/dropped with the stack node; lang_flags is a frontend-defined 16-bit field. A separate always-on constant payload still shadows the same stack depths; it is owned by KitCg, follows the stack in lockstep, and is independent of the frontend type/flags fields.

Planned internal shape, preserving a 64-byte hot stack node:

typedef struct ApiSValue {
  Operand     op;           /* 24 bytes: immediate/local/global/indirect operand */
  ApiDelayed* delayed;      /* off-node SV_CMP/SV_ARITH payload, else NULL       */
  const void* lang_type;    /* opaque to CG; lang/c stores a const Type*         */
  KitCgTypeId type;         /* CG type id                                        */
  KitCgLocal  source_local; /* owned/fixed local tracking                        */
  ApiBitField bitfield;     /* 12 bytes; bit_width != 0 => bit-field PLACE       */
  uint16_t    lang_flags;   /* frontend-defined (C: LVALUE/MODIFIABLE/...)       */
  uint16_t    flags;        /* kind:2, res:2, pinned:1, lvalue:1, spare:10       */
} ApiSValue;                /* 64 bytes, 8-byte aligned                          */

kind is one of SV_OPERAND, SV_CMP, or SV_ARITH; res is one of RES_INHERENT, RES_LOCAL, or RES_FIXED_LOCAL; pinned and lvalue are booleans. These fit in six bits, leaving ten spare bits in the packed word. ApiDelayed remains an off-node pooled extension for delayed compare/arithmetic fusion only; it is not a language-fact or constant-value store.

typedef struct KitCgSlotInfo {
  KitCgTypeId cg_type;     /* read-only echo of the node's CG type, for queries */
  const void* lang_type;   /* opaque to CG; lang/c stores a const Type*          */
  uint16_t    lang_flags;  /* frontend-defined (C: LVALUE/MODIFIABLE/...)        */
} KitCgSlotInfo;

kit_cg_slot_info reads {lang_type, lang_flags} from ApiSValue and fills cg_type from stack[i].type at query time. The C flags are frontend-private bits and must fit in uint16_t; suggested C layout:

C_LVALUE        /* a C lvalue (distinct from CG place-ness; e.g. a const lvalue) */
C_MODIFIABLE    /* modifiable lvalue (not const/array/func/void)                  */
C_BITFIELD      /* C-semantic bit-field marker (geometry lives on the CG place)   */
C_NULL_PTR_CONST/* integer 0 / null-pointer-constant                             */
C_REGISTER      /* `register` storage class — forbids `&`                         */

PcgLvAux.is_subobject disappears: with eager places (4.3) the place operand is the exact sub-object, so the to_rvalue struct-materialize heuristic (parse_expr.c:1040) collapses to "leave the place as-is."

4.2 Slot queries and retag

KitCgSlotInfo kit_cg_slot_info(KitCg*, uint32_t depth_from_top); /* depth 0 == top */
void          kit_cg_retag_top(KitCg*, const void* lang_type, uint16_t lang_flags);
void          kit_cg_retag_at(KitCg*, uint32_t depth_from_top,
                              const void* lang_type, uint16_t lang_flags);
void          kit_cg_set_top_flags(KitCg*, uint16_t set, uint16_t clear);

kit_cg_stack_depth already exists (include/kit/cg.h:706). Retag touches only the inline lang fields — never the node's CG type (the producing op sets that). C-type changes that carry no CG op (qualifier strip on a struct lvalue; the deref retype) become a retag. Today's C queries — pcg_top_type, pcg_top2_type, pcg_top_is_lvalue/_modifiable_lvalue/_null_ptr_const/_bitfield/ _register, and the pcg_retag_* family — are replaced by direct kit_cg_slot_info reads and kit_cg_retag_* calls at the parser sites (small static inline accessors over the flag bits are fine; there is no pcg layer).

Producer rule: an op that produces a fresh result slot (load, call result, binop/cmp result, convert, addr, push_*) clears that slot's inline lang facts (lang_type=NULL, lang_flags=0). The parser stamps the C facts immediately after via kit_cg_retag_top. Structural ops (dup/dup2/swap/rot3/drop) move/copy whole ApiSValue nodes, so lang facts move with the value. This is exactly today's "emit op, then pcg_retag_top" idiom, with structural movement done automatically inside CG.

4.3 Eager places; folding behind the CG API (D4)

The parser builds places naively and immediately; CG folds. One new op is needed; the rest already exist.

/* PLACE -> PLACE: project to a sub-object at a known byte offset and field type.
 * Folds the offset into the place operand (no record-layout lookup — the
 * frontend already knows the offset from its own C layout). Errors if TOS is not
 * a PLACE. Bit-field geometry, when present, is attached separately via
 * kit_cg_field_bits. */
void kit_cg_field_at(KitCg*, int64_t byte_offset, KitCgTypeId field_type);

Composition (all folding decided inside CG, as kit_cg_field already does):

C expression CG op sequence result
x (local) push_local x PLACE
*p deref 0 PLACE
s.f field_at(off_f, ty_f) PLACE
p->f deref 0; field_at(off_f, ty_f) PLACE
a[i] decay aelem* (addr; bitcast); push i; elem 0 PLACE
a[i].f elem 0; field_at(off_f, ty_f) PLACE
&e build place; addr VALUE(ptr)
read of any lvalue e build place; load VALUE

deref(0); field_at(off) collapses inside CG to one OPK_INDIRECT(base=p, ofs=off) memop; elem; field_at keeps the fused [base + i*scale + off] form (the kit_cg_field INDIRECT branch already does this). The "stride differs from access type" case (arr[i].f) is handled by the natural elem (scales by sizeof(elem)) then field_at (adds off_f) composition — so the C frontend no longer needs kit_cg_elem_scaled for it.

Folding intelligence that currently lives in the parser moves into the CG place ops, where it belongs:

pcg_materialize_lv_to_ptr, pcg_lv_to_memop_place, pcg_lv_member, pcg_lv_subscript, pcg_decay_array, and the whole PcgLvAux struct are deleted. kit_cg_field (by index) and kit_cg_elem/kit_cg_elem_scaled stay for toy/wasm.

4.4 Fused load/store (D6)

void kit_cg_load(KitCg*, KitCgMemAccess);        /* [place]        -> [value]   (exists) */
void kit_cg_store(KitCg*, KitCgMemAccess);       /* [place, value] -> []        (exists) */
void kit_cg_store_keep(KitCg*, KitCgMemAccess);  /* [place, value] -> [value]            */

kit_cg_store already is the [place,value] -> [] primitive; the parser names the discard intent through it (store_void == store, kept as the documented spelling). kit_cg_store_keep adds the C assignment-expression value preservation (a = b, if ((x=f()))) as one op, so the parser drops the dup/rot3/swap choreography it does today (cg_adapter.c:769-773). CG keeps the delayed-RHS-into-local fast path (kit_cg_store, src/cg/memory.c:506-551) in both forms.

4.5 Unevaluated mode: fold + types, zero emission (D3, D11)

Unevaluated mode is a CG execution mode, not the full definition of C "unevaluated operand" legality. It serves two implementation needs that share one stack machine:

These uses share the same CG mechanics, but not the same C legality rules. CG answers "what value/type would this expression stack produce if it can be folded"; the parser's constant guard (§4.7) answers "is this syntax permitted in this C constant-expression category."

void kit_cg_unevaluated_push(KitCg*);  /* enter fold+type-only mode (nestable) */
void kit_cg_unevaluated_pop(KitCg*);

Contract while unevaluated > 0:

&&, ||, and ?: need explicit handling; they are not solved by making labels and branches no-op. In emitting mode they keep their normal lowering through labels, branches, and temporaries. In unevaluated mode the parser's control-shape helpers parse each syntactic operand once, compute the C result type/flags from KitCgSlotInfo, and push a known or unknown result constant:

The important boundary is that the parser may keep semantic branches for C control-shape constructs. What disappears is parser-owned emission bookkeeping: Parser.suppress_codegen, pcg_emit_enabled, duplicate shadow-stack updates, and forks whose only purpose was "emit this CG op or do not emit it." Parser-side semantic side-effects in suppressed regions (VLA-size bookkeeping, _Generic association collection, diagnostics) still run on the normal parse path.

4.6 Calls (D8)

C call arguments already flow through the CG stack: the C call path drives kit_cg_call(nargs, fn_type, attrs), which consumes the callee+args segment. There is no second frontend arg list to remove. No mark API is added. The parser stamps the result slot's C type via retag.

4.7 Constant-expression evaluation on CG (D11)

C integer-constant-expression evaluation stops being a parser subsystem. Today eval_const_intcexpr_cond (parse_expr.c) is a complete second expression grammar (cexpr_mul/add/shift/rel/eq/band/bxor/bor/land/…) plus a second constant-arithmetic engine (cint_*, with 128-bit lo/hi, casts, conversions) — and it re-implements offsetof / __builtin_constant_p / enum-constant handling separately from the real parser, a standing drift hazard. All of it is deleted in the ICE cutover phase.

"A real constant-value model" means CG no longer treats "constant" as a property of ApiSValue.operand == OPK_IMM. The current kit_cg_top_const_int can only answer for <=64-bit immediate operands. The new model is a stack-parallel constant payload owned by KitCg: every CG stack slot has known/unknown state plus integer bits, width, signedness, and result CG type. It can represent a folded value even when normal emission would have produced a local, a wide value lowered through a runtime helper, or no emitted value at all because unevaluated mode is active.

Proposed public shape:

typedef struct KitCgConstInt {
  uint64_t lo;       /* low bits, always truncated to width */
  uint64_t hi;       /* high bits for width > 64 */
  uint16_t width;    /* 1..128 for integer constants */
  uint8_t  is_signed;
  uint8_t  known;
} KitCgConstInt;

int  kit_cg_top_const_int_ex(KitCg*, KitCgConstInt* out);
int  kit_cg_top_const_i64(KitCg*, int64_t* out); /* convenience */
void kit_cg_push_const_int(KitCg*, KitCgTypeId type, const KitCgConstInt* v);

The exact names can change, but the surface must be width-complete and must not require frontends to inspect ApiSValue or know which operands are immediates.

Constant payload policy:

Fold coverage required before the ICE cutover:

The replacement parser helper is one pass over the normal grammar plus a guard:

CConstEval eval_const_int(Parser* p, SrcLoc loc, CConstKind kind) {
  CConstGuardMark mark = c_const_guard_push(p, kind);

  kit_cg_unevaluated_push(p->cg);
  parse_conditional(p);                         /* the real grammar */
  int ok_const = kit_cg_top_const_int_ex(p->cg, &value);
  KitCgSlotInfo info = kit_cg_slot_info(p->cg, 0);
  kit_cg_drop(p->cg);
  kit_cg_unevaluated_pop(p->cg);

  int ok_guard = c_const_guard_pop(p, mark);
  if (!ok_guard || !ok_const || !c_type_is_integer(info.lang_type))
    perr(loc, "integer constant expression required");
  return (CConstEval){ value, info.lang_type };
}

The syntactic/semantic guard is mandatory. A foldable result is not the same thing as a legal C integer constant expression:

Implement the guard as parser mode/counter state, not a second expression grammar. Normal parse routines call small note functions when active, for example "comma operator in integer constant expression" or "assignment in static initializer constant." The guard also has explicit "not evaluated" submodes for sizeof non-VLA operands and short-circuited &&/||/?: arms. At the end, both checks must pass: the guard says the syntax is legal for the requested C constant category, and CG says the stack top has a known value of the required type.

Minimum guard event list for the ICE cutover:

Keep the categories separate:


5. What moves where

Today in pcg / Parser Target owner
PcgSlot.type (const Type*) inline ApiSValue.lang_type (opaque)
PcgSlot.cg_id gone — read the node type via kit_cg_slot_info
PcgSlot.flags (C value flags) inline ApiSValue.lang_flags
Parser.cg_slot_stack / cg_type_sp / cg_type_cap gone — the CG stack is the typed stack
PcgLvAux (offset/scale/base_kind/bit_*) + pcg_materialize_lv_to_ptr / pcg_lv_to_memop_place / pcg_lv_member / pcg_lv_subscript / pcg_decay_array gone — CG place ops (deref/field_at/elem/addr/field_bits) fold automatically
Parser.suppress_codegen + pcg_emit_enabled forks CG unevaluated mode; parser keeps only semantic forks required by C control-shape constructs
pcg_dup/swap/drop (mirror onto shadow stack) direct kit_cg_dup/swap/drop (inline lang facts move with ApiSValue; CG updates the constant payload)
assignment dup/rot3/swap sequences kit_cg_store / kit_cg_store_keep
cg_adapter.c / cg_adapter.h / the pcg_* layer deleted — parser drives kit_cg_* directly
cexpr_* grammar + cint_* constant-arith engine (parse_expr.c) deleted in the ICE cutover — parser owns the legality guard; CG owns constant-value folding (§4.7)
C conversions, usual-arithmetic-conversions, binop/cmp/atomic op maps, lvalue-legality, null-ptr/register/bitfield C rules plain helper functions the parser calls (no stack mechanics, no pcg_ prefix)

After the cutover there is no adapter layer. The parser calls kit_cg_* directly and stamps C facts with kit_cg_retag_top/set_top_flags. The genuinely C-semantic operations survive as ordinary functions in the parse module — the op-enum maps, the conversion lattice (today's pcg_convert), inc/dec, call/atomic/intrinsic/inline-asm shaping — each driving kit_cg_* + retag directly, with no parallel stack and no EA state.


6. Op-by-op: inline lang + constant lockstep

Every stack-mutating CG op maintains inline lang facts and the constant payload so depths stay in sync. Grouped by effect:

Implementation choke points: api_push/api_pop (src/cg/value.c:161/187) and the structural ops kit_cg_dup/dup2/swap/drop/rot3 (src/cg/memory.c:598-735). ApiSValue copies carry inline lang facts automatically, while producers must clear them explicitly. Routing constant-payload movement through api_push/api_pop (which every producer/consumer already calls) covers most ops automatically; the structural ops and scope-edge movers need explicit handling.


7. Cutover plan (clean break, gate = correctness + determinism)

No dual-stack interim, no byte-identity gate. Each phase is either CG infrastructure with no C frontend cutover, or a one-time frontend cutover that removes the old path for that subsystem. Do not land a phase where the parser's shadow stack and inline CG lang facts both authoritatively model live expressions.

  1. CG inline lang fields + slot queries. Add the packed 64-byte ApiSValue layout, KitCgSlotInfo, kit_cg_slot_info, kit_cg_retag_top/_at, and kit_cg_set_top_flags; wire producer clearing, structural copies, and scope-edge movers. Add focused CG API tests for producer clearing, structural/scope movement, and query/retag behavior. No C frontend change yet.
  2. CG place + store ops. Add kit_cg_field_at and kit_cg_store_keep; move the SEXT-load hint decision into kit_cg_load; confirm deref/elem/ field_at/field_bits compose to fused operands without parser help. Add targeted place tests for local/global load+store, s.f, p->f, a[i], a[i].f, bit-fields, displacement overflow, volatile/atomic accesses, & of each lvalue form, assignment value preservation, ++/--, and compound assignment.
  3. CG unevaluated + constant payload substrate. Add kit_cg_unevaluated_push/pop, the stack-side constant payload, the width-complete constant query/push API, <=64 div/rem folding, and the required 128-bit bit-pattern folds excluding div/rem. No C frontend cutover yet. Add CG API tests for file-scope/no-function use, unknown constants including i128 div/rem, dummy handles, no target/local calls, nested unevaluated mode, and inline-lang/constant lockstep under suppression.
  4. C frontend one-stack cutover. Use inline lang fields for C; replace parser stack reads/writes with kit_cg_slot_info + retag; replace EA folding with eager places; replace assignment choreography with store/store_keep; move Parser.suppress_codegen uses to CG unevaluated mode for type-only paths; delete cg_slot_stack, cg_type_sp, cg_type_cap, PcgSlot, PcgLvAux, and parser-owned effective-address folding. The existing cexpr_*/cint_* subsystem may remain only because ICE is a later subsystem; it must not keep or resurrect a live expression shadow stack.
  5. Adapter deletion and helper relocation. Remove cg_adapter.c/.h and the pcg_ prefix. Move the C-semantic helpers (op maps, conversion lattice, inc/dec, atomic/intrinsic/inline-asm shaping) into ordinary parse-module functions that call kit_cg_* directly. Update doc/FRONTENDS.md to describe the durable parser → CG contract.
  6. ICE cutover. Add the parser CConstGuard; reimplement eval_const_int over the normal parse path under unevaluated mode (§4.7); delete the cexpr_* grammar and cint_* engine. Re-test every ICE context: case labels, array sizes (block + file scope), enum values, bit-field widths, alignas, _Static_assert, designated-init indices, invalid ICE syntax that still folds, sizeof not-evaluated exceptions, short-circuit exceptions, i128 constant expressions, i128 div/rem rejection/unknown behavior, and divide-by-zero diagnostics for supported div/rem widths.

Use targeted gates and redirect output to files per AGENTS.md:


8. Requirements checklist (the agent verifies against this)


9. Expected payoff

CG API self time is ~19% of compile, half of it the stack/memory family, so the directly visible ceiling of this cleanup is ~9-10% of compile. The real win is larger only insofar as deleting the parser's duplicate stack + EA bookkeeping also removes frontend work (one push/pop instead of two; no PcgLvAux maintenance; no if (emit) branching). A realistic target is 5-10% total compile improvement, plus the qualitative payoff: one expression stack, no fragile choreography, "parse expression, drive CG," and a clean home for future frontend instrumentation — with single-pass liveness and temp reclaim preserved.


10. Non-goals