kit

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

commit 3a93557bc838c194609de1e670e7f831f74fb61e
parent a7a3338d08760d46faf61a0fc6f82f01b62c8cc1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu,  4 Jun 2026 01:58:34 -0700

cg/asm: reject 64-bit values in register asm constraints on 32-bit targets

A 64-bit scalar bound to a register constraint ("r"/"f"/"x"/"w") needs a
register pair on a 32-bit target, which the inline-asm lowering does not
model -- it would bind the value to a single GPR and silently truncate to
the low word. Reject it up front in kit_cg_inline_asm at all three operand
sites (output, input, early-clobber output) via api_is_wide8_scalar_type;
the source can use a memory ("m") constraint or split into two 32-bit
operands instead.

Add a red-green err test (invalid_asm_wide_reg_rv32) compiled for rv32 via
a new err/<name>.ccargs sidecar in the toy ERR lane (per-case cc flags).

Convert the toy asm fixtures that fed a 64-bit value to a register operand
(19, 102, 105, 110) to isize so the operand fits one GPR on every target;
they keep exercising register asm operands and now pass on the rv32 exec
lane without a skip. Same conversion fixes 20_cg_api_inline_asm_full, which
was already red on native after integer-literal widening made bare
inout("+r", 7) an i8 that no longer matched its @asm<i64> result; with empty
templates it now also passes on rv32, so its stale .rv32.skip is dropped.

Diffstat:
Msrc/cg/asm.c | 23++++++++++++++++++++++-
Mtest/toy/cases/102_typed_asm_operands.toy | 4++--
Mtest/toy/cases/105_typed_asm_record_outputs.toy | 4++--
Mtest/toy/cases/110_typed_asm_named_outputs.toy | 4++--
Mtest/toy/cases/19_cg_api_variadic_asm.toy | 4++--
Dtest/toy/cases/20_cg_api_inline_asm_full.rv32.skip | 1-
Mtest/toy/cases/20_cg_api_inline_asm_full.toy | 12++++++------
Atest/toy/err/invalid_asm_wide_reg_rv32.ccargs | 1+
Atest/toy/err/invalid_asm_wide_reg_rv32.expected | 1+
Atest/toy/err/invalid_asm_wide_reg_rv32.toy | 9+++++++++
Mtest/toy/run.sh | 14+++++++++++---
11 files changed, 58 insertions(+), 19 deletions(-)

