Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fab69be
parser: mark match-arm calls as return-used when a match is a block v…
medvednikov Jul 31, 2026
0efc15b
parser: recurse through ParExpr when marking match/if block-value cal…
medvednikov Aug 2, 2026
439a513
parser, v3: handle unsafe-wrapped match/if block values with propagat…
medvednikov Aug 2, 2026
1546c32
parser, checker, v3: handle cast/as-cast-wrapped match/if block value…
medvednikov Aug 2, 2026
b210e50
parser, checker, cgen, v3: look through unsafe wrappers for cast-wrap…
medvednikov Aug 2, 2026
77a271a
parser, checker, v3: traverse both infix operands for match/if block …
medvednikov Aug 2, 2026
b3b5a13
parser: mark match/if arm calls in call arguments as return-used (#28…
medvednikov Aug 2, 2026
e3c7894
parser: recurse through nested call arguments for block-value match/i…
medvednikov Aug 2, 2026
f46183f
parser: recurse through infix operands inside call arguments for bloc…
medvednikov Aug 2, 2026
eb4b179
parser, checker: handle array-literal match/if block values with prop…
medvednikov Aug 2, 2026
353a90a
parser, checker: handle struct-field match values and scope the array…
medvednikov Aug 2, 2026
23236ba
parser, checker: handle map-literal match/if block values with propag…
medvednikov Aug 2, 2026
9225506
parser, checker, v3: handle prefix-expression match/if block values w…
medvednikov Aug 2, 2026
7020743
parser, checker, v3: handle index-expression match/if block values wi…
medvednikov Aug 2, 2026
d61a5db
checker, parser, v3: value-context left-hand match for membership and…
medvednikov Aug 2, 2026
be7a393
parser, checker: value-context map keys and array spreads for match/i…
medvednikov Aug 2, 2026
6bb7234
parser, checker, v3: struct/map update and string-interp match/if blo…
medvednikov Aug 2, 2026
58fba14
parser, checker, v3: handle dump/likely-wrapped match/if block values…
medvednikov Aug 2, 2026
4095e5f
parser, checker: handle multi-return match/if block values with propa…
medvednikov Aug 2, 2026
9c8e75b
parser, checker: method-call receiver and slice-bound match/if block …
medvednikov Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion vlib/v/checker/checker.v
Original file line number Diff line number Diff line change
Expand Up @@ -4867,7 +4867,18 @@ pub fn (mut c Checker) expr(mut node ast.Expr) ast.Type {
return c.array_init(mut node)
}
ast.AsCast {
node.expr_type = c.expr(mut node.expr)
if c.expected_type == ast.void_type && operand_is_value_match_or_if(node.expr) {
// A `match`/`if` operand of an `as` cast is a value expression, e.g.
// `(match x { ... }) as Variant`. Give it a non-void expected type so
// it is checked as an expression (`is_expr`) even when nested in a
// void context (e.g. an if-branch), instead of being typed as `void`.
old_expected_type := c.expected_type
c.expected_type = node.typ
node.expr_type = c.expr(mut node.expr)
c.expected_type = old_expected_type
} else {
node.expr_type = c.expr(mut node.expr)
}
expr_type_sym := c.table.sym(node.expr_type)
type_sym := c.table.sym(c.unwrap_generic(node.typ))
if mut node.expr is ast.Ident {
Expand Down Expand Up @@ -5414,6 +5425,23 @@ fn integer_literal_from_pointer_cast_expr(expr ast.Expr) ?ast.IntegerLiteral {
}
}

// operand_is_value_match_or_if reports whether an expression is a `match`/`if`
// expression used as a value, looking through transparent `(...)` and
// `unsafe { }` wrappers (including compositions like `unsafe { match ... }`).
// Such an operand of a cast or infix expression must be checked with a non-void
// expected type so it is treated as `is_expr`, even when the surrounding expected
// type is void (e.g. nested inside an if-branch) — otherwise it is mistyped as
// `void`.
fn operand_is_value_match_or_if(expr ast.Expr) bool {
if expr is ast.ParExpr {
return operand_is_value_match_or_if(expr.expr)
}
if expr is ast.UnsafeExpr {
return operand_is_value_match_or_if(expr.expr)
}
return expr is ast.MatchExpr || expr is ast.IfExpr
Comment on lines +5461 to +5467

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unwrap unsafe operands when recognizing value matches

Although the separate unsafe and cast spellings are covered, composing them—for example i64(unsafe { match value { First { lower_first(value)! } Second { lower_second(value)! } } }) in an if-expression branch—still returns false here because this predicate only peels ParExpr. cast_expr therefore leaves expected_type as void; unsafe_expr forwards that void context to the match, which is checked as a statement rather than a value, so valid code is rejected. The v3 predicate has the equivalent unsafe-block gap; recurse through this wrapper in both implementations and add a composition regression.

AGENTS.md reference: AGENTS.md:L652-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b210e50. You're right — the predicates only peeled (...). Recursing through unsafe { } in both, plus one extra fix the composition surfaced:

  • vlib/v checker: cast_operand_is_value_match_or_if now also recurses through ast.UnsafeExpr, so i64(unsafe { match ... }) (in any paren composition) is checked as a value.
  • vlib/v cgen: recognizing the composed as-cast spelling exposed a pre-existing bug — as_cast_operand_needs_tmp_eval didn't look through ast.UnsafeExpr either, so (unsafe { match ... }) as Variant emitted invalid C (a temp decl inside the __as_cast argument). It now recurses through the wrapper like the ParExpr case; this also fixes the direct (non-if-branch) spelling that was already broken.
  • vlib/v3: is_value_match_or_if_operand now looks through unsafe { } (a .block whose value tail is the expression) and a trailing expr_stmt.

Both regression tests now cover i64(unsafe { match ... }) and (unsafe { match ... }) as Circle. Verified: v3 still self-hosts, and compiler_errors_test (1617 snapshots) + the cast/sumtype/as suites are unchanged.

}

fn (mut c Checker) cast_expr(mut node ast.CastExpr) ast.Type {
// Given: `Outside( Inside(xyz) )`,
// node.expr_type: `Inside`
Expand Down Expand Up @@ -5454,6 +5482,13 @@ fn (mut c Checker) cast_expr(mut node ast.CastExpr) ast.Type {
c.expected_type = base_to_type
} else if node.expr is ast.IndexExpr && to_type.has_flag(.option) {
c.expected_type = to_type
} else if c.expected_type == ast.void_type && operand_is_value_match_or_if(node.expr) {
// A `match`/`if` operand of a cast is a value expression, e.g.
// `i64(match x { ... })`. Propagate the cast target as its expected type
// so it is checked as an expression (`is_expr`) even in contexts where
// the surrounding expected type is void (e.g. nested inside an if-branch),
// instead of being mistyped as `void` ("does not return a value").
c.expected_type = base_to_type
}
expr_is_ident_or_cast := node.expr is ast.Ident || node.expr is ast.CastExpr
node.expr_type = c.expr(mut node.expr) // type to be casted
Expand Down
19 changes: 14 additions & 5 deletions vlib/v/checker/infix.v
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,29 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
}
// In bool contexts like `assert` and `return`, short enum literals on the left
// need the right operand type first, so `.a == x` resolves `.a` correctly.
mut check_right_type_first_for_left_short_enum := false
mut check_right_type_first := false
if node.op in [.eq, .ne] && node.left is ast.EnumVal {
left_enum := node.left as ast.EnumVal
if left_enum.enum_name.len == 0 {
if node.right is ast.EnumVal {
right_enum := node.right as ast.EnumVal
check_right_type_first_for_left_short_enum = right_enum.enum_name.len > 0
check_right_type_first = right_enum.enum_name.len > 0
} else {
check_right_type_first_for_left_short_enum = true
check_right_type_first = true
}
}
}
if !check_right_type_first && c.expected_type == ast.void_type
&& operand_is_value_match_or_if(node.left) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the membership element type for a left-hand match

When an inferred branch tail uses a membership expression such as (match node { First { lower_first(node)! } Second { lower_second(node)! } }) in [1, 2], this path checks the right operand first and later uses its full type ([]int) as the expected type of the left-hand match. The match arms return int, so valid code is rejected as though each arm needed to return []int; not in and map membership have the same issue. Derive the array element/map key type for membership operators, or force the match to be a value without imposing the right operand's container type, and add a propagation regression test.

AGENTS.md reference: AGENTS.md:L657-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in d61a5db. You're right that imposing the right operand's container type is wrong — I switched infix_expr to force the left match to be a value via the force_value_match_or_if flag (the same mechanism used for array/map/prefix/index operands), so its arms infer their own type and no container type is imposed. This replaces the earlier check-right-first-for-match path.

Confirmed the concrete-int case was passing before, but the flag is genuinely needed for context-dependent arms: I added an enum-shorthand regression ((match v { First { make_color(v)! } ... }) in [Color.red, Color.blue], arms returning .red/.green) which fails under the old container-type imposition and passes now. in, !in, and map membership all work. Regression test covers (match ...) in [1, 2]; compiler_errors_test + the infix/enum/membership suites are unchanged.

// A `match`/`if` value operand on the left, e.g. `(match x { ... }) + 1`,
// would otherwise be checked with the (void) surrounding expected type and
// mistyped as a statement (e.g. when nested inside an if-branch). Resolve
// the right operand's type first and use it as the expected type so the
// left operand is checked as a value expression.
check_right_type_first = true
}
mut right_type := ast.void_type
if check_right_type_first_for_left_short_enum {
if check_right_type_first {
right_type = c.expr(mut node.right)
if right_type == ast.no_type {
node.right_type = right_type
Expand Down Expand Up @@ -229,7 +238,7 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
}
}
}
if !check_right_type_first_for_left_short_enum {
if !check_right_type_first {
right_type = c.expr(mut node.right)
if right_type == ast.no_type {
node.right_type = right_type
Expand Down
3 changes: 3 additions & 0 deletions vlib/v/gen/c/cgen.v
Original file line number Diff line number Diff line change
Expand Up @@ -13976,6 +13976,9 @@ fn as_cast_operand_needs_tmp_eval(expr ast.Expr) bool {
ast.ParExpr {
as_cast_operand_needs_tmp_eval(expr.expr)
}
ast.UnsafeExpr {
as_cast_operand_needs_tmp_eval(expr.expr)
}
ast.SelectorExpr {
as_cast_operand_needs_tmp_eval(expr.expr)
}
Expand Down
131 changes: 70 additions & 61 deletions vlib/v/parser/parser.v
Original file line number Diff line number Diff line change
Expand Up @@ -578,70 +578,79 @@ fn (mut p Parser) parse_block_no_scope(is_top_level bool) []ast.Stmt {
}

fn (mut p Parser) mark_last_call_return_as_used(mut last_stmt ast.Stmt) {
match mut last_stmt {
ast.ExprStmt {
match mut last_stmt.expr {
ast.CallExpr {
// last stmt on block is CallExpr
last_stmt.expr.is_return_used = true
if last_stmt.expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := last_stmt.expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
}
ast.ConcatExpr {
// last stmt on block is: a, b, c := ret1(), ret2(), ret3()
for mut expr in last_stmt.expr.vals {
if mut expr is ast.CallExpr {
expr.is_return_used = true
}
}
}
ast.IfExpr {
// last stmt on block is: if .. { foo() } else { bar() }
for mut branch in last_stmt.expr.branches {
if branch.stmts.len > 0 {
mut last_if_stmt := branch.stmts.last()
p.mark_last_call_return_as_used(mut last_if_stmt)
}
}
if mut last_stmt is ast.ExprStmt {
p.mark_last_call_expr_return_as_used(mut last_stmt.expr)
}
}

fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle array-literal match values

Fresh evidence beyond the already-fixed call/infix spellings is an assignment-RHS branch ending in [match value { First { lower_first(value)! } Second { lower_second(value)! } }]: this visitor has no ast.ArrayInit case, so it never reaches the match or marks its arm calls as return-used. Because match-arm blocks are parsed after inside_assign_rhs is cleared, C generation treats those propagated calls as discarded while assigning the match temporary and can emit the same empty assignment this change is intended to prevent; recurse through array elements and add this composition to the regression test.

AGENTS.md reference: AGENTS.md:L657-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in eb4b179. This one needed both a parser and a checker change:

  • parser: added an ast.ArrayInit case to mark_last_call_expr_return_as_used (recurse through elements that contain a value match/if) and to expr_contains_value_match_or_if (so array literals nested in call arguments are covered).
  • checker: recognizing the array element then surfaced a typing gap — in a void context (nested in an if-branch) the element match/if was checked with a void expected type and mistyped as a statement (invalid void array element type). An expected-type override doesn't work here since the element type is inferred and any concrete type mistypes the arms (I confirmed none/array-type both break it), so I added an inside_array_init_value_elem flag set around the element check in array_init and honored by match_expr/if_expr to force is_expr while leaving the expected type void so the arms infer their own type.

Verified: [match ...], [(match ...)], [unsafe { match ... }], [i64(match ...)], and [100, match ...] all compile+run on both backends. v3 already handled array elements, so it needed no code change (regression test added there too). The flag is only set for value match/if array elements in a void context, so all other matches are unaffected — compiler_errors_test (1617 snapshots) + the array/match/if/option/result/sumtype suites are unchanged.

Both regression tests now cover [match ...].

match mut expr {
ast.CallExpr {
// last stmt on block is CallExpr
expr.is_return_used = true
Comment on lines +722 to +724

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Traverse call arguments for block-value matches

When an assignment-RHS block ends with a call such as wrap(match value { First { lower_first(value)! } Second { lower_second(value)! } }), the match-arm tails are parsed in statement context, but this case marks only the outer wrap call and never visits its arguments. The propagated inner calls therefore retain is_return_used == false, allowing C generation to reproduce the empty branch assignment this change is intended to prevent; recurse into call arguments containing value match/if expressions and add regression coverage for this composition.

AGENTS.md reference: AGENTS.md:L657-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b3b5a13. The CallExpr case now recurses into arguments that hold a block-value match/if, marking their arm calls as return-used. To avoid touching plain arguments, the recursion is gated by a new expr_is_wrapped_match_or_if predicate that looks through (...)/unsafe/cast/as-cast wrappers, so only arguments that actually contain a value match/if are traversed — wrap(match ...), wrap((match ...)), wrap(unsafe { match ... }), add(100, match ...), and wrap(if ... { foo()! } else { ... }) all work now.

No checker fix was needed here (unlike the cast/infix cases): a call argument already gets its expected type from the parameter, so the match is typed correctly and only the parser marking was missing. The v3 backend already routes arguments through the parameter type and handled this composition, so it needed no code change — a regression test is added there too.

Both regression tests now cover wrap(match ...). Verified compiler_errors_test (1617 snapshots) + the call/fn/match/option/result suites are unchanged.

if expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
}
ast.ConcatExpr {
// last stmt on block is: a, b, c := ret1(), ret2(), ret3()
for mut val in expr.vals {
if mut val is ast.CallExpr {
val.is_return_used = true
}
ast.InfixExpr {
if last_stmt.expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := last_stmt.expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
// last stmt has infix expr with CallExpr: foo()? + 'a'
mut left_expr := last_stmt.expr.left
for {
mut next_left_expr := ast.Expr(ast.EmptyExpr{})
if mut left_expr is ast.InfixExpr {
if left_expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := left_expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
next_left_expr = left_expr.left
} else if mut left_expr is ast.CallExpr {
left_expr.is_return_used = true
if left_expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := left_expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
break
} else {
break
}
left_expr = next_left_expr
continue
}
}
}
ast.ParExpr {
// last stmt on block is parenthesized: ( match .. { a { foo() } } )
p.mark_last_call_expr_return_as_used(mut expr.expr)
Comment on lines +834 to +836

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recurse through unsafe expression wrappers

When an assignment-RHS branch uses a value such as (unsafe { match value { First { lower_first(value)! } Second { lower_second(value)! } } }), the parser retains an ast.UnsafeExpr around the match, but this helper stops at that wrapper and leaves both propagated calls with is_return_used == false; C generation then reaches the same empty assignment path this change is intended to fix. Fresh evidence beyond the previously covered ParExpr case is that UnsafeExpr is another transparent wrapper whose C generator directly emits its inner expression, so recurse through expr.expr here and add regression coverage for this spelling.

AGENTS.md reference: AGENTS.md:L652-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 439a513 — and this one needed fixing in both backends:

  • vlib/v: added an ast.UnsafeExpr case in mark_last_call_expr_return_as_used that recurses through expr.expr, mirroring the ParExpr case.
  • vlib/v3: the same spelling exposed a matching gap in the v3 backend. transform_block_expr_for_type only treated expr_stmt/nested-block tails as value tails, so unsafe { match ... } (a block whose tail is a bare match_stmt) fell back to the type-less transform_block_expr and emitted a malformed empty ternary _ifexpr = (!ok ? : ). It now treats a value-producing match_stmt/if_expr tail as the value expression, so the target type reaches the propagating arms.

Both regression tests now cover (unsafe { match .. }). Verified v3 still self-hosts with the transform change, and confirmed (by diffing a pre-change vs post-change v3 build) that the fix is neutral to unrelated tests.

}
ast.UnsafeExpr {
// last stmt on block is unsafe-wrapped: unsafe { match .. { a { foo() } } }
p.mark_last_call_expr_return_as_used(mut expr.expr)
}
ast.CastExpr {
// last stmt on block is cast-wrapped: i64(match .. { a { foo() } })
p.mark_last_call_expr_return_as_used(mut expr.expr)
}
ast.AsCast {
// last stmt on block is as-cast-wrapped: (match .. { a { foo() } }) as T
p.mark_last_call_expr_return_as_used(mut expr.expr)
}
ast.IfExpr {
// last stmt on block is: if .. { foo() } else { bar() }
for mut branch in expr.branches {
if branch.stmts.len > 0 {
mut last_if_stmt := branch.stmts.last()
p.mark_last_call_return_as_used(mut last_if_stmt)
}
ast.ComptimeCall, ast.ComptimeSelector, ast.PrefixExpr, ast.SelectorExpr {
if last_stmt.expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := last_stmt.expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
}
}
ast.MatchExpr {
// last stmt on block is: match .. { a { foo() } b { bar() } }
for mut branch in expr.branches {
if branch.stmts.len > 0 {
mut last_match_stmt := branch.stmts.last()
p.mark_last_call_return_as_used(mut last_match_stmt)
}
else {}
}
}
ast.InfixExpr {
if expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
// last stmt has infix expr with value operands, e.g.
// `foo()? + 'a'` or `1 + (match value { First { bar()! } })`.
// Recurse into both sides so nested/wrapped match/if/call values on
// either operand are marked as return-used.
p.mark_last_call_expr_return_as_used(mut expr.left)
p.mark_last_call_expr_return_as_used(mut expr.right)
}
ast.ComptimeCall, ast.ComptimeSelector, ast.PrefixExpr, ast.SelectorExpr {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle prefixed match block values

In the current vlib/v parser/checker path, when an inferred assignment branch ends in a prefix expression such as -(match value { First { lower_first(value)! } Second { lower_second(value)! } }), this case inspects only the prefix's or_block and never traverses expr.right. The inner match is consequently checked in void context and its propagated calls remain return-unused, so valid value-producing code is rejected or reaches the same empty-value codegen path; recurse through the prefix operand, supply its value context in the checker, and add regression coverage.

AGENTS.md reference: AGENTS.md:L657-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 9225506. Three parts, since (unlike the array/map/struct cases) v3 also needed a change here:

  • parser: mark_last_call_expr_return_as_used had PrefixExpr grouped with the or-block-only cases; split it out to recurse into expr.right, and added PrefixExpr to expr_contains_value_match_or_if (so prefixed values nested in call arguments are covered).
  • checker: the prefix operand match/if was checked with a void expected type and mistyped as void. prefix_expr now sets the value-required flag around the operand check. Since the flag now covers array elements, map values and prefix operands, I renamed it inside_container_value_elem -> force_value_match_or_if; it's still consumed by match_expr/if_expr so nested statement match/if are unaffected.
  • vlib/v3: transform_prefix_expr lowered the operand with plain transform_expr; a value match/if operand now goes through the shared transform_value_operand helper (renamed from transform_infix_operand).

Verified: -(match ...), ~(match ...), !(match ...) (bool arms), wrap(-(match ...)), and [-(match ...)] all compile+run identically on both backends. v3 self-hosts; compiler_errors_test (1617 snapshots) + the prefix/operator/match/array suites are unchanged. Both regression tests now cover -(match ...).

if expr.or_block.stmts.len > 0 {
mut or_block_last_stmt := expr.or_block.stmts.last()
p.mark_last_call_return_as_used(mut or_block_last_stmt)
}
}
else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recurse through cast wrappers when marking value calls

When the block tail wraps the match in an ast.CastExpr, for example result := if value := node { i64(match value { First { lower_first(value)! } Second { lower_second(value)! } }) } else { 0 }, this match falls through the default case and its branch calls retain is_return_used == false. C generation then treats the match as a value but emits empty branch assignments, reproducing the invalid C this patch addresses; recurse through CastExpr.expr (and similarly transparent AsCast operands) and add a regression case.

AGENTS.md reference: AGENTS.md:L652-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1546c32. Investigating this turned up that the parser recursion alone was necessary but not sufficient — there were two distinct bugs, so both backends needed a checker/transform fix in addition to the parser change:

vlib/v (main compiler)

  • checker: a match/if operand of a cast (CastExpr or AsCast, possibly parenthesized) was checked with a void expected type when nested in a void context (an if-branch), so it was mistyped as void — the case failed at type-checking (expression does not return a value so it cannot be cast / cannot cast non-sum type voidusingas``) before ever reaching cgen. Fixed by giving such an operand the cast target as its expected type (is_expr).
  • parser: with the type fixed, cgen then emitted the exact _t = ; you predicted, because mark_last_call_expr_return_as_used had no CastExpr/AsCast case. Added both, recursing through expr.expr, mirroring ParExpr/UnsafeExpr.

vlib/v3

  • transform_cast_expr (and transform_as_expr) lowered the operand with plain transform_expr (statement context), so the propagating arms produced the empty ternary _ifexpr = (!ok ? : ). A value match/if operand is now routed through transform_expr_for_type.

Both regression tests now cover i64(match ...) and (match ...) as Circle. Verified: v3 still self-hosts, compiler_errors_test (1617 snapshots) + 35 cast/sumtype/match tests are unchanged, and that both the checker and parser fixes are individually required (checker-only still emits the empty C).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Traverse dump operands when marking value calls

When an assignment-RHS branch ends in dump(match value { First { lower_first(value)! } Second { lower_second(value)! } }), this visitor falls through for ast.DumpExpr, even though the checker treats the dump operand as a value and C generation directly emits that operand. The propagated match-arm calls therefore remain return-unused and can recreate the empty match-value assignment; recurse through DumpExpr.expr and add regression coverage for this spelling.

AGENTS.md reference: AGENTS.md:L657-L658

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 58fba14. ast.DumpExpr is now handled in both parser traversals (mark_last_call_expr_return_as_used and expr_contains_value_match_or_if), recursing into DumpExpr.expr. The checker already treats the dumped operand as a value, so only the parser marking was missing. On v3, transform_dump_expr now routes the operand through the shared transform_value_operand helper.

Verified dump(match ...) compiles+runs on both backends. Regression covers it in the vlib/v test (left out of the v3 exact-output test since dump writes to stderr, which os.execute folds into the captured output).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Traverse lock-expression block values

When an inferred assignment branch ends in lock state { match value { First { lower_first(value)! } Second { lower_second(value)! } } }, the new visitor falls through for ast.LockExpr, so it never marks the match-arm calls as return-used. lock_expr also checks its tail under the surrounding void expected type, causing the nested match to remain a statement; the valid lock value is therefore rejected or reaches the same empty propagated-value codegen path. Recurse into the lock's final statement and preserve its value context, with a regression test for this form.

AGENTS.md reference: AGENTS.md:L652-L658

Useful? React with 👍 / 👎.

Expand Down
Loading
Loading