Skip to content

v3: print the operand values of failed asserts again - #28920

Open
medvednikov wants to merge 5 commits into
masterfrom
fix/28901-assert-failure-details
Open

medvednikov wants to merge 5 commits into
masterfrom
fix/28901-assert-failure-details

Conversation

@medvednikov

@medvednikov medvednikov commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

A failed assert foo() == 'www' outside of a test only printed V panic: Assertion failed... and the source line. The C backend printed operand values only for integer and float comparisons, and it read them from the children of the lowered condition. For most other comparisons those children are not the source operands:

  • string == becomes string__eq(a, b), and >/<= swap the arguments of string__lt;
  • struct == becomes a field by field && chain;
  • array == becomes array_eq_raw(a, b, sizeof(T)).

The test-file path used the same nodes. It printed false/false for struct operands, nothing for arrays, swapped values for string >, and evaluated operands like a.pop() a second time to print them.

Fix:

  • transform (transform_assert_stmt): while it lowers an assert comparison, the transformer records the lowered form of both source operands (hooks in transform_expr and transform_expr_for_type, active only for that comparison). If the lowering stored an operand in a temp (like __eq_rhs_0), the temp is used. The lowering of the condition itself is unchanged. The source spans of the operands are appended to the assert marker, for the labels.
  • cgen (.assert_stmt): operands with side effects are stored in temps before the condition, and the condition reads the temps. Operands without side effects are read again. An operand is only reported when one of these works, so the failure path never evaluates an operand twice. The temps keep the source order: when the right operand gets a temp, the left one gets one too (unless it is a constant), so in assert c.n == bump(mut c) the value of c.n is read before bump runs. If the left operand can not be copied into a temp, the right one stays in the condition, and no values are printed. Struct literals count as having side effects when a field they do not set has a default with side effects. That includes the fields of nested, generic and embedded structs, which cgen fills in. Index expressions that call a [] method count as having side effects too. So neither is evaluated again for the report. Outside of tests, the failure is printed in the V 0.5 format of builtin.__print_assert_failure, which doc/docs.md also shows for @[assert_continues].

Before:

V panic: Assertion failed...
code.v:6: > assert foo() == 'www'

After:

code.v:6: FAIL: fn main.main: assert foo() == 'www'
   left value: foo() = zzz
  right value: 'www' = www
V panic: Assertion failed...

The code.v:6:NaN: in the issue is how the playground shows code.v:6: FAIL:; V 0.5 printed FAIL: there. Like V 0.5, an operand whose source text already is its value is printed once (right value: [1, 2]), and a message is printed as message: ....

Other changes in the same code:

  • @[assert_continues] functions no longer print V panic: Assertion failed..., since they do not panic.
  • In test builds, asserts in helper functions use the test runner format too, instead of printing V panic before the failure goes to the test runner.
  • Test asserts without operand values print their message again. sizeof()/__offsetof() types in the printed expression are qualified again, which vlib/v/tests/failing_tests_test.v expects (it fails on master).
  • Values whose str code does not compile yet are printed as <value>: C structs (their V declaration need not match the C layout, like anonymous members), functions, and options with a non-scalar payload inside other values.
  • Comparisons with none, and operands that are value if/match expressions, still print no values.

Fixes #28901

Validation

  • Issue repro prints the output above. Checked by hand, in programs and in test files: strings, string </>, structs with and without str(), arrays, nested arrays, fixed arrays, maps, sum types, enums, floats, u64, options and results with ?/or, channel receive, interface method calls, shared values, mut parameters and receivers, for mut values, generics, comptime $for fields, and asserts in defer, or, match, unsafe, $if and loops. Operands with side effects are evaluated once. Also compiled with -cstrict -cc clang, -cc tcc, -autofree, -gc none and -prod.

  • New tests pass; they fail on master:

    • vlib/v/slow_tests/inout/v3_assert_failure_values.v3.v: the program format, with a call operand that must run once, structs, a custom str(), arrays, a mut parameter, an enum, a float and messages.
    • vlib/v/compiler_tests/assert_failure_values_test.v: the issue repro, and the test runner format (call operand run once, struct and array values, string >, message without values, assert in a helper), and the evaluation order of a plain left operand and a right operand that changes it (b.n == bump(mut b), b.s == grow(mut b)). On master, the int case fails correctly but the string case passes. Also operands with calls that are not among their children: Outer{}.inner.id, Gen[int]{}.id and Embed{}.id with a field default next_id(), and g[0] with a [] method. Without this they were evaluated again for the report.
      Re-run after the review fixes (cgen: read the left assert operand before capturing the right one, cgen: do not read assert operands with hidden calls again), with the results below:
  • ./v -silent vlib/v/compiler_errors_test.v: 1720 passed, 5 skipped.

  • ./v -silent vlib/v/slow_tests/inout/compiler_test.v: 88 ok, 9 expected panics, 1 skipped, 0 errors.

  • ./v -silent test vlib/v/gen/c/ vlib/v/transform/ plus assert_failure_values_test.v and test_file_harness_codegen_test.v: 44 passed, 1 failed. test_file_harness_codegen_test.v still fails on two unrelated checks (lines 401 and 617: Assertion failed for assert false, and the top-level statement error), which fail on master too. Master also fails its line 150, which this PR fixes.

  • ./v -silent test vlib/v/tests/: 2277 passed, 14 failed, 7 skipped. The same 14 files fail on master: builtin_overflow_test, check_in_is_consistency_test, enum_bitfield_test, enum_from_generic_static_method_test, fn_call_mut_ref_args_test, fn_with_opt_or_res_of_multi_return_test, generic_muls_test, for_in_containers_of_fixed_array_test, for_in_ref_arr_test, option_generic_array_test, struct_aligned_test, struct_heap_large_fixed_array_test, vls/goto_def_test, vls/autocomplete_module_test.

  • vlib/v/tests/failing_tests_test.v now passes. vlib/v/compiler_tests/test_file_cli_run_test.v fails the same way as on master (it expects Assertion failed for assert false in a test).

  • Other assert tests pass: the 15 vlib/v/tests/**/*assert*_test.v files, sum_smartcast_enum_codegen_test.v, assert_stderr_shadow_codegen_test.v, assert_wide_integer_literal_test.v, cmd/v/assert_compat_test.v, cmd/tools/vdoc/vdoc_run_examples_test.v.

