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 §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 aretest/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 commit244f1039). 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) mapsALIAS → DW_TAG_typedefandSOURCE_BASE → DW_TAG_base_type + DW_ATE_*. Reached at three live sites — the subprogram type atkit_cg_func_begin_attrsand locals/params inapi_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 fromKIT_CG_ABI_SIGNEXT/ZEROEXTattrs; load signedness comes from theKIT_CG_MEM_SOURCE_SIGNEDflag. 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 callsdebug_type_record_beginthen immediatelydebug_type_record_end, neverdebug_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_typeemitsDW_TAG_enumeration_typewithDW_TAG_enumeratorchildren over the enum's underlying type. The underlying type may itself be a source-base.
What it costs
api_unalias_typeis called pervasively: ~33 per-op hot-path sites (src/cg/value.c,arith.c,call.c,memory.c), ~9 more insidetype.citself (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 intype.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 byapi_type_info_fill),pred_bits/pred_valid(predicate bitset computed on the unaliased type). - A two-headed type-info surface (
kit_cg_type_viewidentity prefix vskit_cg_type_infofull snapshot) plusstorage_id,kit_cg_type_resolve_alias, andkit_cg_type_same_storage, all there to manage the alias↔storage split.kit_cg_type_is_voidis a two-hop throughstorage_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_NONEis invalid/absent only.244f1039— addedSOURCE_BASEand 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 nowe->cg.kind), folded the oldunaliased/unalias_filledfields into a singleinfo.storage_idfilled byapi_type_info_fill, and madekit_cg_type_infodelegate tokit_cg_type_view. This cleaned up the cost but did not remove the unaliasing — the pervasiveness above is fully intact, andstorage_idis 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.
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.
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_BASEfromKitCgTypeKind. kit_cg_type_resolve_aliasreturns its argument (identity). Keep as a trivial documented accessor or remove;kit_cg_type_same_storagereduces to structural storage comparison with no alias step.KitCgTypeInfo.storage_idalways equalsid. Decision point (Open Q1): keep the field as a documented== ididentity (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 itswhile (ALIAS||SOURCE_BASE)walk;info.storage_id = id.api_unalias_typeis deleted. Its ~33 hot-path callers and ~9 in-type.ccallers use the id directly.pred_bits/pred_validno longer need an unalias step — the kind is now exact, socg_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_BASErecursion deletes fromcg_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_idbecomes 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 ast->dbg_id, structurally identical totype_cg_lower. The existing 13-entrykScalarDbgtable now driveskit_cg_debug_baseinstead ofkit_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 — setattrs.debug_type = type_cg_debug(t). - Enum lowering keeps passing enumerators to
kit_cg_type_enum(operational); the enum's debug underlying spelling rides ontype_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_typea resolver: suppliedKitCgDebugTypewins;NONEderives the default from the operational type (today's behavior). - Add
type_cg_debugto the C frontend; setdebug_typeat decl sites. SOURCE_BASE/ALIASstill exist at this point; the goal is for debug to stop reading them. Verify DWARF byte-identity (make test-debug test-dwarf,kit cc -gspot checks forDW_ATE_unsigned/signed_char/unsigned_charand 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_scalarreturns the builtin). - Delete
kit_cg_type_alias/source_base+ accessors, the twoKitCgTypeKindvalues,api_unalias_type, and the recursion/prologues listed in §5. - Collapse
storage_idtoid(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.calias/source-base cases asKitCgDebugTypebuilder + emission tests. - Add a debug-channel test: a local of
unsigned, asigned 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 cases244f1039introduced.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 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
KitCgTypeKindhas noALIAS/SOURCE_BASE;KitCgTypeIdis storage/ABI only.api_unalias_typeand the per-backend alias/source-base prologues are gone.storage_id == ideverywhere (field kept as identity or removed).- No CG hot-path op resolves aliases; predicates read the exact kind.
- DWARF output for the
244f1039capabilities 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
- Keep or remove
KitCgTypeInfo.storage_id? It becomes vestigial (== id). Keeping it as a documented identity is zero-churn and leaveskit_cg_type_resolve_aliasmeaningful 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. - 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.
kit_cg_debug_of_typevs a fully parallel debug graph. The plan leans onof_typeto reuse existing record/enum/func emission. If a future need arises to spell those differently from their operational shape (rare), the builder set would growkit_cg_debug_struct/union/enum/func. Prefer the minimalof_type-based set until such a need is concrete.