diff --git a/src/cg/asm.c b/src/cg/asm.c @@ -42,6 +42,23 @@ int api_asm_is_reg_constraint(char c) { return c == 'r' || c == 'f' || c == 'x' || c == 'w'; } +/* A register ('r'/'f'/'x'/'w') asm operand must live in a single hardware + * register. A 64-bit scalar on a 32-bit target does not fit one: it would need + * a register pair, which this inline-asm lowering does not model, so binding it + * to a single register would silently truncate to the low word. Reject it up + * front; the source can use a memory ("m") constraint (the value is already + * memory-resident) or split it into two 32-bit operands instead. Wider scalars + * on a 64-bit target (i128/f128) take a different lowering and are not this + * helper's concern. */ +static void api_asm_reject_wide_reg(KitCg* g, KitCgTypeId ty) { + if (api_is_wide8_scalar_type(g->c, ty)) { + compiler_panic(g->c, g->cur_loc, + "KitCg: 64-bit value in a register asm constraint is not " + "supported on a 32-bit target; use a memory (\"m\") " + "constraint or split into two 32-bit operands"); + } +} + void api_asm_memory_clobber_sv(KitCg* g, ApiSValue* sv, CGLocal local_id) { (void)g; (void)sv; @@ -177,7 +194,9 @@ void kit_cg_inline_asm(KitCg* g, KitCgInlineAsm asm_block) { * output (riscv 'f', x86 'x', aarch64 'w') in an FP register. */ if (api_asm_is_reg_constraint(body[0])) { KitCgTypeId oty = outs[i].type ? outs[i].type : fallback_ty; - CGLocal r = api_alloc_temp_local(g, oty); + CGLocal r; + api_asm_reject_wide_reg(g, oty); + r = api_alloc_temp_local(g, oty); out_ops[i] = api_op_local(r, oty); out_local_owned[i] = 1; } else { @@ -213,6 +232,7 @@ void kit_cg_inline_asm(KitCg* g, KitCgInlineAsm asm_block) { } in_ops[i] = bound; } else if (api_asm_is_reg_constraint(s[0])) { + api_asm_reject_wide_reg(g, ity); in_ops[i] = api_force_local(g, &in_svs[i], ity); } else if (s[0] == 'i') { if (!api_sv_op_is(&in_svs[i], OPK_IMM)) { @@ -252,6 +272,7 @@ void kit_cg_inline_asm(KitCg* g, KitCgInlineAsm asm_block) { continue; } oty = outs[i].type ? outs[i].type : fallback_ty; + api_asm_reject_wide_reg(g, oty); r = api_alloc_temp_local(g, oty); for (u32 k = 0; k < total_inputs; ++k) { if ((in_ops[k].kind == OPK_LOCAL && in_ops[k].v.local == r) || diff --git a/test/toy/cases/102_typed_asm_operands.toy b/test/toy/cases/102_typed_asm_operands.toy @@ -1,7 +1,7 @@ fn __user_main(): i64 { - let value: i64 = @asm<i64>( + let value: i64 = @asm<isize>( "", - outputs(inout("+r", 42 as i64)), + outputs(inout("+r", 42 as isize)), inputs(), clobbers(), flags(.volatile) diff --git a/test/toy/cases/105_typed_asm_record_outputs.toy b/test/toy/cases/105_typed_asm_record_outputs.toy @@ -1,7 +1,7 @@ fn __user_main(): i64 { - return @asm<record { lo: i64, hi: i64 }>( + return @asm<record { lo: isize, hi: isize }>( "", - outputs(inout("+r", 20 as i64), inout("+r", 22 as i64)), + outputs(inout("+r", 20 as isize), inout("+r", 22 as isize)), inputs(), clobbers(), flags(.volatile) diff --git a/test/toy/cases/110_typed_asm_named_outputs.toy b/test/toy/cases/110_typed_asm_named_outputs.toy @@ -1,7 +1,7 @@ fn __user_main(): i64 { - let pair = @asm<record { lo: i64, hi: i64 }>( + let pair = @asm<record { lo: isize, hi: isize }>( "", - outputs(hi = inout("+r", 20 as i64), lo = inout("+r", 22 as i64)) + outputs(hi = inout("+r", 20 as isize), lo = inout("+r", 22 as isize)) ); @asm<void>("", outputs(), inputs(in("r", pair.lo), in("r", pair.hi))); return 0; diff --git a/test/toy/cases/19_cg_api_variadic_asm.toy b/test/toy/cases/19_cg_api_variadic_asm.toy @@ -18,9 +18,9 @@ fn sum_first(n: i64, ...): i64 { } fn __user_main(): i64 { - var v: i64 = @asm<i64>( + var v: i64 = @asm<isize>( "", - outputs(inout("+r", 42 as i64)), + outputs(inout("+r", 42 as isize)), inputs(), clobbers(), flags(.volatile) diff --git a/test/toy/cases/20_cg_api_inline_asm_full.rv32.skip b/test/toy/cases/20_cg_api_inline_asm_full.rv32.skip @@ -1 +0,0 @@ -inline-asm fixture uses target-specific mnemonics not applicable to rv32 diff --git a/test/toy/cases/20_cg_api_inline_asm_full.toy b/test/toy/cases/20_cg_api_inline_asm_full.toy @@ -1,9 +1,9 @@ fn __user_main(): i64 { let slot: i64 = 31; - var imm: i64 = @asm<i64>( + var imm: i64 = @asm<isize>( "", - outputs(inout("+r", 7)), + outputs(inout("+r", 7 as isize)), inputs(in("i", 7)), clobbers(), flags(.volatile) @@ -16,16 +16,16 @@ fn __user_main(): i64 { clobbers(), flags(.volatile) ); - var inout: i64 = @asm<i64>( + var inout: i64 = @asm<isize>( "", - outputs(inout("+r", 9)), + outputs(inout("+r", 9 as isize)), inputs(), clobbers(), flags(.volatile) ); - let early_probe: i64 = @asm<i64>( + let early_probe: isize = @asm<isize>( "", - outputs(out("=&r", value: i64)), + outputs(out("=&r", value: isize)), inputs(in("r", 8), in("r", 9)), clobbers(), flags(.volatile) diff --git a/test/toy/err/invalid_asm_wide_reg_rv32.ccargs b/test/toy/err/invalid_asm_wide_reg_rv32.ccargs @@ -0,0 +1 @@ +-target riscv32-none-elf -march=rv32imafc_zicsr_zifencei -mabi=ilp32f -ffreestanding diff --git a/test/toy/err/invalid_asm_wide_reg_rv32.expected b/test/toy/err/invalid_asm_wide_reg_rv32.expected @@ -0,0 +1 @@ +64-bit value in a register asm constraint is not supported on a 32-bit target diff --git a/test/toy/err/invalid_asm_wide_reg_rv32.toy b/test/toy/err/invalid_asm_wide_reg_rv32.toy @@ -0,0 +1,9 @@ +// A 64-bit value bound to a register ("r") asm constraint cannot be honored on +// a 32-bit target: the value needs a register pair, which the inline-asm path +// does not model, so it would silently truncate to the low 32 bits. Reject it. +// Compiled for riscv32 (see the .ccargs sidecar); on a 64-bit host an i64 fits +// a single GPR and this is accepted. +fn main(): i64 { + @asm<void>("", outputs(), inputs(in("r", 42 as i64))); + return 0; +} diff --git a/test/toy/run.sh b/test/toy/run.sh @@ -33,6 +33,8 @@ # <name>.wasm.skip opts the case out of path W (with reason) # <name>.link.skip opts the case out of path L (with reason) # err/<name>.expected expected diagnostic substring for compile-fail cases +# err/<name>.ccargs extra `kit cc -c` flags for that err case (e.g. a +# -target triple for a target-specific diagnostic) # # Filtering: # ./run.sh [name_filter] [paths] @@ -535,14 +537,20 @@ kit_lane_W() { # ---- err corpus (compile-fail cases) --------------------------------------- # cc MUST fail; the .expected file holds diagnostic substring(s) that must -# appear in stderr (grep -F -f). cc is invoked native (no -target), matching the -# original which used `kit cc -c` on the host arch. +# appear in stderr (grep -F -f). cc is invoked native (no -target) by default, +# matching the original which used `kit cc -c` on the host arch. A case that +# needs a non-host target (e.g. a 32-bit-only diagnostic) supplies the extra cc +# flags in an err/<name>.ccargs sidecar; its contents are word-split onto the +# `kit cc -c` command line. kit_lane_ERR() { local label="$KIT_BASE/E" local obj="$KIT_WORK/$KIT_BASE.o" local err="$KIT_WORK/cc.err" local exp="${KIT_SRC%.toy}.expected" - if "$KIT" cc -c "$KIT_SRC" -o "$obj" > "$KIT_WORK/cc.out" 2> "$err"; then + local ccargs_file="${KIT_SRC%.toy}.ccargs" + local ccargs="" + [ -f "$ccargs_file" ] && ccargs="$(cat "$ccargs_file")" + if "$KIT" cc -c $ccargs "$KIT_SRC" -o "$obj" > "$KIT_WORK/cc.out" 2> "$err"; then kit_fail "$label" "expected compile failure, got success" return fi