Skip to content

Latest commit

 

History

History
372 lines (331 loc) · 21 KB

File metadata and controls

372 lines (331 loc) · 21 KB

Codegen Registers And ABI

kimicc still emits target assembly directly from the parser AST. It now has a small MIR package for target-sensitive semantic facts and compile-test interpretation, but not yet a full instruction-level machine IR or a general register allocator. The Darwin ARM64 backend is built around a fixed scratch-register discipline plus explicit Darwin ARM64 C ABI argument and return placement. The linux/amd64 backend uses MIR semantic facts for size, alignment, field layout, offsetof paths, global-expression typing, and covered integer constant folding while it continues to emit assembly from the parser AST.

This document describes the current implementation contract. It is not a claim of complete C ABI conformance for every C type or language feature. The accurate claim is narrower: the backend is ABI-aware for the supported ARM64 macOS C subset, and that subset is guarded by focused tests and Clang differentials.

Register Discipline

Most C objects live in stack slots. Registers are transient codegen resources used while evaluating one expression or crossing a function-call boundary.

Register Current role
x0-x7 Integer/pointer argument registers and integer return registers at ABI boundaries.
s0-s7, d0-d7 Floating-point argument registers and floating-point return registers at ABI boundaries.
x8 Hidden struct-return destination pointer for indirect aggregate returns.
x9 Primary expression result register and address result register.
x10 Secondary scalar register: right-hand operand, high half of __int128, or temporary value.
x11, x12, x13 Scratch temporaries for address arithmetic, bit-fields, atomics, copies, and masks.
x15 Saved indirect callee address before a blr.
x16 Large stack-frame adjustment scratch.
x29, x30 Frame pointer and link register, saved/restored in the normal prologue/epilogue.
x18 Reserved by Apple platforms; codegen does not use it.
sp Evaluation spill stack, call temporary area, outgoing stack arguments, and local frame allocation.

The generator avoids keeping user variables in registers across expressions. This keeps correctness local and makes nested calls simple, at the cost of extra loads and stores.

Function Frames

Codegen::gen_func starts each function by clearing per-function state, assigning stack slots to parameters, collecting all local declarations and hidden temporaries, and rounding the frame to 16 bytes. It then emits:

