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:
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) |