Skip to content
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

Merged
merged 17 commits into from
Dec 31, 2024

Conversation

zygoloid
Copy link
Contributor

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.

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.
zygoloid added a commit to zygoloid/carbon-lang that referenced this pull request Dec 19, 2024
toolchain/check/eval.cpp Outdated Show resolved Hide resolved
@@ -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,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 771 to 772
static auto GetIntAtSuitableWidth(Context& context, IntId bit_width_id,
IntId int_id) -> llvm::APInt {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

}

default:
// Break to do additional setup for other builtin kinds.
Copy link
Contributor

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?

Copy link
Contributor Author

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 =
Copy link
Contributor

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()?)

Copy link
Contributor Author

@zygoloid zygoloid Dec 20, 2024

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.

Copy link
Contributor

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`.
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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!)

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@chandlerc chandlerc left a 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 =
Copy link
Contributor

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.

toolchain/check/eval.cpp Show resolved Hide resolved
toolchain/check/testdata/builtins/int/and.carbon Outdated Show resolved Hide resolved
toolchain/lower/handle_call.cpp Show resolved Hide resolved
toolchain/lower/handle_call.cpp Outdated Show resolved Hide resolved
toolchain/sem_ir/builtin_function_kind.cpp Outdated Show resolved Hide resolved
toolchain/sem_ir/builtin_function_kind.cpp Outdated Show resolved Hide resolved
// 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),
Copy link
Contributor

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?

Copy link
Contributor Author

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 returns 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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

zygoloid added a commit to zygoloid/carbon-lang that referenced this pull request Dec 27, 2024
zygoloid added a commit to zygoloid/carbon-lang that referenced this pull request Dec 30, 2024
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.
@zygoloid zygoloid requested a review from chandlerc December 30, 2024 22:32
github-merge-queue bot pushed a commit that referenced this pull request Dec 31, 2024
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.
Copy link
Contributor

@chandlerc chandlerc left a 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)

toolchain/sem_ir/builtin_function_kind.cpp Outdated Show resolved Hide resolved
@zygoloid zygoloid requested a review from chandlerc December 31, 2024 02:00
Copy link
Contributor

@chandlerc chandlerc left a 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]
Copy link
Contributor

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]
Copy link
Contributor

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.

@zygoloid zygoloid enabled auto-merge December 31, 2024 06:23
@zygoloid zygoloid added this pull request to the merge queue Dec 31, 2024
Merged via the queue into carbon-language:trunk with commit 4a7aefe Dec 31, 2024
8 checks passed
@zygoloid zygoloid deleted the toolchain-intliteral-ops branch December 31, 2024 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants