Skip to content

internal/loopir: keep a named constant bound's name in emitted code - #13

Merged
mauri870 merged 1 commit into
masterfrom
negative-index-fixes
Nov 3, 2026
Merged

mauri870 merged 1 commit into
masterfrom
negative-index-fixes

Conversation

@mauri870

@mauri870 mauri870 commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Follow-up to #2.

#2 started replacing every constant start or limit with its value, so that the checks parsing Trip.Start/Trip.Limit as text would also see named constants. The emitted file is compiled for every build, though, and a named constant can have a different value in another one. With const width = 32 for amd64 and 64 for arm64, loopvec -split on amd64 hard-coded 32 into the vector loop of for i := 0; i < width; i++, while the scalar fallback kept width. The arm64 build would then vectorize only half the elements. plusOne and i > low loops had always done the same for <= limits and loops that count down.

What changes:

  • Constant names are kept. Trip carries the values of a constant start and limit. The checks read those values instead of parsing text: the negative-index check, the short-loop rule and the copy check. A constant is written as its value only when it's built from literals alone (16, 1 - 1). A constant that uses a name keeps the name. i <= width-1 comes out as width.
  • Dead code removed. negativeSource could no longer return true after internal/loopir: reject a constant offset that puts an index below zero #2.
  • The negative-index check can't overflow. It negates the offset instead of adding it to the bound. It also runs in the same pass over the loop body as the stored-offset checks.
  • Bounds pushed past the largest int are rejected. A positive offset that pushes a constant start or limit past the largest int is rejected as that bound. for i := 0; i < math.MaxInt64; i++ { dst[i] = a[i+1] } used to produce an overlap window of math.MaxInt64 + 1, which doesn't compile.
  • Tests:
    • offset_negative_constant.txt now checks the rewritten source and runs the loops under both builds.
    • The rejected loops moved to offset_negative_constant_no_rewrite.txt, which adds a loop counting down to 0, a range over a slice, and the two loops pushed past the largest int.
    • bound_arch_constant.txt covers the GOARCH case for <, <=, counting down and >.
  • AGENTS.md documents the new rejections and the rule for keeping names.

Tested locally:

  • bound_arch_constant.txt fails against internal/loopir: reject a constant offset that puts an index below zero #2's internal/loopir, which writes _ = dst[32-1] where the test expects _ = dst[width-1].
  • make test and make test-qemu-arm64 pass.
  • make test-update and make fix leave no diff, and make lint reports 0 issues.
  • loopvec -json gives identical output before and after over std (1663 loops) and gonum (3252 loops).

In #2, bound started spelling every constant start or limit as its
value, so that the checks that parse Trip.Start and Trip.Limit as text
would see named constants too. The emitted file is compiled for every
build, though, and a named constant can have another value in another
one. With const width = 32 in a file for amd64 and 64 in one for arm64,
loopvec -split on amd64 wrote 32 into the vector loop of a
for i := 0; i < width; i++ loop, while the fallback kept width, so the
arm64 build vectorized only half of the elements. plusOne and a loop
counting down to i > low had always done the same for <= limits and
loops that count down.

Trip now carries the values of a constant start and limit, and the
checks read those instead of parsing the text: the negative-index check
from #2, the short-loop rule, and the copy check. A constant is spelled
as its value only when it is written with literals alone, and one that
names a constant keeps the name. x - k + 1 is written as x - (k-1) for a
constant x as well, so i <= width-1 gives width rather than width-1+1.
negativeSource could no longer return true after #2 and is removed.

The negative-index check now runs in the same walk as the stored-offset
checks and negates the offset rather than adding it to the bound, so a
constant limit near the largest int no longer overflows into a wrong
rejection. A positive offset that pushes a constant start or limit past
the largest int is rejected as that bound: the emitted code adds them,
and for i := 0; i < math.MaxInt64; i++ { dst[i] = a[i+1] } produced an
overlap window of math.MaxInt64 + 1, which does not compile.

offset_negative_constant.txt now checks the rewritten source and runs
the loops under both builds, and the rejected loops, including loops
that count down and range over a slice, move to
offset_negative_constant_no_rewrite.txt. bound_arch_constant.txt pins
the GOARCH case for <, <=, and loops that count down. The vectorized
sets of std and gonum do not change.
@mauri870
mauri870 enabled auto-merge (squash) November 3, 2026 21:15
@mauri870
mauri870 merged commit 4b42aa1 into master Nov 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant