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