-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for operators on Core.IntLiteral
.
#4716
Add support for operators on Core.IntLiteral
.
#4716
Conversation
Also add support for mixed comparison between different integer types.
Stop trying to allow operations on IntLiterals to be lowered. That's not possible in general because we don't necessarily have a value at runtime for the IntLiteral.
toolchain/check/eval.cpp
Outdated
@@ -769,6 +766,15 @@ static auto DiagnoseDivisionByZero(Context& context, SemIRLoc loc) -> void { | |||
context.emitter().Emit(loc, CompileTimeDivisionByZero); | |||
} | |||
|
|||
// Get an integer at a suitable bit-width: either its actual width if it has a | |||
// fixed width, or the canonical width from the value store if not. | |||
static auto GetIntAtSuitableWidth(Context& context, IntId bit_width_id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might it make sense to make this behavior part of GetAtWidth
, rather than a separate function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I think the need to deal with the "unknown width" case in exactly this way is probably specific to the evaluation logic, and other users of IntStore
are unlikely to want it -- especially in something with such an innocuous name :) It looks like all the current calls to GetAtWidth
are in this file, but I think that's just because lower/constant.cpp hasn't been updated to use it yet -- this is precisely GetAtWidth
written longhand, and doesn't want this special casing for an invalid width.
toolchain/check/eval.cpp
Outdated
static auto GetIntAtSuitableWidth(Context& context, IntId bit_width_id, | ||
IntId int_id) -> llvm::APInt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems potentially pretty confusing that this function takes its parameters in the opposite order from GetAtWidth
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, especially given the parameters are all the same types. I'm a little bit uneasy with having the bit width last given the signatures of GetIntAtSuitableWidth
/ GetIntsAtSuitableWidth
-- having the "variadic" part last feels better to me -- but matching GetAtWidth
and making the parameter order match the order in which the things are mentioned in the name is probably reasonable. Done.
toolchain/check/eval.cpp
Outdated
} | ||
|
||
default: | ||
// Break to do additional setup for other builtin kinds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow: if we break here, we don't just do "additional setup", we actually perform the operations, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Pre-existing.) Yeah, this seems confusing -- and in fact the shift logic is completely different to the rest of this, so I've split that out into a different function.
if (result.overflow && !lhs_bit_width_id.is_valid()) { | ||
// Retry with a larger bit width. Most operations can only overflow by one | ||
// bit, but signed n-bit multiplication can overflow to 2n-1 bits. | ||
int new_width = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not do this before the first attempt, and avoid the need to retry? If the concern is that 2n bits could be too expensive if it's not needed, it seems like we could compute a tighter upper bound pretty efficiently (something like lhs_val.ceilLogBase2() + rhs_val.ceilLogBase2()
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally planned to do that, but @chandlerc suggested it'd be better to do it this way -- IIUC the rationale is that we'll almost never need to go to a wider size than the inputs (because they've already been rounded up to a multiple of 64 bits by the IntStore
), so it's better to speculatively assume that the result will fit than to spend time computing a width -- especially because any wider upper bound will require a heap allocation (APInt
heap allocates integers wider than 64 bits) and wider operations get more expensive pretty quickly in APInt
at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
But it seems good to capture this rationale in the comments as otherwise it is a bit mysterious why we wait to see the overflow before doing this.
var a_lit: Core.IntLiteral() = 12; | ||
var an_i32: i32 = 34; | ||
|
||
// This can't be valid: we don't have a compile-time or runtime integer value for `n`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "n" refer to in this context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, missed this when I renamed the variable. Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is less_eq
meaningfully different from greater_eq
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I'd just already updated both before I decided it wasn't worth it. I can revert the changes to one of them if you like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the thinking here is that while sdiv
has a special rule about 0, that rule is sufficiently orthogonal to fixed-vs-variable width that we don't need separate coverage of non-fixed-width cases?
In cases like this where we're relying on tests on another operation to provide coverage indirectly, I wonder if it might make sense to have a comment explaining that, and pointing to the tests that are believed to provide that coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Thinking about this again, I think it probably is worth testing the weird case where sdiv can overflow -- but not for IntLiteral. And while testing that I think it also makes sense to explicitly test division by zero, which is another overflow-like case but one that can happen for IntLiteral. Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might it make sense to have a test of non-fixed-width overflow, or would that be too slow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just too slow. I tried forming a value somewhat near our bit limit using a left-shift (which I think is the fastest way we have to do that) and it ran for a very long time just doing the shift. I think the APInt
multiply algorithm is probably quadratic in the length of the int, too... (I can't imagine it's doing a Fourier transform to speed it up!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know LLVM IR yet, so it could take me a while to review the changes in this directory, especially since IIUC the golden outputs are supposed to be reviewed more carefully in lower
than in check
. Feel free to find another reviewer for this part if you want to expedite things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've asked @chandlerc to take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LLVM IR looks pretty good. Left some comments throughout.
A higher level meta comment though: I think we need a way to suppress the SemIR from test splits where we're able to fully validate the behavior with the type system as you're doing with Expect(<some value>)
, or where we're just testing diagnostics. The SemIR created by these file splits is huge and completely unhelpful given that they are self enforcing.
Not sure that's strictly necessary prior to landing this PR, but it was already hard to just navigate the SemIR added by this PR.
And if anything, we should be leveraging all the opportunities we have to directly test things the way you are and bypass the more complex SemIR-based testing and only do that in a few places where we really want to zoom into how this is represented, not how it behaves.
if (result.overflow && !lhs_bit_width_id.is_valid()) { | ||
// Retry with a larger bit width. Most operations can only overflow by one | ||
// bit, but signed n-bit multiplication can overflow to 2n-1 bits. | ||
int new_width = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
But it seems good to capture this rationale in the comments as otherwise it is a bit mysterious why we wait to see the overflow before doing this.
// the RHS with the LHS bit width. | ||
CARBON_CHECK(rhs.type_id == lhs.type_id, "Heterogeneous builtin integer op!"); | ||
llvm::APInt rhs_val = context.ints().GetAtWidth(rhs.int_id, lhs_bit_width_id); | ||
return {.lhs = context.ints().GetAtWidth(lhs_id, bit_width_id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on APIntBinaryOperands talks about RVO, but doesn't this function defeat that by returning a named variable in one path and a temporary in another?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This return
statement will have RVO / copy elision applied, regardless of whatever else the function does, because it's directly constructing an instance of the return type in the returned expression.
The other return
statement will typically have NRVO applied, because all the return
s in the scope of result
return result
(though NRVO is not guaranteed by the language rules).
// Retry with a larger bit width. Most operations can only overflow by one | ||
// bit, but signed n-bit multiplication can overflow to 2n-1 bits. | ||
int new_width = | ||
builtin_kind == SemIR::BuiltinFunctionKind::IntSMul |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this not seem to imply that IntUMul maybe have introduced wrapping on the first ComputeBinaryIntOpResult()
attempt, and wouldn't if we'd increased its bitwidth? Is that desirable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsigned operations aren't meaningful on an unsized integer type, so that's not even possible here. I've extended the comment to explain and added a CHECK
.
Add `EXTRA-ARGS:` support to file_test, to add arguments without overriding the default arguments. Use `EXTRA-ARGS: --no-dump-sem-ir` to turn off SemIR dumping and thus SemIR testing in the int builtin tests, which validate correct behavior through diagnostics instead. This doesn't get us any closer to supporting more targeted SemIR dumping / testing, but hopefully this is a generally useful feature for argument testing. Requested in review of carbon-language#4716.
Add `EXTRA-ARGS:` support to file_test, to add arguments without overriding the default arguments. Use `EXTRA-ARGS: --no-dump-sem-ir` to turn off SemIR dumping and thus SemIR testing in the int builtin tests, which validate correct behavior through diagnostics instead. This doesn't get us any closer to supporting more targeted SemIR dumping / testing, but this seems to be a generally useful feature anyway. Most existing tests using `ARGS` have been switched over to using `EXTRA-ARGS`. Requested in review of #4716.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I think this is mostly waiting to get rebased on the test improvement so the churn there is removed, but I should also go back through other comments I suspect)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some diagnostic improvements for the future I've flagged below, but those shouldn't be blocking. I think this is already at the point of a pretty huge improvement over the status quo and so motivated to land it and start iterating.
There may be some more API improvements possible, especially around eval.cpp
, but again, I think those can reasonably be done as follow-ups if needed.
LGTM
@@ -9,9 +9,9 @@ | |||
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/tuple/access/fail_negative_indexing.carbon | |||
|
|||
var a: (i32, i32) = (12, 6); | |||
// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:17: error: cannot access member of interface `Core.Negate` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess] | |||
// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:14: error: tuple element index `-10` is past the end of type `(i32, i32)` [TupleIndexOutOfBounds] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow-up: we should update this diagnostic to special case negative literals.
@@ -9,7 +9,7 @@ | |||
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/index/fail_negative_indexing.carbon | |||
|
|||
var c: [i32; 2] = (42, 42); | |||
// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:16: error: cannot access member of interface `Core.Negate` in type `Core.IntLiteral` that does not implement that interface [MissingImplInMemberAccess] | |||
// CHECK:STDERR: fail_negative_indexing.carbon:[[@LINE+3]]:16: error: array index `-10` is past the end of type `[i32; 2]` [ArrayIndexOutOfBounds] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow-up: we should update this diagnostic to special case negative literals.
Fixes integer builtins to produce the correct values (and not CHECK-fail) when used on integer literals. Also adds impls to the prelude to use the new builtins to perform operations on integer literals.
Perhaps most importantly, this allows directly initializing
i32
values with negative numbers, as the negation operation on integer literals now works.For testing I've added tests for use of literals with one operator in each class (addition, multiplication, ordering, bitwise, etc) for which there are distinct rules or overflow behavior, rather than exhaustively testing all the combinations. This is aimed at finding a good tradeoff between maintainability of the tests and thorough test coverage.
Also fixes lowering of heterogeneous shifts and comparisons. These are currently disabled when one of the operands is an integer literal, but we may want to allow that when the integer literal operand has a known constant value.