Updated expectations:

  • vlib/v/slow_tests/inout/v3_assert_operand_once.out, v3_assert_percent_label.out, v3_assert_unsigned_value.out: the header is now file:line: FAIL: fn main.main: assert ... and V panic: Assertion failed... is the last line, as in V 0.5. The values are unchanged.
  • vlib/v/compiler_tests/test_file_harness_codegen_test.v: test_v3_assertion_operands_run_once_and_stats_count_executed_assertions expected the program format (left value: values.pop() = 1) for an assert in a test function, and failed on master since test functions use the test runner format. It now checks Left value (len: 1): `1` .

A failed `assert foo() == 'www'` outside of a test only printed
`V panic: Assertion failed...` and the source line. The C backend printed
operand values only for integer and float comparisons, reading the
children of the lowered condition. Those are not the source operands for
most other comparisons: string `==` becomes `string__eq(a, b)`, `>` swaps
the arguments of `string__lt`, struct `==` becomes a field by field `&&`
chain, array `==` gets a third argument. Test files printed the values of
such nodes, so they reported `false` for struct operands and evaluated
operands like `a.pop()` a second time.

The transformer now records the lowered form of both source operands while
it lowers an assert comparison, and uses the temp instead when the lowering
stored an operand in one. The lowering of the condition is unchanged. The
source spans of the operands are appended to the assert marker, for their
labels. The C backend reads operands without side effects again, and
stores operands with side effects in temps that the condition reads. An
operand is only reported when one of these works, so it is never evaluated
twice.

Outside of tests, a failure is printed in the format of V 0.5
(`builtin.__print_assert_failure`), which doc/docs.md also shows:

    code.v:6: FAIL: fn main.main: assert foo() == 'www'
       left value: foo() = zzz
      right value: 'www' = www
    V panic: Assertion failed...

`@[assert_continues]` functions no longer print the `V panic` line. In a
test build, asserts in helper functions use the test runner format too,
asserts without values print their message, and `sizeof()`/`__offsetof()`
types are qualified with their module again. Values whose `str` code does
not compile yet (C structs, functions, options inside other values) are
printed as `<value>`.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T00:20:59.454862Z 0438da1 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1643d295e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/stmt.v Outdated
The temps for assert operands with side effects are evaluated ahead of the
condition. When only the right operand needed one, the left one was read
afterwards, in the condition. So in `assert c.n == bump(mut c)`, `bump`
ran first, and the assert passed as `1 == 1`.

Capture the left operand too, when the right one is captured, unless it is
a constant. When the left operand can not be copied into a temp, leave the
right one in the condition, and report no values. Do the same when the
condition does not read the temp of the left operand.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e1ea3546f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/stmt.v Outdated
Comment thread vlib/v/gen/c/stmt.v
A failed assert reads the operands without side effects again, to report
them. Two kinds of operands looked like that, but call functions:

- struct literals, whose unset fields get their defaults from cgen,
  including the fields of nested, generic and embedded structs, which are
  not among the children of the literal, like `Outer{}` for an `Item`
  field with `id int = next_id()`;
- index expressions on a type with a `[]` method.

Reading them again ran those calls a second time, and could report a
value that differs from the compared one. A struct literal can now be
read again only when the defaults of the fields it does not set have no
side effects, and an overloaded index never.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f8c4b0335

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/stmt.v
A failed assert reads the operands without side effects again, to report
them. When the comparison calls a user defined `==` or `<` method, also
for the fields, elements or variants of the operands, that method can
change what an operand reads, like a global `current` in
`assert current == Item{}`, whose `Item.==` changes `current`. The report
then showed the changed value, not the compared one.

Copy the operands of such comparisons into temps before the comparison
too, unless they are constants. When the lowered comparison does not
read those temps, like for arrays, whose elements are compared in a loop
ahead of the assert, report no values.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ea18d41d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/stmt.v
…lace

The temps that keep the operands of an assert whose comparison calls a
user defined operator method are plain C copies. For arrays, maps,
pointers, channels, sum types and interfaces, and structs, fixed arrays
and options that contain them, such a copy still shares storage with the
operand, which the method can change in place, like a `Bag.==` method
that increments `bag.items[0]`. The report then showed the changed value,
not the compared one.

Report no values for such comparisons.

This branch has not been deployed

No deployments
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.

Assertion error lost its detailed information

1 participant