You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After assert err is MyError, subsequent uses of err in the same block are smart-cast to MyError, but code generation passes the address of the IError box rather than the address of the payload it holds. Every field read through that cast returns garbage — in practice a zeroed struct — with no crash and no diagnostic.
if err is MyError { ... } generates the correct unwrapping, so the two forms of the same smart cast disagree. The value returned by err.msg() therefore changes depending on whether a preceding assert err is MyError is present, which is the opposite of what a type assertion is supposed to guarantee.
This is not specific to IError; it is the assert <interface> is <Type> smart cast that is wrong. It shows up most often with errors because or blocks are where interface values are most commonly narrowed.
Reproduction Steps
Calling the same method before and after the assertion returns two different values:
structMyError {
Error
pub:
reason string
}
pub fn (e &MyError) msg() string {
return'my error: ${e.reason}'
}
fnfails() ! {
return MyError{
reason: 'boom'
}
}
fnmain() {
fails() or {
before:= err.msg()
assert err isMyErrorafter:= err.msg()
println('before the type assert : [${before}]')
println('after the type assert : [${after}]')
println('equal : ${before==after}')
}
}
The same defect makes a test assertion fail, and there it also depends on -stats:
v test repro_test.v fails on test_with_the_type_assert, while v -stats test repro_test.v reports both tests as passing. Only the second test is affected; removing the assert err is MyError line makes it pass.
Expected Behavior
err.msg() returns my error: boom whether or not it is preceded by assert err is MyError, and v test and v -stats test agree on the outcome.
Current Behavior
before the type assert : [my error: boom]
after the type assert : [my error: ]
equal : false
The override is dispatched correctly — the my error: prefix comes from MyError.msg — but e.reason reads as an empty string, because the receiver does not point at the MyError value.
Both reproductions are deterministic: 3 runs each, identical results. They reproduce with -cc clang, -cc gcc and -cc tcc, in debug and with -prod, which rules out the C compiler and its optimiser.
Possible Solution
The generated C shows the two smart casts side by side. With if err is MyError:
if (err._typ==1654694011) {
println(MyError__msg((main__MyError*)(err._object)));
With assert err is MyError:
int_t1= (int)(err._typ);
if (!(_t1==1654694011)) { /* panic */ }
println(MyError__msg(&err));
The if form dereferences err._object and casts it to the concrete type; the assert form passes &err, which is an IError*. Since IError begins with _typ and _object, the callee reads the box header as if it were the struct, so reason lands on unrelated bytes.
The fix is to make the smart cast registered by assert <iface> is <Type> emit the same (T*)(x._object) unwrapping that the if form already emits, rather than taking the address of the interface variable.
Additional Information/Context
The -stats discrepancy in the second reproduction is an observation, not a separate claim about its cause: with -stats the assertion goes through the test runner's reporting path and the wrong cast does not appear, so the same source passes. It is worth mentioning because it makes the bug easy to misdiagnose — a failing test suite that turns green as soon as it is run verbosely looks like flakiness rather than miscompilation.
Found while writing vlib/crypto/scram, where the idiomatic way to test a typed error is assert err is SomeError followed by an assertion on its message. The workaround is to assert on the message first and narrow the type afterwards, or to use if err is SomeError { ... }, but both hide a silent miscompilation rather than fixing it. Any code that reads fields through an assert ... is ... cast is currently reading garbage.
|V full version |V 0.5.2 7895be1be8401122b9c014f774b0ec0ec4bc0073.28128d9
|OS |macos, macOS, 26.6, 25G72
|Processor |12 cpus, 64bit, little endian, Apple M3 Pro
|V executable |/Users/my-account/v/v
|V home dir |OK, value: /Users/my-account/v
|VMODULES |OK, value: /Users/my-account/.vmodules
|VTMP |OK, value: /tmp/v_501
|Git version |git version 2.50.1 (Apple Git-155)
|V git status |0.5.2-278-g5b7fa858
|.git/config present |true
|cc version |Apple clang version 21.0.0 (clang-2100.1.1.101)
|gcc version |Apple clang version 21.0.0 (clang-2100.1.1.101)
|clang version |Apple clang version 21.0.0 (clang-2100.1.1.101)
|tcc version |tcc version 0.9.28rc 2026-07-24 HEAD@85ba3ae8 (AArch64 Darwin)
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.
Describe the bug
After
assert err is MyError, subsequent uses oferrin the same block are smart-cast toMyError, but code generation passes the address of theIErrorbox rather than the address of the payload it holds. Every field read through that cast returns garbage — in practice a zeroed struct — with no crash and no diagnostic.if err is MyError { ... }generates the correct unwrapping, so the two forms of the same smart cast disagree. The value returned byerr.msg()therefore changes depending on whether a precedingassert err is MyErroris present, which is the opposite of what a type assertion is supposed to guarantee.This is not specific to
IError; it is theassert <interface> is <Type>smart cast that is wrong. It shows up most often with errors becauseorblocks are where interface values are most commonly narrowed.Reproduction Steps
Calling the same method before and after the assertion returns two different values:
The same defect makes a test assertion fail, and there it also depends on
-stats:v test repro_test.vfails ontest_with_the_type_assert, whilev -stats test repro_test.vreports both tests as passing. Only the second test is affected; removing theassert err is MyErrorline makes it pass.Expected Behavior
err.msg()returnsmy error: boomwhether or not it is preceded byassert err is MyError, andv testandv -stats testagree on the outcome.Current Behavior
The override is dispatched correctly — the
my error:prefix comes fromMyError.msg— bute.reasonreads as an empty string, because the receiver does not point at theMyErrorvalue.For the test file:
Both reproductions are deterministic: 3 runs each, identical results. They reproduce with
-cc clang,-cc gccand-cc tcc, in debug and with-prod, which rules out the C compiler and its optimiser.Possible Solution
The generated C shows the two smart casts side by side. With
if err is MyError:With
assert err is MyError:The
ifform dereferenceserr._objectand casts it to the concrete type; theassertform passes&err, which is anIError*. SinceIErrorbegins with_typand_object, the callee reads the box header as if it were the struct, soreasonlands on unrelated bytes.The fix is to make the smart cast registered by
assert <iface> is <Type>emit the same(T*)(x._object)unwrapping that theifform already emits, rather than taking the address of the interface variable.Additional Information/Context
The
-statsdiscrepancy in the second reproduction is an observation, not a separate claim about its cause: with-statsthe assertion goes through the test runner's reporting path and the wrong cast does not appear, so the same source passes. It is worth mentioning because it makes the bug easy to misdiagnose — a failing test suite that turns green as soon as it is run verbosely looks like flakiness rather than miscompilation.Found while writing
vlib/crypto/scram, where the idiomatic way to test a typed error isassert err is SomeErrorfollowed by an assertion on its message. The workaround is to assert on the message first and narrow the type afterwards, or to useif err is SomeError { ... }, but both hide a silent miscompilation rather than fixing it. Any code that reads fields through anassert ... is ...cast is currently reading garbage.V version
V 0.5.2 7895be1.28128d9 (git
0.5.2-278-g5b7fa858)Environment details (OS name and version, etc.)
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.