Skip to content

v3: assert captures a negated out-of-range integer literal into an int temp, truncating the comparison #28050

Description

@hunterjsb

Describe the bug

On macOS arm64, a bare (uncast) integer literal that does not fit in 32 bits is truncated to its low 32 bits when it is compared against an i64 inside assert. The comparison yields the wrong answer, and assert's own diagnostic prints the literal as a different number than the one written in the source:

assert get_plain() == -123456789012345
   left value: get_plain() = -123456789012345
  right value: -123456789012345 = 2045911175

2045911175 is exactly -123456789012345 & 0xFFFFFFFF.

The same comparison written in a plain if evaluates correctly on the same platform, so this appears specific to the code assert generates rather than to comparison codegen in general.

The same file passes on ubuntu-latest (x86_64), so identical source produces different results per architecture.

Possibly related: V's checker rejects the very same literal when it is bound rather than compared —

const c = -123456789012345  // error: overflow in implicit type `int`, use explicit type casting instead
y := -123456789012345       // error: overflow in implicit type `int`, use explicit type casting instead

so the literal is being typed as int in those positions and correctly diagnosed, while the assert comparison position silently accepts it and then truncates. Promoting the literal to the other operand's type (or raising the same overflow error) in that position would presumably fix it.

Reproduction Steps

probe/literal_test.v:

fn get() !i64 {
	return i64(-123456789012345)
}

fn get_plain() i64 {
	return i64(-123456789012345)
}

// FAILS on macOS arm64
fn test_unwrap_eq_bare_literal() ! {
	assert get()! == -123456789012345
}

// FAILS on macOS arm64
fn test_plain_eq_bare_literal() {
	assert get_plain() == -123456789012345
}

// PASSES — same comparison outside of assert
fn test_if_comparison() {
	mut hit := false
	if get_plain() == -123456789012345 {
		hit = true
	}
	assert hit, 'if-comparison against bare literal did not match'
}

// PASSES — explicit cast on both sides (the workaround)
fn test_unwrap_eq_cast_literal() ! {
	assert get()! == i64(-123456789012345)
}
v test probe/

Runnable branch, with a CI workflow that runs it on macos-latest and ubuntu-latest side by side:
https://github.com/we-be/protobuf.v/blob/probe-i64-literal/probe/literal_test.v

Expected Behavior

All four tests pass. -123456789012345 is representable as i64, the left operand is i64, and the same literal compares correctly in a plain if on the same machine — so the assert comparison should agree.

(Alternatively, if a bare literal in this position is genuinely meant to be int, it should raise the same overflow in implicit type int error the checker already raises for const c = -123456789012345, rather than silently truncating.)

Current Behavior

macOS arm64:

FAIL    21.020 ms probe/literal_test.v
V panic: Assertion failed...
probe/literal_test.v:16: fn test_unwrap_eq_bare_literal
assert get()! == -123456789012345
  right value: -123456789012345 = 2045911175
V panic: Assertion failed...
probe/literal_test.v:21: fn test_plain_eq_bare_literal
assert get_plain() == -123456789012345
   left value: get_plain() = -123456789012345
  right value: -123456789012345 = 2045911175
Summary for all V _test.v files: 1 failed, 1 total.

test_if_comparison and test_unwrap_eq_cast_literal pass.

ubuntu-latest x86_64: all four pass.

CI run showing both platforms on the same commit:
https://github.com/we-be/protobuf.v/actions/runs/31235511455

Possible Solution

In the assert comparison position, infer the untyped integer literal from the other operand's type (here i64) instead of defaulting it to int. If defaulting to int is intended, apply the existing overflow in implicit type int check there too so it fails loudly at compile time instead of silently truncating at runtime.

Additional Information/Context

  • Found while building a protobuf library for V, where test vectors compare decoded i64/sint64 values against literals. Real impact is limited by the workaround (cast both sides), but it is easy to hit and fails only on Apple Silicon, so it presents as a mysterious platform-specific test failure.
  • Reproduced on three separate CI runs over three days (2026-08-05, 2026-08-08 ×2), each against a freshly downloaded refs/heads/master tarball.
  • Not reproducible on x86_64 Linux with either gcc 16.1.1 or clang 22.1.4.

V version

V 0.5.2, built from https://codeload.github.com/vlang/v/legacy.tar.gz/refs/heads/master on 2026-08-08 (installed by vlang/setup-v@v1; the tarball has no .git, so v version reports a bare V 0.5.2 with no commit hash).

Locally on Linux (where it does not reproduce): V 0.5.2 e65ec29e8738bfc1875d722161b9babae372eb86.

Environment details (OS name and version, etc.)

Failing platform — GitHub Actions macos-latest:

Image: macos-26-arm64 / 20260728.0273
Arch:  arm64 (Apple Silicon)
V:     V 0.5.2 (master tarball, 2026-08-08)

Passing platform — GitHub Actions ubuntu-latest:

Image: ubuntu24 / 20260720.247
Arch:  x86_64
V:     V 0.5.2 (master tarball, 2026-08-08)

Local machine where it also does not reproduce (v doctor):

V full version      | V 0.5.2 e65ec29e8738bfc1875d722161b9babae372eb86
OS                  | linux, "Bazzite" (Fedora Atomic)
Processor           | 12 cpus, 64bit, little endian, AMD Ryzen 5 3600 6-Core Processor
cc version          | cc (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)
clang version       | Homebrew clang version 22.1.4
glibc version       | ldd (GNU libc) 2.43

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions