kit

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

commit 2b1b03c21d1b949107cf692febb5a709969608f0
parent 93aafda346d271d17ce2ea95abb2083e4ff896b2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 06:49:38 -0700

perf(cg): -O0 assignment coalescing — RHS flows straight into the local

Two coordinated changes remove the temp->local routing `mov` for the common
`x = <expr>` / `int x = <expr>`:

1. frontend (cg_adapter): pcg_store always dup'd the RHS to preserve the
   assignment's expression value, but 34 of 36 store sites immediately drop
   that value (`pcg_store; pcg_drop` — statements and initializers). Add
   pcg_store_void (keep_result=0) that consumes [place, value] without the
   dup, and convert those 34 sites. The un-dup'd RHS now reaches kit_cg_store
   still delayed (the 2 result-keeping sites — `a=b=c`, `if((x=f()))` — keep
   pcg_store).

2. cg (kit_cg_store): when a still-delayed arith/cmp is stored into a scalar
   local, materialize it DIRECTLY into the local (api_materialize_{arith,cmp}_to
   with dst = the local) instead of into a temp that is then copied in. The
   producer targets the consumer's storage — no temp, no copy.

  chain(): add w8,w9,#1; mov w11,w8; lsl w8,w11,#1; mov w12,w8; sub w8,w12,#3;
           mov w13,w8   ->   add w8,w9,#1; lsl w11,w8,#1; sub w12,w11,#3

sqlite3.c -c -O0 (arm64-macOS): compile instrs 2,150.9M -> 2,145.0M (-5.9M);
emitted insns 520,854 -> 518,757; object 2,477,912 -> 2,469,528 B.
Byte-deterministic (compile x2 identical).

Green: test-toy 1392/0, test-parse-ok 3920/0, test-parse-err 129/0,
test-smoke-x64 3/0, test-smoke-rv64 3/0; sqlite end-to-end 84|2.

Diffstat:
Mlang/c/parse/cg_adapter.c | 37++++++++++++++++++++++++++++---------
Mlang/c/parse/cg_adapter.h | 2++
Mlang/c/parse/parse.c | 9+++------
Mlang/c/parse/parse_expr.c | 48++++++++++++++++--------------------------------
Mlang/c/parse/parse_init.c | 33+++++++++++----------------------
Mlang/c/parse/parse_stmt.c | 9+++------
Mlang/c/parse/parse_type.c | 3+--
Msrc/cg/memory.c | 39++++++++++++++++++++++++++++++++-------
8 files changed, 96 insertions(+), 84 deletions(-)

diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c @@ -684,7 +684,13 @@ void pcg_branch_false(Parser* p, CGLabel l) { /* Store [lv, rv] -> [rv]. The expression-value of an assignment is the * assigned rvalue, so the store sequence must leave a copy of rv on TOS. */ -void pcg_store(Parser* p) { +/* keep_result == 0: consume [place, value] and leave nothing — the assignment's + * value is discarded (the dominant `expr;` / initializer case). Skipping the + * dup lets a still-delayed RHS flow straight into the destination local + * (kit_cg_store's scalar-local fast path), killing the temp->local routing mov. + * keep_result == 1: leave the assignment's value on the stack for an enclosing + * expression (`a = b = c`, `if ((x = f()))`). */ +static void pcg_store_impl(Parser* p, int keep_result) { const Type* lv_ty = pcg_top2_type(p); const Type* rv_ty = pcg_top_type(p); const Type* mem_ty = lv_ty; @@ -703,11 +709,15 @@ void pcg_store(Parser* p) { rv_ty && (rv_ty->kind == TY_INT128 || rv_ty->kind == TY_UINT128 || rv_ty->kind == TY_LDOUBLE); if (pcg_lv_is_trivial_local(lv) && !wide) { - /* The destination place is already on the CG stack. Keep a copy of rv as - * the assignment's value. Stack: [place, value]. */ - kit_cg_dup(p->cg); - kit_cg_rot3(p->cg); - kit_cg_swap(p->cg); + /* The destination place is already on the CG stack: [place, value]. When + * the result is kept, dup rv first so a copy survives the store; when it + * is discarded, store straight from [place, value] so the RHS (possibly a + * still-delayed arith/cmp) lands directly in the local. */ + if (keep_result) { + kit_cg_dup(p->cg); + kit_cg_rot3(p->cg); + kit_cg_swap(p->cg); + } kit_cg_store(p->cg, access); } else { /* Stash rv into a temp so the destination place can be built from the @@ -740,15 +750,24 @@ void pcg_store(Parser* p) { kit_cg_push_local(p->cg, tmp); kit_cg_load(p->cg, rv_access); /* [dst, value] */ kit_cg_store(p->cg, access); /* [] */ - kit_cg_push_local(p->cg, tmp); - kit_cg_load(p->cg, rv_access); /* [value] */ + if (keep_result) { + kit_cg_push_local(p->cg, tmp); + kit_cg_load(p->cg, rv_access); /* [value] */ + } } } pcg_drop_type(p); pcg_drop_type(p); - pcg_push_type(p, rv_ty); + if (keep_result) pcg_push_type(p, rv_ty); } +void pcg_store(Parser* p) { pcg_store_impl(p, 1); } + +/* Store whose assignment value is discarded (the `expr;` / initializer case). + * Replaces the `pcg_store(p); pcg_drop(p)` idiom; avoids materializing the RHS + * into a temp just to drop it. */ +void pcg_store_void(Parser* p) { pcg_store_impl(p, 0); } + void pcg_deref(Parser* p, const Type* pointee) { const Type* ptr_ty = pcg_top_type(p); if (pointee && pointee->kind == TY_FUNC) { diff --git a/lang/c/parse/cg_adapter.h b/lang/c/parse/cg_adapter.h @@ -300,6 +300,8 @@ void pcg_jump(Parser*, CGLabel); void pcg_branch_true(Parser*, CGLabel); void pcg_branch_false(Parser*, CGLabel); void pcg_store(Parser*); +/* Store whose assignment value is discarded (replaces `pcg_store; pcg_drop`). */ +void pcg_store_void(Parser*); void pcg_deref(Parser*, const Type*); /* ---- Lvalue auxiliary access ---- diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -568,8 +568,7 @@ static void store_top_to_size_slot(Parser* p, FrameSlot slot) { pcg_push_local_typed(p, slot, ty_size_t(p)); pcg_swap(p); coerce_top_to_lvalue(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } static void reset_vla_pending(Parser* p) { @@ -947,8 +946,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { pcg_alloca(p); pcg_push_local_typed(p, ptr_slot, ptr_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); sym_entry = scope_lookup(p, name); if (sym_entry && sym_entry->kind == SEK_LOCAL) { sym_entry->vla_byte_slot = byte_slot; @@ -1007,8 +1005,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { perr(p, "%.*s", KIT_SLICE_ARG(kit_slice_cstr(chk.message))); } coerce_top_to_lvalue(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } } else if (p->auto_var_init == KIT_AUTOVAR_ZERO && var_ty && !(var_ty->kind == TY_ARRAY && var_ty->arr.incomplete)) { diff --git a/lang/c/parse/parse_expr.c b/lang/c/parse/parse_expr.c @@ -1440,8 +1440,7 @@ static int parse_builtin_overflow_call(Parser* p, Sym name, SrcLoc loc) { ptr_slot = builtin_tmp_slot(p, ptr_ty); pcg_push_local_typed(p, ptr_slot, ptr_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_set_loc(p, loc); if (pcg_emit_enabled(p)) { @@ -1455,15 +1454,13 @@ static int parse_builtin_overflow_call(Parser* p, Sym name, SrcLoc loc) { ov_slot = builtin_tmp_slot(p, bool_ty); pcg_push_local_typed(p, ov_slot, bool_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_push_local_typed(p, ptr_slot, ptr_ty); pcg_load(p); pcg_deref(p, out_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_push_local_typed(p, ov_slot, bool_ty); pcg_load(p); @@ -1554,13 +1551,11 @@ static int parse_builtin_fp_cmp_call(Parser* p, Sym name, SrcLoc loc) { /* stack: [a, b] */ pcg_push_local_typed(p, slot_b, common); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); /* stack: [a] */ pcg_push_local_typed(p, slot_a, common); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); /* stack: [] */ pcg_push_local_typed(p, slot_a, common); pcg_load(p); @@ -1673,8 +1668,7 @@ static int parse_builtin_fabs_call(Parser* p, Sym name, SrcLoc loc) { slot = builtin_tmp_slot(p, ty); pcg_push_local_typed(p, slot, ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_set_loc(p, loc); pcg_push_local_typed(p, slot, ty); @@ -1687,8 +1681,7 @@ static int parse_builtin_fabs_call(Parser* p, Sym name, SrcLoc loc) { pcg_push_local_typed(p, slot, ty); pcg_load(p); pcg_unop(p, UO_NEG); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_label_place(p, L_nonneg); pcg_push_local_typed(p, slot, ty); pcg_load(p); @@ -1698,8 +1691,7 @@ static int parse_builtin_fabs_call(Parser* p, Sym name, SrcLoc loc) { pcg_branch_false(p, L_nonzero); pcg_push_local_typed(p, slot, ty); pcg_push_float(p, 0.0, ty); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_label_place(p, L_nonzero); pcg_push_local_typed(p, slot, ty); pcg_load(p); @@ -2029,8 +2021,7 @@ static int try_parse_builtin_call(Parser* p) { FrameSlot eslot = pcg_local(p, &fsd); pcg_push_local_typed(p, eslot, eptr_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_push_local_typed(p, eslot, eptr_ty); pcg_load(p); @@ -2063,8 +2054,7 @@ static int try_parse_builtin_call(Parser* p) { FrameSlot okslot = pcg_local(p, &okd); pcg_push_local_typed(p, okslot, ok_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); FrameSlotDesc pd; memset(&pd, 0, sizeof pd); @@ -2075,8 +2065,7 @@ static int try_parse_builtin_call(Parser* p) { FrameSlot pslot = pcg_local(p, &pd); pcg_push_local_typed(p, pslot, val_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_push_local_typed(p, okslot, ok_ty); pcg_load(p); @@ -2087,8 +2076,7 @@ static int try_parse_builtin_call(Parser* p) { pcg_deref(p, val_ty); pcg_push_local_typed(p, pslot, val_ty); pcg_load(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_label_place(p, L_done); pcg_push_local_typed(p, okslot, ok_ty); @@ -3271,8 +3259,7 @@ static FrameSlot ll_tmp_slot(Parser* p, const Type* ty) { static void ll_store_const(Parser* p, FrameSlot tmp, const Type* ty, i64 v) { pcg_push_local_typed(p, tmp, ty); pcg_push_int(p, v, ty); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } static void parse_land(Parser* p) { @@ -3398,8 +3385,7 @@ static void parse_ternary(Parser* p) { if (pcg_top_type(p) != then_store_ty) pcg_convert(p, then_store_ty); pcg_push_local_typed(p, tmp, then_store_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_jump(p, L_end); pcg_label_place(p, L_else); expect_punct(p, ':', "':' in ternary"); @@ -3424,8 +3410,7 @@ static void parse_ternary(Parser* p) { } pcg_push_local_typed(p, tmp, result_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_label_place(p, L_end); if (common && common != result_ty) { FrameSlotDesc cfsd; @@ -3442,8 +3427,7 @@ static void parse_ternary(Parser* p) { pcg_convert(p, common); pcg_push_local_typed(p, ctmp, common); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_push_local_typed(p, ctmp, common); return; } diff --git a/lang/c/parse/parse_init.c b/lang/c/parse/parse_init.c @@ -214,8 +214,7 @@ static void emit_copy_leaf(Parser* p, FrameSlot dst_slot, pcg_deref(p, leaf_ty); pcg_lv_member(p, (i64)src_off, leaf_ty, 0, 0, 0); pcg_load(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } /* Walk a (possibly nested) aggregate, emitting a leaf load+store for each @@ -276,8 +275,7 @@ void emit_struct_copy_into_slot(Parser* p, FrameSlot dst_slot, src_ptr_slot = pcg_local(p, &fsd); pcg_push_local_typed(p, src_ptr_slot, ptr_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); emit_walk_copy(p, dst_slot, dst_arr_ty, dst_off, src_ptr_slot, ptr_ty, 0, ty); } @@ -299,8 +297,7 @@ void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, if (f->flags & FIELD_BITFIELD) { push_record_field_lv(p, slot, arr_ty, offset, ty, i); pcg_push_int(p, 0, f->type); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); continue; } { @@ -322,8 +319,7 @@ void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, } push_subobject_lv(p, slot, arr_ty, offset, ty); pcg_push_int(p, 0, ty); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } static void init_field_at(Parser* p, FrameSlot slot, const Type* arr_ty, @@ -341,8 +337,7 @@ static void init_field_at(Parser* p, FrameSlot slot, const Type* arr_ty, if (!chk.ok) perr(p, "%.*s", KIT_SLICE_ARG(kit_slice_cstr(chk.message))); } coerce_top_to_lvalue(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); return; } init_at(p, slot, arr_ty, rec_offset + L->fields[field_index].offset, f->type); @@ -363,14 +358,12 @@ static void init_string_at(Parser* p, FrameSlot slot, const Type* arr_ty, push_subobject_lv(p, slot, arr_ty, offset + (u32)i * elem_size, elem_ty); pcg_push_int(p, (i64)decode_lit_unit_le(bytes + i * elem_size, elem_size), elem_ty); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } for (; i < count; ++i) { push_subobject_lv(p, slot, arr_ty, offset + (u32)i * elem_size, elem_ty); pcg_push_int(p, 0, elem_ty); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } kit_compiler_context(p->c)->heap->free(kit_compiler_context(p->c)->heap, bytes, 0); @@ -528,8 +521,7 @@ static u32 init_struct_fields(Parser* p, FrameSlot slot, const Type* arr_ty, if (!(zf->flags & FIELD_ZERO_WIDTH)) { push_record_field_lv(p, slot, arr_ty, offset, ty, zero_lo); pcg_push_int(p, 0, zf->type); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } } else { u32 zoff = offset + L->fields[zero_lo].offset; @@ -582,8 +574,7 @@ static u32 init_struct_fields(Parser* p, FrameSlot slot, const Type* arr_ty, if (!(f->flags & FIELD_ZERO_WIDTH)) { push_record_field_lv(p, slot, arr_ty, offset, ty, j); pcg_push_int(p, 0, f->type); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } } else { u32 foff = offset + L->fields[j].offset; @@ -626,8 +617,7 @@ static void init_aggregate_remainder(Parser* p, FrameSlot slot, if (!(f->flags & FIELD_ZERO_WIDTH)) { push_record_field_lv(p, slot, arr_ty, offset, ty, i); pcg_push_int(p, 0, f->type); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } } else { const Type* fty = init_field_type_at(ty, i); @@ -763,8 +753,7 @@ void init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset, if (!chk.ok) perr(p, "%.*s", KIT_SLICE_ARG(kit_slice_cstr(chk.message))); } coerce_top_to_lvalue(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); if (had_brace) { accept_punct(p, ','); expect_punct(p, '}', "'}' after scalar initializer"); diff --git a/lang/c/parse/parse_stmt.c b/lang/c/parse/parse_stmt.c @@ -468,8 +468,7 @@ static void parse_switch_stmt(Parser* p) { pcg_push_local_typed(p, ctx.value_slot, vty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); pcg_jump(p, L_dispatch); @@ -702,8 +701,7 @@ static void parse_asm_stmt(Parser* p) { slot = pcg_local(p, &fsd); pcg_push_local_typed(p, slot, ptr_ty); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); lv.addr_slot = slot; lv.ptr_ty = ptr_ty; } @@ -843,8 +841,7 @@ static void parse_asm_stmt(Parser* p) { AsmOutLValue* lv = &out_lvs[i]; asm_out_lvalue_push(p, lv); pcg_swap(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); } } } diff --git a/lang/c/parse/parse_type.c b/lang/c/parse/parse_type.c @@ -1462,8 +1462,7 @@ int parse_decl_suffix(Parser* p, DeclSuffix* out) { pcg_push_local_typed(p, out->vla_count_slot, fsd.type); pcg_swap(p); coerce_top_to_lvalue(p); - pcg_store(p); - pcg_drop(p); + pcg_store_void(p); p->vla_pending = 1; ++p->vla_mark; p->vla_pending_count_slot = out->vla_count_slot; diff --git a/src/cg/memory.c b/src/cg/memory.c @@ -475,6 +475,37 @@ void kit_cg_store(KitCg* g, KitCgMemAccess access) { rv = api_make_wide8_int_const(g, rv.op.v.imm, ty); } + /* Does this store land a scalar value straight into a local's own storage + * (a plain `x = <expr>` / `int x = <expr>`, not bit-field/aggregate/indirect)? + */ + int scalar_local_place = + !is_bitfield && base.source_local != KIT_CG_LOCAL_NONE && + base.op.kind == OPK_LOCAL && + !api_sv_local_storage_is_aggregate(g, &base) && + !cg_type_is_aggregate(g->c, api_sv_type(&base)) && + !cg_type_is_aggregate(g->c, ty) && + api_unalias_type(g->c, api_sv_type(&base)) == + api_unalias_type(g->c, ty); + + /* A still-delayed arith/cmp value going into a scalar local: emit the op + * DIRECTLY into the local instead of materializing it into a temp and then + * copying that in. Kills the temp->local routing mov for the common + * `x = <expr>` — the producer targets the consumer's storage. (Equivalence: + * api_materialize_*_to with dst=base.op emits the same op the old temp got, + * just landing in base's storage; binops read both sources before writing the + * dst, so a self-referential RHS like `b = b + c` stays correct.) */ + if (scalar_local_place && (rv.kind == SV_ARITH || rv.kind == SV_CMP)) { + Operand dst = base.op; + if (rv.kind == SV_ARITH) + api_materialize_arith_to(g, &rv, dst); + else + api_materialize_cmp_to(g, &rv, dst); + api_local_const_clear(api_local_from_handle(g, base.source_local)); + api_release(g, &base); + api_release(g, &rv); + return; + } + /* General scalar / bit-field store. Compute the source operand first so its * local lifetime doesn't overlap any addressing arith. */ api_ensure_local(g, &rv); @@ -485,13 +516,7 @@ void kit_cg_store(KitCg* g, KitCgMemAccess access) { } /* Scalar local-resident place, plain store: copy into the local. */ - if (!is_bitfield && base.source_local != KIT_CG_LOCAL_NONE && - base.op.kind == OPK_LOCAL && - !api_sv_local_storage_is_aggregate(g, &base) && - !cg_type_is_aggregate(g->c, api_sv_type(&base)) && - !cg_type_is_aggregate(g->c, ty) && - api_unalias_type(g->c, api_sv_type(&base)) == - api_unalias_type(g->c, ty)) { + if (scalar_local_place) { Operand dst = base.op; if (src.kind == OPK_IMM) { T->load_imm(T, dst, src.v.imm);