kit

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

commit f44d51f84ea58f6dfeb441dc11b94927f80ea128
parent 04332745f571e228baecdc3f3a9803532fc98106
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 11 Jun 2026 17:32:30 -0700

refactor(cg): split the CG operand vocabulary out of cgtarget.h into cgir.h

cgtarget.h (824 lines) bundled two distinct things: the shared CG codegen
vocabulary (the op enums, Operand, MemAccess, the call/func/switch/scope
descriptors) and the CgTarget backend contract (the vtable + CGBackend
registry). So the native backends, the recorder IR, the interpreter, and mc.h
all included the full 824-line contract purely to get Operand/BinOp/MemAccess,
with zero use of the vtable — the dependency graph read "the native backend
depends on the CgTarget contract", which is false.

Extract the vocabulary into cg/cgir.h; cgtarget.h now `#include`s it and keeps
only the contract. Consumers that speak operands but not the vtable
(native_target.h, mc.h, cg/ir.h, ir_eval.h, c_emit.h, the interp) depend on
cgir.h; the 7 genuine contract users (native_direct_target.h, ir_recorder.h,
wasm/internal.h, opt.h, arch.h, check_target.c, cg/internal.h) include
cgtarget.h.

This also surfaced and fixed a latent fragility: the cg implementation files
were reaching the CgTarget vtable through the accidental chain
internal.h -> asm.h -> mc.h -> cgtarget.h. Cutting mc.h down to the vocabulary
exposed it; the genuine contract users (internal.h, opt.h, ir_recorder.h,
wasm/internal.h) now include cgtarget.h directly instead of freeloading on a
transitive include.

Pure header reorganization: byte-identical across the full 60-category gate;
test-cg-api/opt/isa/toy + test-smoke-x64 green.

Diffstat:
Msrc/arch/c_target/c_emit.h | 2+-
Msrc/arch/mc.h | 6+++++-
Msrc/arch/native_target.h | 2+-
Msrc/arch/wasm/internal.h | 1+
Asrc/cg/cgir.h | 484+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/cg/cgtarget.h | 470+------------------------------------------------------------------------------
Msrc/cg/internal.h | 2+-
Msrc/cg/ir.h | 2+-
Msrc/cg/ir_eval.h | 4++--
Msrc/cg/ir_recorder.h | 1+
Msrc/interp/engine.c | 2+-
Msrc/interp/interp.h | 2+-
Msrc/interp/lower.c | 2+-
Msrc/opt/opt.h | 1+
14 files changed, 502 insertions(+), 479 deletions(-)

