Skip to content

Sema: Improve comptime arithmetic undef handling #24674

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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

Justus2308
Copy link
Contributor

@Justus2308 Justus2308 commented Aug 3, 2025

This commit expands on the foundations laid by #23177
and moves even more Sema-only functionality from Value
to Sema.arith. Specifically all shift and bitwise operations,
@truncate, @bitReverse and @byteSwap have been moved and
adapted to the new rules around undefined.

Especially the comptime shift operations have been basically
rewritten, fixing many open issues in the process.

New rules applied to operators:

  • <<, @shlExact, @shlWithOverflow, >>, @shrExact: compile error if any operand is undef
  • <<|, ~, ^, @truncate, @bitReverse, @byteSwap: return undef if any operand is undef
  • &, |: Return undef if both operands are undef, turn undef into actual 0xAA bytes otherwise

Additionally this commit canonicalizes the representation of
aggregates with all-undefined members in the InternPool by
disallowing them and enforcing the usage of a single typed
undef value instead. This reduces the amount of edge cases
and fixes a bunch of bugs related to partially undefined vecs.

List of operations directly affected by this patch:

  • <<, <<|, @shlExact, @shlWithOverflow
  • >>, @shrExact
  • &, |, ~, ^ and their atomic rmw + reduce pendants
  • @truncate, @bitReverse, @byteSwap

Resolves #16466 *
Resolves #21266
Resolves #21943
Resolves #23034
Resolves #24392

* (was already kind of resolved before this patch I think, but addresses the comment)

Most of the LOC in this patch are test cases, but if it's too big to review please let me know and I can try to split it into 2-3 smaller commits

@mlugg
Copy link
Member

mlugg commented Aug 6, 2025

Just based on the PR description: what's the rationale behind treating << and >> differently? The way I see it, both can exhibit IB, so both should have the "compile error on undef" rule. (If I said to do this elsewhere, which I cannot rule out because I say silly things sometimes, I... probably disagree with myself? :P)

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 6, 2025

Oh I think you have a point - I for some reason only paid attention to the bits being shifted out, not the ones being shifted in, but if my mental model is correct this could also cause IB:

var x: u7 = 0;
x >>= 1; // bit #7 is undefined/not part of x, so could shift *in* a 1!

Copy link
Member

@mlugg mlugg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like great work, thank you! I have a few things I'd like you to look over, but this shouldn't be too far from mergeable.

}
/// Given an integer or boolean type, creates an value of that with the bit pattern 0xAA.
/// This is used to convert undef values into 0xAA when performing e.g. bitwise operations.
/// TODO: Eliminate this function and everything it stands for (related: #19634).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good comment

// @as(u8, undefined)
// @as(u8, undefined)
// @as(@Vector(2, u8), [runtime value])
// @as(@Vector(2, u8), [runtime value])
// @as(@Vector(2, u8), undefined)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, these changes worried me at first, but actually, this is great! Your undef canonicalization has made .{ undefined, undefined } +% .{ runtime, runtime } return comptime-known undef. Lovely work!

@Justus2308
Copy link
Contributor Author

Thank you for the review! I've added the right shift compile error and a couple of test for now, I'll have a look at all of the other comments tomorrow

@mlugg
Copy link
Member

mlugg commented Aug 7, 2025

Oh I think you have a point - I for some reason only paid attention to the bits being shifted out, not the ones being shifted in, but if my mental model is correct this could also cause IB:

var x: u7 = 0;
x >>= 1; // bit #7 is undefined/not part of x, so could shift *in* a 1!

I think you're misunderstanding where the IB comes from here. Shifting any well-defined u7 by 1 is legal, in either direction. The IB in << and >> comes from the fact that for a N-bit integer, it is only allowed to shift it by less than N bits. This is enforced by the type system to the fullest extent possible -- e.g. if the LHS is a u8, the RHS is forced to be a u3 -- but because we don't currently have #3806, the type system can't enforce it for weird LHS types like u7, so my_u7 << runtime_known(7) is Illegal Behavior.

@Justus2308
Copy link
Contributor Author

I left the aggregate interns that use byte storage and the ones in the type info logic alone for now. There are also about 100 aggregate interns left outside of Sema I'll deal with those later.

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 7, 2025

