Fix subword comparisons on RV64

6cdb32137429733911b794de54decf4d774712b7e49eebac4a8693ea1a404d28
RV64 ordered comparisons consumed subword values without
canonicalizing them to their declared width. Word operations can leave
`u32` values sign-extended to 64 bits, causing signed and unsigned
comparisons to inspect incorrect upper bits.

Normalize both operands with the appropriate sign or zero extension
before selecting ordered comparisons, and only use immediate forms for
canonical immediates.
Alexis Sellier committed ago 1 parent a6f4d48e
lib/std/arch/rv64/isel.rad +95 -49
615 615
            // For SLT: sign-extension needed (signed comparison).
616 616
            // For ULT: zero-extension needed (unsigned magnitude comparison).
617 617
            // For EQ/NE with W32: sign-extension is cheaper.
618 618
            // For EQ/NE with W8/W16: keep zero-extension.
619 619
            // Skip extension for zero register.
620 -
            // Determine extension mode: sign-extend for W32 or SLT,
621 -
            // zero-extend otherwise.
622 -
            let mut useSext: bool = undefined;
620 +
            let mut signed = false;
623 621
            if let case il::CmpOp::Slt = op {
624 -
                set useSext = true;
625 -
            } else {
626 -
                set useSext = typ == il::Type::W32;
622 +
                set signed = true;
627 623
            }
628 -
            if useSext {
629 -
                if not aIsZero and not isExtendedImm(a, typ, true) {
630 -
                    emitSext(s.e, rs1, rs1, typ);
631 -
                }
632 -
                if not bIsZero and not isExtendedImm(b, typ, true) {
633 -
                    emitSext(s.e, rs2, rs2, typ);
634 -
                }
635 -
            } else {
636 -
                if not aIsZero and not isExtendedImm(a, typ, false) {
637 -
                    emitZext(s.e, rs1, rs1, typ);
638 -
                }
639 -
                if not bIsZero and not isExtendedImm(b, typ, false) {
640 -
                    emitZext(s.e, rs2, rs2, typ);
641 -
                }
624 +
            let useSext = cmpUsesSext(typ, signed);
625 +
            if not aIsZero and not isExtendedImm(a, typ, useSext) {
626 +
                emitCmpExt(s.e, rs1, rs1, typ, useSext);
627 +
            }
628 +
            if not bIsZero and not isExtendedImm(b, typ, useSext) {
629 +
                emitCmpExt(s.e, rs2, rs2, typ, useSext);
642 630
            }
643 631
            // Block-argument moves must only execute on the taken path.
644 632
            // When `thenArgs` is non-empty, invert the branch so that the
645 633
            // then-moves land on the fall-through (taken) side.
646 634
            //
747 735
            emit::emit(s.e, encode::ebreak());
748 736
        },
749 737
    }