diff --git a/src/arch/c_target/c_emit.h b/src/arch/c_target/c_emit.h @@ -9,7 +9,7 @@ #include <kit/core.h> -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "core/core.h" typedef CGLocal CLocal; diff --git a/src/arch/mc.h b/src/arch/mc.h @@ -3,7 +3,7 @@ #include <kit/compile.h> -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "core/core.h" #include "obj/obj.h" @@ -21,6 +21,10 @@ * on Debug: this forward decl plus debug_emit_row (declared by the few * backend TUs that actually emit line rows). */ typedef struct Debug Debug; +/* mc.h speaks the CG operand vocabulary (cgir.h) and only passes an opaque + * CgTarget* through arch_lower_indexed, so it forward-declares the contract + * rather than depending on cgtarget.h. */ +typedef struct CgTarget CgTarget; /* Native-only register id. The semantic CgTarget surface uses CGLocal; this * remains here for MC/native helpers and disabled native backends. */ diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -4,7 +4,7 @@ #include <string.h> #include "arch/mc.h" -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "cg/type.h" #include "core/core.h" #include "core/slice.h" /* Slice, for resolve_name */ diff --git a/src/arch/wasm/internal.h b/src/arch/wasm/internal.h @@ -23,6 +23,7 @@ #include <kit/core.h> #include "arch/mc.h" +#include "cg/cgtarget.h" /* WTarget embeds CgTarget base */ #include "core/core.h" #include "obj/obj.h" #include "opt/ir.h" diff --git a/src/cg/cgir.h b/src/cg/cgir.h @@ -0,0 +1,484 @@ +#ifndef KIT_CG_CGIR_H +#define KIT_CG_CGIR_H + +/* The CG codegen vocabulary: the operand model, the op enums (BinOp/UnOp/CmpOp/ + * ConvKind/IntrinKind), memory-access descriptors, and the call/func/switch/ + * scope descriptors. These are the data types the CgTarget vtable (cgtarget.h) + * and the recorder IR (cg/ir.h) are both built from. Split out of cgtarget.h so + * a consumer that only speaks operands (the native backends, the recorder IR, + * the interpreter) need not depend on the backend contract. */ + +#include <kit/cg.h> + +#include "core/core.h" +#include "obj/obj.h" + +typedef u32 CGLocal; +#define CG_LOCAL_NONE 0u + +/* Vector / SIMD forward compat: vector ops will arrive as new variants in + * the BinOp, UnOp, CmpOp, ConvKind families. Backend switches over these + * enums must use `default:` (unreachable / panic) rather than exhaustive + * case lists, so adding a new variant later does not silently mis-handle on + * backends that haven't been taught about it. Vector loads/stores reuse the + * existing load/store methods with vector-typed Operands and appropriate + * MemAccess. */ + +/* Integer/float binary ops. Edge-case semantics are fully defined (no undefined + * behavior) in doc/IR.md: iadd/isub/imul (and UO_NEG) wrap modulo 2^width; + * sdiv/udiv/srem/urem and the shifts have a portable default plus an opt-in + * target-defined mode selected per instruction via CgIrInstFlag (src/cg/ir.h). + * FP ops are strict IEEE-754 in the target's default rounding/exception + * environment; there is no FP remainder op (the frontend calls fmod). */ +typedef enum BinOp { + BO_IADD, + BO_ISUB, + BO_IMUL, + BO_SDIV, + BO_UDIV, + BO_SREM, + BO_UREM, + BO_FADD, + BO_FSUB, + BO_FMUL, + BO_FDIV, + BO_AND, + BO_OR, + BO_XOR, + BO_SHL, + BO_SHR_S, + BO_SHR_U, +} BinOp; + +typedef enum UnOp { + UO_NEG, + UO_FNEG, + UO_NOT, /* logical: 0/1 */ + UO_BNOT, /* bitwise ~ */ +} UnOp; + +/* Compares producing i1. The 10 integer members (CMP_EQ..CMP_GE_U) are total + * and 1:1 with KitCgIntCmpOp; on integers CMP_EQ/CMP_NE are plain equality. + * + * The 12 floating-point members form a disjoint block laid out *after* the + * integer block, in the same order as the public KitCgFpCmpOp, and are + * IEEE-complete: each predicate encodes ordered (NaN -> false) vs unordered + * (NaN -> true) explicitly, so the distinction reaches every backend. The + * identity used throughout the backends is unordered-R == NOT(ordered-not-R) + * (e.g. ULT == !(OGE), UNE == !(OEQ)). CMP_OEQ_F is the FP boundary: an op is a + * floating compare iff op >= CMP_OEQ_F. */ +typedef enum CmpOp { + CMP_EQ, + CMP_NE, + CMP_LT_S, + CMP_LE_S, + CMP_GT_S, + CMP_GE_S, + CMP_LT_U, + CMP_LE_U, + CMP_GT_U, + CMP_GE_U, + /* Ordered FP relationals (NaN -> false). */ + CMP_OEQ_F, + CMP_ONE_F, + CMP_OLT_F, + CMP_OLE_F, + CMP_OGT_F, + CMP_OGE_F, + /* Unordered FP relationals (NaN -> true). */ + CMP_UEQ_F, + CMP_UNE_F, + CMP_ULT_F, + CMP_ULE_F, + CMP_UGT_F, + CMP_UGE_F, +} CmpOp; + +/* Conversions. Widths must order correctly (sext/zext widen, trunc narrows, + * bitcast preserves byte size). itof, fext, and ftrunc round to nearest-even; + * ftoi_s/ftoi_u round toward zero with a portable saturating out-of-range + * default (NaN -> 0) and an opt-in target-defined mode + * (CG_IR_INST_TARGET_FPTOINT_EDGES in src/cg/ir.h). Full rules in doc/IR.md. */ +typedef enum ConvKind { + CV_SEXT, + CV_ZEXT, + CV_TRUNC, + CV_ITOF_S, + CV_ITOF_U, + CV_FTOI_S, + CV_FTOI_U, + CV_FEXT, + CV_FTRUNC, + CV_BITCAST, +} ConvKind; + +/* Atomic op kinds (KitCgAtomicOp) and memory orders (KitCgMemOrder) come + * straight from the public API. Which orders are legal depends on the atomic + * op: load excludes release/acq_rel; store excludes acquire/consume/acq_rel; + * CAS failure order is one of relaxed/consume/acquire/seq_cst and no stronger + * than success. See the Atomics edge-case rules in doc/IR.md (mirrored by + * kit_cg_atomic_is_legal). */ + +/* Compiler-intrinsic kinds dispatched through CgTarget.intrinsic and carried + * on IR_INTRINSIC via IRIntrinAux.kind. The set is bounded: a backend + * must know each one to choose inline-vs-libcall. Hint intrinsics + * (EXPECT/TRAP/PREFETCH/ASSUME_ALIGNED) ride the same dispatch: + * the backend decides whether they emit an instruction or a no-op. + * `unreachable` is NOT here: it is a first-class control terminator with + * its own CgTarget hook (see below), not an intrinsic. + * + * Not every C builtin lives here. Parser-evaluated builtins + * (__builtin_offsetof, __builtin_constant_p, __builtin_choose_expr, + * __builtin_types_compatible_p) fold at parse and never reach IR. Builtins + * that already have dedicated CgTarget methods (alloca, va_*, atomics) keep + * them. Returns-twice and no-return control intrinsics use this dispatch so + * opt can preserve their CFG effects without growing backend vtable hooks. */ +typedef enum IntrinKind { + INTRIN_NONE = 0, + + /* bit ops */ + INTRIN_POPCOUNT, + INTRIN_CTZ, + INTRIN_CLZ, + INTRIN_BSWAP, + + /* memory. memcpy/memset are the dedicated copy_bytes/set_bytes hooks + * (kit_cg_memcpy/_memset); only memmove flows through the intrinsic path. */ + INTRIN_MEMMOVE, + INTRIN_PREFETCH, + INTRIN_ASSUME_ALIGNED, + + /* hints */ + INTRIN_EXPECT, + INTRIN_TRAP, + + /* OS trap: args[0] is the syscall number, args[1..6] are integer/pointer + * payloads; dsts[0] receives the target long result. */ + INTRIN_SYSCALL, + + /* non-local control */ + INTRIN_SETJMP, + INTRIN_LONGJMP, + + /* checked arith — multi-result (value, overflow_flag) */ + INTRIN_SADD_OVERFLOW, + INTRIN_UADD_OVERFLOW, + INTRIN_SSUB_OVERFLOW, + INTRIN_USUB_OVERFLOW, + INTRIN_SMUL_OVERFLOW, + INTRIN_UMUL_OVERFLOW, + + /* baremetal CPU control — single-instruction, no operands unless noted. + * dsts/args empty except IRQ_SAVE (dsts[0] = saved interrupt state) and + * IRQ_RESTORE (args[0] = state to restore). Privileged forms (WFI/WFE/SEV + * and the IRQ family) trap at user level; backends still emit the one + * instruction and frontends gate any runtime use behind a capability test. */ + INTRIN_CPU_NOP, + INTRIN_CPU_YIELD, + INTRIN_WFI, + INTRIN_WFE, + INTRIN_SEV, + INTRIN_ISB, + INTRIN_DMB, + INTRIN_DSB, + INTRIN_IRQ_SAVE, + INTRIN_IRQ_RESTORE, + INTRIN_IRQ_ENABLE, + INTRIN_IRQ_DISABLE, + + /* frame-pointer-chain introspection — value-producing, single immediate + * operand (the constant level). args[0] is the level (OPK_IMM); dsts[0] is + * the void* result. Lowered as an unrolled FP walk; modeled as an ordinary + * frame-dependent memory read (IR_INTRINSIC is already conservatively + * side-effecting in opt, so it is never hoisted, CSE'd, or eliminated). */ + INTRIN_FRAME_ADDRESS, + INTRIN_RETURN_ADDRESS, +} IntrinKind; + +typedef enum OpKind { + OPK_IMM, + OPK_LOCAL, /* typed semantic local */ + OPK_GLOBAL, /* address: symbol+addend, not a load */ + OPK_INDIRECT, /* [local + ofs], with optional indexed local */ +} OpKind; + +typedef enum CGLocalFlag { + CG_LOCAL_FLAG_NONE = 0, + CG_LOCAL_ADDR_TAKEN = 1u << 0, + CG_LOCAL_MEMORY_REQUIRED = 1u << 1, +} CGLocalFlag; + +typedef struct CGLocalDesc { + KitCgTypeId type; + Sym name; + SrcLoc loc; + u32 size; + u32 align; + u32 flags; /* CGLocalFlag */ +} CGLocalDesc; + +typedef enum MemFlag { + MF_NONE = 0, + MF_VOLATILE = 1u << 0, + MF_ATOMIC = 1u << 1, + MF_RESTRICT = 1u << 2, + MF_READONLY = 1u << 3, + MF_WRITEONLY = 1u << 4, + MF_UNALIGNED = 1u << 5, +} MemFlag; + +typedef enum AliasKind { + ALIAS_UNKNOWN, + ALIAS_LOCAL, + ALIAS_GLOBAL, + ALIAS_PARAM, + ALIAS_HEAP, + ALIAS_STRING, +} AliasKind; + +typedef struct AliasRoot { + u8 kind; /* AliasKind */ + u8 pad[3]; + union { + i32 local_id; + ObjSymId global; + u32 param_idx; + Sym string_id; + } v; +} AliasRoot; + +typedef struct MemAccess { + KitCgTypeId type; /* codegen object type accessed */ + u32 size; /* ABI byte size of this access (storage-unit size for a + * bit-field) */ + u32 align; /* known byte alignment; 0 means unknown */ + u16 flags; /* MemFlag */ + u16 addr_space; + /* Bit-field rider: when bf_width != 0 this access is a bit-field, so `load` + * extracts (shift+mask+extend) and `store` inserts (read-modify-write) within + * the storage unit described by {type,size}. The CgTarget impls translate + * this to the physical NativeTarget bitfield_load/store (or the recorder IR + * op); the semantic CgTarget no longer carries a separate bit-field method. + */ + u16 bf_offset; /* target-endian bit offset within the storage unit */ + u16 bf_width; /* 0 => not a bit-field access */ + u8 bf_signed; /* signed extraction on load */ + u8 bf_pad[3]; + AliasRoot alias; +} MemAccess; + +typedef struct ConstBytes { + KitCgTypeId type; + const u8* bytes; /* ABI representation, little/big endian per target */ + u32 size; + u32 align; +} ConstBytes; + +typedef struct AggregateAccess { + KitCgTypeId type; + u32 size; + u32 align; + MemAccess mem; +} AggregateAccess; + +typedef struct BitFieldAccess { + KitCgTypeId field_type; + MemAccess storage; + u32 storage_offset; /* byte offset from record base */ + u16 bit_offset; /* target-endian bit offset within storage unit */ + u16 bit_width; /* may be 0 for zero-width layout barriers */ + u8 signed_; + u8 pad[3]; +} BitFieldAccess; + +/* Reconstruct the BitFieldAccess a CgTarget impl needs from the bit-field + * MemAccess that rides the generic load/store (bf_width != 0). The storage unit + * is {m.type, m.size}; the bit geometry is the bf_* rider. */ +static inline BitFieldAccess bf_from_mem(MemAccess m) { + BitFieldAccess bf = {0}; + bf.field_type = m.type; + bf.storage = m; + bf.storage.bf_offset = 0; + bf.storage.bf_width = 0; + bf.storage.bf_signed = 0; + bf.bit_offset = m.bf_offset; + bf.bit_width = m.bf_width; + bf.signed_ = m.bf_signed; + return bf; +} + +typedef struct Operand { + u8 kind; + u8 pad[3]; + KitCgTypeId type; + union { + i64 imm; + CGLocal local; + struct { + ObjSymId sym; + i64 addend; + } global; + struct { + CGLocal base; + CGLocal index; /* CG_LOCAL_NONE when no index operand */ + u8 log2_scale; /* 0..3 -> 1/2/4/8 bytes; ignored when no index */ + i32 ofs; + } ind; + } v; +} Operand; + +typedef struct CGParamDesc { + u32 index; + Sym name; + KitCgTypeId type; + u32 size; + u32 align; + u32 flags; /* CGLocalFlag */ + SrcLoc loc; +} CGParamDesc; + +/* text_section_id and group_id are per-function so that -ffunction-sections, + * __attribute__((section)) on functions, and COMDAT for C11 inline-with- + * external-definition all work with no extra plumbing. Decl.section_id already + * carries the user's request; CG/decl decides the section name policy + * (default .text, vs .text.<sym> under -ffunction-sections, vs explicit + * attribute). The backend just writes to the named section. */ +/* Phase 2 attribute-derived hints. The backends are free to ignore these; + * they exist so the parser can communicate _Noreturn / __attribute__ + * info down to CG without forcing every backend to consult the Decl. */ +typedef enum CGFuncDescFlag { + CGFD_NONE = 0, + CGFD_NORETURN = 1u << 0, +} CGFuncDescFlag; + +typedef struct CGFuncDesc { + ObjSymId sym; + ObjSecId text_section_id; + ObjGroupId group_id; /* OBJ_GROUP_NONE if none */ + KitCgTypeId fn_type; + KitCgTypeId result_type; /* KIT_CG_TYPE_NONE/void == no result */ + const CGParamDesc* params; + u32 nparams; + SrcLoc loc; + u32 flags; /* CGFuncDescFlag */ + KitCgInlinePolicy inline_policy; + u16 sym_bind; /* SymBind */ + u16 sym_kind; /* SymKind */ + u8 sym_vis; /* SymVis */ + u8 atomize; + u8 pad[2]; +} CGFuncDesc; + +typedef enum CGCallFlag { + CG_CALL_NONE = 0, + /* Sibling call. The target emits a tail-position call and does NOT emit a + * return-style continuation. CG will not invoke target->ret afterwards. + * + * Realizability is verified before this flag is set: CG only sets it after + * tail_call_unrealizable_reason() returns NULL for the same desc and call + * state, so the target can emit the sibling call unconditionally. The + * target may assert/compiler_panic if the flag is set on an unrealizable + * desc, but that is an internal-consistency check — fallback and + * diagnostics for unrealizable tail calls are CG's responsibility, not the + * target's. */ + CG_CALL_TAIL = 1u << 0, +} CGCallFlag; + +typedef struct CGCallDesc { + KitCgTypeId fn_type; + Operand callee; + const CGLocal* args; + CGLocal result; /* CG_LOCAL_NONE == void callee (no result) */ + u32 nargs; + u16 flags; /* CGCallFlag */ + u8 tail_policy; /* KitCgTailPolicy; meaningful when CG_CALL_TAIL is set. + * The opt recorder accepts every tail and preserves this so + * the replay can pick: emit tail (realizable), fall back to + * call+ret (ALLOWED), or diagnose (MUST). */ + u8 pad; + KitCgInlinePolicy inline_policy; +} CGCallDesc; + +typedef u32 Label; +#define LABEL_NONE 0 + +typedef enum ScopeKind { + SCOPE_BLOCK, /* break exits forward */ + SCOPE_LOOP, /* break exits forward; continue uses explicit target */ +} ScopeKind; + +typedef u32 CGScope; +#define CG_SCOPE_NONE 0u + +typedef struct CGScopeDesc { + u8 kind; /* ScopeKind */ + u8 pad[3]; + Label break_label; /* explicit target for break; LABEL_NONE => target creates + one */ + Label continue_label; /* explicit target for continue; LABEL_NONE for + non-loops */ + KitCgTypeId result_type; /* reserved for structured expression results */ +} CGScopeDesc; + +typedef struct AsmConstraint { + const char* str; /* GCC-style: "r", "=&r", "+m", "i", "0" ... */ + Sym name; /* GCC `[name]` symbolic operand; 0 if absent */ + KitCgTypeId type; /* codegen type of the bound expression (output lvalue or + input rvalue). Drives type width for the binder. + NULL only for hand-built test constraints (binder + falls back to a 64-bit int default). */ + Sym reg; /* Explicit hard-register name ("r10"/"x8"/...) this operand + must occupy — a GNU local register variable bound as an + operand; 0 = unconstrained. Only the target's register + file resolves the name to a physical register. */ + u8 dir; /* KitCgAsmDir */ + u8 pad[3]; +} AsmConstraint; + +typedef struct CGSwitchCase { + /* Bit pattern matched against the selector; interpreted using + * selector_type's width and signedness (signed comparison uses + * sign-extension to selector_type's width). */ + u64 value; + Label label; +} CGSwitchCase; + +typedef struct CGSwitchDesc { + Operand selector; /* OPK_LOCAL or OPK_IMM */ + KitCgTypeId selector_type; + Label default_label; /* LABEL_NONE means "fall through past the switch" */ + const CGSwitchCase* cases; + u32 ncases; + u8 hint; /* KitCgSwitchHint */ + u8 opt_level; /* 0/1/2; reads policy in cg_lower_switch_default */ + u8 pad[2]; +} CGSwitchDesc; + +typedef struct CGLocalStaticDataDesc { + ObjSymId sym; + KitCgTypeId type; + KitCgDataDefAttrs attrs; + u32 align; +} CGLocalStaticDataDesc; + +typedef enum CGDebugLocKind { + CG_DEBUG_LOC_NONE, + CG_DEBUG_LOC_FRAME, + CG_DEBUG_LOC_REG, + CG_DEBUG_LOC_GLOBAL, +} CGDebugLocKind; + +typedef struct CGDebugLoc { + u8 kind; /* CGDebugLocKind */ + u8 pad[3]; + union { + /* Offset in the same target-defined frame-base coordinate system that the + * target/debugger pair uses to materialize frame-relative variables. CG + * treats this as opaque target data and only maps it into the debug + * producer's generic frame-location form. */ + i32 frame_ofs; + u32 reg; + ObjSymId global; + } v; +} CGDebugLoc; +#endif diff --git a/src/cg/cgtarget.h b/src/cg/cgtarget.h @@ -6,475 +6,7 @@ #include "core/core.h" #include "obj/obj.h" - -typedef u32 CGLocal; -#define CG_LOCAL_NONE 0u - -/* Vector / SIMD forward compat: vector ops will arrive as new variants in - * the BinOp, UnOp, CmpOp, ConvKind families. Backend switches over these - * enums must use `default:` (unreachable / panic) rather than exhaustive - * case lists, so adding a new variant later does not silently mis-handle on - * backends that haven't been taught about it. Vector loads/stores reuse the - * existing load/store methods with vector-typed Operands and appropriate - * MemAccess. */ - -/* Integer/float binary ops. Edge-case semantics are fully defined (no undefined - * behavior) in doc/IR.md: iadd/isub/imul (and UO_NEG) wrap modulo 2^width; - * sdiv/udiv/srem/urem and the shifts have a portable default plus an opt-in - * target-defined mode selected per instruction via CgIrInstFlag (src/cg/ir.h). - * FP ops are strict IEEE-754 in the target's default rounding/exception - * environment; there is no FP remainder op (the frontend calls fmod). */ -typedef enum BinOp { - BO_IADD, - BO_ISUB, - BO_IMUL, - BO_SDIV, - BO_UDIV, - BO_SREM, - BO_UREM, - BO_FADD, - BO_FSUB, - BO_FMUL, - BO_FDIV, - BO_AND, - BO_OR, - BO_XOR, - BO_SHL, - BO_SHR_S, - BO_SHR_U, -} BinOp; - -typedef enum UnOp { - UO_NEG, - UO_FNEG, - UO_NOT, /* logical: 0/1 */ - UO_BNOT, /* bitwise ~ */ -} UnOp; - -/* Compares producing i1. The 10 integer members (CMP_EQ..CMP_GE_U) are total - * and 1:1 with KitCgIntCmpOp; on integers CMP_EQ/CMP_NE are plain equality. - * - * The 12 floating-point members form a disjoint block laid out *after* the - * integer block, in the same order as the public KitCgFpCmpOp, and are - * IEEE-complete: each predicate encodes ordered (NaN -> false) vs unordered - * (NaN -> true) explicitly, so the distinction reaches every backend. The - * identity used throughout the backends is unordered-R == NOT(ordered-not-R) - * (e.g. ULT == !(OGE), UNE == !(OEQ)). CMP_OEQ_F is the FP boundary: an op is a - * floating compare iff op >= CMP_OEQ_F. */ -typedef enum CmpOp { - CMP_EQ, - CMP_NE, - CMP_LT_S, - CMP_LE_S, - CMP_GT_S, - CMP_GE_S, - CMP_LT_U, - CMP_LE_U, - CMP_GT_U, - CMP_GE_U, - /* Ordered FP relationals (NaN -> false). */ - CMP_OEQ_F, - CMP_ONE_F, - CMP_OLT_F, - CMP_OLE_F, - CMP_OGT_F, - CMP_OGE_F, - /* Unordered FP relationals (NaN -> true). */ - CMP_UEQ_F, - CMP_UNE_F, - CMP_ULT_F, - CMP_ULE_F, - CMP_UGT_F, - CMP_UGE_F, -} CmpOp; - -/* Conversions. Widths must order correctly (sext/zext widen, trunc narrows, - * bitcast preserves byte size). itof, fext, and ftrunc round to nearest-even; - * ftoi_s/ftoi_u round toward zero with a portable saturating out-of-range - * default (NaN -> 0) and an opt-in target-defined mode - * (CG_IR_INST_TARGET_FPTOINT_EDGES in src/cg/ir.h). Full rules in doc/IR.md. */ -typedef enum ConvKind { - CV_SEXT, - CV_ZEXT, - CV_TRUNC, - CV_ITOF_S, - CV_ITOF_U, - CV_FTOI_S, - CV_FTOI_U, - CV_FEXT, - CV_FTRUNC, - CV_BITCAST, -} ConvKind; - -/* Atomic op kinds (KitCgAtomicOp) and memory orders (KitCgMemOrder) come - * straight from the public API. Which orders are legal depends on the atomic - * op: load excludes release/acq_rel; store excludes acquire/consume/acq_rel; - * CAS failure order is one of relaxed/consume/acquire/seq_cst and no stronger - * than success. See the Atomics edge-case rules in doc/IR.md (mirrored by - * kit_cg_atomic_is_legal). */ - -/* Compiler-intrinsic kinds dispatched through CgTarget.intrinsic and carried - * on IR_INTRINSIC via IRIntrinAux.kind. The set is bounded: a backend - * must know each one to choose inline-vs-libcall. Hint intrinsics - * (EXPECT/TRAP/PREFETCH/ASSUME_ALIGNED) ride the same dispatch: - * the backend decides whether they emit an instruction or a no-op. - * `unreachable` is NOT here: it is a first-class control terminator with - * its own CgTarget hook (see below), not an intrinsic. - * - * Not every C builtin lives here. Parser-evaluated builtins - * (__builtin_offsetof, __builtin_constant_p, __builtin_choose_expr, - * __builtin_types_compatible_p) fold at parse and never reach IR. Builtins - * that already have dedicated CgTarget methods (alloca, va_*, atomics) keep - * them. Returns-twice and no-return control intrinsics use this dispatch so - * opt can preserve their CFG effects without growing backend vtable hooks. */ -typedef enum IntrinKind { - INTRIN_NONE = 0, - - /* bit ops */ - INTRIN_POPCOUNT, - INTRIN_CTZ, - INTRIN_CLZ, - INTRIN_BSWAP, - - /* memory. memcpy/memset are the dedicated copy_bytes/set_bytes hooks - * (kit_cg_memcpy/_memset); only memmove flows through the intrinsic path. */ - INTRIN_MEMMOVE, - INTRIN_PREFETCH, - INTRIN_ASSUME_ALIGNED, - - /* hints */ - INTRIN_EXPECT, - INTRIN_TRAP, - - /* OS trap: args[0] is the syscall number, args[1..6] are integer/pointer - * payloads; dsts[0] receives the target long result. */ - INTRIN_SYSCALL, - - /* non-local control */ - INTRIN_SETJMP, - INTRIN_LONGJMP, - - /* checked arith — multi-result (value, overflow_flag) */ - INTRIN_SADD_OVERFLOW, - INTRIN_UADD_OVERFLOW, - INTRIN_SSUB_OVERFLOW, - INTRIN_USUB_OVERFLOW, - INTRIN_SMUL_OVERFLOW, - INTRIN_UMUL_OVERFLOW, - - /* baremetal CPU control — single-instruction, no operands unless noted. - * dsts/args empty except IRQ_SAVE (dsts[0] = saved interrupt state) and - * IRQ_RESTORE (args[0] = state to restore). Privileged forms (WFI/WFE/SEV - * and the IRQ family) trap at user level; backends still emit the one - * instruction and frontends gate any runtime use behind a capability test. */ - INTRIN_CPU_NOP, - INTRIN_CPU_YIELD, - INTRIN_WFI, - INTRIN_WFE, - INTRIN_SEV, - INTRIN_ISB, - INTRIN_DMB, - INTRIN_DSB, - INTRIN_IRQ_SAVE, - INTRIN_IRQ_RESTORE, - INTRIN_IRQ_ENABLE, - INTRIN_IRQ_DISABLE, - - /* frame-pointer-chain introspection — value-producing, single immediate - * operand (the constant level). args[0] is the level (OPK_IMM); dsts[0] is - * the void* result. Lowered as an unrolled FP walk; modeled as an ordinary - * frame-dependent memory read (IR_INTRINSIC is already conservatively - * side-effecting in opt, so it is never hoisted, CSE'd, or eliminated). */ - INTRIN_FRAME_ADDRESS, - INTRIN_RETURN_ADDRESS, -} IntrinKind; - -typedef enum OpKind { - OPK_IMM, - OPK_LOCAL, /* typed semantic local */ - OPK_GLOBAL, /* address: symbol+addend, not a load */ - OPK_INDIRECT, /* [local + ofs], with optional indexed local */ -} OpKind; - -typedef enum CGLocalFlag { - CG_LOCAL_FLAG_NONE = 0, - CG_LOCAL_ADDR_TAKEN = 1u << 0, - CG_LOCAL_MEMORY_REQUIRED = 1u << 1, -} CGLocalFlag; - -typedef struct CGLocalDesc { - KitCgTypeId type; - Sym name; - SrcLoc loc; - u32 size; - u32 align; - u32 flags; /* CGLocalFlag */ -} CGLocalDesc; - -typedef enum MemFlag { - MF_NONE = 0, - MF_VOLATILE = 1u << 0, - MF_ATOMIC = 1u << 1, - MF_RESTRICT = 1u << 2, - MF_READONLY = 1u << 3, - MF_WRITEONLY = 1u << 4, - MF_UNALIGNED = 1u << 5, -} MemFlag; - -typedef enum AliasKind { - ALIAS_UNKNOWN, - ALIAS_LOCAL, - ALIAS_GLOBAL, - ALIAS_PARAM, - ALIAS_HEAP, - ALIAS_STRING, -} AliasKind; - -typedef struct AliasRoot { - u8 kind; /* AliasKind */ - u8 pad[3]; - union { - i32 local_id; - ObjSymId global; - u32 param_idx; - Sym string_id; - } v; -} AliasRoot; - -typedef struct MemAccess { - KitCgTypeId type; /* codegen object type accessed */ - u32 size; /* ABI byte size of this access (storage-unit size for a - * bit-field) */ - u32 align; /* known byte alignment; 0 means unknown */ - u16 flags; /* MemFlag */ - u16 addr_space; - /* Bit-field rider: when bf_width != 0 this access is a bit-field, so `load` - * extracts (shift+mask+extend) and `store` inserts (read-modify-write) within - * the storage unit described by {type,size}. The CgTarget impls translate - * this to the physical NativeTarget bitfield_load/store (or the recorder IR - * op); the semantic CgTarget no longer carries a separate bit-field method. - */ - u16 bf_offset; /* target-endian bit offset within the storage unit */ - u16 bf_width; /* 0 => not a bit-field access */ - u8 bf_signed; /* signed extraction on load */ - u8 bf_pad[3]; - AliasRoot alias; -} MemAccess; - -typedef struct ConstBytes { - KitCgTypeId type; - const u8* bytes; /* ABI representation, little/big endian per target */ - u32 size; - u32 align; -} ConstBytes; - -typedef struct AggregateAccess { - KitCgTypeId type; - u32 size; - u32 align; - MemAccess mem; -} AggregateAccess; - -typedef struct BitFieldAccess { - KitCgTypeId field_type; - MemAccess storage; - u32 storage_offset; /* byte offset from record base */ - u16 bit_offset; /* target-endian bit offset within storage unit */ - u16 bit_width; /* may be 0 for zero-width layout barriers */ - u8 signed_; - u8 pad[3]; -} BitFieldAccess; - -/* Reconstruct the BitFieldAccess a CgTarget impl needs from the bit-field - * MemAccess that rides the generic load/store (bf_width != 0). The storage unit - * is {m.type, m.size}; the bit geometry is the bf_* rider. */ -static inline BitFieldAccess bf_from_mem(MemAccess m) { - BitFieldAccess bf = {0}; - bf.field_type = m.type; - bf.storage = m; - bf.storage.bf_offset = 0; - bf.storage.bf_width = 0; - bf.storage.bf_signed = 0; - bf.bit_offset = m.bf_offset; - bf.bit_width = m.bf_width; - bf.signed_ = m.bf_signed; - return bf; -} - -typedef struct Operand { - u8 kind; - u8 pad[3]; - KitCgTypeId type; - union { - i64 imm; - CGLocal local; - struct { - ObjSymId sym; - i64 addend; - } global; - struct { - CGLocal base; - CGLocal index; /* CG_LOCAL_NONE when no index operand */ - u8 log2_scale; /* 0..3 -> 1/2/4/8 bytes; ignored when no index */ - i32 ofs; - } ind; - } v; -} Operand; - -typedef struct CGParamDesc { - u32 index; - Sym name; - KitCgTypeId type; - u32 size; - u32 align; - u32 flags; /* CGLocalFlag */ - SrcLoc loc; -} CGParamDesc; - -/* text_section_id and group_id are per-function so that -ffunction-sections, - * __attribute__((section)) on functions, and COMDAT for C11 inline-with- - * external-definition all work with no extra plumbing. Decl.section_id already - * carries the user's request; CG/decl decides the section name policy - * (default .text, vs .text.<sym> under -ffunction-sections, vs explicit - * attribute). The backend just writes to the named section. */ -/* Phase 2 attribute-derived hints. The backends are free to ignore these; - * they exist so the parser can communicate _Noreturn / __attribute__ - * info down to CG without forcing every backend to consult the Decl. */ -typedef enum CGFuncDescFlag { - CGFD_NONE = 0, - CGFD_NORETURN = 1u << 0, -} CGFuncDescFlag; - -typedef struct CGFuncDesc { - ObjSymId sym; - ObjSecId text_section_id; - ObjGroupId group_id; /* OBJ_GROUP_NONE if none */ - KitCgTypeId fn_type; - KitCgTypeId result_type; /* KIT_CG_TYPE_NONE/void == no result */ - const CGParamDesc* params; - u32 nparams; - SrcLoc loc; - u32 flags; /* CGFuncDescFlag */ - KitCgInlinePolicy inline_policy; - u16 sym_bind; /* SymBind */ - u16 sym_kind; /* SymKind */ - u8 sym_vis; /* SymVis */ - u8 atomize; - u8 pad[2]; -} CGFuncDesc; - -typedef enum CGCallFlag { - CG_CALL_NONE = 0, - /* Sibling call. The target emits a tail-position call and does NOT emit a - * return-style continuation. CG will not invoke target->ret afterwards. - * - * Realizability is verified before this flag is set: CG only sets it after - * tail_call_unrealizable_reason() returns NULL for the same desc and call - * state, so the target can emit the sibling call unconditionally. The - * target may assert/compiler_panic if the flag is set on an unrealizable - * desc, but that is an internal-consistency check — fallback and - * diagnostics for unrealizable tail calls are CG's responsibility, not the - * target's. */ - CG_CALL_TAIL = 1u << 0, -} CGCallFlag; - -typedef struct CGCallDesc { - KitCgTypeId fn_type; - Operand callee; - const CGLocal* args; - CGLocal result; /* CG_LOCAL_NONE == void callee (no result) */ - u32 nargs; - u16 flags; /* CGCallFlag */ - u8 tail_policy; /* KitCgTailPolicy; meaningful when CG_CALL_TAIL is set. - * The opt recorder accepts every tail and preserves this so - * the replay can pick: emit tail (realizable), fall back to - * call+ret (ALLOWED), or diagnose (MUST). */ - u8 pad; - KitCgInlinePolicy inline_policy; -} CGCallDesc; - -typedef u32 Label; -#define LABEL_NONE 0 - -typedef enum ScopeKind { - SCOPE_BLOCK, /* break exits forward */ - SCOPE_LOOP, /* break exits forward; continue uses explicit target */ -} ScopeKind; - -typedef u32 CGScope; -#define CG_SCOPE_NONE 0u - -typedef struct CGScopeDesc { - u8 kind; /* ScopeKind */ - u8 pad[3]; - Label break_label; /* explicit target for break; LABEL_NONE => target creates - one */ - Label continue_label; /* explicit target for continue; LABEL_NONE for - non-loops */ - KitCgTypeId result_type; /* reserved for structured expression results */ -} CGScopeDesc; - -typedef struct AsmConstraint { - const char* str; /* GCC-style: "r", "=&r", "+m", "i", "0" ... */ - Sym name; /* GCC `[name]` symbolic operand; 0 if absent */ - KitCgTypeId type; /* codegen type of the bound expression (output lvalue or - input rvalue). Drives type width for the binder. - NULL only for hand-built test constraints (binder - falls back to a 64-bit int default). */ - Sym reg; /* Explicit hard-register name ("r10"/"x8"/...) this operand - must occupy — a GNU local register variable bound as an - operand; 0 = unconstrained. Only the target's register - file resolves the name to a physical register. */ - u8 dir; /* KitCgAsmDir */ - u8 pad[3]; -} AsmConstraint; - -typedef struct CGSwitchCase { - /* Bit pattern matched against the selector; interpreted using - * selector_type's width and signedness (signed comparison uses - * sign-extension to selector_type's width). */ - u64 value; - Label label; -} CGSwitchCase; - -typedef struct CGSwitchDesc { - Operand selector; /* OPK_LOCAL or OPK_IMM */ - KitCgTypeId selector_type; - Label default_label; /* LABEL_NONE means "fall through past the switch" */ - const CGSwitchCase* cases; - u32 ncases; - u8 hint; /* KitCgSwitchHint */ - u8 opt_level; /* 0/1/2; reads policy in cg_lower_switch_default */ - u8 pad[2]; -} CGSwitchDesc; - -typedef struct CGLocalStaticDataDesc { - ObjSymId sym; - KitCgTypeId type; - KitCgDataDefAttrs attrs; - u32 align; -} CGLocalStaticDataDesc; - -typedef enum CGDebugLocKind { - CG_DEBUG_LOC_NONE, - CG_DEBUG_LOC_FRAME, - CG_DEBUG_LOC_REG, - CG_DEBUG_LOC_GLOBAL, -} CGDebugLocKind; - -typedef struct CGDebugLoc { - u8 kind; /* CGDebugLocKind */ - u8 pad[3]; - union { - /* Offset in the same target-defined frame-base coordinate system that the - * target/debugger pair uses to materialize frame-relative variables. CG - * treats this as opaque target data and only maps it into the debug - * producer's generic frame-location form. */ - i32 frame_ofs; - u32 reg; - ObjSymId global; - } v; -} CGDebugLoc; +#include "cg/cgir.h" /* Forward-declared (same as arch/mc.h) so a CgTarget can carry an optional * Debug producer without this header depending on debug/debug.h. */ diff --git a/src/cg/internal.h b/src/cg/internal.h @@ -16,10 +16,10 @@ #include "core/segvec.h" #include "core/slice.h" #include "core/strbuf.h" +#include "cg/cgtarget.h" #include "debug/debug.h" #include "obj/obj.h" -typedef struct CgTarget CgTarget; typedef uint32_t ObjSymId; typedef enum SResidency { diff --git a/src/cg/ir.h b/src/cg/ir.h @@ -1,7 +1,7 @@ #ifndef KIT_CG_IR_H #define KIT_CG_IR_H -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "core/arena.h" #include "core/hashmap.h" diff --git a/src/cg/ir_eval.h b/src/cg/ir_eval.h @@ -6,7 +6,7 @@ * (cg/fold.c's api_try_fold_int_*, opt/pass_o2.c's gvn_fold_*, and the * width/convert helpers in opt/pass_combine.c + pass_simplify.c). * - * Dependency-light by design: it pulls in only cg/cgtarget.h (for the BinOp / + * Dependency-light by design: it pulls in only cg/cgir.h (for the BinOp / * UnOp / CmpOp / ConvKind op tags and the integer typedefs) and operates on * plain (op tag, width-in-bits, i64 bit-patterns). It knows NOTHING about * Compiler, cg/internal.h, or the optimizer's IR types, so BOTH the semantic @@ -18,7 +18,7 @@ * scalar bit width (1..64); the eval entry points return 0 for op tags they do * not evaluate, leaving *out untouched. */ -#include "cg/cgtarget.h" +#include "cg/cgir.h" /* Low `width` bits set (width>=64 -> all ones). */ u64 kit_ir_width_mask(u32 width); diff --git a/src/cg/ir_recorder.h b/src/cg/ir_recorder.h @@ -1,6 +1,7 @@ #ifndef KIT_CG_IR_RECORDER_H #define KIT_CG_IR_RECORDER_H +#include "cg/cgtarget.h" /* the recorder is a CgTarget that records into CgIr */ #include "cg/ir.h" typedef struct CgIrRecorder CgIrRecorder; diff --git a/src/interp/engine.c b/src/interp/engine.c @@ -10,7 +10,7 @@ #include <string.h> #include "abi/abi.h" -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "cg/type.h" #include "core/arena.h" #include "core/core.h" diff --git a/src/interp/interp.h b/src/interp/interp.h @@ -12,7 +12,7 @@ #include <kit/interp.h> #include "abi/abi.h" -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "core/core.h" #include "core/slice.h" #include "obj/obj.h" diff --git a/src/interp/lower.c b/src/interp/lower.c @@ -14,7 +14,7 @@ * `Operand`/`CGCallDesc`/... to their Opt* forms via macros, so the semantic * cg structs (CgIrLocalStatic*Aux, reused verbatim as the opt aux pointers) * have to be parsed first. This mirrors opt/opt.h's include order. */ -#include "cg/cgtarget.h" +#include "cg/cgir.h" #include "cg/ir.h" #include "cg/type.h" #include "core/arena.h" diff --git a/src/opt/opt.h b/src/opt/opt.h @@ -3,6 +3,7 @@ #include "arch/mc.h" #include "arch/native_target.h" +#include "cg/cgtarget.h" /* CgFinishPolicy + the CgTarget the optimizer lowers to */ #include "cg/ir.h" #include "opt/ir.h"