commit 51c694af85e1ba3b7eae6763b22431cd5802087b
parent e55ca1f0335e253b0d5b7c61cf5a5e34c1cc6970
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 15 Jun 2026 09:59:46 -0700
docs: update CG stack API plan
Diffstat:
1 file changed, 196 insertions(+), 117 deletions(-)
diff --git a/doc/plan/CG-STACK-API.md b/doc/plan/CG-STACK-API.md
@@ -25,19 +25,19 @@ compatibility mode and no byte-identity gate.
| # | Decision | Choice |
|---|---|---|
-| D1 | Keep the stack API, add a stack-owned frontend slot sidecar | yes |
+| 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, sidecar facts, and constant facts while target emission is off |
+| 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 | **forced** — `lang/` builds only against `include/kit/cg.h` (no `src/cg` includes) |
-| D10 | Sidecar is a parallel array, not inlined into `ApiSValue` | **forced** — `ApiSValue` is `_Static_assert`'d `<= 64` bytes and is near the limit (`src/cg/value.c:14`) |
+| 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-owned side 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 128-bit integer folding in CG, including div/rem.** CG already has i128/u128 types and emitted i128 ops call runtime helpers (`__divti3`, `__udivti3`, `__modti3`, `__umodti3`). Factor the runtime two-limb arithmetic into a shared word-int implementation compiled into `libkit.a`, with runtime ABI wrappers and CG constant folding both calling it through sensible `{lo,hi}` signatures. Do not include `rt/lib/int64/int64.c` wholesale into `libkit` and do not make compile-time folding depend on target runtime helper symbols. |
+| 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. |
---
@@ -86,7 +86,7 @@ shadow-stack edit.
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. The sidecar never owns 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.
@@ -98,43 +98,69 @@ shadow-stack edit.
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.** The sidecar is a parallel
- array (D10).
-7. **No global state.** The sidecar, constant payload, and unevaluated counter
- hang off `KitCg`.
-8. **The sidecar is opt-in.** It is allocated only for a frontend that enables
- it; toy/wasm leave it off and pay nothing.
+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 + an opaque lang sidecar
+### 4.1 One stack + inline opaque lang facts
-A parallel array `KitCg.lang_side`, the same length as `KitCg.stack`, allocated
-lazily when the frontend calls `kit_cg_lang_enable`. Entry *i* shadows
-`stack[i]`. CG moves sidecar entries in exact lockstep with the value stack
-(§6). NULL `lang_side` means "no sidecar"; all sidecar ops degrade to no-ops /
-zeroed reads. A separate constant payload shadows the same stack depths whenever
-constant tracking is enabled; it is owned by CG and follows the same lockstep
-rules as the language sidecar, but it is not a C type/flags store.
+`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:
+
+```c
+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.
```c
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* */
- uint32_t lang_flags; /* frontend-defined (C: LVALUE/MODIFIABLE/...) */
+ uint16_t lang_flags; /* frontend-defined (C: LVALUE/MODIFIABLE/...) */
} KitCgSlotInfo;
```
-The sidecar stores only `{lang_type, lang_flags}`; `cg_type` in the returned
-struct is filled from `stack[i].type` at query time. The C flags are
-frontend-private bits; suggested C layout:
+`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) */
@@ -151,29 +177,28 @@ C_REGISTER /* `register` storage class — forbids `&`
### 4.2 Slot queries and retag
```c
-void kit_cg_lang_enable(KitCg*); /* alloc + track sidecar */
KitCgSlotInfo kit_cg_slot_info(KitCg*, uint32_t depth_from_top); /* depth 0 == top */
-void kit_cg_retag_top(KitCg*, const void* lang_type, uint32_t lang_flags);
+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, uint32_t lang_flags);
-void kit_cg_set_top_flags(KitCg*, uint32_t set, uint32_t clear);
+ 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 sidecar — 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`,
+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 sidecar
+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 sidecar entries so `lang_side[i]` always tracks `stack[i]`. This is
-exactly today's "emit op, then `pcg_retag_top`" idiom, with the structural half
+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)
@@ -269,10 +294,10 @@ void kit_cg_unevaluated_pop(KitCg*);
Contract while `unevaluated > 0`:
-- **Maintained:** stack depth, `KitCgSlotInfo` sidecar facts, and the constant
- payload. Every producing operation leaves a stack slot of the requested CG
- type when a type is available; structural ops move/copy both side payloads in
- lockstep with the value stack.
+- **Maintained:** stack depth, inline lang facts, and the constant payload.
+ Every producing operation leaves a stack slot of the requested CG type when a
+ type is available; structural ops move/copy `ApiSValue` nodes and update the
+ constant payload in lockstep with the value stack.
- **Folded:** integer `push`/`cast`/`unop`/`binop`/`cmp` update the constant
payload using the CG fold core. A producing op that cannot fold pushes an
unknown constant payload, not an emitted value. Unknown is the value-level
@@ -283,6 +308,15 @@ Contract while `unevaluated > 0`:
that the parser reaches while suppressed return benign dummy handles; those
handles exist only to keep one parse path shaped correctly and cannot be used
to recover emitted values later.
+- **Early op rule:** every stack-mutating op must test unevaluated mode before
+ it forces operands into locals, allocates temps/locals, materializes delayed
+ values, calls target hooks, or updates emitted-code-only analyses. In this
+ path the op performs only its declared stack effect, inline-lang preservation
+ or clearing, constant-payload movement, and constant-payload folding/unknown
+ marking.
+ This matters for ops that are "structural" in API shape but not in the current
+ implementation: `dup` can emit a copy today, `deref`/`addr` can allocate a
+ pointer temp, and place/field/elem ops can materialize address arithmetic.
- **File-scope safe:** the mode touches no function state, so it works before a
function is open. This is required for file-scope array bounds, enum values,
bit-field widths, and static initializers.
@@ -294,7 +328,7 @@ Contract while `unevaluated > 0`:
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 the sidecar, and push a known or unknown result constant:
+type/flags from `KitCgSlotInfo`, and push a known or unknown result constant:
- `&&` and `||` can fold from the known left operand without requiring the
short-circuited operand to be value-known, but the skipped operand is still
@@ -331,11 +365,12 @@ 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 parallel stack-owned
-payload: 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.
+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:
@@ -356,34 +391,40 @@ 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:
+
+- **Always-on:** every `KitCg` maintains the payload for every value-stack slot,
+ regardless of frontend, optimization level, or unevaluated depth. This matches
+ CG's existing design pressure: the semantic layer already folds immediate
+ arithmetic, delayed arithmetic/compare, and local-constant loads for `-O0`
+ code quality, and C already queries constant-ness during normal expression
+ parsing.
+- **Stack fact, not operand fact:** `kit_cg_top_const_int_ex` reads the payload,
+ not `ApiSValue.op.kind`. `OPK_IMM` becomes only one way to seed a known
+ payload. Values materialized as locals, delayed arith/cmp nodes, or wide
+ constants may still carry known payload bits.
+- **Unknown is explicit:** fresh runtime values (`load` from unknown memory,
+ call results, atomics, volatile access, inline asm, address values, and
+ unsupported arithmetic such as i128 div/rem) produce an unknown payload.
+- **Local-constant forwarding is separate:** `ApiSourceLocal.const_*` tracking
+ may seed the payload for an actual emitted load from a tracked local, but it is
+ still an emitting-mode optimization. The parser's `CConstGuard` decides whether
+ a local identifier is a legal C constant-expression operand; ordinary locals
+ do not become ICE operands just because CG can forward their current value.
+
Fold coverage required before the ICE cutover:
- **<=64-bit integers:** add/sub/mul, div/rem, bitwise ops, shifts, comparisons,
integer casts, and booleanization. Div/rem by zero must fail cleanly with a
diagnostic path the parser can report; it must not silently produce an
arbitrary constant.
-- **128-bit integers:** CG already has i128/u128 types and emitted i128
- arithmetic uses `libkit_rt` helpers. For compile-time folding, create a small
- shared word-int implementation compiled into `libkit.a` and used by CG through
- signatures like:
-
- ```c
- typedef struct KitInt128Words { uint64_t lo, hi; } KitInt128Words;
-
- int kit_int128_udivmod(KitInt128Words n, KitInt128Words d,
- KitInt128Words* q, KitInt128Words* r);
- int kit_int128_sdivmod(KitInt128Words n, KitInt128Words d,
- KitInt128Words* q, KitInt128Words* r);
- ```
-
- The implementation should factor the same `ut_add`/`ut_sub`/`ut_mul`/
- `ut_udivmod` algorithms currently in `rt/lib/int64/int64.c`. The runtime
- `__*ti3` ABI functions become thin wrappers around that shared implementation
- (or include the same shared `.inc`), so CG and runtime stay bit-for-bit
- aligned without duplicating logic. Keep the shared surface over explicit
- `{lo,hi}` words so the compiler does not depend on being linked against
- `libkit_rt`, on target ABI helper names, or on host support for TI-mode
- integers.
+- **128-bit integers:** move the existing parser `CConstInt` two-limb behavior
+ for add/sub/mul, bitwise ops, shifts, comparisons, and casts into CG so
+ existing static i128 initializer coverage remains supported. Compile-time
+ i128 div/rem is not required in this plan: emitted i128 div/rem lowers through
+ runtime helpers, and helper calls are dynamic code. CG marks i128 div/rem
+ payloads unknown until a true compile-time word-int division implementation is
+ added.
The replacement parser helper is one pass over the normal grammar plus a guard:
@@ -430,6 +471,36 @@ 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:
+
+- evaluated comma operator;
+- evaluated assignment and compound assignment;
+- evaluated pre/post increment and decrement;
+- evaluated function call, including builtin calls that are not specifically
+ permitted by the active constant category;
+- identifier/reference classification: enum constants are legal ICE operands,
+ ordinary objects/functions are not, regardless of any CG local-const fact;
+- floating constant use, including the narrow case where a floating constant is
+ immediately cast to an integer type and the broader cases where it is illegal
+ for ICE;
+- pointer/address expressions, string-literal addresses, label addresses, and
+ relocation-like constants, which belong to the static-address category rather
+ than C ICE;
+- `sizeof` / `_Alignof`: type-name forms are immediate constants; expression
+ forms enter a not-evaluated submode unless the operand is a VLA, where the VLA
+ size expression is evaluated and must be guarded normally;
+- `_Generic`: the controlling expression is not evaluated; selected and
+ unselected associations follow the standard's type/diagnostic rules, and only
+ the selected expression contributes the resulting value category;
+- `offsetof`: accepted through the parser's existing builtin path, but member
+ designator syntax must stay constrained to the builtin's rules;
+- short-circuit `&&`/`||` and `?:`: skipped operands are parsed under the
+ correct not-evaluated submode; selected/evaluated operands are guarded
+ normally;
+- unsupported arithmetic that CG marks unknown, including i128 div/rem in this
+ plan, reports "constant expression required" only after the guard has also
+ accepted the syntax.
+
Keep the categories separate:
- **C ICE:** integer result plus the C ICE guard; used by case labels, enum
@@ -448,13 +519,13 @@ Keep the categories separate:
| Today in `pcg` / `Parser` | Target owner |
|---|---|
-| `PcgSlot.type` (`const Type*`) | CG sidecar `lang_type` (opaque) |
+| `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) | CG sidecar `lang_flags` |
+| `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` (sidecar moves automatically) |
+| `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) |
@@ -469,12 +540,13 @@ directly, with no parallel stack and no EA state.
---
-## 6. Op-by-op: the sidecar + constant lockstep set
+## 6. Op-by-op: inline lang + constant lockstep
-Every stack-mutating CG op maintains both `lang_side` and the constant payload so
-depths stay in sync. Grouped by effect:
+Every stack-mutating CG op maintains inline lang facts and the constant payload
+so depths stay in sync. Grouped by effect:
-- **Producers (push a fresh slot, clear sidecar, set known/unknown const):**
+- **Producers (push a fresh slot, clear inline lang facts, set known/unknown
+ const):**
`push_int`, `push_float`,
`push_null`, `push_local`, `push_local_addr`, `push_symbol_addr`,
`push_label_addr`, `load`, `addr`, `deref`, `field`/`field_at`/`elem`/
@@ -482,30 +554,31 @@ depths stay in sync. Grouped by effect:
call result, `atomic_load`/`atomic_rmw`/`atomic_cmpxchg` result,
`intrinsic` result, overflow-builtin result, `inline_asm` outputs.
- **Retypers (1→1, keep depth):** `trunc`/`sext`/`zext`/`bitcast`/`fpext`/
- `fptrunc`/`int<->float`/`ptr<->int`, `int_unop`/`fp_unop`. Result sidecar
- cleared; parser retags. The constant payload is converted/folded when CG can
- do so, otherwise marked unknown.
+ `fptrunc`/`int<->float`/`ptr<->int`, `int_unop`/`fp_unop`. Result inline lang
+ facts are cleared; parser retags. The constant payload is converted/folded
+ when CG can do so, otherwise marked unknown.
- **Combiners (N→1):** `int_binop`/`fp_binop`/`int_cmp`/`fp_cmp` (2→1),
`field`/`elem` (consume base/index), `store_keep` (2→1), `call`/`call_symbol`
(N→0/1), `va_copy` (2→0), `atomic_store` (2→0), `atomic_cmpxchg` (3→…).
Integer combiners fold the constant payload when operands are known and the op
is supported; otherwise the result payload is unknown.
-- **Pure structural:** `dup` (copy top sidecar + constant payload), `dup2`,
- `swap`, `rot3`, `drop`, `store`/`store_void` (2→0). These move side payloads
- with no semantic change.
+- **Pure structural:** `dup` (copy top `ApiSValue` + constant payload), `dup2`,
+ `swap`, `rot3`, `drop`, `store`/`store_void` (2→0). These move inline lang
+ facts and constant payloads with no semantic change.
- **Consumers (→0):** `branch_true`/`branch_false`/`switch`/`computed_goto`,
`ret`.
-- **Scope edges:** `api_scope_store_results`/`api_scope_push_results` move both
- side payloads with carried results when active. (C uses only void scopes, so
- this is inert for C, but must be correct for any future result-carrying
- frontend that enables the sidecar.)
+- **Scope edges:** `api_scope_store_results`/`api_scope_push_results` move
+ carried `ApiSValue` results and their constant payloads when active. (C uses
+ only void scopes, so this is inert for C, but must be correct for any future
+ result-carrying frontend.)
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`). Routing side-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.
+(`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.
---
@@ -514,13 +587,15 @@ handling.
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 the CG sidecar both authoritatively model live expressions.
-
-1. **CG sidecar + slot queries.** Add `KitCgSlotInfo`, `lang_side`,
- `kit_cg_lang_enable`, `kit_cg_slot_info`, `kit_cg_retag_top`/`_at`, and
- `kit_cg_set_top_flags`; wire lockstep through `api_push`/`api_pop`,
- structural ops, and scope-edge movers. Add focused CG API tests for
- producer/consumer/structural/scope lockstep. No C frontend change yet.
+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
@@ -531,13 +606,11 @@ shadow stack and the CG sidecar both authoritatively model live expressions.
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 folds including div/rem via the shared word-int unit compiled into
- `libkit.a`. Add runtime wrapper tests or existing runtime tests to confirm the
- `__*ti3` ABI helpers still use the same implementation. No C frontend cutover
- yet. Add CG API tests for
- file-scope/no-function use, unknown constants, dummy handles, no target/local
- calls, nested unevaluated mode, and side-payload lockstep under suppression.
-4. **C frontend one-stack cutover.** Enable the lang sidecar for C; replace
+ 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
@@ -555,12 +628,13 @@ shadow stack and the CG sidecar both authoritatively model live expressions.
`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, and i128
- constant expressions including div/rem and divide-by-zero diagnostics.
+ 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`:
-- Sidecar/constant substrate: `make test-cg-api test-toy`.
+- Inline-lang/constant substrate: `make test-cg-api test-toy`.
- C frontend cutover: `make test-cg-api test-parse test-pp test-toy`.
- Codegen-sensitive place/store changes: `make test-cg-api test-isa
test-aa64-inline` plus targeted native smoke cases for x64/aa64/rv64 as
@@ -582,20 +656,24 @@ Use targeted gates and redirect output to files per `AGENTS.md`:
- R4 CG never dereferences the lang payload; it is `const void*`. (§3.4)
- R5 No backend (`CgTarget`/NDT/NT/MC) change; place ops lower through existing
contracts. (§3.5)
-- R6 `ApiSValue` stays `<= 64` bytes; sidecar is a parallel array. (§3.6, D10)
-- R7 No global state; sidecar, constant payload, and unevaluated counter hang off
- `KitCg`. (§3.7)
-- R8 Sidecar opt-in; toy/wasm pay nothing. (§3.8)
+- R6 `ApiSValue` is 64 bytes with inline `const void* lang_type`,
+ `uint16_t lang_flags`, and a packed `uint16_t flags` word. (§3.6, D10)
+- R7 No global state; inline lang facts live in `ApiSValue`, while the constant
+ payload and unevaluated counter hang off `KitCg`. (§3.7)
+- R8 No language side allocation or enable call; constant payload is always-on
+ for every `KitCg`. (§3.8, §3.10)
- R9 Suppression: type+flags+constant facts maintained, zero emission, zero
temp/local alloc, no forced CG-type lowering for type-only paths; parser loses
emission-bookkeeping forks, not semantic forks required by C control-shape
- constructs. (§4.5)
+ constructs. Every op checks unevaluated mode before local/temp allocation,
+ delayed materialization, target calls, or emitted-code-only analysis updates.
+ (§4.5)
- R10 Eager places; all EA folding (const offset, dynamic index, displacement
overflow, SEXT-load hint) decided inside CG; `PcgLvAux` deleted. (§4.3, D4)
- R11 Bit-field geometry rides the CG place (`field_bits`); C keeps a semantic
`C_BITFIELD` flag for `sizeof`/`&` rejection. (§4.1, §4.3)
-- R12 C value flags (MODIFIABLE / NULL_PTR_CONST / REGISTER / LVALUE) in
- `lang_flags`. (§4.1)
+- R12 C value flags (MODIFIABLE / NULL_PTR_CONST / REGISTER / LVALUE) fit in
+ `uint16_t lang_flags`. (§4.1)
- R13 `store` / `store_keep` replace assignment dup/rot3/swap. (§4.4)
- R14 No call-mark API; C args stay on the CG stack. (§4.6, D8)
- R15 ICE evaluation moves to CG's constant-value model plus a parser-owned
@@ -610,12 +688,13 @@ Use targeted gates and redirect output to files per `AGENTS.md`:
- R20 `eval_const_int` is reimplemented over the normal parse path with
`CConstGuard`; the `cexpr_*` grammar and `cint_*` engine are deleted; all ICE
contexts and invalid foldable-but-not-ICE cases are re-tested. (§4.7, §7.6)
-- R21 CG exposes a width-complete integer constant payload API; frontends do not
- inspect `ApiSValue` or `OPK_IMM` to determine constant-ness. (§4.7, D12)
-- R22 <=64-bit and 128-bit div/rem folding move into CG with clean
- divide-by-zero failure; the 128-bit path uses a shared `{lo,hi}` word-int
- implementation compiled into `libkit.a`, with runtime `__*ti3` helpers as
- wrappers or include-site users of the same implementation. (§4.7, D13)
+- R21 CG exposes an always-on, width-complete integer constant payload API;
+ frontends do not inspect `ApiSValue` or `OPK_IMM` to determine constant-ness,
+ and local-const forwarding is not C ICE legality. (§4.7, D12)
+- R22 <=64-bit div/rem folding moves into CG with clean divide-by-zero failure;
+ 128-bit add/sub/mul/bitwise/shift/compare/cast folding moves into CG, while
+ i128 div/rem returns unknown until a true compile-time word-int division
+ implementation exists. (§4.7, D13)
- R23 Cutovers are one-time subsystem moves with old paths removed; no dual-stack
or backcompat phase is introduced. (§7)