Skip to content

Commit 2a4e06b

Browse files
committed
Sema: rewrite comptime arithmetic
This commit reworks how Sema handles arithmetic on comptime-known values, fixing many bugs in the process. The general pattern is that arithmetic on comptime-known values is now handled by the new namespace `Sema.arith`. Functions handling comptime arithmetic no longer live on `Value`; this is because some of them can emit compile errors, so some *can't* go on `Value`. Only semantic analysis should really be doing arithmetic on `Value`s anyway, so it makes sense for it to integrate more tightly with `Sema`. This commit also implements more coherent rules surrounding how `undefined` interacts with comptime and mixed-comptime-runtime arithmetic. The rules are as follows. * If an operation cannot trigger Illegal Behavior, and any operand is `undefined`, the result is `undefined`. This includes operations like `0 *| undef`, where the LHS logically *could* be used to determine a defined result. This is partly to simplify the language, but mostly to permit codegen backends to represent `undefined` values as completely invalid states. * If an operation *can* trigger Illegal Behvaior, and any operand is `undefined`, then Illegal Behavior results. This occurs even if the operand in question isn't the one that "decides" illegal behavior; for instance, `undef / 1` is undefined. This is for the same reasons as described above. * An operation which would trigger Illegal Behavior, when evaluated at comptime, instead triggers a compile error. Additionally, if one operand is comptime-known undef, such that the other (runtime-known) operand isn't needed to determine that Illegal Behavior would occur, the compile error is triggered. * The only situation in which an operation with one comptime-known operand has a comptime-known result is if that operand is undefined, in which case the result is either undefined or a compile error per the above rules. This could potentially be loosened in future (for instance, `0 * rt` could be comptime-known 0 with a runtime assertion that `rt` is not undefined), but at least for now, defining it more conservatively simplifies the language and allows us to easily change this in future if desired. This commit fixes many bugs regarding the handling of `undefined`, particularly in vectors. Along with a collection of smaller tests, two very large test cases are added to check arithmetic on `undefined`. The operations which have been rewritten in this PR are: * `+`, `+%`, `+|`, `@addWithOverflow` * `-`, `-%`, `-|`, `@subWithOverflow` * `*`, `*%`, `*|`, `@mulWithOverflow` * `/`, `@divFloor`, `@divTrunc`, `@divExact` * `%`, `@rem`, `@mod` Other arithmetic operations are currently unchanged. Resolves: #22743 Resolves: #22745 Resolves: #22748 Resolves: #22749 Resolves: #22914
1 parent aa3db7c commit 2a4e06b

27 files changed

+11381
-2665
lines changed

src/Sema.zig

+387-1,366
Large diffs are not rendered by default.

src/Sema/arith.zig

+1,609
Large diffs are not rendered by default.

src/Value.zig

+231-971
Large diffs are not rendered by default.

src/Zcu.zig

+40-8
Original file line numberDiff line numberDiff line change
@@ -1716,9 +1716,25 @@ pub const SrcLoc = struct {
17161716
const node = node_off.toAbsolute(src_loc.base_node);
17171717

17181718
switch (tree.nodeTag(node)) {
1719-
.assign => {
1720-
return tree.nodeToSpan(tree.nodeData(node).node_and_node[0]);
1721-
},
1719+
.assign,
1720+
.assign_mul,
1721+
.assign_div,
1722+
.assign_mod,
1723+
.assign_add,
1724+
.assign_sub,
1725+
.assign_shl,
1726+
.assign_shl_sat,
1727+
.assign_shr,
1728+
.assign_bit_and,
1729+
.assign_bit_xor,
1730+
.assign_bit_or,
1731+
.assign_mul_wrap,
1732+
.assign_add_wrap,
1733+
.assign_sub_wrap,
1734+
.assign_mul_sat,
1735+
.assign_add_sat,
1736+
.assign_sub_sat,
1737+
=> return tree.nodeToSpan(tree.nodeData(node).node_and_node[0]),
17221738
else => return tree.nodeToSpan(node),
17231739
}
17241740
},
@@ -1727,9 +1743,25 @@ pub const SrcLoc = struct {
17271743
const node = node_off.toAbsolute(src_loc.base_node);
17281744

17291745
switch (tree.nodeTag(node)) {
1730-
.assign => {
1731-
return tree.nodeToSpan(tree.nodeData(node).node_and_node[1]);
1732-
},
1746+
.assign,
1747+
.assign_mul,
1748+
.assign_div,
1749+
.assign_mod,
1750+
.assign_add,
1751+
.assign_sub,
1752+
.assign_shl,
1753+
.assign_shl_sat,
1754+
.assign_shr,
1755+
.assign_bit_and,
1756+
.assign_bit_xor,
1757+
.assign_bit_or,
1758+
.assign_mul_wrap,
1759+
.assign_add_wrap,
1760+
.assign_sub_wrap,
1761+
.assign_mul_sat,
1762+
.assign_add_sat,
1763+
.assign_sub_sat,
1764+
=> return tree.nodeToSpan(tree.nodeData(node).node_and_node[1]),
17331765
else => return tree.nodeToSpan(node),
17341766
}
17351767
},
@@ -2209,9 +2241,9 @@ pub const LazySrcLoc = struct {
22092241
node_offset_field_default: Ast.Node.Offset,
22102242
/// The source location points to the type of an array or struct initializer.
22112243
node_offset_init_ty: Ast.Node.Offset,
2212-
/// The source location points to the LHS of an assignment.
2244+
/// The source location points to the LHS of an assignment (or assign-op, e.g. `+=`).
22132245
node_offset_store_ptr: Ast.Node.Offset,
2214-
/// The source location points to the RHS of an assignment.
2246+
/// The source location points to the RHS of an assignment (or assign-op, e.g. `+=`).
22152247
node_offset_store_operand: Ast.Node.Offset,
22162248
/// The source location points to the operand of a `return` statement, or
22172249
/// the `return` itself if there is no explicit operand.

test/behavior/floatop.zig

-4
Original file line numberDiff line numberDiff line change
@@ -1696,10 +1696,6 @@ test "comptime fixed-width float non-zero divided by zero produces signed Inf" {
16961696
}
16971697
}
16981698

1699-
test "comptime_float zero divided by zero produces zero" {
1700-
try expect((0.0 / 0.0) == 0.0);
1701-
}
1702-
17031699
test "comptime float compared with runtime int" {
17041700
const f = 10.0;
17051701
var i: usize = 0;

test/behavior/math.zig

+131-6
Original file line numberDiff line numberDiff line change
@@ -1265,12 +1265,6 @@ test "allow signed integer division/remainder when values are comptime-known and
12651265

12661266
try expect(5 % 3 == 2);
12671267
try expect(-6 % 3 == 0);
1268-
1269-
var undef: i32 = undefined;
1270-
_ = &undef;
1271-
if (0 % undef != 0) {
1272-
@compileError("0 as numerator should return comptime zero independent of denominator");
1273-
}
12741268
}
12751269

12761270
test "quad hex float literal parsing accurate" {
@@ -1861,3 +1855,134 @@ test "runtime int comparison to inf is comptime-known" {
18611855
comptime S.doTheTest(f64, 123);
18621856
comptime S.doTheTest(f128, 123);
18631857
}
1858+
1859+
test "float divide by zero" {
1860+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1861+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1862+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1863+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1864+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1865+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1866+
if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
1867+
1868+
const S = struct {
1869+
fn doTheTest(comptime F: type, zero: F, one: F) !void {
1870+
try expect(math.isPositiveInf(@divTrunc(one, zero)));
1871+
try expect(math.isPositiveInf(@divFloor(one, zero)));
1872+
1873+
try expect(math.isNan(@rem(one, zero)));
1874+
try expect(math.isNan(@mod(one, zero)));
1875+
}
1876+
};
1877+
1878+
try S.doTheTest(f16, 0, 1);
1879+
comptime S.doTheTest(f16, 0, 1) catch unreachable;
1880+
1881+
try S.doTheTest(f32, 0, 1);
1882+
comptime S.doTheTest(f32, 0, 1) catch unreachable;
1883+
1884+
try S.doTheTest(f64, 0, 1);
1885+
comptime S.doTheTest(f64, 0, 1) catch unreachable;
1886+
1887+
try S.doTheTest(f80, 0, 1);
1888+
comptime S.doTheTest(f80, 0, 1) catch unreachable;
1889+
1890+
try S.doTheTest(f128, 0, 1);
1891+
comptime S.doTheTest(f128, 0, 1) catch unreachable;
1892+
}
1893+
1894+
test "partially-runtime integer vector division would be illegal if vector elements were reordered" {
1895+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1896+
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
1897+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1898+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1899+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1900+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1901+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1902+
1903+
var lhs: @Vector(2, i8) = .{ -128, 5 };
1904+
const rhs: @Vector(2, i8) = .{ 1, -1 };
1905+
1906+
const expected: @Vector(2, i8) = .{ -128, -5 };
1907+
1908+
lhs = lhs; // suppress error
1909+
1910+
const trunc = @divTrunc(lhs, rhs);
1911+
try expect(trunc[0] == expected[0]);
1912+
try expect(trunc[1] == expected[1]);
1913+
1914+
const floor = @divFloor(lhs, rhs);
1915+
try expect(floor[0] == expected[0]);
1916+
try expect(floor[1] == expected[1]);
1917+
1918+
const exact = @divExact(lhs, rhs);
1919+
try expect(exact[0] == expected[0]);
1920+
try expect(exact[1] == expected[1]);
1921+
}
1922+
1923+
test "float vector division of comptime zero by runtime nan is nan" {
1924+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1925+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1926+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1927+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1928+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1929+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1930+
if (builtin.zig_backend == .stage2_x86_64 and builtin.target.ofmt != .elf and builtin.target.ofmt != .macho) return error.SkipZigTest;
1931+
1932+
const ct_zero: @Vector(1, f32) = .{0};
1933+
var rt_nan: @Vector(1, f32) = .{math.nan(f32)};
1934+
1935+
rt_nan = rt_nan; // suppress error
1936+
1937+
try expect(math.isNan((@divTrunc(ct_zero, rt_nan))[0]));
1938+
try expect(math.isNan((@divFloor(ct_zero, rt_nan))[0]));
1939+
try expect(math.isNan((ct_zero / rt_nan)[0]));
1940+
}
1941+
1942+
test "float vector multiplication of comptime zero by runtime nan is nan" {
1943+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1944+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1945+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1946+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1947+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1948+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1949+
1950+
const ct_zero: @Vector(1, f32) = .{0};
1951+
var rt_nan: @Vector(1, f32) = .{math.nan(f32)};
1952+
1953+
rt_nan = rt_nan; // suppress error
1954+
1955+
try expect(math.isNan((ct_zero * rt_nan)[0]));
1956+
try expect(math.isNan((rt_nan * ct_zero)[0]));
1957+
}
1958+
1959+
test "comptime float vector division of zero by nan is nan" {
1960+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1961+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1962+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1963+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1964+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1965+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1966+
1967+
const ct_zero: @Vector(1, f32) = .{0};
1968+
const ct_nan: @Vector(1, f32) = .{math.nan(f32)};
1969+
1970+
comptime assert(math.isNan((@divTrunc(ct_zero, ct_nan))[0]));
1971+
comptime assert(math.isNan((@divFloor(ct_zero, ct_nan))[0]));
1972+
comptime assert(math.isNan((ct_zero / ct_nan)[0]));
1973+
}
1974+
1975+
test "comptime float vector multiplication of zero by nan is nan" {
1976+
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
1977+
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
1978+
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
1979+
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1980+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
1981+
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest;
1982+
1983+
const ct_zero: @Vector(1, f32) = .{0};
1984+
const ct_nan: @Vector(1, f32) = .{math.nan(f32)};
1985+
1986+
comptime assert(math.isNan((ct_zero * ct_nan)[0]));
1987+
comptime assert(math.isNan((ct_nan * ct_zero)[0]));
1988+
}

test/behavior/vector.zig

+2
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,8 @@ test "zero multiplicand" {
13161316
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
13171317
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
13181318
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
1319+
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; // TODO
1320+
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
13191321

13201322
const zeros = @Vector(2, u32){ 0.0, 0.0 };
13211323
var ones = @Vector(2, u32){ 1.0, 1.0 };

test/behavior/x86_64/math.zig

+10-91
Original file line numberDiff line numberDiff line change
@@ -21685,20 +21685,7 @@ test mulUnsafe {
2168521685
}
2168621686

2168721687
inline fn multiply(comptime Type: type, lhs: Type, rhs: Type) @TypeOf(lhs * rhs) {
21688-
if (@inComptime() and @typeInfo(Type) == .vector) {
21689-
// workaround https://github.com/ziglang/zig/issues/22743
21690-
// TODO: return @select(Scalar(Type), boolAnd(lhs == lhs, rhs == rhs), lhs * rhs, lhs + rhs);
21691-
// workaround https://github.com/ziglang/zig/issues/22744
21692-
var res: Type = undefined;
21693-
for (0..@typeInfo(Type).vector.len) |i| res[i] = lhs[i] * rhs[i];
21694-
return res;
21695-
}
21696-
// workaround https://github.com/ziglang/zig/issues/22745
21697-
// TODO: return lhs * rhs;
21698-
var rt_lhs = lhs;
21699-
var rt_rhs = rhs;
21700-
_ = .{ &rt_lhs, &rt_rhs };
21701-
return rt_lhs * rt_rhs;
21688+
return lhs * rhs;
2170221689
}
2170321690
test multiply {
2170421691
const test_multiply = binary(multiply, .{});
@@ -21715,24 +21702,8 @@ test divide {
2171521702
try test_divide.testFloatVectors();
2171621703
}
2171721704

21718-
inline fn divTrunc(comptime Type: type, lhs: Type, rhs: Type) Type {
21719-
switch (@typeInfo(Scalar(Type))) {
21720-
else => @compileError(@typeName(Type)),
21721-
.int => return @divTrunc(lhs, rhs),
21722-
.float => {
21723-
if (@inComptime()) {
21724-
// workaround https://github.com/ziglang/zig/issues/22748
21725-
return @trunc(lhs / rhs);
21726-
}
21727-
// workaround https://github.com/ziglang/zig/issues/22748
21728-
// workaround https://github.com/ziglang/zig/issues/22749
21729-
// TODO: return @divTrunc(lhs, rhs);
21730-
var rt_lhs = lhs;
21731-
var rt_rhs = rhs;
21732-
_ = .{ &rt_lhs, &rt_rhs };
21733-
return @divTrunc(rt_lhs, rt_rhs);
21734-
},
21735-
}
21705+
inline fn divTrunc(comptime Type: type, lhs: Type, rhs: Type) @TypeOf(@divTrunc(lhs, rhs)) {
21706+
return @divTrunc(lhs, rhs);
2173621707
}
2173721708
test divTrunc {
2173821709
const test_div_trunc = binary(divTrunc, .{ .compare = .approx_int });
@@ -21742,56 +21713,17 @@ test divTrunc {
2174221713
try test_div_trunc.testFloatVectors();
2174321714
}
2174421715

21745-
inline fn divFloor(comptime Type: type, lhs: Type, rhs: Type) Type {
21746-
switch (@typeInfo(Scalar(Type))) {
21747-
else => @compileError(@typeName(Type)),
21748-
.int => return @divFloor(lhs, rhs),
21749-
.float => {
21750-
if (@inComptime()) {
21751-
// workaround https://github.com/ziglang/zig/issues/22748
21752-
return @floor(lhs / rhs);
21753-
}
21754-
// workaround https://github.com/ziglang/zig/issues/22748
21755-
// workaround https://github.com/ziglang/zig/issues/22749
21756-
// TODO: return @divFloor(lhs, rhs);
21757-
var rt_lhs = lhs;
21758-
var rt_rhs = rhs;
21759-
_ = .{ &rt_lhs, &rt_rhs };
21760-
return @divFloor(rt_lhs, rt_rhs);
21761-
},
21762-
}
21716+
inline fn divFloor(comptime Type: type, lhs: Type, rhs: Type) @TypeOf(@divFloor(lhs, rhs)) {
21717+
return @divFloor(lhs, rhs);
2176321718
}
2176421719
test divFloor {
2176521720
const test_div_floor = binary(divFloor, .{ .compare = .approx_int });
2176621721
try test_div_floor.testFloats();
2176721722
try test_div_floor.testFloatVectors();
2176821723
}
2176921724

21770-
// workaround https://github.com/ziglang/zig/issues/22748
21771-
// TODO: @TypeOf(@rem(lhs, rhs))
21772-
inline fn rem(comptime Type: type, lhs: Type, rhs: Type) Type {
21773-
switch (@typeInfo(Scalar(Type))) {
21774-
else => @compileError(@typeName(Type)),
21775-
.int => return @rem(lhs, rhs),
21776-
.float => {
21777-
if (@inComptime()) {
21778-
// workaround https://github.com/ziglang/zig/issues/22748
21779-
switch (@typeInfo(Type)) {
21780-
else => return if (rhs != 0) @rem(lhs, rhs) else nan(Type),
21781-
.vector => |info| {
21782-
var res: Type = undefined;
21783-
inline for (0..info.len) |i| res[i] = if (rhs[i] != 0) @rem(lhs[i], rhs[i]) else nan(Scalar(Type));
21784-
return res;
21785-
},
21786-
}
21787-
}
21788-
// workaround https://github.com/ziglang/zig/issues/22748
21789-
// TODO: return @rem(lhs, rhs);
21790-
var rt_rhs = rhs;
21791-
_ = &rt_rhs;
21792-
return @rem(lhs, rt_rhs);
21793-
},
21794-
}
21725+
inline fn rem(comptime Type: type, lhs: Type, rhs: Type) @TypeOf(@rem(lhs, rhs)) {
21726+
return @rem(lhs, rhs);
2179521727
}
2179621728
test rem {
2179721729
const test_rem = binary(rem, .{});
@@ -21801,25 +21733,16 @@ test rem {
2180121733
try test_rem.testFloatVectors();
2180221734
}
2180321735

21804-
inline fn mod(comptime Type: type, lhs: Type, rhs: Type) Type {
21736+
inline fn mod(comptime Type: type, lhs: Type, rhs: Type) @TypeOf(@mod(lhs, rhs)) {
21737+
// workaround llvm backend bugs
2180521738
if (@inComptime()) {
2180621739
const scalarMod = struct {
2180721740
fn scalarMod(scalar_lhs: Scalar(Type), scalar_rhs: Scalar(Type)) Scalar(Type) {
21808-
// workaround https://github.com/ziglang/zig/issues/22748
21809-
if (scalar_rhs == 0) return nan(Scalar(Type));
2181021741
const scalar_rem = @rem(scalar_lhs, scalar_rhs);
2181121742
return if (scalar_rem == 0 or math.signbit(scalar_rem) == math.signbit(scalar_rhs)) scalar_rem else scalar_rem + scalar_rhs;
2181221743
}
2181321744
}.scalarMod;
21814-
// workaround https://github.com/ziglang/zig/issues/22748
2181521745
switch (@typeInfo(Type)) {
21816-
// workaround llvm backend bugs
21817-
// TODO: else => return if (rhs != 0) @mod(lhs, rhs) else nan(Type),
21818-
// TODO: .vector => |info| {
21819-
// TODO: var res: Type = undefined;
21820-
// TODO: inline for (0..info.len) |i| res[i] = if (rhs[i] != 0) @mod(lhs[i], rhs[i]) else nan(Scalar(Type));
21821-
// TODO: return res;
21822-
// TODO: },
2182321746
else => return scalarMod(lhs, rhs),
2182421747
.vector => |info| {
2182521748
var res: Type = undefined;
@@ -21828,11 +21751,7 @@ inline fn mod(comptime Type: type, lhs: Type, rhs: Type) Type {
2182821751
},
2182921752
}
2183021753
}
21831-
// workaround https://github.com/ziglang/zig/issues/22748
21832-
// TODO: return @mod(lhs, rhs);
21833-
var rt_rhs = rhs;
21834-
_ = &rt_rhs;
21835-
return @mod(lhs, rt_rhs);
21754+
return @mod(lhs, rhs);
2183621755
}
2183721756
test mod {
2183821757
const test_mod = binary(mod, .{});

test/cases/compile_errors/add_assign_on_undefined_value.zig

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,5 @@ comptime {
44
}
55

66
// error
7-
// backend=stage2
8-
// target=native
97
//
10-
// :3:10: error: use of undefined value here causes undefined behavior
8+
// :3:5: error: use of undefined value here causes undefined behavior

0 commit comments

Comments
 (0)