750 738
}
751 739
740 +
/// Choose the cheapest canonical representation that preserves the comparison.
741 +
/// RV64 word operations naturally sign-extend, and sign-extension preserves
742 +
/// unsigned ordering when both operands have the same declared width.
743 +
fn cmpUsesSext(typ: il::Type, signed: bool) -> bool {
744 +
    return signed or typ == il::Type::W32;
745 +
}
746 +
747 +
/// Extend a comparison operand to its selected canonical representation.
748 +
fn emitCmpExt(
749 +
    e: *mut emit::Emitter,
750 +
    rd: gen::Reg,
751 +
    rs: gen::Reg,
752 +
    typ: il::Type,
753 +
    useSext: bool
754 +
) {
755 +
    if useSext {
756 +
        emitSext(e, rd, rs, typ);
757 +
    } else {
758 +
        emitZext(e, rd, rs, typ);
759 +
    }
760 +
}
761 +
762 +
/// Truncate and extend an immediate exactly as its register operand would be.
763 +
fn canonicalCmpImm(imm: i64, typ: il::Type, useSext: bool) -> i64 {
764 +
    if useSext {
765 +
        match typ {
766 +
            case il::Type::W8 => return (imm as i8) as i64,
767 +
            case il::Type::W16 => return (imm as i16) as i64,
768 +
            case il::Type::W32 => return (imm as i32) as i64,
769 +
            case il::Type::W64 => return imm,
770 +
        }
771 +
    } else {
772 +
        match typ {
773 +
            case il::Type::W8 => return (imm as u8) as i64,
774 +
            case il::Type::W16 => return (imm as u16) as i64,
775 +
            case il::Type::W32 => return (imm as u32) as i64,
776 +
            case il::Type::W64 => return imm,
777 +
        }
778 +
    }
779 +
}
780 +
752 781
/// Check if a value is an immediate that's already correctly extended.
753 782
/// `loadImm` produces the exact 64-bit value; this checks whether that value
754 783
/// already matches what sign/zero-extension to the given type would produce.
755 784
fn isExtendedImm(val: il::Val, typ: il::Type, signed: bool) -> bool {
756 785
    if let case il::Val::Imm(imm) = val {
889 918
            selectShift(s, rd, rs1, b, ShiftOp::Sra, typ, super::SCRATCH2),
890 919
        case il::BinOp::Ushr =>
891 920
            selectShift(s, rd, rs1, b, ShiftOp::Srl, typ, super::SCRATCH2),
892 921
        case il::BinOp::Eq, il::BinOp::Ne => {
893 922
            let rs2 = resolveVal(s, super::SCRATCH2, b);
894 -
            // Canonicalize both operands to the declared width.
895 -
            if typ == il::Type::W32 {
896 -
                emitSext(s.e, rs1, rs1, typ);
897 -
                if not isExtendedImm(b, typ, true) {
898 -
                    emitSext(s.e, rs2, rs2, typ);
899 -
                }
900 -
            } else {
901 -
                emitZext(s.e, rs1, rs1, typ);
902 -
                if not isExtendedImm(b, typ, false) {
903 -
                    emitZext(s.e, rs2, rs2, typ);
904 -
                }
923 +
            let useSext = cmpUsesSext(typ, false);
924 +
            emitCmpExt(s.e, rs1, rs1, typ, useSext);
925 +
            if not isExtendedImm(b, typ, useSext) {
926 +
                emitCmpExt(s.e, rs2, rs2, typ, useSext);
905 927
            }
906 928
            emit::emit(s.e, encode::xor(rd, rs1, rs2));
907 929
            if let case il::BinOp::Eq = op {
908 930
                emit::emit(s.e, encode::sltiu(rd, rd, 1));
909 931
            } else {
910 932
                emit::emit(s.e, encode::sltu(rd, super::ZERO, rd));
911 933
            }
912 934
        }
913 935
        case il::BinOp::Slt =>
914 -
            selectCmp(s, rd, rs1, b, CmpOp::Slt, super::SCRATCH2),
936 +
            selectCmp(s, typ, rd, rs1, b, CmpOp::Slt, false, super::SCRATCH2),
915 937
        case il::BinOp::Ult =>
916 -
            selectCmp(s, rd, rs1, b, CmpOp::Ult, super::SCRATCH2),
917 -
        case il::BinOp::Sge, il::BinOp::Uge => {
918 -
            let rs2 = resolveVal(s, super::SCRATCH2, b);
919 -
            if let case il::BinOp::Sge = op {
920 -
                emit::emit(s.e, encode::slt(rd, rs1, rs2));
921 -
            } else {
922 -
                emit::emit(s.e, encode::sltu(rd, rs1, rs2));
923 -
            }
924 -
            emit::emit(s.e, encode::xori(rd, rd, 1));
925 -
        }
938 +
            selectCmp(s, typ, rd, rs1, b, CmpOp::Ult, false, super::SCRATCH2),
939 +
        case il::BinOp::Sge =>
940 +
            selectCmp(s, typ, rd, rs1, b, CmpOp::Slt, true, super::SCRATCH2),
941 +
        case il::BinOp::Uge =>
942 +
            selectCmp(s, typ, rd, rs1, b, CmpOp::Ult, true, super::SCRATCH2),
926 943
    }
927 944
}
928 945
929 946
/// Select a unary ALU operation.
930 947
fn selectAluUnOp(s: *mut Selector, op: il::UnOp, typ: il::Type, rd: gen::Reg, rs: gen::Reg) {
1161 1178
        }
1162 1179
    }
1163 1180
    emitParallelMoves(s, &dsts[..], args);
1164 1181
}
1165 1182
1166 -
/// Select comparison with immediate optimization.
1167 -
fn selectCmp(s: *mut Selector, rd: gen::Reg, rs1: gen::Reg, b: il::Val, op: CmpOp, scratch: gen::Reg) {
1168 -
    // Try immediate optimization first.
1183 +
/// Select a comparison with immediate optimization.
1184 +
fn selectCmp(
1185 +
    s: *mut Selector,
1186 +
    typ: il::Type,
1187 +
    rd: gen::Reg,
1188 +
    rs1: gen::Reg,
1189 +
    b: il::Val,
1190 +
    op: CmpOp,
1191 +
    invert: bool,
1192 +
    scratch: gen::Reg
1193 +
) {
1194 +
    let mut signed = false;
1195 +
    if let case CmpOp::Slt = op {
1196 +
        set signed = true;
1197 +
    }
1198 +
    let useSext = cmpUsesSext(typ, signed);
1199 +
    emitCmpExt(s.e, rs1, rs1, typ, useSext);
1200 +
1201 +
    // Canonicalizing the immediate can expose an immediate instruction even
1202 +
    // when the IL value used a different representation for the same width.
1203 +
    let mut rhs = b;
1169 1204
    if let case il::Val::Imm(imm) = b {
1170 -
        if encode::isSmallImm64(imm) {
1171 -
            let simm = imm as i32;
1205 +
        let canonical = canonicalCmpImm(imm, typ, useSext);
1206 +
        set rhs = il::Val::Imm(canonical);
1207 +
        if encode::isSmallImm64(canonical) {
1208 +
            let simm = canonical as i32;
1172 1209
            match op {
1173 1210
                case CmpOp::Slt => emit::emit(s.e, encode::slti(rd, rs1, simm)),
1174 1211
                case CmpOp::Ult => emit::emit(s.e, encode::sltiu(rd, rs1, simm)),
1175 1212
            }
1213 +
            if invert {
1214 +
                emit::emit(s.e, encode::xori(rd, rd, 1));
1215 +
            }
1176 1216
            return;
1177 1217
        }
1178 1218
    }
1179 -
    // Fallback: load into register.
1180 -
    let rs2 = resolveVal(s, scratch, b);
1219 +
1220 +
    let rs2 = resolveVal(s, scratch, rhs);
1221 +
    if not isExtendedImm(rhs, typ, useSext) {
1222 +
        emitCmpExt(s.e, rs2, rs2, typ, useSext);
1223 +
    }
1181 1224
    match op {
1182 1225
        case CmpOp::Slt => emit::emit(s.e, encode::slt(rd, rs1, rs2)),
1183 1226
        case CmpOp::Ult => emit::emit(s.e, encode::sltu(rd, rs1, rs2)),
1184 1227
    }
1228 +
    if invert {
1229 +
        emit::emit(s.e, encode::xori(rd, rd, 1));
1230 +
    }
1185 1231
}
test/tests/rv64.u32.compare.rad added +84 -0
1 +
//! returns: 0
2 +
//! Ordered comparisons must canonicalize computed subword values, register
3 +
//! operands, and immediates to the representation selected for their width.
4 +
5 +
fn addU8(a: u8, b: u8) -> u8 {
6 +
    return a + b;
7 +
}
8 +
9 +
fn addU16(a: u16, b: u16) -> u16 {
10 +
    return a + b;
11 +
}
12 +
13 +
fn addU32(a: u32, b: u32) -> u32 {
14 +
    return a + b;
15 +
}
16 +
17 +
fn addI8(a: i8, b: i8) -> i8 {
18 +
    return a + b;
19 +
}
20 +
21 +
fn addI16(a: i16, b: i16) -> i16 {
22 +
    return a + b;
23 +
}
24 +
25 +
fn addI32(a: i32, b: i32) -> i32 {
26 +
    return a + b;
27 +
}
28 +
29 +
fn u8Expressions(value: u8, limit: u8) -> bool {
30 +
    return value < 0xFF and value >= 0x80 and value < limit and limit >= value;
31 +
}
32 +
33 +
fn u16Expressions(value: u16, limit: u16) -> bool {
34 +
    return value < 0xFFFF and value >= 0x8000 and value < limit and limit >= value;
35 +
}
36 +
37 +
fn u32Expressions(value: u32, limit: u32) -> bool {
38 +
    return value < 0xFFFFFFFF and value >= 0x80000000
39 +
        and value < limit and limit >= value;
40 +
}
41 +
42 +
fn i8Expressions(value: i8, limit: i8) -> bool {
43 +
    return value < 0 and value >= -128 and value < limit and limit >= value;
44 +
}
45 +
46 +
fn i16Expressions(value: i16, limit: i16) -> bool {
47 +
    return value < 0 and value >= -32768 and value < limit and limit >= value;
48 +
}
49 +
50 +
fn i32Expressions(value: i32, limit: i32) -> bool {
51 +
    return value < 0 and value >= -2147483648
52 +
        and value < limit and limit >= value;
53 +
}
54 +
55 +
fn u32Branches(value: u32, limit: u32) -> bool {
56 +
    if value >= 0xFFFFFFFF {
57 +
        return false;
58 +
    }
59 +
    if value < 0x80000000 {
60 +
        return false;
61 +
    }
62 +
    if value < limit {
63 +
        return true;
64 +
    }
65 +
    return false;
66 +
}
67 +
68 +
@default fn main() -> i32 {
69 +
    let highU8 = addU8(0x7F, 1);
70 +
    let highU16 = addU16(0x7FFF, 1);
71 +
    let highU32 = addU32(0x7FFFFFFF, 1);
72 +
    let minI8 = addI8(127, 1);
73 +
    let minI16 = addI16(32767, 1);
74 +
    let minI32 = addI32(2147483647, 1);
75 +
76 +
    assert u8Expressions(highU8, 0xFE);
77 +
    assert u16Expressions(highU16, 0xFFFE);
78 +
    assert u32Expressions(highU32, 0xFFFFFFFE);
79 +
    assert i8Expressions(minI8, -1);
80 +
    assert i16Expressions(minI16, -1);
81 +
    assert i32Expressions(minI32, -1);
82 +
    assert u32Branches(highU32, 0xFFFFFFFE);
83 +
    return 0;
84 +
}