kit

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

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:

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)

What it costs

Recent movement (reconciled)

Three same-day changes touched this area and the plan is written against the current working tree:

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

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

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


5. Internal Architecture

5.1 Type table

CgApiType loses the wrapper-specific state once the kinds are gone:

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)

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

Phase 2: Delete the wrappers and all unaliasing

Once debug no longer reads the wrappers, cut over in one clean break:

Phase 3: Tests


8. Validation

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


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.