stp x29, x30, [sp, #-16]!
mov x29, sp
sub sp, sp, #frame_size

Parameters are copied from ABI locations into their stack slots at function entry. Integer and pointer parameters come from x0-x7 or caller stack slots. float and double parameters come from s0-s7 / d0-d7 or stack slots. Aggregate parameters are copied according to their current size class.

After that entry copy, reads and writes of C locals normally go through emit_load_local, emit_store_local, gen_addr, and typed load/store helpers. Because x29 points at the saved {x29, x30} pair, __builtin_frame_address(0) returns x29 and __builtin_return_address(0) loads the saved link register from [x29, #8]; nonzero depths currently return null. Return-address extract/frob helpers are identity operations.

Expression Evaluation

gen_expr leaves scalar expression results in x9. Address-producing paths also leave the address in x9.

Binary scalar expressions use a stack spill instead of allocating another live register:

  1. Evaluate the left operand into x9.
  2. Spill x9 to [sp, #-16]!.
  3. Evaluate the right operand into x9.
  4. Move the right result to x10.
  5. Pop the left result back into x9.
  6. Emit the operation using x9 and x10.

__int128 values use the pair x9:x10, with x9 holding the low half and x10 holding the high half. Floating-point values are often carried as raw bits in integer scratch registers between AST operations, then moved into sN/dN registers for floating-point arithmetic, calls, or returns. Covered conditional expressions compute a C result type for scalar, pointer, and function-pointer branches, then convert only the selected arm to that type before control flow rejoins.

Calls

Calls have their own ABI placement pass. The backend first classifies each argument, computes the needed temporary stack area, evaluates all arguments into temporary stack slots, and only then reloads those saved values into ABI locations. This avoids nested calls clobbering partially prepared argument registers.

For non-variadic calls:

  • integer and pointer arguments are assigned to x0-x7, with overflow in outgoing stack slots;
  • float and double arguments are assigned to s0-s7 / d0-d7, with overflow in outgoing stack slots;
  • 128-bit integer arguments consume two integer slots;
  • small struct/union arguments are copied into one or two integer slots;
  • larger struct/union arguments are copied to a caller-owned temporary area and passed by pointer;
  • indirect callees are evaluated and saved before argument reloading, then called with blr x15.

For variadic calls, the backend also builds the Darwin ARM64 unnamed-argument stack area so va_start/va_arg can read variadic arguments from the expected location. Known external variadic functions such as printf, snprintf, open, fcntl, and ioctl are treated as variadic even when only an external declaration is visible.

Returns And Aggregates

Scalar returns are moved to ABI return registers:

  • integer and pointer results use x0;
  • float uses s0;
  • double uses d0;
  • __int128 uses x0:x1.

Structs and unions are split into two current size classes:

  • aggregates up to 16 bytes are register aggregates and are returned or passed through one or two integer slots;
  • aggregates larger than 16 bytes are indirect aggregates and use caller-owned storage, with return destinations passed in hidden x8.

Assignments and initializers copy aggregate storage with explicit address-based paths, and call results for register aggregates are materialized when a later operation needs an address or field access.

Global Initializers

Darwin ARM64 global scalar initializers are folded through the initializer expression's C type before conversion to the destination object type. This keeps integer subexpressions such as 1 / 2 in integer semantics when initializing floating storage, preserves explicit floating-to-integer initializer conversions, and applies the covered usual arithmetic conversions for unsigned integer division, modulo, shifts, and comparisons. Pointer and function-pointer global initializers can also emit relocations to globals, functions, and aggregate subobjects. Pointer arithmetic in those relocations is scaled by the effective pointer type, including casted pointer bases and address-of casted pointer subscripts. Global aggregate initializers emit layout padding and covered bit-field storage units, including grouped struct bit-fields and designated union bit-fields.

ABI Compliance Status

The backend is intended to match the Darwin ARM64 C ABI for the supported feature set. Several high-risk ABI areas have focused tests, including:

  • narrow scalar parameter signedness;
  • prototype-driven scalar argument conversion;
  • floating-point argument and return registers;
  • mixed scalar conditional-expression conversions;
  • conditional function-pointer calls preserving argument and return metadata;
  • 16-byte aggregate pass/return behavior;
  • large aggregate arguments and hidden result pointers;
  • variadic calls and va_arg layout;
  • global scalar constant initializers;
  • global aggregate initializers, including mixed nested field/index designators, plus pointer-relocation and bit-field initializers;
  • direct Mach-O object mode versus assembly mode.

The project also keeps broader Clang differential corpora and external-project smoke gates. See docs/sqlite-conformance-plan.md for the evolving confidence matrix and residual known failures.

Do not treat this as full ABI compliance yet. Known reasons include:

  • there is no independent ABI classification table separate from codegen;
  • _Float16 and __float128 are parsed only as unsupported placeholder types, while Linux/amd64 long double and _Float64x are parsed as distinct x87 ABI types but rejected when value lowering is required. Linux/glibc _Float32, _Float64, and _Float32x declarations are accepted as the covered float/double ABI peers; decimal/vendor floating spellings are rejected explicitly. Vector/SIMD C types and HFA/HVA aggregate rules are not modeled as first-class supported types;
  • unusual packing, alignment, and extension combinations need more differential coverage;
  • the full C language and GNU/Clang extension surface is still growing.

In short: the current Darwin ARM64 backend is intentionally ABI-aware and correct for the covered subset, but complete C ABI compliance remains a conformance goal rather than a finished claim.

Linux/amd64 Backend Status

The linux-amd64 backend is separate from the Darwin ARM64 backend. It emits GNU assembler syntax for the System V x86-64 ABI and uses a similar simple model: C locals are stack slots, expression results use rax, and short-lived temporaries use scratch registers such as r10 and r11. Scalar integer operations use signed or unsigned x86-64 instructions according to C's usual arithmetic conversions for the covered integer subset. Narrow integer and _Bool scalar casts, initializers, assignments, and returns are converted to the destination type before storage or ABI return placement; unsigned 64-bit integer to float/double conversions use an explicit high-bit path instead of signed cvtsi2s*, and float/double to unsigned 64-bit integer conversions use the corresponding high-bit split around 2^63. Mixed scalar ternary arms are converted to the selected conditional-expression result type before control flow rejoins. Mixed integer/floating compound assignments for non-__int128 integer lvalues use floating arithmetic before converting back to the lvalue type, including covered integer bit-field lvalues. Covered integer bit-field compound assignments apply integer promotions before the arithmetic operation and then store back through the bit-field mask. Direct calls use declared parameter types to select scalar argument registers and conversions; typed function-pointer calls preserve parsed parameter types and apply the same scalar argument conversions before indirect calls. Pointer +, -, +=, and -= scale integer operands by the pointed element size for the covered object pointer cases. Unnamed variadic call arguments use C default argument promotions before ABI classification, and calls through declarations with no recorded parameter types apply the same promotions to supplied arguments. Narrow integer call results are normalized from their ABI return subregister before the value is used as an expression.

Its current ABI coverage is intentionally narrower than the Darwin backend: integer and pointer parameters use rdi, rsi, rdx, rcx, r8, and r9; float and double parameters use xmm0 through xmm7; integer and pointer returns use rax; __int128/__uint128_t transport uses paired integer eightbytes in rdx:rax for returns and two integer argument slots for calls, parameters, and va_arg, with truth tests checking both halves. Covered 128-bit arithmetic includes add, subtract, multiply, bitwise operations, shifts, comparisons, compound assignment, and division/remainder through the normal __divti3/__udivti3/__modti3/__umodti3 helper calls. Floating-point returns use xmm0; ELF symbols do not use the Darwin leading underscore; scalar overflow arguments are copied to the System V stack argument area. Direct and indirect function-pointer calls use the same argument and return placement. Direct variadic calls set %al, variadic callees save the System V register-save area, and __builtin_va_* lowering is implemented for scalar values plus the covered small and memory-class aggregate va_arg subset. Common memory and string builtins are lowered to Linux libc calls before normal x86-64 call lowering; fortified strlcpy/strlcat checked builtins drop their object-size argument before the libc call. Fortified formatted-output builtins lower to snprintf, vsnprintf, sprintf, vsprintf, printf, vprintf, fprintf, and vfprintf after dropping fortify metadata arguments. Common scalar hint/query and bit-manipulation builtins lower directly without external calls; __builtin_unpredictable returns its value operand like the other no-op prediction hints, and object-size query builtins conservatively report unknown size. Scalar alignment query/rounding builtins __builtin_is_aligned, __builtin_align_up, and __builtin_align_down lower directly with integer mask operations on the value representation. __builtin_flt_rounds currently reports the default round-to-nearest mode. __builtin_frame_address exposes rbp; __builtin_return_address(0) loads [rbp + 8], nonzero depths return null, and return-address extract/frob helpers are identity operations on x86-64. Common signed integer absolute-value builtins __builtin_abs, __builtin_labs, and __builtin_llabs also lower directly at the target result width. __builtin_alloca dynamically subtracts a 16-byte-aligned size from rsp and the function epilogue restores rsp from rbp. Common floating builtins including __builtin_fabs*, __builtin_nan*, __builtin_huge_val*, __builtin_inf*, __builtin_copysign*, __builtin_sqrt*, and the ordered/unordered __builtin_is* predicates also lower directly; floating comparisons explicitly account for unordered NaN conditions. float and double compound assignments plus prefix/postfix increment/decrement keep their arithmetic in SSE registers and store the converted result back through the lvalue type. Covered integer-width atomic builtins lower directly: loads and stores use x86-64 scalar memory operations, GNU and C11 exchange plus byte test-and-set/clear use xchg, compare-exchange and read-modify-write operations, including nand, use lock cmpxchg, C11 weak compare-exchange uses the same non-spurious lowering as strong compare-exchange, legacy __sync_* compare-and-swap and read-modify-write builtins, including signed/unsigned min/max, use the same update loop, thread fences and __sync_synchronize use mfence, signal fences are no-ops in emitted code, and lock-free query builtins report true for the covered 1-, 2-, 4-, and 8-byte scalar widths. The common overflow-checking builtins __builtin_add_overflow, __builtin_sub_overflow, and __builtin_mul_overflow, plus their signed and unsigned typed spellings such as __builtin_sadd_overflow and __builtin_umulll_overflow, lower directly for integer result pointer types up to 64 bits, using x86-64 carry/overflow flags and storing the truncated result through the supplied pointer. The Docker smoke also verifies that common Ubuntu 24.04 system headers preprocess far enough to expose glibc typedefs such as uint8_t, uintptr_t, size_t, ptrdiff_t, and va_list, then compiles and links a small Linux/amd64 executable using those typedefs. The compiled probe includes representative unsigned max-value macro comparisons, including UINTPTR_MAX and SIZE_MAX, in _Static_assert expressions. That is a targeted system-header gate, not a complete libc or System V ABI conformance claim. A separate smoke passes kimicc's generated va_list state to glibc vsnprintf, vsprintf, vprintf, vwprintf, vswprintf, vfprintf, vfwprintf, vdprintf, vasprintf, vsscanf, vscanf, vwscanf, vswscanf, vfscanf, and vfwscanf, and also passes a va_copy-copied cursor back to vsnprintf, which covers real v-function interop paths while leaving broader libc va_list compatibility open. Small scalar-field struct/union arguments and returns up to 16 bytes are classified recursively by eightbyte, including nested structs/unions and arrays of supported scalar fields, and passed or returned in the covered integer, SSE, mixed integer/SSE, and overflow-stack subset. When the final aggregate eightbyte is only partially occupied, register loads and stores use that partial object width rather than an unconditional 8-byte memory access, including the scratch copy used for register-sourced aggregate va_arg. Memory-class aggregate arguments are passed by value on the overflow stack, and memory-class aggregate returns use the System V hidden result pointer. Actual aggregate object copies and va_arg overflow-stack scratch copies use the exact C object size so nested small struct assignments and packed varargs do not overwrite following fields; only ABI staging areas and overflow cursor movement are rounded to eightbyte slots. Local aggregate brace initializers and compound literals are supported for the covered scalar, array, struct, union, nested-designator, and bit-field initializer cases; compound literals are materialized in stack storage. Global floating and integer scalar constant initializers are folded through the source expression's C type before conversion to the destination object type, so integer subexpressions such as 1 / 2 keep integer-division semantics and explicit or implicit floating-to-integer initializer conversions initialize integer storage. Integer constant folding uses the covered usual arithmetic conversions, including unsigned division, modulo, right shifts, and relational comparisons. Initialized global arrays, structs, and unions are emitted with layout padding and designated-initializer holes for the covered aggregate cases, including mixed nested field/index designators, bit-field storage units that do not overlap earlier streamed scalar fields, and pointer/function-pointer relocations to globals, functions, and aggregate subobjects. Pointer arithmetic in global relocations is scaled by the effective pointer type, including casted pointer bases and address-of casted subscripts such as (char *)array + n and &((char *)array)[n]. __builtin_offsetof folds simple, nested, and constant array-index member designators using the same covered aggregate layout model. The shared frontend also folds covered C11 _Generic selections to the chosen association expression before backend lowering; the controlling expression is parsed for type selection but not emitted or evaluated. GNU typeof/__typeof__ type specifiers are similarly resolved in the parser; expression operands are parsed only to infer their type. GNU __auto_type locals are resolved in the parser from their initializer type for covered scalar and pointer expressions. C11 _Alignof(type) and GNU alignof/__alignof__ type or expression operands are folded to the covered parser type model's alignment. GNU __builtin_choose_expr and __builtin_types_compatible_p are handled the same way for the covered parser type model. GNU __builtin_classify_type is also folded before backend lowering. _Alignas, numeric GNU __attribute__((aligned(N))), and bare or empty GNU aligned attributes participate in x86-64 aggregate layout, ELF global alignment, and stack-local frame realignment. Functions that need a local frame alignment greater than 16 bytes save and restore r13 when using it as the aligned local-frame base. GNU __attribute__((mode(...))) maps common Linux header scalar modes such as byte, HI, DI, pointer, word, and unwind_word onto the corresponding covered parser scalar types before ABI layout and codegen. GNU __attribute__((packed)) participates in aggregate layout, and packed aggregates with unaligned fields become System V memory-class arguments and returns. The same unaligned-field rule is applied when a non-packed outer aggregate contains a packed member at an unaligned offset. Integer bit-fields use packed storage-unit layout for the covered cases and are lowered with read-modify-write updates. Linux/amd64 also lowers covered GNU symbol attributes for ELF weak bindings, aliases, custom sections, visibility, and constructor/destructor arrays. Parser-accepted no-op GNU diagnostic, optimization, allocation, and sanitizer attributes are ignored during lowering. Switch dispatch emits explicit case comparisons, uses the promoted control expression type, preserves default and fallthrough behavior, and compares 32-bit integer switches through eax. Vector, x87/complex, vector varargs, direct ELF object emission, and Linux JIT loading are still future work. Complete libc va_list interoperability is also open. See docs/linux-amd64-target.md for the current test workflow.

References