rv64: Reject unsafe spilled block arguments

565d18630e9ce92c79aa33d322d795f655dcb05c4553e5005bd9131289f7e535
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 e4622019
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 {