rv64: Reject unsafe spilled block arguments

c08a46cac1c8b2f94ac06c0de40e91c3bd866eb391aac24f729bc71d937b7e3a
RV64 block-argument emission wrote spilled destinations eagerly during
parallel assignment. A destination spill slot could overwrite an old
value still needed as a later source, silently corrupting live values.

Detect overlapping spill assignments before emitting the edge and stop
code generation for this unsupported case. Safe spilled block arguments
continue to use eager stores without reserving frame staging space.

Assisted-by: Codex:gpt-5.6-sol
Alexis Sellier committed ago 1 parent 7fec2230
lib/std/arch/rv64/isel.rad +36 -1
1146 1146
}
1147 1147
1148 1148
/// Emit moves from block arguments to target block's parameter registers.
1149 1149
///
1150 1150
/// Handles spilled destinations directly, then delegates to [`emitParallelMoves`]
1151 -
/// for the remaining register-to-register parallel move resolution.
1151 +
/// for the remaining register-to-register parallel move resolution. Edges that
1152 +
/// would overwrite an unconsumed spill source are unsupported.
1152 1153
fn emitBlockArgs(s: *mut Selector, func: *il::Fn, target: u32, args: *mut [il::Val]) {
1153 1154
    if args.len == 0 {
1154 1155
        return;
1155 1156
    }
1156 1157
    let block = &func.blocks[target];
1157 1158
    assert args.len == block.params.len, "emitBlockArgs: argument/parameter count mismatch";
1158 1159
    assert args.len <= MAX_BLOCK_ARGS, "emitBlockArgs: too many block arguments";
1159 1160
1161 +
    // The parallel-move resolver only handles register destinations. Keep eager
1162 +
    // stores for independent spill slots, but reject dependencies that would
1163 +
    // require stack staging rather than silently miscompiling them.
1164 +
    for arg, i in args {
1165 +
        if let dstSlot = regalloc::spill::spillSlot(&s.ralloc.spill, block.params[i].value) {
1166 +
            let mut changesSlot = true;
1167 +
            if let case il::Val::Reg(src) = arg {
1168 +
                if let srcSlot = regalloc::spill::spillSlot(&s.ralloc.spill, src) {
1169 +
                    if srcSlot == dstSlot {
1170 +
                        set changesSlot = false;
1171 +
                    }
1172 +
                }
1173 +
            }
1174 +
            if changesSlot {
1175 +
                for source, j in args {
1176 +
                    if let case il::Val::Reg(src) = source {
1177 +
                        if let sourceSlot = regalloc::spill::spillSlot(&s.ralloc.spill, src) {
1178 +
                            if sourceSlot == dstSlot {
1179 +
                                if let sourceDstSlot = regalloc::spill::spillSlot(
1180 +
                                    &s.ralloc.spill, block.params[j].value
1181 +
                                ) {
1182 +
                                    assert sourceDstSlot == dstSlot or j < i,
1183 +
                                        "emitBlockArgs: overlapping spilled block arguments are unsupported";
1184 +
                                } else {
1185 +
                                    panic "emitBlockArgs: overlapping spilled block arguments are unsupported";
1186 +
                                }
1187 +
                            }
1188 +
                        }
1189 +
                    }
1190 +
                }
1191 +
            }
1192 +
        }
1193 +
    }
1194 +
1160 1195
    // Destination registers for each arg.
1161 1196
    // Zero means the destination is spilled or skipped.
1162 1197
    let mut dsts: [gen::Reg; MAX_BLOCK_ARGS] = [super::ZERO; MAX_BLOCK_ARGS];
1163 1198
1164 1199
    for arg, i in args {