Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
120 changes: 119 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 {} }`
force_value_match_or_if bool // force a value `match`/`if` in a value-required void context (array element, map value, prefix operand) to be an expression, e.g. `[match x {..}]`, `{'k': match x {..}}`, `-(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 @@ -3080,7 +3081,19 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type {
node.is_field_typ = node.is_field_typ || c.comptime.is_comptime_selector_type(node)
old_selector_expr := c.inside_selector_expr
c.inside_selector_expr = true
// A value `match`/`if` selector receiver, e.g. `(match x { ... }).field`, in a
// void context (nested in an if-branch) must be checked as an expression so its
// arms produce values, instead of being typed `void` ("does not return a value").
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.expr) {
c.force_value_match_or_if = true
restore_force_value = true
}
mut typ := c.expr(mut node.expr)
if restore_force_value {
c.force_value_match_or_if = false
}
expr_is_auto_deref_var := node.expr.is_auto_deref_var()
receiver_uses_wrapped_smartcast := typ.has_option_or_result()
|| c.table.sym(c.unwrap_generic(typ)).kind in [.interface, .sum_type, .any]
Expand Down Expand Up @@ -4867,7 +4880,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 @@ -5281,7 +5305,19 @@ pub fn (mut c Checker) expr(mut node ast.Expr) ast.Type {
return c.unsafe_expr(mut node)
}
ast.Likely {
// A value `match`/`if` operand, e.g. `_likely_(match x { ... })`, in a
// void context (nested in an if-branch) must be checked as an expression
// so its arms produce values, instead of being typed `void`.
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.expr) {
c.force_value_match_or_if = true
restore_force_value = true
}
ltype := c.expr(mut node.expr)
if restore_force_value {
c.force_value_match_or_if = false
}
if !c.check_types(ltype, ast.bool_type) {
ltype_sym := c.table.sym(ltype)
lname := if node.is_likely { '_likely_' } else { '_unlikely_' }
Expand Down Expand Up @@ -5414,6 +5450,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 +5507,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 Expand Up @@ -6932,7 +6992,19 @@ fn (mut c Checker) check_known_struct_name(ident ast.Ident) ? {
fn (mut c Checker) concat_expr(mut node ast.ConcatExpr) ast.Type {
mut mr_types := []ast.Type{}
for mut expr in node.vals {
// A value `match`/`if` multi-return value, e.g. `match x { ... }, 9`, in a
// void context (nested in an if-branch) must be checked as an expression so
// its arms produce values, instead of being typed `void`.
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(expr) {
c.force_value_match_or_if = true
restore_force_value = true
}
mut typ := c.expr(mut expr)
if restore_force_value {
c.force_value_match_or_if = false
}
if typ == ast.nil_type {
// nil and voidptr produces the same struct type name
typ = ast.voidptr_type
Expand Down Expand Up @@ -7903,7 +7975,19 @@ fn (mut c Checker) get_base_name(node &ast.Expr) string {
fn (mut c Checker) prefix_expr(mut node ast.PrefixExpr) ast.Type {
old_inside_ref_lit := c.inside_ref_lit
c.inside_ref_lit = c.inside_ref_lit || node.op == .amp
// A value `match`/`if` prefix operand, e.g. `-(match x { ... })`, in a void
// context (nested in an if-branch) must be checked as an expression so its arms
// produce values, instead of being typed `void` ("value after `-` is void").
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.right) {
c.force_value_match_or_if = true
restore_force_value = true
}
right_type := c.expr(mut node.right)
if restore_force_value {
c.force_value_match_or_if = false
}
c.inside_ref_lit = old_inside_ref_lit
node.right_type = right_type
mut expr := node.right
Expand Down Expand Up @@ -8459,12 +8543,33 @@ fn (mut c Checker) index_expr(mut node ast.IndexExpr) ast.Type {
}
}
if mut node.index is ast.RangeExpr { // [1..2]
// A value `match`/`if` range bound, e.g. `values[(match x { ... })..]`, in a
// void context (nested in an if-branch) must be checked as an expression so
// its arms produce values, instead of being typed `void`.
if node.index.has_low {
mut restore_low := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.index.low) {
c.force_value_match_or_if = true
restore_low = true
}
index_type := c.expr(mut node.index.low)
if restore_low {
c.force_value_match_or_if = false
}
c.check_index(typ_sym, node.index.low, index_type, true, node.is_gated)
}
if node.index.has_high {
mut restore_high := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.index.high) {
c.force_value_match_or_if = true
restore_high = true
}
index_type := c.expr(mut node.index.high)
if restore_high {
c.force_value_match_or_if = false
}
c.check_index(typ_sym, node.index.high, index_type, true, node.is_gated)
}
// array[1..2] => array
Expand Down Expand Up @@ -8509,7 +8614,20 @@ fn (mut c Checker) index_expr(mut node ast.IndexExpr) ast.Type {
c.warn('`or {}` block required when indexing a map with sum type value', node.pos)
}
} else {
// A value `match`/`if` index, e.g. `values[match x { ... }]`, in a void
// context (nested in an if-branch) must be checked as an expression so
// its arms produce values, instead of being typed `void` ("non-integer
// index `void`").
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.index) {
c.force_value_match_or_if = true
restore_force_value = true
}
index_type := c.expr(mut node.index)
if restore_force_value {
c.force_value_match_or_if = false
}
if node.is_gated && (typ.is_ptr() || typ.is_pointer()
|| typ_sym.kind !in [.array, .array_fixed, .string]) {
c.error('`#[]` negative indexing is only supported for arrays, fixed arrays, and strings',
Expand Down
68 changes: 67 additions & 1 deletion vlib/v/checker/containers.v
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,20 @@ fn (mut c Checker) array_init(mut node ast.ArrayInit) ast.Type {
}

if node.has_update_expr {
// `[...base, e1, e2]` — array update/spread literal
// `[...base, e1, e2]` — array update/spread literal.
// A value `match`/`if` spread operand, e.g. `[...(match x { ... })]`, in a
// void context (nested in an if-branch) must be checked as an expression so
// its arms produce values, instead of being typed `void`.
mut restore_force_value := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.update_expr) {
c.force_value_match_or_if = true
restore_force_value = true
}
update_typ := c.expr(mut node.update_expr)
if restore_force_value {
c.force_value_match_or_if = false
}
// Resolve through type aliases so `type Ints = []int; [...Ints(...)]`
// is accepted; use final_sym to look past aliases of arrays.
update_sym := c.table.final_sym(update_typ)
Expand Down Expand Up @@ -507,7 +519,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.force_value_match_or_if
&& operand_is_value_match_or_if(expr) {
c.force_value_match_or_if = 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.force_value_match_or_if = 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 @@ -911,7 +938,19 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type {
map_type = c.expected_type
}
if node.has_update_expr {
// A value `match`/`if` map update operand, e.g. `{ ...(match x { .. }), k: v }`,
// in a void context must be checked as an expression so its arms produce
// values, instead of being typed `void` ("non-map type").
mut restore_force_value := false
if map_type == ast.void_type && c.expected_type == ast.void_type
&& !c.force_value_match_or_if && operand_is_value_match_or_if(node.update_expr) {
c.force_value_match_or_if = true
restore_force_value = true
}
update_type := c.expr(mut node.update_expr)
if restore_force_value {
c.force_value_match_or_if = false
}
if map_type != ast.void_type {
if update_type != map_type {
msg := c.expected_msg(update_type, map_type)
Expand All @@ -934,12 +973,39 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type {
} else if node.keys.len > 0 {
// `{'age': 20}`
mut key_ := node.keys[0]
// A value `match`/`if` map key, e.g. `{(match x { ... }): v}`, in a void
// context determines the map key type and must be checked as an
// expression (like the value below). Same flag mechanism.
mut restore_key_flag := false
if c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(key_) {
c.force_value_match_or_if = true
restore_key_flag = true
}
map_key_type = ast.mktyp(c.expr(mut key_))
if restore_key_flag {
c.force_value_match_or_if = false
}
if node.keys[0].is_auto_deref_var() {
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.force_value_match_or_if
&& 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.force_value_match_or_if = true
restore_container_flag = true
}
map_val_type = ast.mktyp(c.expr(mut val_))
if restore_container_flag {
c.force_value_match_or_if = 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 value-required flag so it applies only to this outer node, not to
// nested statement-level match/if inside the branches. A value `if` in a
// value-required void context (`[if ...]`, `{'k': if ...}`, `-(if ...)`) must
// still be treated as an expression.
force_value := c.force_value_match_or_if
c.force_value_match_or_if = 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 force_value {
node_is_expr = true
}
}
if c.expected_type == ast.void_type && node_is_expr {
Expand Down
25 changes: 20 additions & 5 deletions vlib/v/checker/infix.v
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,20 @@ 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
}
}
}
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 All @@ -135,7 +135,22 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
node.right_type = right_type
c.expected_type = right_type
}
// A `match`/`if` value operand on the left, e.g. `(match x { ... }) + 1` or
// `(match x { ... }) in [1, 2]`, would otherwise be checked with the (void)
// surrounding expected type and mistyped as a statement (e.g. when nested inside
// an if-branch). Force it to be checked as a value expression so its arms infer
// their own type, without imposing the right operand's type (which for a
// membership operator is a container, not the element/key type).
mut restore_force_value := false
if !check_right_type_first && c.expected_type == ast.void_type && !c.force_value_match_or_if
&& operand_is_value_match_or_if(node.left) {
c.force_value_match_or_if = true
restore_force_value = true
}
mut left_type := c.expr(mut node.left)
if restore_force_value {
c.force_value_match_or_if = false
}
if left_type == ast.no_type {
node.left_type = left_type
return ast.void_type
Expand Down Expand Up @@ -229,7 +244,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
Loading
Loading