kit

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

commit 20c503f2205fc65f0715651760fb19035c429301
parent 75ba470e205bb1f666c1160cae5461a4bc4d8859
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 13:44:41 -0700

plan: cg type debug split

Diffstat:
Adoc/plan/CG-TYPE-DEBUG-SPLIT.md | 373+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/CG-TYPES.md | 5+++++
Mdoc/plan/README.md | 1+
3 files changed, 379 insertions(+), 0 deletions(-)

diff --git a/doc/plan/CG-TYPE-DEBUG-SPLIT.md b/doc/plan/CG-TYPE-DEBUG-SPLIT.md @@ -0,0 +1,373 @@ +# CG type/debug split: move source spelling off the operational type id + +**Goal.** Make `KitCgTypeId` a storage/ABI/operational identity *only*, and carry +source-facing spelling (primitive sign+name, typedef names, enum underlying +spelling) on a separate, optional debug channel consumed at the points where +debug info is actually produced. The result removes pervasive alias-resolution +from the CG hot path: with no transparent wrapper kinds left in the operational +lattice, `api_unalias_type`, the `storage_id`-vs-`id` split, and the +per-backend `if (ALIAS) recurse; if (SOURCE_BASE) recurse` boilerplate all +disappear. + +This resolves [CG-TYPES.md](CG-TYPES.md) §10 Open Question 1 +("Should `KIT_CG_TYPE_ALIAS` remain a true public kind, or should source aliases +move entirely into debug/C-backend metadata?") — extended to cover +`KIT_CG_TYPE_SOURCE_BASE`, which was added after that question was written. + +This is a **clean-break redesign, no backcompat**, in the spirit of CG-TYPES.md. +The capability that `KIT_CG_TYPE_SOURCE_BASE` delivers (faithful signed/unsigned/ +char debug, plus enum enumerators) is **preserved** — only its placement moves. + +--- + +## 1. Current State + +The public type lattice (`include/kit/cg.h`, `src/cg/type.c`) contains two +*transparent wrapper* kinds whose only purpose is debug-info spelling: + +- `KIT_CG_TYPE_ALIAS` — a named typedef over a base type (`kit_cg_type_alias`). + **Dead for every real frontend**: the only callers are `test/api/cg_type_test.c`. + The C frontend never creates aliases (so C typedef names are already absent + from DWARF today). +- `KIT_CG_TYPE_SOURCE_BASE` — a source-facing scalar spelling (name + + `KitCgDebugEncoding`) over a width-only storage builtin (`kit_cg_type_source_base`, + added by commit `244f1039`). The C frontend lowers **every** integer/char + scalar to one of these (`lang/c/type/type.c`, `type_cg_scalar`), so in practice + almost every integer id flowing through CG is a wrapper. + +Both kinds are *transparent to storage*: every ABI/codegen/predicate site strips +them to the terminal builtin via `api_unalias_type` before use. + +### What the wrappers buy us (measured) + +- The **only** consumer of the exact wrapper kind is debug info: `api_debug_type` + (`src/cg/debug.c`) maps `ALIAS → DW_TAG_typedef` and + `SOURCE_BASE → DW_TAG_base_type + DW_ATE_*`. Reached at three live sites — the + subprogram type at `kit_cg_func_begin_attrs` and locals/params in + `api_debug_emit_source_locals` (`src/cg/session.c`). +- `src/opt/` has **zero** references to either kind. The optimizer only ever + sees unaliased storage types. +- Nothing in ABI/calling-convention/operation lowering reads the wrapper: + signedness of operations comes from op selection (`SDIV`/`UDIV`, `sext`/`zext`) + and from `KIT_CG_ABI_SIGNEXT`/`ZEROEXT` attrs; load signedness comes from the + `KIT_CG_MEM_SOURCE_SIGNED` flag. The encoding on a source-base is a debug + vehicle, never consulted for codegen. +- **Struct/union member types do not reach DWARF at all today**: `api_debug_type`'s + record case calls `debug_type_record_begin` then immediately + `debug_type_record_end`, never `debug_type_record_field`. So source-base + spelling on field types — and on interned func-sig param types — is consumed by + nothing. +- Enums *do* now thread enumerators (commit `244f1039`): `api_debug_type` emits + `DW_TAG_enumeration_type` with `DW_TAG_enumerator` children over the enum's + underlying type. The underlying type may itself be a source-base. + +### What it costs + +- `api_unalias_type` is called pervasively: ~33 per-op hot-path sites + (`src/cg/value.c`, `arith.c`, `call.c`, `memory.c`), ~9 more inside `type.c` + itself (the float/i128 predicates, predicate-bits fill, complete/sized/valid, + enum-base validation, `same_storage`), plus the recursive alias-stripping + prologue duplicated across **every ABI backend** (`src/abi/abi.c`, + `abi_rv64.c`, `abi_sysv_x64.c`, `abi_aapcs64.c`, `abi_win64_x64.c`, + `src/arch/wasm/abi.c`) and ~10 functions in `type.c`/`fold.c` + (`cg_type_pointee`, `cg_type_func_ret_id`, `kit_cg_type_int_width/float_width`, + `api_int_like_width`, `api_type_is_bool`, `cg_type_complete_id`, + `cg_type_sized_id`, `cg_type_same_storage_rec`). +- A per-type-entry caching subsystem that exists largely to amortize the above: + `storage_id` (the unalias terminal, filled once by `api_type_info_fill`), + `pred_bits`/`pred_valid` (predicate bitset computed on the *unaliased* type). +- A two-headed type-info surface (`kit_cg_type_view` identity prefix vs + `kit_cg_type_info` full snapshot) plus `storage_id`, + `kit_cg_type_resolve_alias`, and `kit_cg_type_same_storage`, all there to + manage the alias↔storage split. `kit_cg_type_is_void` is a two-hop through + `storage_id`. + +### Recent movement (reconciled) + +Three same-day changes touched this area and the plan is written against the +*current* working tree: + +- `64afd07c` (clean cutover) — direct-index ids; void is always the void + builtin; `KIT_CG_TYPE_NONE` is invalid/absent only. +- `244f1039` — *added* `SOURCE_BASE` and threaded enum enumerators into debug + info. This plan reverses the **placement** of that spelling (onto a debug + channel) without dropping the **capability**. +- Uncommitted working tree — consolidated the unalias caching: deleted the + redundant `CgApiTypeKind`/`CgApiType.kind` (entry kind is now `e->cg.kind`), + folded the old `unaliased`/`unalias_filled` fields into a single + `info.storage_id` filled by `api_type_info_fill`, and made `kit_cg_type_info` + delegate to `kit_cg_type_view`. This *cleaned up* the cost but did not remove + the unaliasing — the pervasiveness above is fully intact, and `storage_id` is + now load-bearing. + +This consolidation is evidence for the split, and makes it cleaner to land: the +unalias walk is now concentrated in one place (`api_type_info_fill`) instead of +two. + +--- + +## 2. Problems + +### 2.1 One id, two roles + +`KitCgTypeId` is overloaded to be both the storage/ABI/operational identity (what +every op needs; width-only for integers) and the source spelling for debug (name ++ signedness). To make one id serve both, transparent wrappers carry role-2 data +but must be seen through for every role-1 use. That see-through is the pervasive +`api_unalias_type`. + +### 2.2 Distinct spellings force distinct operational ids + +Two C types (`int`, `unsigned int`) share storage `i32`, so a spelling cannot +hang off the shared storage id — each spelling needs its own id. As long as those +ids are *operational* and handed to ops, ops must map them back to storage. The +only way to remove unaliasing is for ops to receive storage ids and for spelling +to travel separately. + +### 2.3 The payoff is tiny and shrinking + +The exact wrapper kind is consumed at three debug sites; `ALIAS` is dead; struct +members (the bulk of would-be spelled types) are not emitted at all. The cost — +pervasive unalias + a caching subsystem + per-backend boilerplate + a split type +API — is paid everywhere to feed a narrow, cold consumer. + +--- + +## 3. Design Decisions + +| # | Decision | Choice | +|---|---|---| +| E1 | Operational lattice | `KitCgTypeId` kinds shrink to `VOID, BOOL, INT(width), FLOAT(width), PTR, ARRAY, FUNC, RECORD, ENUM, VARARG_STATE`. No `ALIAS`, no `SOURCE_BASE`. | +| E2 | Storage identity | `storage_id == id` for every operational type. `api_unalias_type` is deleted; `kit_cg_type_resolve_alias` becomes identity; `kit_cg_type_is_void` is a single kind check. | +| E3 | Debug spelling home | A separate, optional `KitCgDebugType` handle, built only when debug info is requested, consumed only at debug emission. | +| E4 | Debug graph reuse | `KitCgDebugType` references operational ids for structural/nominal pieces (records, enums, funcs) via `kit_cg_debug_of_type`; the frontend overrides only leaves (primitive spellings) and inserts typedefs. No duplication of record/enum/func debug emission. | +| E5 | Attachment | Decl-bearing APIs gain an optional `KitCgDebugType` field; `0` (`NONE`) means "derive the default from the operational type" — preserving today's behavior for frontends that do not set it. | +| E6 | Interning purity | Debug types never enter interned operational descriptors (`KitCgFuncSig`/`KitCgFuncParam`), so spelling never splits structural interning. | +| E7 | Capability preservation | Faithful signed/unsigned/char debug and enum enumerators (from `244f1039`) are preserved via the debug channel; DWARF output is byte-identical. | +| E8 | Non-C frontends | Toy and Wasm pass `0` and rely on the existing default derivation (which they already use today — `void`/`bool`/`float` are never wrapped). No change required. | + +--- + +## 4. New Public Shape + +### 4.1 Debug-type handle and builders + +Thin public wrappers over the existing `src/debug/` DebugType producer (which +already has base/typedef/ptr/array/record/enum/func builders). Available only on +a `KitCg` with debug enabled; no-ops returning `NONE` otherwise. + +```c +typedef uint32_t KitCgDebugType; /* 0 = derive from the operational type */ +#define KIT_CG_DEBUG_TYPE_NONE 0u + +/* A named primitive leaf (DW_TAG_base_type): "int"/signed/4, etc. */ +KIT_API KitCgDebugType kit_cg_debug_base(KitCg*, KitSym name, + KitCgDebugEncoding, uint32_t bytes); +/* A named typedef (DW_TAG_typedef) over another debug type. */ +KIT_API KitCgDebugType kit_cg_debug_typedef(KitCg*, KitSym name, KitCgDebugType); +/* Structural debug types mirroring operational composition. */ +KIT_API KitCgDebugType kit_cg_debug_ptr(KitCg*, KitCgDebugType pointee); +KIT_API KitCgDebugType kit_cg_debug_array(KitCg*, KitCgDebugType elem, + uint64_t count); +/* Derive a debug type from an operational type: records/enums/funcs (with their + * existing emission) and default-named scalars. The leaf-override + typedef + * builders above are layered on top of this. */ +KIT_API KitCgDebugType kit_cg_debug_of_type(KitCg*, KitCgTypeId); +``` + +Composition examples (frontend side): + +- `int*` → `debug_ptr(debug_base("int", SIGNED, 4))` +- `unsigned` → `debug_base("unsigned int", UNSIGNED, 4)` +- `struct S*` → `debug_ptr(debug_of_type(S))` +- `double` → `debug_of_type(f64)` (default name is correct) +- `typedef T int; T x;` → `debug_typedef("T", debug_base("int", SIGNED, 4))` + (this *gains* typedef DIEs, which the current design silently drops) + +`KitCgDebugEncoding` is unchanged — it is already the clean debug-only enum; it +moves from `kit_cg_type_source_base` to `kit_cg_debug_base`. + +### 4.2 Attachment points + +Debug-only fields; `0` ⇒ derive default from the operational type. + +```c +typedef struct KitCgLocalAttrs { + KitSym name; + uint32_t align; + uint32_t flags; + KitCgDebugType debug_type; /* NEW: 0 = derive from the local's type */ +} KitCgLocalAttrs; /* covers both kit_cg_local and kit_cg_param */ + +typedef struct KitCgFuncAttrs { + /* ... existing ... */ + KitCgDebugType debug_type; /* NEW: the subprogram's type DIE; 0 = derive */ +} KitCgFuncAttrs; /* NOT in the interned KitCgFuncSig (E6) */ + +typedef struct KitCgObjectAttrs { + /* ... existing ... */ + KitCgDebugType debug_type; /* NEW: global object's type DIE; 0 = derive */ +} KitCgObjectAttrs; +``` + +`api_debug_type(KitCgTypeId)` becomes a resolver over `KitCgDebugType`: when a +decl site supplies a debug type, emit that; when it supplies `NONE`, fall back to +the operational-type walk that exists today. Record member types (when DWARF +members are eventually emitted) will carry `KitCgDebugType` on `KitCgField` at +that time; nothing depends on it now. + +### 4.3 Operational API simplifications + +- Delete `kit_cg_type_alias`, `kit_cg_type_source_base`, and their accessors + (`kit_cg_type_alias_name/base`, `kit_cg_type_source_base_name/storage/encoding`). +- Delete `KIT_CG_TYPE_ALIAS`, `KIT_CG_TYPE_SOURCE_BASE` from `KitCgTypeKind`. +- `kit_cg_type_resolve_alias` returns its argument (identity). Keep as a trivial + documented accessor or remove; `kit_cg_type_same_storage` reduces to structural + storage comparison with no alias step. +- `KitCgTypeInfo.storage_id` always equals `id`. Decision point (Open Q1): + keep the field as a documented `== id` identity (zero churn to the struct/ABI) + or remove it. + +--- + +## 5. Internal Architecture + +### 5.1 Type table + +`CgApiType` loses the wrapper-specific state once the kinds are gone: + +- `api_type_info_fill` (`src/cg/type.c`) drops its `while (ALIAS||SOURCE_BASE)` + walk; `info.storage_id = id`. +- `api_unalias_type` is deleted. Its ~33 hot-path callers and ~9 in-`type.c` + callers use the id directly. +- `pred_bits`/`pred_valid` no longer need an unalias step — the kind is now + exact, so `cg_type_is_int`/`_float`/`_ptr`/etc. read the kind directly. The + bitset cache can be kept (one indexed load) or dropped (kind lookup is already + one load); prefer dropping it to shed state unless a measurement says keep. +- The `ALIAS`/`SOURCE_BASE` recursion deletes from `cg_type_pointee`, + `cg_type_func_ret_id`/`_result_id`/`_param_id`, `kit_cg_type_int_width`, + `kit_cg_type_float_width`, `api_int_like_width`, `api_type_is_bool`, + `cg_type_complete_id`, `cg_type_sized_id`, `cg_type_same_storage_rec`. + +### 5.2 ABI layer + +The `if (t->kind == KIT_CG_TYPE_ALIAS) recurse; if (... SOURCE_BASE) recurse` +prologue deletes from `abi_cg_type_info` (`src/abi/abi.c`) and from every +per-arch classifier (`abi_rv64.c`, `abi_sysv_x64.c`, `abi_aapcs64.c`, +`abi_win64_x64.c`, `src/arch/wasm/abi.c`). ABI queries see only real storage +kinds. + +### 5.3 Debug producer + +`src/debug/` is untouched. `src/cg/debug.c` changes from "walk the operational +`CgType` and special-case `ALIAS`/`SOURCE_BASE`" to "walk a `KitCgDebugType` +graph; for an operational-type reference, dispatch into the existing +record/enum/func/ptr/array emission." Enum enumerators stay on the operational +enum entry (nothing unaliases an enum, so they cost no pervasiveness); only the +enum's *underlying spelling* comes from the debug channel. + +--- + +## 6. Frontend Impact + +### 6.1 C frontend (the only affected frontend) + +- `type_cg_scalar` (`lang/c/type/type.c`) stops wrapping in source-base; returns + the bare builtin. `t->cg_id` becomes a pure storage id, used unchanged at all + operational sites (memaccess, convert, params, locals, fields, calls). +- Add a parallel lazy `type_cg_debug(Type*) → KitCgDebugType`, cached as + `t->dbg_id`, structurally identical to `type_cg_lower`. The existing 13-entry + `kScalarDbg` table now drives `kit_cg_debug_base` instead of + `kit_cg_type_source_base`. The two lowerings can share one traversal (they + recurse in lockstep) if that is cleaner. +- At decl sites — `kit_cg_local`, `kit_cg_param`, `kit_cg_func_begin_attrs`, + global object definition — set `attrs.debug_type = type_cg_debug(t)`. +- Enum lowering keeps passing enumerators to `kit_cg_type_enum` (operational); + the enum's debug underlying spelling rides on `type_cg_debug`. + +### 6.2 Toy / Wasm + +No change. They pass `0` for `debug_type` and get today's default derivation. + +--- + +## 7. Implementation Plan + +### Phase 1: Add the debug channel, migrate C behind it + +- Add `KitCgDebugType`, the builders, and the decl-attr fields. +- Make `api_debug_type` a resolver: supplied `KitCgDebugType` wins; `NONE` + derives the default from the operational type (today's behavior). +- Add `type_cg_debug` to the C frontend; set `debug_type` at decl sites. +- `SOURCE_BASE`/`ALIAS` still exist at this point; the goal is for debug to stop + *reading* them. Verify DWARF byte-identity (`make test-debug test-dwarf`, + `kit cc -g` spot checks for `DW_ATE_unsigned`/`signed_char`/`unsigned_char` and + enumerator DIEs). + +### Phase 2: Delete the wrappers and all unaliasing + +Once debug no longer reads the wrappers, cut over in one clean break: + +- Stop the C frontend creating source-base ids (`type_cg_scalar` returns the + builtin). +- Delete `kit_cg_type_alias`/`source_base` + accessors, the two `KitCgTypeKind` + values, `api_unalias_type`, and the recursion/prologues listed in §5. +- Collapse `storage_id` to `id` (or remove the field — Open Q1). +- Simplify `kit_cg_type_resolve_alias`/`is_void`/`same_storage`. + +### Phase 3: Tests + +- Rewrite `test/api/cg_type_test.c` alias/source-base cases as `KitCgDebugType` + builder + emission tests. +- Add a debug-channel test: a local of `unsigned`, a `signed char`, an enum, and + a typedef each produce the expected DWARF DIE. + +--- + +## 8. Validation + +- `make test-debug test-dwarf` — primary gate; DWARF must be byte-identical for + the signed/unsigned/char/enum cases `244f1039` introduced. +- `make test-cg-api` — type API contract. +- `make test-parse test-toy test-opt` — frontend + mid-end unaffected. +- `make test-smoke-x64 test-smoke-rv64` — end-to-end with `-g`. +- Ecosystem / byte-identity gate (see [PERF.md](PERF.md) and the ecosystem + harness): object output must be unchanged; DWARF must match. + +Scenario coverage: `int`/`unsigned`/`char`/`signed char`/`unsigned char` locals +and params; enums with enumerators; typedef DIEs (new, previously dropped); +function pointers; records (members still opaque — confirm no regression); +`va_list`; non-C frontends emit unchanged debug via the default path. + +--- + +## 9. Acceptance Criteria + +- `KitCgTypeKind` has no `ALIAS`/`SOURCE_BASE`; `KitCgTypeId` is storage/ABI only. +- `api_unalias_type` and the per-backend alias/source-base prologues are gone. +- `storage_id == id` everywhere (field kept as identity or removed). +- No CG hot-path op resolves aliases; predicates read the exact kind. +- DWARF output for the `244f1039` capabilities is byte-identical; typedef DIEs + are additionally available. +- Toy and Wasm are unchanged. +- No global state, no VLAs; debug-type state hangs off `KitCg`. + +--- + +## 10. Open Questions + +1. **Keep or remove `KitCgTypeInfo.storage_id`?** It becomes vestigial (`== id`). + Keeping it as a documented identity is zero-churn and leaves + `kit_cg_type_resolve_alias` meaningful as a trivial accessor; removing it is + cleaner but touches the public struct and every reader. Prefer **remove** for + a true clean break, consistent with CG-TYPES.md's no-backcompat stance. +2. **Should enum enumerators also move to the debug channel?** They are + debug-only in principle, but they already live on the operational enum entry + and cost no pervasiveness (nothing unaliases an enum). Prefer **leave on the + operational enum** to minimize churn; revisit only if member-type debug work + wants a uniform debug-graph representation. +3. **`kit_cg_debug_of_type` vs a fully parallel debug graph.** The plan leans on + `of_type` to reuse existing record/enum/func emission. If a future need arises + to spell those differently from their operational shape (rare), the builder + set would grow `kit_cg_debug_struct/union/enum/func`. Prefer the minimal + `of_type`-based set until such a need is concrete. diff --git a/doc/plan/CG-TYPES.md b/doc/plan/CG-TYPES.md @@ -574,6 +574,11 @@ Important scenario coverage: move entirely into debug/C-backend metadata after the cutover? The plan above keeps alias ids because they already serve source-facing consumers, but the storage semantics do not require them. + **Resolved → move them out.** See [CG-TYPE-DEBUG-SPLIT.md](CG-TYPE-DEBUG-SPLIT.md): + `KitCgTypeId` becomes storage/ABI-only and source spelling (now including the + `KIT_CG_TYPE_SOURCE_BASE` added since this question was written) moves onto a + separate optional debug-type channel, deleting `api_unalias_type` and the + per-backend unalias boilerplate. 2. Should function types reject incomplete by-value records at construction, or permit declaration-like signatures and reject only when defining/calling? Prefer rejection unless a frontend has a concrete declaration use case. diff --git a/doc/plan/README.md b/doc/plan/README.md @@ -12,6 +12,7 @@ shrinks to whatever remains open. | [OPTIMIZER.md](OPTIMIZER.md) | Completing the O2 SSA mid-end, expanded inlining, -O0/-O1 performance work, machine register-constraint improvements. | [../OPT.md](../OPT.md) | | [PERF.md](PERF.md) | Making kit the fastest `-O0` C compiler with code as dense as tcc: current compile-speed + code-size standings, how to reproduce them (macOS instruction counts, Linux callgrind, the `make bench-cc` scaling guard), and the ranked forward-looking levers on both axes. | [../ARCH.md](../ARCH.md) | | [CG-TYPES.md](CG-TYPES.md) | Clean-break redesign of the public CG type/type-id system: direct-index handles, real void types, explicit alias/storage semantics, ABI-owned layout, and two-phase nominal records. | [../CODEGEN.md](../CODEGEN.md), [../INTERFACES.md](../INTERFACES.md) | +| [CG-TYPE-DEBUG-SPLIT.md](CG-TYPE-DEBUG-SPLIT.md) | Make `KitCgTypeId` storage/ABI-only and move source spelling (primitive sign+name, typedefs, enum underlying) onto a separate debug-type channel: deletes `ALIAS`/`SOURCE_BASE`, `api_unalias_type`, and the per-backend unalias boilerplate. Resolves CG-TYPES.md §10 Q1. | [../CODEGEN.md](../CODEGEN.md), [../DWARF.md](../DWARF.md) | | [CG-STACK-API.md](CG-STACK-API.md) | Refactoring the parser-to-CG expression seam: keep stack-based liveness, move the C frontend's duplicated `pcg` slot state onto CG-owned stack slots, and add parser-shaped place/load/store operations without backend fusion. | [../FRONTENDS.md](../FRONTENDS.md) | | [LEX-PP-API.md](LEX-PP-API.md) | Redesigning the lexer -> preprocessor -> parser boundary around lean tokens, lazy spelling/location materialization, and slot-based handoffs for a high-performance lexer/preprocessor rewrite without frontend layer fusion. | [../FRONTENDS.md](../FRONTENDS.md) | | [LINKER.md](LINKER.md) | Incremental linking: the file-based object-link redesign and remaining non-ELF format coverage. | [../LINK.md](../LINK.md) |