kit

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

commit 21fc76d188106a6891e6a316b6daffbba380f03f
parent 1812cce424a193d2044059895dde09565734344c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 17:46:52 -0700

refactor(cg): unify -O0 read-side materialize into nd_gv (PERF-O0-CODESIZE Lever 1, Part B)

Collapse nd_materialize_operand (materialize into any register) and
nd_materialize_operand_into (materialize into a caller-supplied register) into a
single nd_gv(d, op, want) primitive — kit's analog of tcc's gv(rc) ("generalize
a value into a register, no code if already there"). `want == NULL` is the
generic-class case (returns a freshly pinned scratch or the live cache reg);
`want != NULL` targets a specific already-pinned register, copying/loading the
value in. Both share one residence-resolution path (cache hit, or width-mismatch
flush + load from memory), so there is now a single soundness argument for the
-O0 read side. The two old names remain as thin wrappers.

want is a `const NativeLoc*` (not a bare Reg + REG_ANY sentinel): the into-case
caller (nd_copy) hands a destination whose type/width can differ from the
operand's, and that destination type drives nd_copy_to_reg — so the full loc
must be carried, and NULL cleanly denotes "any register" with no new sentinel.

Pure refactor: branches map 1:1 to the originals. sqlite3.c -c is byte-identical
to the prior commit; test-toy 1392/0, test-smoke-x64/-rv64 3/0.

Diffstat:
Msrc/cg/native_direct_target.c | 63++++++++++++++++++++++++++++++++++++++++-----------------------
1 file changed, 40 insertions(+), 23 deletions(-)

diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -955,44 +955,61 @@ static void nd_flush_operand_addr_locals(NativeDirectTarget* d, Operand op) { if (op.v.ind.index != CG_LOCAL_NONE) nd_flush_local(d, op.v.ind.index); } -static NativeLoc nd_materialize_operand(NativeDirectTarget* d, Operand op) { +/* Materialize OP's value into a register and return it — the tcc gv(rc) analog: + * "generalize this value into a register, doing nothing if it is already there." + * + * want == NULL : ANY register of OP's class (gv with a generic class). A cache + * hit returns the live register pinned, with no code; otherwise + * a fresh scratch is acquired and OP loaded into it. The caller + * releases the pin with nd_release_materialized. + * want != NULL : that SPECIFIC, already-acquired+pinned register (gv into a + * fixed reg). A cache-resident OP costs the one unavoidable + * reg-reg move; a memory / immediate / global / indirect OP is + * loaded straight into *want (no scratch round-trip). *want + * carries the destination type/width the copy uses, which may + * differ from OP's (e.g. a narrowing copy), so it — not OP — + * drives nd_copy_to_reg. Returns *want. + * + * Both modes share one residence resolution (cache hit, or width-mismatch flush + * then memory), so there is a single soundness argument for getting a value into + * a register at -O0. */ +static NativeLoc nd_gv(NativeDirectTarget* d, Operand op, const NativeLoc* want) { NativeAllocClass cls = nd_class_for_type(d, op.type); if (op.kind == OPK_LOCAL) { NativeDirectLocal* l = nd_local(d, op.v.local); if (l->reg != REG_NONE && op.type == l->type && nd_local_cacheable(d, l)) { - /* Cache hit: pin and reuse the live register, no reload. */ - d->scratch_used[l->cls] |= 1u << l->reg; nd_touch_local(d, l); - return nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg); + if (!want) { + /* Cache hit: pin and reuse the live register, no reload. */ + d->scratch_used[l->cls] |= 1u << l->reg; + return nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg); + } + nd_copy_to_reg(d, *want, + nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg)); + return *want; } /* A live entry under a different access width must reach memory before we * bypass the cache for this access. */ if (l->reg != REG_NONE) nd_flush_local(d, op.v.local); } nd_flush_operand_addr_locals(d, op); - return nd_materialize_loc(d, nd_loc_operand(d, op), cls, op.type); + if (!want) + return nd_materialize_loc(d, nd_loc_operand(d, op), cls, op.type); + nd_copy_to_reg(d, *want, nd_loc_operand(d, op)); + return *want; } -/* Materialize OP straight into the already-acquired register WANT, skipping the - * scratch round-trip nd_materialize_operand + a separate move would make for a - * memory / immediate / global / indirect source: nd_copy_to_reg loads each of - * those forms directly into its destination. A cache-resident local still costs - * one reg-reg move (its value lives in another register); everything else lands - * in WANT with a single load. WANT must already be pinned by the caller. */ +static NativeLoc nd_materialize_operand(NativeDirectTarget* d, Operand op) { + return nd_gv(d, op, NULL); +} + +/* Materialize OP straight into the already-acquired register WANT (gv into a + * fixed reg), skipping the scratch round-trip + separate move a memory / + * immediate / global / indirect source would otherwise make. WANT must already + * be pinned by the caller. */ static void nd_materialize_operand_into(NativeDirectTarget* d, NativeLoc want, Operand op) { - if (op.kind == OPK_LOCAL) { - NativeDirectLocal* l = nd_local(d, op.v.local); - if (l->reg != REG_NONE && op.type == l->type && nd_local_cacheable(d, l)) { - nd_touch_local(d, l); - nd_copy_to_reg(d, want, - nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg)); - return; - } - if (l->reg != REG_NONE) nd_flush_local(d, op.v.local); - } - nd_flush_operand_addr_locals(d, op); - nd_copy_to_reg(d, want, nd_loc_operand(d, op)); + nd_gv(d, op, &want); } static NativeLoc nd_dst_scratch(NativeDirectTarget* d, Operand dst) {