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