Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
38 changes: 37 additions & 1 deletion vlib/v/checker/checker.v
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub mut:
inside_ct_attr bool // true inside `[if expr]`
inside_x_is_type bool // true inside the Type expression of `if x is Type {`
inside_x_matches_type bool // true inside the match branch of `match x.type { Type {} }`
inside_container_value_elem bool // true when checking a value `match`/`if` array element or map value, e.g. `[match x { .. }]` / `{'k': match x { .. }}`
anon_struct_should_be_mut bool // true when `mut var := struct { ... }` is used
inside_generic_struct_init bool
inside_integer_literal_cast bool // true inside `int(123)`
Expand Down Expand Up @@ -4867,7 +4868,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 +5426,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 +5483,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
30 changes: 30 additions & 0 deletions vlib/v/checker/containers.v
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,22 @@ fn (mut c Checker) array_init(mut node ast.ArrayInit) ast.Type {
expr_pos)
continue
}
// A value `match`/`if` array element, e.g. `[match x { ... }]`, in a
// void context (e.g. nested in an if-branch) must be checked as an
// expression (`is_expr`) so its arms produce values and infer the
// element type, instead of being lowered as void statements. Signal
// that via a flag rather than a forced expected type, which would
// mistype the arms.
mut restore_array_elem_flag := false
if c.expected_type == ast.void_type && !c.inside_container_value_elem
&& operand_is_value_match_or_if(expr) {
c.inside_container_value_elem = true
restore_array_elem_flag = true
}
typ = c.check_expr_option_or_result_call(expr, c.expr(mut expr))
if restore_array_elem_flag {
c.inside_container_value_elem = false
}
sym := c.table.sym(expected_value_type)
if sym.kind == .interface {
c.type_implements(typ, expected_value_type, expr.pos())
Expand Down Expand Up @@ -939,7 +954,22 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type {
map_key_type = map_key_type.deref()
}
mut val_ := node.vals[0]
// A value `match`/`if` map value, e.g. `{'k': match x { ... }}`, in a
// void context (e.g. nested in an if-branch) determines the map value
// type and must be checked as an expression (`is_expr`) so its arms
// produce values, instead of being lowered as void statements. Signal
// that via a flag (leaving the expected type void so the arms infer
// their own type), consumed by `match_expr`/`if_expr`.
mut restore_container_flag := false
if c.expected_type == ast.void_type && !c.inside_container_value_elem
&& operand_is_value_match_or_if(val_) {

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 Treat inferred map keys as value expressions

When an inferred map literal is the tail of an assignment-RHS branch and its first key is a match, such as {(match value { First { lower_first(value)! } Second { lower_second(value)! } }): 'x'}, the key is checked while expected_type is still void; the new forcing logic is applied only to val_. The key match is therefore classified as a statement and the map is inferred with a void key instead of accepting the valid integer-producing match. Apply equivalent value forcing while inferring the first key and add a propagation 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 be7a393. Investigating this turned up that the first-key case actually fails earlier than the checker: {(match ...): 'x'} as an if-branch value was misparsed as a block, so (match ...) became a discarded statement (expression evaluated but not used) before type-checking. The stmt map-vs-block heuristic only checked peek_token(2) == colon, which a parenthesized key pushes past the ). I made it scan to the matching ) and treat { (…) : … } as a map when a : follows (this also fixes the non-match {(1 + 1): 'x'} spelling).

With parsing fixed, I applied the same value-forcing to the first key in map_init (matching the value path you noted), so the key match is checked as an expression and the map isn't inferred with a void key. {(match ...): v} now works on both backends (v3's parser already handled the paren key). Regression test covers it; compiler_errors_test + the parser/map suites are unchanged.

c.inside_container_value_elem = true
restore_container_flag = true
}
map_val_type = ast.mktyp(c.expr(mut val_))
if restore_container_flag {
c.inside_container_value_elem = false
}
if node.vals[0].is_auto_deref_var() {
map_val_type = map_val_type.deref()
}
Expand Down
8 changes: 8 additions & 0 deletions vlib/v/checker/if.v
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,21 @@ fn (mut c Checker) gen_branch_context_string() string {

fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type {
if_kind := if node.is_comptime { '\$if' } else { 'if' }
// Consume the container-element value flag so it applies only to this outer
// node, not to nested statement-level match/if inside the branches. `[if ...]`
// or `{'k': if ...}` as a value element in a void context must still be treated
// as an expression.
is_container_value_elem := c.inside_container_value_elem
c.inside_container_value_elem = false
mut node_is_expr := false
if node.branches.len > 0 && node.has_else {
stmts := node.branches[0].stmts
if stmts.len > 0 && stmts.last() is ast.ExprStmt && stmts.last().typ != ast.void_type {
node_is_expr = true
} else if node.is_expr {
node_is_expr = true
} else if is_container_value_elem {
node_is_expr = true
}
}
if c.expected_type == ast.void_type && node_is_expr {
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
9 changes: 8 additions & 1 deletion vlib/v/checker/match.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ import v.token
import strings

fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type {
// `c.inside_container_value_elem` marks a value match used as an array element
// or map value in a void context (`[match x { .. }]`, `{'k': match x { .. }}`),
// which must be treated as an expression even though the surrounding expected
// type is void. Consume the flag here so it applies only to this outer element
// node, not to nested statement-level match/if inside the arms.
is_container_value_elem := c.inside_container_value_elem
c.inside_container_value_elem = false
if !node.is_comptime {
node.is_expr = c.expected_type != ast.void_type
node.is_expr = c.expected_type != ast.void_type || is_container_value_elem
}
node.expected_type = c.expected_type
if mut node.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated {
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
Loading
Loading