Benchmarks performed on a MacBook Pro M1 (sadly no poop)

$ hyperfine --prepare 'rm -rf .zig-cache ~/.cache/zig' 'build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig' 'build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig'

master:

  Time (mean ± σ):     26.002 s ±  0.424 s    [User: 25.186 s, System: 1.369 s]
  Range (min … max):   25.744 s … 27.158 s    10 runs
  Time (mean ± σ):     25.932 s ±  0.213 s    [User: 25.162 s, System: 1.367 s]
  Range (min … max):   25.733 s … 26.463 s    10 runs

my branch:

  Time (mean ± σ):     25.796 s ±  0.194 s    [User: 25.016 s, System: 1.372 s]
  Range (min … max):   25.570 s … 26.301 s    10 runs
  Time (mean ± σ):     25.697 s ±  0.161 s    [User: 24.986 s, System: 1.337 s]
  Range (min … max):   25.459 s … 26.072 s    10 runs

So no performance impact or even slightly faster :)

Summary
  build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig ran
    1.01 ± 0.01 times faster than build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 7, 2025

I haven't been able to find the cause for the test errors that disaperared yet. I checked all call sites of failWithUseOfUndef and couldn't find any obvious mistakes. Removing the error note for vectors also doesn't make a difference.
I suspect that it has something to do with isUndef now effectively being more rigorous because it catches vectors full of undef elems which it didn't do before but I'm not sure yet.

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 7, 2025

The only remaining usages of pt.intern(.{ .aggregate = ... that don't use bytes storage are in the Sema typeInfo logic and in x86_64 CodeGen. I don't think it makes sense to replace the usages in typeInfo since it only deals with data that's supplied by InternPool and not directly by the user so it's literally impossible to get any undefined values there. And I didn't dare to touch the x86_64 stuff yet because I have no idea how it works :)

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 7, 2025

Benchmarks for x86_64 backend:

$ hyperfine --prepare 'rm -rf .zig-cache ~/.cache/zig t' 'build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig' 'build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig'
Benchmark 1: build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      9.502 s ±  0.219 s    [User: 14.115 s, System: 2.558 s]
  Range (min … max):    9.196 s …  9.851 s    10 runs

Benchmark 2: build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      9.252 s ±  0.372 s    [User: 13.873 s, System: 2.437 s]
  Range (min … max):    8.784 s …  9.864 s    10 runs

Summary
  build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig ran
    1.03 ± 0.05 times faster than build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig

Wow I wasn't aware that the x86_64 backend is that much faster than llvm (unless I did something wrong?) can't wait for the aarch64 backend to be ready!

@Justus2308 Justus2308 force-pushed the undef-shift-bitwise branch from 64f8a57 to ff57074 Compare August 8, 2025 13:47
@Justus2308
Copy link
Contributor Author

Fixed undef_arith_is_illegal test and rebased

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 8, 2025

I've added a similiar test case to undef_arith_is_illegal for shifts specifically, this revealed that there was some undef detection logic missing for @shlWithOverflow.
I've also added some tests to undef_arith_returns_undef which triggered an assertion in the @truncate logic (now fixed).

@Justus2308
Copy link
Contributor Author

Justus2308 commented Aug 8, 2025

This isn't part of the PR, but I think we should (eventually) put a condensed version of the #23177 description into the segment about undefined in the langref, something like this:

Performing arithmetic operations on undefined values adheres to the following rules:

  • If an operation cannot trigger Illegal Behavior, and any operand is undefined, the result is undefined.
  • If an operation can trigger Illegal Behvaior, and any operand is undefined, then Illegal Behavior results.
  • An operation which would trigger Illegal Behavior, when evaluated at comptime, instead triggers a compile error.
  • 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.

See also:
Illegal Behavior

@Justus2308
Copy link
Contributor Author

Some performance data points with the latest changes applied (once again on a MacBook Pro M1):

aarch64-macos llvm:

$ hyperfine --prepare 'rm -rf .zig-cache ~/.cache/zig t' 'build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig' 'build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig'
Benchmark 1: build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):     25.226 s ±  0.106 s    [User: 24.890 s, System: 1.122 s]
  Range (min … max):   25.121 s … 25.499 s    10 runs

Benchmark 2: build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):     25.393 s ±  0.484 s    [User: 24.798 s, System: 1.180 s]
  Range (min … max):   25.072 s … 26.725 s    10 runs

Summary
  build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig ran
    1.01 ± 0.02 times faster than build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig
Benchmark 1: build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):     25.200 s ±  0.232 s    [User: 24.788 s, System: 1.154 s]
  Range (min … max):   24.922 s … 25.672 s    10 runs

Benchmark 2: build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):     25.170 s ±  0.118 s    [User: 24.761 s, System: 1.143 s]
  Range (min … max):   25.004 s … 25.411 s    10 runs

Summary
  build/release-feature/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig ran
    1.00 ± 0.01 times faster than build/release-master/bin/zig test --test-no-exec -femit-bin=t --zig-lib-dir lib lib/std/std.zig

x86_64-linux self-hosted:

hyperfine --prepare 'rm -rf .zig-cache ~/.cache/zig t' 'build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig' 'build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig'
Benchmark 1: build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      8.909 s ±  0.075 s    [User: 13.742 s, System: 2.067 s]
  Range (min … max):    8.841 s …  9.103 s    10 runs

Benchmark 2: build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      8.813 s ±  0.039 s    [User: 13.690 s, System: 2.010 s]
  Range (min … max):    8.752 s …  8.888 s    10 runs

Summary
  build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig ran
    1.01 ± 0.01 times faster than build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
Benchmark 1: build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      8.939 s ±  0.031 s    [User: 13.788 s, System: 2.059 s]
  Range (min … max):    8.885 s …  8.992 s    10 runs

Benchmark 2: build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig
  Time (mean ± σ):      8.802 s ±  0.036 s    [User: 13.705 s, System: 1.999 s]
  Range (min … max):    8.756 s …  8.879 s    10 runs

Summary
  build/release-feature/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig ran
    1.02 ± 0.01 times faster than build/release-master/bin/zig test --test-no-exec -femit-bin=t -fno-llvm -target x86_64-linux-musl --zig-lib-dir lib lib/std/std.zig

@jacobly0
Copy link
Member

jacobly0 commented Aug 9, 2025

fyi the CI failures are caused by invalid Air: %18 = aggregate_init(struct { @Vector(2, i1), @Vector(2, u1) }, [%10!, @.zero_u1])

@Justus2308
Copy link
Contributor Author

Thank you so much for the pointer I think that just saved me a lot of time lol

@Justus2308 Justus2308 force-pushed the undef-shift-bitwise branch from 332fe4a to db71832 Compare August 9, 2025 00:38
@Justus2308
Copy link
Contributor Author

Bug is fixed, also I rebased on master once again

@Justus2308
Copy link
Contributor Author

The CI failures are caused by #24754

@Justus2308 Justus2308 requested a review from mlugg August 10, 2025 22:40
This commit expands on the foundations laid by ziglang#23177
and moves even more `Sema`-only functionality from `Value`
to `Sema.arith`. Specifically all shift and bitwise operations,
`@truncate`, `@bitReverse` and `@byteSwap` have been moved and
adapted to the new rules around `undefined`.

Especially the comptime shift operations have been basically
rewritten, fixing many open issues in the process.

New rules applied to operators:
* `<<`, `@shlExact`, `@shlWithOverflow`, `>>`, `@shrExact`: compile error if any operand is undef
* `<<|`, `~`, `^`, `@truncate`, `@bitReverse`, `@byteSwap`: return undef if any operand is undef
* `&`, `|`: Return undef if both operands are undef, turn undef into actual `0xAA` bytes otherwise

Additionally this commit canonicalizes the representation of
aggregates with all-undefined members in the `InternPool` by
disallowing them and enforcing the usage of a single typed
`undef` value instead. This reduces the amount of edge cases
and fixes a bunch of bugs related to partially undefined vecs.

List of operations directly affected by this patch:
* `<<`, `<<|`, `@shlExact`, `@shlWithOverflow`
* `>>`, `@shrExact`
* `&`, `|`, `~`, `^` and their atomic rmw + reduce pendants
* `@truncate`, `@bitReverse`, `@byteSwap`
@Justus2308 Justus2308 force-pushed the undef-shift-bitwise branch from db71832 to 6bcb207 Compare August 11, 2025 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants