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
This commit is contained in:
mlugg
2025-03-10 03:31:36 +00:00
committed by Matthew Lugg
parent aa3db7cc15
commit 2a4e06bcb3
27 changed files with 11233 additions and 2517 deletions

View File

@@ -4,7 +4,5 @@ comptime {
}
// error
// backend=stage2
// target=native
//
// :3:10: error: use of undefined value here causes undefined behavior
// :3:5: error: use of undefined value here causes undefined behavior

View File

@@ -1,24 +0,0 @@
comptime {
const undef: i64 = undefined;
const not_undef: i64 = 32;
// If either of the operands are zero, then the other operand is returned.
@compileLog(undef + 0);
@compileLog(not_undef + 0);
@compileLog(0 + undef);
@compileLog(0 + not_undef);
_ = undef + undef;
}
// error
// backend=stage2
// target=native
//
// :11:17: error: use of undefined value here causes undefined behavior
//
// Compile Log Output:
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, 32)

View File

@@ -9,7 +9,5 @@ export fn a() void {
}
// error
// backend=stage2
// target=native
//
// :8:7: error: cannot assign to constant
// :8:5: error: cannot assign to constant

View File

@@ -72,24 +72,22 @@ export fn entry18() void {
}
// error
// backend=stage2
// target=native
//
// :3:5: error: cannot assign to constant
// :7:7: error: cannot assign to constant
// :11:7: error: cannot assign to constant
// :15:7: error: cannot assign to constant
// :19:7: error: cannot assign to constant
// :23:7: error: cannot assign to constant
// :27:7: error: cannot assign to constant
// :31:7: error: cannot assign to constant
// :35:7: error: cannot assign to constant
// :39:7: error: cannot assign to constant
// :43:7: error: cannot assign to constant
// :47:7: error: cannot assign to constant
// :51:7: error: cannot assign to constant
// :55:7: error: cannot assign to constant
// :59:7: error: cannot assign to constant
// :63:7: error: cannot assign to constant
// :67:7: error: cannot assign to constant
// :71:7: error: cannot assign to constant
// :7:5: error: cannot assign to constant
// :11:5: error: cannot assign to constant
// :15:5: error: cannot assign to constant
// :19:5: error: cannot assign to constant
// :23:5: error: cannot assign to constant
// :27:5: error: cannot assign to constant
// :31:5: error: cannot assign to constant
// :35:5: error: cannot assign to constant
// :39:5: error: cannot assign to constant
// :43:5: error: cannot assign to constant
// :47:5: error: cannot assign to constant
// :51:5: error: cannot assign to constant
// :55:5: error: cannot assign to constant
// :59:5: error: cannot assign to constant
// :63:5: error: cannot assign to constant
// :67:5: error: cannot assign to constant
// :71:5: error: cannot assign to constant

View File

@@ -6,8 +6,6 @@ comptime {
}
// error
// backend=stage2
// target=native
//
// :4:15: error: overflow of vector type '@Vector(4, u8)' with value '.{ 6, 8, 256, 12 }'
// :4:15: error: overflow of integer type 'u8' with value '256'
// :4:15: note: when computing vector element at index '2'

View File

@@ -4,7 +4,5 @@ comptime {
}
// error
// backend=stage2
// target=native
//
// :3:10: error: use of undefined value here causes undefined behavior
// :3:5: error: use of undefined value here causes undefined behavior

View File

@@ -1,11 +0,0 @@
comptime {
var a: i64 = undefined;
_ = a / a;
_ = &a;
}
// error
// backend=stage2
// target=native
//
// :3:13: error: use of undefined value here causes undefined behavior

View File

@@ -4,7 +4,5 @@ comptime {
}
// error
// backend=stage2
// target=native
//
// :3:10: error: use of undefined value here causes undefined behavior
// :3:5: error: use of undefined value here causes undefined behavior

View File

@@ -1,38 +0,0 @@
comptime {
const undef: i64 = undefined;
const not_undef: i64 = 32;
// If either of the operands are zero, the result is zero.
@compileLog(undef * 0);
@compileLog(not_undef * 0);
@compileLog(0 * undef);
@compileLog(0 * not_undef);
// If either of the operands are one, the result is the other
// operand, even if it is undefined.
@compileLog(undef * 1);
@compileLog(not_undef * 1);
@compileLog(1 * undef);
@compileLog(1 * not_undef);
// If either of the operands are undefined, it's a compile error
// because there is a possible value for which the addition would
// overflow (max_int), causing illegal behavior.
_ = undef * undef;
}
// error
// backend=stage2
// target=native
//
// :21:17: error: use of undefined value here causes undefined behavior
//
// Compile Log Output:
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, 32)

View File

@@ -1,38 +0,0 @@
comptime {
const undef: i64 = undefined;
const not_undef: i64 = 32;
// If either of the operands are zero, the result is zero.
@compileLog(undef *| 0);
@compileLog(not_undef *| 0);
@compileLog(0 *| undef);
@compileLog(0 *| not_undef);
// If either of the operands are one, result is the other operand.
@compileLog(undef *| 1);
@compileLog(not_undef *| 1);
@compileLog(1 *| undef);
@compileLog(1 *| not_undef);
// If either of the operands are undefined, result is undefined.
@compileLog(undef *| 2);
@compileLog(2 *| undef);
}
// error
// backend=stage2
// target=native
//
// :6:5: error: found compile log statement
//
// Compile Log Output:
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, undefined)

View File

@@ -1,38 +0,0 @@
comptime {
const undef: i64 = undefined;
const not_undef: i64 = 32;
// If either of the operands are zero, the result is zero.
@compileLog(undef *% 0);
@compileLog(not_undef *% 0);
@compileLog(0 *% undef);
@compileLog(0 *% not_undef);
// If either of the operands are one, result is the other operand.
@compileLog(undef *% 1);
@compileLog(not_undef *% 1);
@compileLog(1 *% undef);
@compileLog(1 *% not_undef);
// If either of the operands are undefined, result is undefined.
@compileLog(undef *% 2);
@compileLog(2 *% undef);
}
// error
// backend=stage2
// target=native
//
// :6:5: error: found compile log statement
//
// Compile Log Output:
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, 0)
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, 32)
// @as(i64, undefined)
// @as(i64, undefined)

View File

@@ -18,8 +18,6 @@ export fn function_with_return_type_type() void {
}
// error
// backend=stage2
// target=native
//
// :3:7: error: unable to evaluate comptime expression
// :3:5: note: operation is runtime due to this operand

View File

@@ -3,7 +3,5 @@ export fn foo(a: i32, b: i32) i32 {
}
// error
// backend=stage2
// target=native
//
// :2:12: error: remainder division with 'i32' and 'i32': signed integers and floats must use @rem or @mod

View File

@@ -6,12 +6,21 @@ const Small = enum(u2) {
Five,
};
export fn entry() void {
_ = Small.One;
const SmallUnion = union(enum(u2)) {
one = 1,
two,
three,
four,
};
comptime {
_ = Small;
}
comptime {
_ = SmallUnion;
}
// error
// backend=stage2
// target=native
//
// :6:5: error: enumeration value '4' too large for type 'u2'
// :13:5: error: enumeration value '4' too large for type 'u2'

View File

@@ -4,7 +4,5 @@ comptime {
}
// error
// backend=stage2
// target=native
//
// :3:10: error: use of undefined value here causes undefined behavior
// :3:5: error: use of undefined value here causes undefined behavior

View File

@@ -1,20 +0,0 @@
comptime {
const undef: i64 = undefined;
const not_undef: i64 = 32;
// If the rhs is zero, then the other operand is returned, even if it is undefined.
@compileLog(undef - 0);
@compileLog(not_undef - 0);
_ = undef - undef;
}
// error
// backend=stage2
// target=native
//
// :9:17: error: use of undefined value here causes undefined behavior
//
// Compile Log Output:
// @as(i64, undefined)
// @as(i64, 32)

View File

@@ -1,5 +1,5 @@
const c = @cImport({
_ = 1 + foo;
if (foo == 0) {}
});
extern var foo: i32;
export fn entry() void {
@@ -7,9 +7,7 @@ export fn entry() void {
}
// error
// backend=stage2
// target=native
//
// :2:11: error: unable to evaluate comptime expression
// :2:13: note: operation is runtime due to this operand
// :2:13: error: unable to evaluate comptime expression
// :2:9: note: operation is runtime due to this operand
// :1:11: note: operand to '@cImport' is evaluated at comptime

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff