kit

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

commit e51e9c845ab7a87b875bd188fe1b90bff39705cc
parent 54cd542c9e1c6acb458dfe3ff604e99f1926554a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 09:00:29 -0700

doc: update cg+frontend plans

Diffstat:
Mdoc/plan/CG-STACK-API.md | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mdoc/plan/FRONTEND-SHAPE.md | 213++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 500 insertions(+), 142 deletions(-)

diff --git a/doc/plan/CG-STACK-API.md b/doc/plan/CG-STACK-API.md @@ -10,12 +10,14 @@ 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 also folds constants, so the -parser has one uniform path and one constant evaluator, and (d) **delete the -`pcg` adapter layer** so the parser drives the CG API directly. +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 spec below describes the -end state; §7 is a direct cutover, not an incremental dual-stack migration. +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. --- @@ -25,7 +27,7 @@ end state; §7 is a direct cutover, not an incremental dual-stack migration. |---|---|---| | D1 | Keep the stack API, add a stack-owned frontend slot sidecar | 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 a valueless type+sidecar stack; the parser drives CG uniformly with no `if (emit)` forks | +| 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 | | 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** | @@ -33,7 +35,9 @@ end state; §7 is a direct cutover, not an incremental dual-stack migration. | 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`) | -| D11 | C integer-constant-expression evaluation | **moves into CG.** Unevaluated mode also **folds constants** (not just types); the parser evaluates an ICE by parsing it once through the normal path under unevaluated mode and reading the folded value. The parser's duplicate `cexpr_*` grammar and `cint_*` arithmetic engine are **deleted**. | +| 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. | --- @@ -96,9 +100,15 @@ shadow-stack edit. 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 and the unevaluated counter hang off `KitCg`. +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. +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. --- @@ -110,7 +120,9 @@ 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. +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. ```c typedef struct KitCgSlotInfo { @@ -233,13 +245,22 @@ in both forms. ### 4.5 Unevaluated mode: fold + types, zero emission (D3, D11) -Unevaluated mode serves two needs that share one machine: (1) compute C result -types/flags while emitting nothing (`sizeof` operand, `_Generic` controlling -expr, statically-dead `&&`/`||`/`?:` arms, `extern inline` bodies, constant -initializer paths), and (2) **fold a constant expression** so the parser can read -its value (case labels, array sizes incl. file scope, enum values, bit-field -widths, `alignas`, `_Static_assert`; see §4.7). It is the single mechanism that -replaces both `suppress_codegen` and the parser's `cexpr_*`/`cint_*` evaluator. +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: + +- **Type/diagnostic-only parsing:** `sizeof` non-VLA operands, `_Generic` + controlling expressions and inactive associations, statically-dead expression + arms, `extern inline` bodies when the TU only needs diagnostics, and other + parse paths that must compute C result types/flags without emitting code. +- **Constant-value parsing:** ICE and static arithmetic-constant contexts where + the parser must parse the real expression grammar, suppress target emission, + then read the folded value from CG (§4.7). + +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." ```c void kit_cg_unevaluated_push(KitCg*); /* enter fold+type-only mode (nestable) */ @@ -248,33 +269,48 @@ void kit_cg_unevaluated_pop(KitCg*); Contract while `unevaluated > 0`: -- **Maintained:** stack depth, the sidecar (lang_type/lang_flags), and - **constant folding** of immediate operands — `push_int`/`binop`/`unop`/`cmp` - fold through the existing C-correct core (`api_try_fold_int_binop/unop/cmp`, - `api_fold_result`, `src/cg/fold.c`), so `kit_cg_top_const_int` answers - correctly. Structural ops move sidecar entries normally. -- **Skipped:** every `CgTarget`/emission call, temp/local allocation, const-local - store/load tracking, `-O0` ref accounting, and the aggregate-place validation - in `api_push`. A producing op that *cannot* fold to an immediate pushes a - value-less node (operand = none/poison) — its `kit_cg_top_const_int` is simply - false, which is the "not a constant expression" signal (§4.7). -- **No forced CG-type lowering for the type-only need.** The parser reads C - facts from the sidecar, not the node `type`; push ops tolerate - `KIT_CG_TYPE_NONE`. (Constant folding *does* use the supplied CG type for - width/signedness, so an ICE parse passes real types — cheap, cached.) -- **Function-independent.** Folding allocates nothing and touches no function - state, so the mode is valid at file scope (no open function). Handle-returning - control ops (`label_new`, `scope_begin`, `scope_*_label`) return benign - dummies; `label_place`/`jump`/`branch_*`/`scope_end` tolerate them. The - `&&`/`||`/`?:` parsers fold their all-constant cases via `kit_cg_top_const_int` - (which now works here) and their branch/temp emission no-ops away. - -This generalizes suppression from "skip the value op" to "skip *all* of CG," -letting the parser **delete its `if (emit)` forks** entirely (e.g. the dual-path -loop in `parse_stmt.c:85-118` collapses to one path). `suppress_codegen` moves -out of `Parser` into `KitCg`. Parser-side semantic side-effects in suppressed -regions (VLA-size slot recording for `sizeof`, `_Generic` association -collection, diagnostics) still run, because the parser code path is unchanged. +- **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. +- **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 + "not a constant" signal; it is distinct from "illegal C ICE syntax." +- **Skipped:** every `CgTarget`/emission call, temp allocation, local allocation, + const-local store/load tracking, `-O0` ref accounting, and aggregate-place + validation that exists only to protect emitted operands. Handle-returning APIs + 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. +- **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. +- **Type-only tolerant:** pure type/diagnostic paths may use + `KIT_CG_TYPE_NONE` when no CG type is needed. Constant folding still requires + real CG integer types so CG knows width and signedness. + +`&&`, `||`, 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 the sidecar, 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 + parsed for types and diagnostics under the correct C "not evaluated" guard. +- `?:` folds when the condition is known and the selected arm is known; the + unselected arm is still parsed for type merging and diagnostics under the + correct guard. When the condition is unknown, the result constant is unknown + even if both arms happen to have the same value, unless CG later grows a + deliberate meet rule. + +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) @@ -291,43 +327,120 @@ grammar* (`cexpr_mul`/`add`/`shift`/`rel`/`eq`/`band`/`bxor`/`bor`/`land`/…) p 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**. +All of it is **deleted in the ICE cutover phase**. -The replacement is one helper over the normal path: +"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. +Proposed public shape: + +```c +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); ``` -i64 / CConstInt eval_const_int(p, loc): - kit_cg_unevaluated_push(cg); - parse_conditional(p); /* the REAL grammar, folding as it goes */ - ok = kit_cg_top_const_int(cg, &v); /* (or the i128 variant below) */ - info = kit_cg_slot_info(cg, 0); /* C result type from the sidecar */ - kit_cg_drop(cg); - kit_cg_unevaluated_pop(cg); - if (!ok) perr("constant expression required"); - return { v, info.lang_type }; -``` -Consequences: +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. + +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: -- One grammar, one fold engine. The constant value comes from the same - C-correct fold core used at runtime (`src/cg/fold.c`); the C *type* of the - result comes from the sidecar (the normal path ran usual-arithmetic - conversions). `sizeof`, `_Alignof`, `offsetof`, `_Generic`, and enum constants - are handled once, by the real parser — a correctness win. -- **New API:** a width-complete constant query for i128 ICE, since - `kit_cg_top_const_int` is 64-bit: ```c - int kit_cg_top_const_i128(KitCg*, uint64_t* lo, uint64_t* hi); /* or a bytes form */ + 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); ``` -- **Diagnostics tradeoff:** failure to fold yields a generic "constant - expression required" rather than `cexpr_*`'s targeted messages, and ICE - constraints are enforced semantically (non-foldable → reject) rather than - syntactically (e.g. the comma operator is foldable yet is not an ICE). A light - syntactic guard can be added where a precise diagnostic is required. -- Address/static-initializer constants (`&x + 4`, string-literal addresses, - designated-init relocations) are a *separate* constant category handled in - `parse_init.c`; they are **out of scope** here (a later unification could fold - them onto a CG constant-data path, but not in this plan). + + 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. + +The replacement parser helper is one pass over the normal grammar plus a guard: + +```c +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: + +- `(0, 1)` folds to `1`, but the comma operator is not permitted in an ICE when + it is evaluated. +- `(x = 1, 1)` and `f(), 1` can have foldable tails, but assignment and function + call syntax still make the evaluated expression illegal. +- `sizeof(x++)` can be valid when the operand is non-VLA because the increment is + not evaluated; the guard must understand these not-evaluated exceptions rather + than reject tokens blindly. +- `enum` constants are ICE operands; ordinary objects are not, even if some + optimization or local-const tracking could know their value. +- Floating constants are only allowed in the narrow C cases such as the immediate + operand of a cast to integer type; the fold payload alone cannot encode that + syntactic permission. + +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. + +Keep the categories separate: + +- **C ICE:** integer result plus the C ICE guard; used by case labels, enum + values, bit-field widths, array bounds that require ICE, `alignas`, and + `_Static_assert`. +- **Static arithmetic constants:** can reuse the CG payload and normal parser + path, but may have different C legality from ICE. +- **Static address/relocation constants:** `&x + 4`, string-literal addresses, + and designated-init relocations remain in `parse_init.c` for this plan. A + later project can move them onto a CG constant-data model, but this plan does + not. --- @@ -340,11 +453,11 @@ Consequences: | `PcgSlot.flags` (C value flags) | CG sidecar `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.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) | | 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** — ICE folds on CG under unevaluated mode (§4.7) | +| `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_*` @@ -356,12 +469,13 @@ directly, with no parallel stack and no EA state. --- -## 6. Op-by-op: the sidecar lockstep set +## 6. Op-by-op: the sidecar + constant lockstep set -Every stack-mutating CG op maintains `lang_side` so depths stay in sync. Grouped -by effect: +Every stack-mutating CG op maintains both `lang_side` and the constant payload so +depths stay in sync. Grouped by effect: -- **Producers (push a fresh slot, clear its sidecar):** `push_int`, `push_float`, +- **Producers (push a fresh slot, clear sidecar, 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`/ `elem_scaled` (consume then push a place — clear), `alloca`, `vararg_next`, @@ -369,70 +483,93 @@ by effect: `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. + 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→…). -- **Pure structural:** `dup` (copy top sidecar), `dup2`, `swap`, `rot3`, `drop`, - `store`/`store_void` (2→0). These move sidecar entries with no semantic change. + 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. - **Consumers (→0):** `branch_true`/`branch_false`/`switch`/`computed_goto`, `ret`. -- **Scope edges:** `api_scope_store_results`/`api_scope_push_results` move the - sidecar with carried results when `lang_side` is 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 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.) 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 the sidecar move through `api_push`/`api_pop` -(which every producer/consumer already calls) covers most ops automatically; the -structural ops and scope-edge movers need explicit sidecar handling. +(`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. --- ## 7. Cutover plan (clean break, gate = correctness + determinism) -No dual-stack interim, no byte-identity gate. Land in dependency order: - -1. **CG sidecar + queries.** Add `KitCgSlotInfo`, `lang_side`, - `kit_cg_lang_enable`, `kit_cg_slot_info`, `kit_cg_retag_top`/`_at`, - `kit_cg_set_top_flags`; wire lockstep through `api_push`/`api_pop` + the - structural ops + scope-edge movers. No frontend change yet. -2. **CG unevaluated mode.** Add `kit_cg_unevaluated_push`/`pop` with the - fold-constants + maintain-stack + skip-emission/skip-alloc contract (4.5), - valid at file scope. -3. **CG place + store + const ops.** Add `kit_cg_field_at`, `kit_cg_store_keep`, - and the i128 constant query (`kit_cg_top_const_i128`); confirm - `deref`/`elem`/`field_bits` compose to the fused operand without parser help. -4. **Rewrite the C frontend onto the new API.** Delete `cg_slot_stack` / - `cg_type_sp` / `cg_type_cap` / `PcgSlot` / `PcgLvAux`; drive `kit_cg_*` - directly + retag; replace EA folding with eager places; replace assignment - choreography with `store`/`store_keep`; call `kit_cg_lang_enable` at - function/unit setup; move `suppress_codegen` to the CG unevaluated counter and - drop the parser `if (emit)` forks. -5. **Delete the adapter.** Remove `cg_adapter.c`/`.h` and the `pcg_` prefix; - relocate the C-semantic helpers (op maps, conversion lattice, inc/dec, - atomic/intrinsic/inline-asm shaping) as ordinary parse-module functions - (D7). Update `doc/FRONTENDS.md` to describe the durable parser → CG contract. -6. **Move ICE onto CG (last).** Reimplement `eval_const_int` over the normal - parse path under unevaluated mode (§4.7); delete the `cexpr_*` grammar and the - `cint_*` engine. Sequenced last so the stack rewrite is validated before the - constant subsystem is removed. Re-run every ICE context: case labels, array - sizes (block + file scope), enum values, bit-field widths, `alignas`, - `_Static_assert`, designated-init indices, and i128 constant expressions. - -Gate at each step: - -- `make test-cg-api test-parse test-pp test-toy` (frontend + CG API), -- `make test-cross` / targeted smoke for codegen correctness across arches, -- determinism: same input → identical output across repeated runs, -- toy/wasm unaffected (sidecar off): `test-toy`, wasm front suite. - -Targeted codegen tests for the place rewrite: local/global load+store; `s.f`, -`p->f`, `a[i]`, `a[i].f`; bit-field load/store; assignment-expression value -preservation (`a=b=c`, `if((x=f()))`); `++`/`--` and compound assignment; -`&` of each lvalue form; struct lvalue / sub-object rvalue; volatile and atomic -accesses; `sizeof`/`_Generic`/dead-arm suppression returning the right types. +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. +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 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 + 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, and i128 + constant expressions including div/rem and divide-by-zero diagnostics. + +Use targeted gates and redirect output to files per `AGENTS.md`: + +- Sidecar/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 + appropriate. +- Cross/portability: run `make test-cross TARGET=<selector> DEPTH=smoke` after + the one-stack C cutover is green on native targets, and a broader portability + pass before final handoff. `make test-port` is not a per-phase gate. +- Determinism: same input → identical output across repeated runs; byte identity + against the old implementation is not required. --- @@ -446,10 +583,13 @@ accesses; `sizeof`/`_Generic`/dead-arm suppression returning the right types. - 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 + unevaluated counter hang off `KitCg`. (§3.7) +- 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) -- R9 Suppression: type+flags maintained, zero emission, zero temp/local alloc, no - forced CG-type lowering; parser loses its `if (emit)` forks. (§4.5) +- 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) - 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 @@ -458,19 +598,26 @@ accesses; `sizeof`/`_Generic`/dead-arm suppression returning the right types. `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 stays parser-side and suppression-independent. (§4.5) +- R15 ICE evaluation moves to CG's constant-value model plus a parser-owned + syntactic/semantic C constant guard. (§4.7, D11) - R16 Gate = correctness + determinism, not byte-identity. (D5) - R17 `cg_adapter.c`/`.h` and the `pcg_` prefix deleted; parser drives `kit_cg_*` directly; C-semantic helpers survive as plain functions. (§5, D7) - R18 inc/dec, compound assignment, va_arg, atomics, intrinsics, inline-asm, calls, returns all re-expressed on the new place ops + slot queries. (§6) -- R19 Unevaluated mode folds constants (not just types) and is valid at file - scope; `&&`/`||`/`?:` fold their all-constant cases under it. (§4.5) -- R20 `eval_const_int` reimplemented over the normal parse path; the `cexpr_*` - grammar and `cint_*` engine deleted; all ICE contexts re-tested. (§4.7, §7.6) -- R21 i128 constant query (`kit_cg_top_const_i128`) added; ICE diagnostics - preserved adequately ("constant expression required" + optional syntactic - guards). (§4.7) +- R19 `&&`/`||`/`?:` have explicit unevaluated fold paths; they do not rely on + no-op labels/branches to leave the right stack result. (§4.5) +- 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) +- R23 Cutovers are one-time subsystem moves with old paths removed; no dual-stack + or backcompat phase is introduced. (§7) --- diff --git a/doc/plan/FRONTEND-SHAPE.md b/doc/plan/FRONTEND-SHAPE.md @@ -235,7 +235,218 @@ fusion). What *is* available: posture: bank the scanner lever (−12 %), continue the touch-once grind, treat "within 1.5× of tcc" as a multi-campaign destination — unless the modularity constraints are relaxed (a tcc-style monolithic hot path), which is explicitly out of scope. **The -structural hunt (§D) is closed; the path forward is incremental.** +structural hunt (§D) is closed; the path forward is incremental.** *(Reopened +2026-06-15 — §E: direction to relax the no-fusion constraint, the one lever §D.8 +named as able to move the uniform-density factor.)* + +--- + +## E. Direction (2026-06-15): front-half fusion around a shared tagged cell + +§D.8 is the premise, not a contradiction: the gap is a **uniform ~2.8× +per-operation density** factor — kit and tcc spend instructions in nearly the same +*proportions*, kit is ~2.8× denser **everywhere** — so there is no one phase to +crush, and the only lever that reduces density *across every phase at once* is the +one §D.2 named and then declined: **fusion**. It removes the per-token / per-op +*overhead* — inter-stage call/return per token, the 32 B token copied through +slots, the parallel value stacks, the public value-stack API layer, the CG +type-universe re-query, growable-container bounds checks — which is exactly the +pervasive tax §D.8 measured. The landed lean-token boundary (LEX-PP-API.md) is the +confirming negative: a lean *representation that still copies through separate +layers* nets ≈ −1.3 % (drain-measured lex+pp still **2.1× tcc**), precisely as +§D.6/§D.8 predicted a representation-only change would. **Representation was never +the lever; the handoff and the per-op layering are.** This section relaxes the +no-fusion constraint under explicit direction and specifies the fused design. + +### E.1 What is relaxed; what stays invariant + +**Relaxed.** The front half — **lexer + preprocessor + parser** — fuses around one +shared, in-place **cell** and one value stack. The parser stops being a module that +consumes an abstract token stream *by value*; it shares the cell representation with +the pp output and drives emit off it directly. The standing "leave the modules as +separable as they are" rule (§D.3) is dropped *for the front half*. + +**Invariant — two hard constraints the fusion must preserve:** +1. **The preprocessor stays independently drainable.** `cpp` / `cc -E` / + `KIT_PP_DRAIN` must run lexer+pp to a token stream with **no parser attached**. + Mechanically: the cell's *lexeme* fields are self-contained, the pp reads/writes + only those, and `pp_next` remains the standalone pull. The parser's *semantic* + tags live in a region the pp never touches. +2. **The `CgTarget`→`NativeTarget` backend seam + seven backends stay** (§4.3). The + fused value stack drives emission *through* the kept seam (its two indirect calls + per primitive are not the cost — §D.8). We fuse the *front half*, not the backend. + +One line: **one record flows bytes→token→value with no copy and no re-derivation; +the pp can still be drained at the token stage; the backend seam is untouched.** + +### E.2 The shared cell + +One record serves the whole front half. The lexer/pp fill the **lexeme** view; the +parser resolves a primary **in place** into the **value** view, at which point the +cell *is* a value-stack entry (kit's `ApiSValue`, tcc's `SValue`). The two views +overlap — a resolved value no longer needs spelling/`aux` — so the record stays +small; `loc` and `flags` survive both. Exact layout is an implementation detail to +size against the < 64 B value-node discipline; the shape: + +```c +typedef struct Cell { + u16 kind; /* token kind in lexeme view; value-node kind once resolved */ + u16 flags; /* lexeme: BOL/space/suffix/encoding · value: lvalue/const/... + * (disjoint bit ranges — the two views do not co-live) */ + u32 loc; /* byte offset; kept through to emit for diagnostics + DWARF */ + union { + struct { /* LEXEME — lexer/pp own; the pp drain reads ONLY this */ + u32 aux; /* IDENT: interned Sym · PUNCT/#/##: code · PARAM: idx */ + u32 text; /* spelling ref: (len @loc) for source, or a Sym, tagged */ + } t; + struct { /* VALUE — parser owns; the pp NEVER reads/writes this */ + u32 type; /* int-bitmask scalar type, or a CG type-id (fallback) */ + u32 r; /* storage class / value-stack location (local/reg/const)*/ + i64 c; /* immediate when constant-known (parse-time fold) */ + } v; + } u; +} Cell; +``` + +- **`type` is an int-bitmask for the scalar common case** (signedness × width × + float/ptr/bool), carried on the cell so the hot path never calls + `api_unalias_type`/`pred_bits`/`class`/`size` — the §D.2 "classify-once" facts, + now *inline on the flowing value* rather than a cache beside the heavy `Type`. + Anything a bitmask can't carry (aggregate, bitfield, VLA, `_Atomic`, wide / + `__int128`) sets a `FALLBACK` bit and `type` indexes the full CG type universe; + the slow path is unchanged. This is the §D "int-bitmask type on a lean value slot, + with fallback" made concrete on the unified cell. +- **No separate `PcgSlot` + `ApiSValue`.** Today the front half keeps two parallel + value stacks in lockstep (§0 Pillar 3). The cell *is* the single stack entry; + Tier 3's "CG-owns-one-stack-with-an-opaque-aux-slot" is subsumed — the aux *is* + the value view of the cell. + +### E.3 Data flow (explicit, end to end) + +``` + source bytes + │ scan (cclass / word-at-a-time) ── LEXER ── + ▼ + Cell.t ← {kind, flags(BOL/space/suffix), loc=off, aux=Sym|punct, text=span} + │ written into a small ring slot (cur / next / pending); NOT returned by value + ▼ + ── PREPROCESSOR ── reads/writes only Cell.t + │ · directive recognition, #if skip, include push (source stack) + │ · macro expansion: bodies are Cell[] streams, replayed by pointer; + │ hidesets ride a side-channel keyed by stream position (never on the cell) + │ · __LINE__/__FILE__/paste/stringize synthesize Cell.t (text = interned Sym) + ▼ + pp_next(pp, &slot) ── the drain boundary ── + ├───────────────► cpp / cc -E / KIT_PP_DRAIN : consume Cell.t, serialize/discard. + │ No parser, no Cell.v ever touched. + ▼ + ── PARSER ── tags the slot IN PLACE, then it becomes a value-stack entry + │ primary IDENT: resolve Sym → binding (BindingTab, O(1)) → write Cell.v + │ {type(bitmask|fallback), r=storage, c=const?}; push. + │ primary literal: decode text → Cell.v {type, c}; push. + │ operator: reduce the top Cell.v's on the value stack, driving emit. + ▼ + ── CG-DRIVE ── the value-stack Cell.v's call the CgTarget seam directly + │ (no public ApiSValue push/pop API in between; the cell carries r/type/c) + ▼ + CgTarget → NativeTarget → MCEmitter (KEPT seam; emits bytes) +``` + +The copies that exist today and disappear: (a) `lex_next` sret → `pp` slot → parser +`fetch_tok` by-value return → `p->cur` (three hops → one in-place write); (b) token +→ separate `PcgSlot` *and* `ApiSValue` derivation (→ the primary's cell *is* the +value entry); (c) the per-op `Type`-universe queries (→ bitmask on the cell). The +remaining unavoidable move is `next → cur` promotion for LL(2) lookahead (one struct +move), as today. + +### E.4 Tagging: token → value, in place + +This is the fused step the boundary rewrite set up but did not take. When the parser +accepts a **primary**: + +- **Identifier.** `kind==IDENT`, `u.t.aux` is the interned `Sym`. Resolve once: + keyword via `kw_map`; else binding via `BindingTab` (already O(1), §0 Pillar 2 — + the kit-native `TokenSym.sym_identifier`). Overwrite the cell's value view: + `u.v.type` = the binding's int-bitmask type (or fallback id), `u.v.r` = its storage + (local frame slot / global sym / enum const → `c`). The cell is now an SValue; + push it. **No second lookup, no `PcgSlot`+`ApiSValue` pair, no `Type`-universe + call** for the scalar case. +- **Literal.** Decode `u.t.text` (via the text helper) into `u.v.c` + `u.v.type` + from the suffix/encoding `flags`; push. (Decode stays lazy — only literals that + reach a value are decoded, as now.) + +Operators never "tag a token" — they consume value-cells off the stack and call the +seam. So "the parser tags the token with symbol/type" is precisely: **a primary's +lexeme cell is resolved in place into the value cell it becomes**, fusing the +token stream and the value stack into one representation with one resolution. + +Diagnostics/DWARF: `loc` rides the cell into `u.v`, so `pcg_set_loc` reads it off the +live value entry; line/col still materialize lazily through the pp (LEX-PP-API.md). + +### E.5 How pp drainability survives the fusion + +- The **lexeme view (`Cell.t`) is closed under lex+pp**: every pp operation + (directives, macro expansion, paste/stringize, `__LINE__`) produces and consumes + only `Cell.t`. Nothing in the pp reads `Cell.v`. +- `pp_next(pp, &slot)` is the **single standalone entry**: `cpp`, `cc -E`, and + `KIT_PP_DRAIN` call it in a loop over a slot they own and never tag. The parser is + just *another* caller that happens to then tag the slot. Fusing the pp's internal + pull chain (`lex_next`→`src_next_raw_into`→`pp_pull_into` → one tight `pp_next`, + tcc's `next_nomacro`+`next` shape) is **internal** to the pp module and does not + touch this boundary. +- So "fused" here means **shared representation + in-place tagging + a collapsed + internal pull**, *not* one mega-function. The pp stays a module with a drain entry; + the parser stays a module that drives the seam. What's gone is the *by-value + handoff and the duplicate value representation* between them. + +### E.6 Sequenced build (each byte-identity-gated) + +Order matters: land the cheap copy/handoff collapses first (they're the Tier 1/2/3 +work below, now repurposed as fusion precursors), then the two genuinely-fused steps. + +``` +E-pre Tier 1B + 1A (macro-arg copies; newline→BOL) — relay slimming, byte-id + Tier 2 (out-pointer pull into parser ring) — kills the by-value copy +E-a collapse the pp internal pull chain into one pp_next (next_nomacro shape); + pp stays drainable. Internal-only; byte-id. +E-b introduce Cell; lexer/pp fill Cell.t; parser ring holds Cells; pp drain + reads Cell.t. (Still derives PcgSlot/ApiSValue separately — no value fusion + yet.) Byte-id; this is the representation swap, isolated. +E-c value fusion: a primary's Cell.t resolves in place to Cell.v; Cell IS the + single value-stack entry; delete cg_slot_stack + the ApiSValue duplicate; + drive the CgTarget seam off Cell.v. Byte-id; the big structural step. +E-d int-bitmask type on Cell.v for scalars + fallback bit to the CG type + universe. Byte-id; this is the type-density lever. +``` + +Every step keeps the emitted object **bit-for-bit identical** (it changes how data +flows, not what is emitted) — same gate as the boundary rewrite: `make perf-golden` +→ edit → `make perf-gate` (incl. the splice/`#line`/diagnostic battery), full +pp/parse suites, and verify on **x64 + rv64** for the value-stack steps (shared NDT +infra, §5). `KIT_PP_DRAIN` + the tcc `-bench` drain (PERF.md §3) measure lex+pp after +each step to confirm the gap is actually closing. + +### E.7 Prototype-first; payoff; the floor + +Per §D.4's discipline, **validate the compounding before building the full lane.** +The decisive prototype is **E-c on a reduced grammar slice** (scalar locals + arith + +calls, end to end bytes→emit on the unified cell, no fallback paths) measured +instr/byte against tcc — because §D.6 proved representation alone is ~6 %, the +*fusion* (removed handoff + removed dual stack + removed type re-query) is the +untested variable. If a fused scalar slice does not move materially toward tcc's +density, fusion is also marginal and ~2.5× is the floor for kit's engineering style +(the honest §D.4 stop). If it does, build E-a..E-d in order. + +Payoff and floor, stated honestly: +- This attacks the **pervasive** factor §D.8 found, so unlike the −12 % scanner lever + it is *not* bounded to one phase — it is the credible path toward 1.5× tcc. +- It is **unproven and expensive** (a front-half rewrite, larger than the boundary + rewrite), and it **spends the front-half modularity** (the parser fuses with the + pp output; only pp-drain + the backend seam remain as boundaries). +- The **kept floor**: the `CgTarget` seam's two indirect calls per primitive and the + seven-backend generality remain by §4.3 decision — full tcc parity (a single + monolithic hot path through emission) is still out of scope; the target is *1.5×*, + not parity. ---