From fab69bef1690fe9d71b5dc25f9d41a842b958c82 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 31 Jul 2026 16:17:13 +0300 Subject: [PATCH 01/20] parser: mark match-arm calls as return-used when a match is a block value (fix #28000) A `match` whose arms use `!`/`?` error-propagation, when the match is used as the value of an if-expression (or directly as a block value), emitted invalid C: the if-expression result temp was assigned an empty expression (`_t4 = ;`), giving "expected expression". `mark_last_call_return_as_used` recursively descended into `if` branches to flag the last call's return value as used, but had no case for `match`, so calls in match arms kept `is_return_used == false`. cgen then skipped emitting the unwrapped propagated value. Add a `MatchExpr` case symmetric to the existing `IfExpr` one, marking the last statement of each match branch. The v3 backend already handled this case; a regression test is added there too. --- vlib/v/parser/parser.v | 9 ++ ...h_as_if_expr_value_with_propagation_test.v | 88 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 71 +++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 vlib/v/tests/match_as_if_expr_value_with_propagation_test.v create mode 100644 vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 4d9629d48370b8..d0f6090178c340 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -606,6 +606,15 @@ fn (mut p Parser) mark_last_call_return_as_used(mut last_stmt ast.Stmt) { } } } + ast.MatchExpr { + // last stmt on block is: match .. { a { foo() } b { bar() } } + for mut branch in last_stmt.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) + } + } + } ast.InfixExpr { if last_stmt.expr.or_block.stmts.len > 0 { mut or_block_last_stmt := last_stmt.expr.or_block.stmts.last() diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v new file mode 100644 index 00000000000000..00c39213d66e8f --- /dev/null +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -0,0 +1,88 @@ +// Regression test for https://github.com/vlang/v/issues/28000 +// A `match` whose arms use `!`/`?` propagation, used as the value of an +// `if`-expression (or directly), used to emit invalid C (`_t = ;`) because +// the calls in the match arms were not marked as having their return used. + +struct First {} + +struct Second {} + +type Node = First | Second + +fn lower_first(_ First) !int { + return 1 +} + +fn lower_second(_ Second) !int { + return 2 +} + +fn opt_first(_ First) ?int { + return 10 +} + +fn opt_second(_ Second) ?int { + return 20 +} + +// match inside an if-guard, assigned to a variable (the original repro) +fn select_value(node ?Node) !int { + result := if value := node { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + } else { + 0 + } + return result +} + +// match with `?` option propagation +fn select_opt(node ?Node) ?int { + result := if value := node { + match value { + First { opt_first(value)? } + Second { opt_second(value)? } + } + } else { + 0 + } + return result +} + +// match used directly as the return value +fn direct_match(node Node) !int { + return match node { + First { lower_first(node)! } + Second { lower_second(node)! } + } +} + +// match assigned directly to a variable +fn assign_match(node Node) !int { + x := match node { + First { lower_first(node)! } + Second { lower_second(node)! } + } + return x +} + +fn test_match_as_if_expr_value_with_propagation() { + assert select_value(First{})! == 1 + assert select_value(Second{})! == 2 + assert select_value(none) or { -1 } == 0 +} + +fn test_match_as_if_expr_value_with_option_propagation() { + assert select_opt(First{})? == 10 + assert select_opt(Second{})? == 20 + assert select_opt(none) or { -1 } == 0 +} + +fn test_match_as_return_and_assign_value_with_propagation() { + assert direct_match(First{})! == 1 + assert direct_match(Second{})! == 2 + assert assign_match(First{})! == 1 + assert assign_match(Second{})! == 2 +} diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v new file mode 100644 index 00000000000000..de0c1e59dc1921 --- /dev/null +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -0,0 +1,71 @@ +// Regression test for https://github.com/vlang/v/issues/28000 +// A `match` whose arms use `!`/`?` propagation, used as the value of an +// `if`-expression, must assign the unwrapped result to the if-expression's +// result temp (it used to emit an empty expression `_t = ;`). +import os + +const vexe = @VEXE +const tests_dir = os.dir(@FILE) +const v3_dir = os.dir(tests_dir) +const vlib_dir = os.dir(v3_dir) +const v3_src = os.join_path(v3_dir, 'v3.v') + +fn test_match_as_if_expr_value_with_propagation() { + v3_bin := os.join_path(os.temp_dir(), 'v3_match_as_if_expr_value_propagation_test') + build := + os.execute('${vexe} -gc none -path "${vlib_dir}|@vlib|@vmodules" -o ${v3_bin} ${v3_src}') + assert build.exit_code == 0, build.output + + src := os.join_path(os.temp_dir(), 'v3_match_as_if_expr_value_propagation_input.v') + os.write_file(src, 'module main + +struct First {} +struct Second {} + +type Node = First | Second + +fn lower_first(_ First) !int { + return 1 +} + +fn lower_second(_ Second) !int { + return 2 +} + +fn select_value(node ?Node) !int { + result := if value := node { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + } else { + 0 + } + return result +} + +fn direct_match(node Node) !int { + return match node { + First { lower_first(node)! } + Second { lower_second(node)! } + } +} + +fn main() { + println(select_value(First{})!) + println(select_value(Second{})!) + println(direct_match(Second{})!) +} +') or { + panic(err) + } + + bin := os.join_path(os.temp_dir(), 'v3_match_as_if_expr_value_propagation_out') + compile := os.execute('${v3_bin} ${src} -b c -o ${bin}') + assert compile.exit_code == 0, compile.output + assert !compile.output.contains('C compilation failed'), compile.output + + run := os.execute(bin) + assert run.exit_code == 0, run.output + assert run.output.trim_space() == '1\n2\n2' +} From 0efc15bd2468bab9d46f1fffff49cc35e4deeb13 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 08:07:59 +0300 Subject: [PATCH 02/20] parser: recurse through ParExpr when marking match/if block-value calls as return-used Address PR review: when the block value is parenthesized, e.g. `result := if value := node { (match value { ... }) } else { 0 }`, the parser keeps an `ast.ParExpr` wrapper around the inner `match`. `mark_last_call_return_as_used` had no `ParExpr` case, so the `MatchExpr` arm was never reached, the match-arm calls stayed return-unused, and cgen could still emit the invalid empty assignment (`_t = ;`). Split the per-expression handling into `mark_last_call_expr_return_as_used` (taking `mut expr ast.Expr`) so it can recurse through `ParExpr` by reference, and add a `ParExpr` case. Cover the parenthesized spelling in both the vlib/v and vlib/v3 regression tests. --- vlib/v/parser/parser.v | 135 +++++++++--------- ...h_as_if_expr_value_with_propagation_test.v | 19 +++ ...s_if_expr_value_propagation_codegen_test.v | 15 +- 3 files changed, 103 insertions(+), 66 deletions(-) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index d0f6090178c340..6e8a68a438b9ea 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -578,79 +578,84 @@ 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 - } - } + 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) { + match mut expr { + ast.CallExpr { + // last stmt on block is CallExpr + expr.is_return_used = true + 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.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) - } - } + } + } + ast.ParExpr { + // last stmt on block is parenthesized: ( match .. { a { foo() } } ) + 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.MatchExpr { - // last stmt on block is: match .. { a { foo() } b { bar() } } - for mut branch in last_stmt.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) - } - } + } + } + 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) } - ast.InfixExpr { - if last_stmt.expr.or_block.stmts.len > 0 { - mut or_block_last_stmt := last_stmt.expr.or_block.stmts.last() + } + } + 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 CallExpr: foo()? + 'a' + mut left_expr := 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) } - // 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.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() + 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 } - else {} + left_expr = next_left_expr + continue + } + } + ast.ComptimeCall, ast.ComptimeSelector, ast.PrefixExpr, ast.SelectorExpr { + 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 {} diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 00c39213d66e8f..dbad7470236a51 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -38,6 +38,19 @@ fn select_value(node ?Node) !int { return result } +// parenthesized match value: `( match .. { .. } )` keeps an ast.ParExpr wrapper +fn select_value_paren(node ?Node) !int { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + // match with `?` option propagation fn select_opt(node ?Node) ?int { result := if value := node { @@ -74,6 +87,12 @@ fn test_match_as_if_expr_value_with_propagation() { assert select_value(none) or { -1 } == 0 } +fn test_parenthesized_match_as_if_expr_value_with_propagation() { + assert select_value_paren(First{})! == 1 + assert select_value_paren(Second{})! == 2 + assert select_value_paren(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index de0c1e59dc1921..d585fc58204b25 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -44,6 +44,18 @@ fn select_value(node ?Node) !int { return result } +fn select_value_paren(node ?Node) !int { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + fn direct_match(node Node) !int { return match node { First { lower_first(node)! } @@ -54,6 +66,7 @@ fn direct_match(node Node) !int { fn main() { println(select_value(First{})!) println(select_value(Second{})!) + println(select_value_paren(First{})!) println(direct_match(Second{})!) } ') or { @@ -67,5 +80,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n2' + assert run.output.trim_space() == '1\n2\n1\n2' } From 439a51341a43d47e4b54222bcd7df5470b535cc0 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 09:11:13 +0300 Subject: [PATCH 03/20] parser, v3: handle unsafe-wrapped match/if block values with propagation (#28000) Address PR review: an assignment-RHS branch value such as `(unsafe { match value { First { lower_first(value)! } ... } })` keeps an `ast.UnsafeExpr` around the match, another transparent wrapper (like `ParExpr`) whose C generator emits its inner expression directly. Both backends failed to propagate the value context through it: - vlib/v: `mark_last_call_expr_return_as_used` stopped at `UnsafeExpr`, so the match-arm calls stayed `is_return_used == false` and cgen emitted the invalid empty assignment (`_t = ;`). Add an `UnsafeExpr` case that recurses through `expr.expr`, mirroring the `ParExpr` case. - vlib/v3: `transform_block_expr_for_type` only recognized `expr_stmt` and nested `block` tails as value tails, so an unsafe block whose tail is a bare `match`/`if` (`unsafe { match ... }`) fell back to the type-less `transform_block_expr`, lowering the propagating arms in statement context and emitting a malformed empty ternary (`_ifexpr = (!ok ? : )`). Treat a value-producing `match_stmt`/`if_expr` tail as the tail expression so the target type reaches its branch tails. Cover the `(unsafe { match })` spelling in both regression tests. Verified v3 still self-hosts with the transform change. --- vlib/v/parser/parser.v | 4 ++++ ...h_as_if_expr_value_with_propagation_test.v | 22 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 17 +++++++++++++- vlib/v3/transform/transform.v | 6 ++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 6e8a68a438b9ea..93baef7d6fe27c 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -605,6 +605,10 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) } + 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.IfExpr { // last stmt on block is: if .. { foo() } else { bar() } for mut branch in expr.branches { diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index dbad7470236a51..40ac28cfcc30c2 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -51,6 +51,22 @@ fn select_value_paren(node ?Node) !int { return result } +// unsafe-wrapped match value: `( unsafe { match .. { .. } } )` keeps an +// ast.UnsafeExpr (inside an ast.ParExpr) around the match +fn select_value_unsafe(node ?Node) !int { + result := if value := node { + (unsafe { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + }) + } else { + 0 + } + return result +} + // match with `?` option propagation fn select_opt(node ?Node) ?int { result := if value := node { @@ -93,6 +109,12 @@ fn test_parenthesized_match_as_if_expr_value_with_propagation() { assert select_value_paren(none) or { -1 } == 0 } +fn test_unsafe_wrapped_match_as_if_expr_value_with_propagation() { + assert select_value_unsafe(First{})! == 1 + assert select_value_unsafe(Second{})! == 2 + assert select_value_unsafe(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index d585fc58204b25..fbf7ac7196e812 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -56,6 +56,20 @@ fn select_value_paren(node ?Node) !int { return result } +fn select_value_unsafe(node ?Node) !int { + result := if value := node { + (unsafe { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + }) + } else { + 0 + } + return result +} + fn direct_match(node Node) !int { return match node { First { lower_first(node)! } @@ -67,6 +81,7 @@ fn main() { println(select_value(First{})!) println(select_value(Second{})!) println(select_value_paren(First{})!) + println(select_value_unsafe(Second{})!) println(direct_match(Second{})!) } ') or { @@ -80,5 +95,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 0f4315c75aa35b..dc67d0950ad3ba 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -11052,7 +11052,11 @@ fn (mut t Transformer) transform_block_expr_for_type(_id flat.NodeId, node flat. last := t.a.nodes[int(last_id)] tail_expr_id := if last.kind == .expr_stmt && last.children_count > 0 { t.a.child(&last, 0) - } else if last.kind == .block && t.stmt_value_type(last_id).len > 0 { + } else if last.kind in [.block, .match_stmt, .if_expr] && t.stmt_value_type(last_id).len > 0 { + // A block whose value tail is a bare `match`/`if` expression, e.g. + // `unsafe { match x { ... } }`. Treat the statement-shaped tail as the + // value expression so the target type reaches its (possibly propagating) + // branch tails instead of lowering them in a value-less statement context. last_id } else if !t.is_stmt_kind(last.kind) { last_id From 1546c320ad0a4fe0d28b525b170398fa5dd2da8e Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 09:40:45 +0300 Subject: [PATCH 04/20] parser, checker, v3: handle cast/as-cast-wrapped match/if block values with propagation (#28000) Address PR review: a block-tail value that wraps a `match`/`if` in a cast, e.g. `i64(match value { First { lower_first(value)! } ... })` or `(match value { ... }) as Circle`, needs the same value-context handling as the `ParExpr`/`UnsafeExpr` wrappers. Two distinct bugs had to be fixed for these to compile and run correctly: vlib/v (main compiler): - checker: a `match`/`if` operand of a cast (`ast.CastExpr`/`ast.AsCast`, possibly parenthesized) was checked with a void expected type when nested in a void context (e.g. an if-branch), so it was mistyped as `void` ("expression does not return a value so it cannot be cast" / "cannot cast non-sum type `void` using `as`"). Give such an operand the cast target as its expected type so it is checked as an expression (`is_expr`). - parser: with the type fixed, cgen then emitted the empty assignment (`_t = ;`) because `mark_last_call_expr_return_as_used` had no `CastExpr`/`AsCast` case, leaving the match-arm calls return-unused. Add cases that recurse through the wrapper, mirroring `ParExpr`/`UnsafeExpr`. vlib/v3: - `transform_cast_expr` lowered a non-float/non-pointer cast operand with plain `transform_expr` (statement context), and `transform_as_expr` likewise, so a `match`/`if` operand's propagating arms produced an empty ternary (`_ifexpr = (!ok ? : )`). Route a value `match`/`if` operand through `transform_expr_for_type` (materializing it for the `as` path). Cover `i64(match ...)` and `(match ...) as Circle` in both regression tests. Verified v3 still self-hosts, and that both fixes are required (checker-only still emits the empty C). --- vlib/v/checker/checker.v | 32 ++++++++++- vlib/v/parser/parser.v | 8 +++ ...h_as_if_expr_value_with_propagation_test.v | 54 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 42 ++++++++++++++- vlib/v3/transform/sum.v | 25 +++++++++ vlib/v3/transform/transform.v | 22 ++++++++ 6 files changed, 181 insertions(+), 2 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 61a7c92af6f671..f82ecab38f97fd 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -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 && cast_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 { @@ -5414,6 +5425,18 @@ fn integer_literal_from_pointer_cast_expr(expr ast.Expr) ?ast.IntegerLiteral { } } +// cast_operand_is_value_match_or_if reports whether the (possibly parenthesized) +// operand of a cast is a `match`/`if` expression. Such an operand is a value +// expression that 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 cast_operand_is_value_match_or_if(expr ast.Expr) bool { + if expr is ast.ParExpr { + return cast_operand_is_value_match_or_if(expr.expr) + } + return expr is ast.MatchExpr || expr is ast.IfExpr +} + fn (mut c Checker) cast_expr(mut node ast.CastExpr) ast.Type { // Given: `Outside( Inside(xyz) )`, // node.expr_type: `Inside` @@ -5454,6 +5477,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 && cast_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 diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 93baef7d6fe27c..ad6b4676020e8a 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -609,6 +609,14 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { // 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 { diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 40ac28cfcc30c2..ed85d42858b8fa 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -67,6 +67,48 @@ fn select_value_unsafe(node ?Node) !int { return result } +// cast-wrapped match value: `i64(match .. { .. })` keeps an ast.CastExpr +// around the match (which is also nested in a void-context if-branch) +fn select_value_cast(node ?Node) !i64 { + result := if value := node { + i64(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + i64(0) + } + return result +} + +struct Circle { + r int +} + +struct Square { + s int +} + +type Shape = Circle | Square + +fn make_circle(r int) !Shape { + return Circle{r} +} + +// as-cast wrapped match value: `(match .. { .. }) as Circle` keeps an +// ast.AsCast (around an ast.ParExpr) with propagating arms +fn select_value_ascast(node ?int) !int { + shape := if v := node { + (match v { + 0 { make_circle(v)! } + else { make_circle(v + 1)! } + }) as Circle + } else { + Circle{99} + } + return shape.r +} + // match with `?` option propagation fn select_opt(node ?Node) ?int { result := if value := node { @@ -115,6 +157,18 @@ fn test_unsafe_wrapped_match_as_if_expr_value_with_propagation() { assert select_value_unsafe(none) or { -1 } == 0 } +fn test_cast_wrapped_match_as_if_expr_value_with_propagation() { + assert select_value_cast(First{})! == i64(1) + assert select_value_cast(Second{})! == i64(2) + assert select_value_cast(none) or { i64(-1) } == i64(0) +} + +fn test_as_cast_wrapped_match_as_if_expr_value_with_propagation() { + assert select_value_ascast(0)! == 0 + assert select_value_ascast(5)! == 6 + assert select_value_ascast(none) or { -1 } == 99 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index fbf7ac7196e812..010056b1c43fc1 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -70,6 +70,44 @@ fn select_value_unsafe(node ?Node) !int { return result } +fn select_value_cast(node ?Node) !i64 { + result := if value := node { + i64(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + i64(0) + } + return result +} + +struct Circle { + r int +} + +struct Square { + s int +} + +type Shape = Circle | Square + +fn make_circle(r int) !Shape { + return Circle{r} +} + +fn select_value_ascast(node ?int) !int { + shape := if v := node { + (match v { + 0 { make_circle(v)! } + else { make_circle(v + 1)! } + }) as Circle + } else { + Circle{99} + } + return shape.r +} + fn direct_match(node Node) !int { return match node { First { lower_first(node)! } @@ -82,6 +120,8 @@ fn main() { println(select_value(Second{})!) println(select_value_paren(First{})!) println(select_value_unsafe(Second{})!) + println(select_value_cast(First{})!) + println(select_value_ascast(5)!) println(direct_match(Second{})!) } ') or { @@ -95,5 +135,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n6\n2' } diff --git a/vlib/v3/transform/sum.v b/vlib/v3/transform/sum.v index 78471b009f6615..b68a893fe7b436 100644 --- a/vlib/v3/transform/sum.v +++ b/vlib/v3/transform/sum.v @@ -1288,6 +1288,31 @@ fn (mut t Transformer) transform_as_expr(id flat.NodeId, node flat.Node) flat.No if node.children_count == 0 { return id } + first_child := t.a.child(&node, 0) + if t.is_value_match_or_if_operand(first_child) { + // A `match`/`if` operand of an `as` cast, e.g. `(match x { ... }) as Variant`, + // is a value expression whose (possibly propagating) branch tails must be + // lowered as values. Materialize it into a value temp first, then re-run the + // `as` conversion over that temp (mirrors the option-source path below). + mut operand_type := t.raw_expr_type_without_smartcast(first_child) + if operand_type.len == 0 { + operand_type = t.node_type(first_child) + } + if operand_type.len == 0 { + operand_type = t.resolve_expr_type(first_child) + } + value := t.transform_expr_for_type(first_child, operand_type) + start := t.a.children.len + t.a.children << value + return t.transform_as_expr(id, flat.Node{ + kind: .as_expr + value: node.value + typ: node.typ + children_start: start + children_count: 1 + pos: node.pos + }) + } expr_id := t.a.child(&node, 0) // `as` converts from the expression's storage type. Inside an `is` branch, // `node_type` reports the smartcast target instead; using that here makes an diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index dc67d0950ad3ba..12734d2d6b6552 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -17097,6 +17097,22 @@ fn (mut t Transformer) transform_postfix_expr(id flat.NodeId, node flat.Node) fl }) } +// is_value_match_or_if_operand reports whether the (possibly parenthesized) node +// is a `match`/`if` expression used as a value, e.g. a cast operand like +// `i64(match x { ... })`. Such an operand must be transformed with its target +// type so its (possibly propagating) branch tails are lowered as values. +@[direct_array_access] +fn (t &Transformer) is_value_match_or_if_operand(id flat.NodeId) bool { + if int(id) < 0 { + return false + } + node := t.a.nodes[int(id)] + if node.kind == .paren && node.children_count > 0 { + return t.is_value_match_or_if_operand(t.a.child(&node, 0)) + } + return node.kind in [.match_stmt, .if_expr] +} + // transform_cast_expr transforms transform cast expr data for transform. @[direct_array_access] fn (mut t Transformer) transform_cast_expr(id flat.NodeId, node flat.Node) flat.NodeId { @@ -17278,6 +17294,12 @@ fn (mut t Transformer) transform_cast_expr(id flat.NodeId, node flat.Node) flat. new_children << t.transform_expr_for_type(child_id, target_type) } else if target_type in ['voidptr', 'byteptr', 'charptr'] { new_children << t.transform_expr_preserving_pointer_value(child_id) + } else if t.is_value_match_or_if_operand(child_id) { + // A `match`/`if` cast operand is a value expression whose (possibly + // propagating) branch tails must be lowered as values, e.g. + // `i64(match x { ... foo()! ... })`. Plain `transform_expr` would + // lower them in statement context and emit an empty ternary. + new_children << t.transform_expr_for_type(child_id, target_type) } else { new_children << t.transform_expr(child_id) } From b210e505825d4f7c29dca0a6672a77cc8f73e23f Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 09:58:56 +0300 Subject: [PATCH 05/20] parser, checker, cgen, v3: look through unsafe wrappers for cast-wrapped match/if values (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review: composing the cast and unsafe wrappers, e.g. `i64(unsafe { match value { First { lower_first(value)! } ... } })` in an if-branch, was still rejected because the value-match predicates only peeled `(...)` parens, not `unsafe { }`. `cast_expr` therefore left `expected_type` void, `unsafe_expr` forwarded that void context to the match, and it was checked as a statement ("expression does not return a value so it cannot be cast"). - vlib/v checker: `cast_operand_is_value_match_or_if` now also recurses through `ast.UnsafeExpr`, so a cast operand of `unsafe { match/if ... }` (in any composition with parens) is checked as a value. - vlib/v cgen: recognizing the composed as-cast spelling then surfaced a pre-existing bug — `as_cast_operand_needs_tmp_eval` did not look through `ast.UnsafeExpr`, so `(unsafe { match ... }) as Variant` emitted invalid C (a temp declaration inside the `__as_cast` argument). It now recurses through the wrapper, matching the existing `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`, so `transform_cast_expr`/`transform_as_expr` route the operand through `transform_expr_for_type`. Cover `i64(unsafe { match ... })` and `(unsafe { match ... }) as Circle` in both regression tests. Verified v3 still self-hosts and compiler_errors_test plus the cast/sumtype/as suites are unchanged. --- vlib/v/checker/checker.v | 14 +++--- vlib/v/gen/c/cgen.v | 3 ++ ...h_as_if_expr_value_with_propagation_test.v | 43 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 32 +++++++++++++- vlib/v3/transform/transform.v | 16 ++++--- 5 files changed, 97 insertions(+), 11 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index f82ecab38f97fd..921b0af113a48e 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -5425,15 +5425,19 @@ fn integer_literal_from_pointer_cast_expr(expr ast.Expr) ?ast.IntegerLiteral { } } -// cast_operand_is_value_match_or_if reports whether the (possibly parenthesized) -// operand of a cast is a `match`/`if` expression. Such an operand is a value -// expression that 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`. +// cast_operand_is_value_match_or_if reports whether the operand of a cast is a +// `match`/`if` expression, looking through transparent `(...)` and `unsafe { }` +// wrappers (including compositions like `i64(unsafe { match ... })`). Such an +// operand is a value expression that 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 cast_operand_is_value_match_or_if(expr ast.Expr) bool { if expr is ast.ParExpr { return cast_operand_is_value_match_or_if(expr.expr) } + if expr is ast.UnsafeExpr { + return cast_operand_is_value_match_or_if(expr.expr) + } return expr is ast.MatchExpr || expr is ast.IfExpr } diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 1a25d52da67200..387c2a5f077600 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -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) } diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index ed85d42858b8fa..c43c510590ad66 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -81,6 +81,21 @@ fn select_value_cast(node ?Node) !i64 { return result } +// composed wrappers: cast around unsafe around match, `i64(unsafe { match .. })` +fn select_value_cast_unsafe(node ?Node) !i64 { + result := if value := node { + i64(unsafe { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + }) + } else { + i64(0) + } + return result +} + struct Circle { r int } @@ -109,6 +124,22 @@ fn select_value_ascast(node ?int) !int { return shape.r } +// composed wrappers: as-cast around unsafe around match, +// `(unsafe { match .. }) as Circle` +fn select_value_ascast_unsafe(node ?int) !int { + shape := if v := node { + (unsafe { + match v { + 0 { make_circle(v)! } + else { make_circle(v + 1)! } + } + }) as Circle + } else { + Circle{99} + } + return shape.r +} + // match with `?` option propagation fn select_opt(node ?Node) ?int { result := if value := node { @@ -163,12 +194,24 @@ fn test_cast_wrapped_match_as_if_expr_value_with_propagation() { assert select_value_cast(none) or { i64(-1) } == i64(0) } +fn test_cast_unsafe_wrapped_match_as_if_expr_value_with_propagation() { + assert select_value_cast_unsafe(First{})! == i64(1) + assert select_value_cast_unsafe(Second{})! == i64(2) + assert select_value_cast_unsafe(none) or { i64(-1) } == i64(0) +} + fn test_as_cast_wrapped_match_as_if_expr_value_with_propagation() { assert select_value_ascast(0)! == 0 assert select_value_ascast(5)! == 6 assert select_value_ascast(none) or { -1 } == 99 } +fn test_as_cast_unsafe_wrapped_match_as_if_expr_value_with_propagation() { + assert select_value_ascast_unsafe(0)! == 0 + assert select_value_ascast_unsafe(5)! == 6 + assert select_value_ascast_unsafe(none) or { -1 } == 99 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 010056b1c43fc1..223b2b56af7082 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -82,6 +82,20 @@ fn select_value_cast(node ?Node) !i64 { return result } +fn select_value_cast_unsafe(node ?Node) !i64 { + result := if value := node { + i64(unsafe { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + }) + } else { + i64(0) + } + return result +} + struct Circle { r int } @@ -108,6 +122,20 @@ fn select_value_ascast(node ?int) !int { return shape.r } +fn select_value_ascast_unsafe(node ?int) !int { + shape := if v := node { + (unsafe { + match v { + 0 { make_circle(v)! } + else { make_circle(v + 1)! } + } + }) as Circle + } else { + Circle{99} + } + return shape.r +} + fn direct_match(node Node) !int { return match node { First { lower_first(node)! } @@ -121,7 +149,9 @@ fn main() { println(select_value_paren(First{})!) println(select_value_unsafe(Second{})!) println(select_value_cast(First{})!) + println(select_value_cast_unsafe(Second{})!) println(select_value_ascast(5)!) + println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) } ') or { @@ -135,5 +165,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 12734d2d6b6552..94e447120fb82e 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -17097,19 +17097,25 @@ fn (mut t Transformer) transform_postfix_expr(id flat.NodeId, node flat.Node) fl }) } -// is_value_match_or_if_operand reports whether the (possibly parenthesized) node -// is a `match`/`if` expression used as a value, e.g. a cast operand like -// `i64(match x { ... })`. Such an operand must be transformed with its target -// type so its (possibly propagating) branch tails are lowered as values. +// is_value_match_or_if_operand reports whether the node is a `match`/`if` +// expression used as a value, e.g. a cast operand like `i64(match x { ... })`. +// It looks through transparent wrappers: `(...)` parens, `unsafe { }` (a `.block` +// whose value tail is the expression), and a trailing `expr_stmt` — including +// compositions like `i64(unsafe { match ... })`. Such an operand must be +// transformed with its target type so its (possibly propagating) branch tails +// are lowered as values. @[direct_array_access] fn (t &Transformer) is_value_match_or_if_operand(id flat.NodeId) bool { if int(id) < 0 { return false } node := t.a.nodes[int(id)] - if node.kind == .paren && node.children_count > 0 { + if node.kind in [.paren, .expr_stmt] && node.children_count > 0 { return t.is_value_match_or_if_operand(t.a.child(&node, 0)) } + if node.kind == .block && node.children_count > 0 { + return t.is_value_match_or_if_operand(t.a.child(&node, node.children_count - 1)) + } return node.kind in [.match_stmt, .if_expr] } From 77a271ae2d828c53dddeb851d41d77564db18c68 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 10:21:19 +0300 Subject: [PATCH 06/20] parser, checker, v3: traverse both infix operands for match/if block values (#28000) Address PR review: an assignment-RHS block ending in an infix expression such as `1 + (match value { First { lower_first(value)! } ... })` (or with the match on the left, `(match ...) + 10`) still recreated the invalid empty C. - vlib/v parser: `mark_last_call_expr_return_as_used`'s `InfixExpr` case only walked a left-associative `expr.left` chain and never looked at `expr.right` (and stopped on a wrapped-match left). It now recurses into both operands via the helper, so nested/wrapped match/if/call values on either side are marked as return-used. This also fixes `a()! + b()!`, where the old loop marked only the left call. - vlib/v checker: a value `match`/`if` on the *left* of an infix was checked with the (void) surrounding expected type and mistyped as a statement when nested in an if-branch. `infix_expr` now resolves the right operand's type first and uses it as the expected type for such a left operand (reusing the existing check-right-first path). Renamed the shared predicate `cast_operand_is_value_match_or_if` -> `operand_is_value_match_or_if`. - vlib/v3: `transform_infix_expr` lowered both operands with plain `transform_expr`; a value `match`/`if` operand now goes through `transform_expr_for_type` (new `transform_infix_operand` helper). Cover `1 + (match ...)` and `(match ...) + 10` in both regression tests. Verified v3 self-hosts and compiler_errors_test plus the enum/infix/match/ option/result suites are unchanged. --- vlib/v/checker/checker.v | 23 +++++------ vlib/v/checker/infix.v | 19 +++++++--- vlib/v/parser/parser.v | 29 +++----------- ...h_as_if_expr_value_with_propagation_test.v | 38 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 28 +++++++++++++- vlib/v3/transform/transform.v | 21 +++++++++- 6 files changed, 116 insertions(+), 42 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 921b0af113a48e..d6c06453265038 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -4867,7 +4867,7 @@ pub fn (mut c Checker) expr(mut node ast.Expr) ast.Type { return c.array_init(mut node) } ast.AsCast { - if c.expected_type == ast.void_type && cast_operand_is_value_match_or_if(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 @@ -5425,18 +5425,19 @@ fn integer_literal_from_pointer_cast_expr(expr ast.Expr) ?ast.IntegerLiteral { } } -// cast_operand_is_value_match_or_if reports whether the operand of a cast is a -// `match`/`if` expression, looking through transparent `(...)` and `unsafe { }` -// wrappers (including compositions like `i64(unsafe { match ... })`). Such an -// operand is a value expression that 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 cast_operand_is_value_match_or_if(expr ast.Expr) bool { +// 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 cast_operand_is_value_match_or_if(expr.expr) + return operand_is_value_match_or_if(expr.expr) } if expr is ast.UnsafeExpr { - return cast_operand_is_value_match_or_if(expr.expr) + return operand_is_value_match_or_if(expr.expr) } return expr is ast.MatchExpr || expr is ast.IfExpr } @@ -5481,7 +5482,7 @@ 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 && cast_operand_is_value_match_or_if(node.expr) { + } 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 diff --git a/vlib/v/checker/infix.v b/vlib/v/checker/infix.v index 3395f9cbedbc45..e2e6c1518857a3 100644 --- a/vlib/v/checker/infix.v +++ b/vlib/v/checker/infix.v @@ -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) { + // 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 @@ -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 diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index ad6b4676020e8a..5a690ca6df67b2 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -640,29 +640,12 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { 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 CallExpr: foo()? + 'a' - mut left_expr := 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 - } + // 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 { if expr.or_block.stmts.len > 0 { diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index c43c510590ad66..56e670cbabe7f5 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -96,6 +96,32 @@ fn select_value_cast_unsafe(node ?Node) !i64 { return result } +// match on the right of an infix expression: `1 + (match .. { .. })` +fn select_value_infix_right(node ?Node) !int { + result := if value := node { + 1 + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + +// match on the left of an infix expression: `(match .. { .. }) + 10` +fn select_value_infix_left(node ?Node) !int { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + 10 + } else { + 0 + } + return result +} + struct Circle { r int } @@ -212,6 +238,18 @@ fn test_as_cast_unsafe_wrapped_match_as_if_expr_value_with_propagation() { assert select_value_ascast_unsafe(none) or { -1 } == 99 } +fn test_infix_right_match_as_if_expr_value_with_propagation() { + assert select_value_infix_right(First{})! == 2 + assert select_value_infix_right(Second{})! == 3 + assert select_value_infix_right(none) or { -1 } == 0 +} + +fn test_infix_left_match_as_if_expr_value_with_propagation() { + assert select_value_infix_left(First{})! == 11 + assert select_value_infix_left(Second{})! == 12 + assert select_value_infix_left(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 223b2b56af7082..dc189b90bb9338 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -96,6 +96,30 @@ fn select_value_cast_unsafe(node ?Node) !i64 { return result } +fn select_value_infix_right(node ?Node) !int { + result := if value := node { + 1 + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + +fn select_value_infix_left(node ?Node) !int { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + 10 + } else { + 0 + } + return result +} + struct Circle { r int } @@ -150,6 +174,8 @@ fn main() { println(select_value_unsafe(Second{})!) println(select_value_cast(First{})!) println(select_value_cast_unsafe(Second{})!) + println(select_value_infix_right(First{})!) + println(select_value_infix_left(Second{})!) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -165,5 +191,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 94e447120fb82e..bd0c050369061f 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14515,6 +14515,23 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f }) } +// transform_infix_operand transforms an infix operand, routing a value +// `match`/`if` operand (e.g. `1 + (match x { ... })`) through +// `transform_expr_for_type` so its (possibly propagating) branch tails are +// lowered as values instead of in a value-less statement context. +fn (mut t Transformer) transform_infix_operand(id flat.NodeId) flat.NodeId { + if t.is_value_match_or_if_operand(id) { + mut typ := t.node_type(id) + if typ.len == 0 { + typ = t.resolve_expr_type(id) + } + if typ.len > 0 && typ != 'void' { + return t.transform_expr_for_type(id, typ) + } + } + return t.transform_expr(id) +} + // transform_infix_expr transforms transform infix expr data for transform. fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat.NodeId { if node.children_count < 2 { @@ -14621,13 +14638,13 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat lhs_id := t.a.children[node.children_start] rhs_id := t.a.children[node.children_start + 1] pending_start := t.pending_stmts.len - new_lhs := t.transform_expr(lhs_id) + new_lhs := t.transform_infix_operand(lhs_id) mut lhs_pending := []flat.NodeId{} if t.pending_stmts.len > pending_start { lhs_pending = t.pending_stmts[pending_start..].clone() t.pending_stmts = t.pending_stmts[..pending_start].clone() } - new_rhs := t.transform_expr(rhs_id) + new_rhs := t.transform_infix_operand(rhs_id) if lhs_pending.len > 0 { rhs_pending := t.pending_stmts[pending_start..].clone() t.pending_stmts = t.pending_stmts[..pending_start].clone() From b3b5a13a0f5c1375176be45fffa6041ef8ffa984 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 10:31:31 +0300 Subject: [PATCH 07/20] parser: mark match/if arm calls in call arguments as return-used (#28000) Address PR review: an assignment-RHS block ending in a call whose argument is a block-value match/if, e.g. `wrap(match value { First { lower_first(value)! } ... })`, recreated the invalid empty C. The `CallExpr` case marked only the outer call and never visited its arguments, so the propagated inner calls kept `is_return_used == false`. Recurse into call arguments that hold a (possibly `(...)`/`unsafe`/cast/as-cast wrapped) value match/if via a new `expr_is_wrapped_match_or_if` predicate that gates the recursion, so only such arguments are traversed (plain arguments are left untouched). The v3 backend already routes call arguments through the parameter type and handled this case; a regression test is added there too. Cover `wrap(match ...)` in both regression tests. --- vlib/v/parser/parser.v | 22 ++++++++++++++++++ ...h_as_if_expr_value_with_propagation_test.v | 23 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 19 ++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 5a690ca6df67b2..b5ca74cb0426ea 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -583,6 +583,21 @@ fn (mut p Parser) mark_last_call_return_as_used(mut last_stmt ast.Stmt) { } } +// expr_is_wrapped_match_or_if reports whether an expression is a `match`/`if` +// value expression, looking through transparent `(...)`, `unsafe { }`, cast and +// `as`-cast wrappers. Used to decide whether a call argument holds a block-value +// match/if whose arm calls must be marked as return-used. +fn (p &Parser) expr_is_wrapped_match_or_if(expr ast.Expr) bool { + return match expr { + ast.MatchExpr, ast.IfExpr { true } + ast.ParExpr { p.expr_is_wrapped_match_or_if(expr.expr) } + ast.UnsafeExpr { p.expr_is_wrapped_match_or_if(expr.expr) } + ast.CastExpr { p.expr_is_wrapped_match_or_if(expr.expr) } + ast.AsCast { p.expr_is_wrapped_match_or_if(expr.expr) } + else { false } + } +} + fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { match mut expr { ast.CallExpr { @@ -592,6 +607,13 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { mut or_block_last_stmt := expr.or_block.stmts.last() p.mark_last_call_return_as_used(mut or_block_last_stmt) } + // an argument may itself be a block-value match/if, e.g. + // `wrap(match value { First { foo()! } })`; mark its arm calls too. + for mut arg in expr.args { + if p.expr_is_wrapped_match_or_if(arg.expr) { + p.mark_last_call_expr_return_as_used(mut arg.expr) + } + } } ast.ConcatExpr { // last stmt on block is: a, b, c := ret1(), ret2(), ret3() diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 56e670cbabe7f5..122313a671f8d2 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -122,6 +122,23 @@ fn select_value_infix_left(node ?Node) !int { return result } +fn wrap(x int) int { + return x * 10 +} + +// match as a call argument: `wrap(match .. { .. })` +fn select_value_callarg(node ?Node) !int { + result := if value := node { + wrap(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -250,6 +267,12 @@ fn test_infix_left_match_as_if_expr_value_with_propagation() { assert select_value_infix_left(none) or { -1 } == 0 } +fn test_call_argument_match_as_if_expr_value_with_propagation() { + assert select_value_callarg(First{})! == 10 + assert select_value_callarg(Second{})! == 20 + assert select_value_callarg(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index dc189b90bb9338..90b52c2f636a85 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -120,6 +120,22 @@ fn select_value_infix_left(node ?Node) !int { return result } +fn wrap(x int) int { + return x * 10 +} + +fn select_value_callarg(node ?Node) !int { + result := if value := node { + wrap(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -176,6 +192,7 @@ fn main() { println(select_value_cast_unsafe(Second{})!) println(select_value_infix_right(First{})!) println(select_value_infix_left(Second{})!) + println(select_value_callarg(Second{})!) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -191,5 +208,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n6\n6\n2' } From e3c78948cfe2e59355dd4e068c07ff8035f97ec9 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 10:39:56 +0300 Subject: [PATCH 08/20] parser: recurse through nested call arguments for block-value match/if (#28000) Address PR review: the direct call-argument handling missed nested compositions such as `wrap(wrap(match value { First { lower_first(value)! } ... }))`. The outer argument is itself an `ast.CallExpr`, so the gate predicate returned false and the inner call and match were never visited, leaving the propagated arm calls return-unused and recreating the invalid empty C. Extend the predicate (renamed `expr_is_wrapped_match_or_if` -> `expr_contains_value_match_or_if`) to also look through a nested `CallExpr`'s arguments, so a call argument whose subtree contains a value match/if is recursed into and its arm calls are marked as return-used. The gate still skips argument trees that contain no match/if, so plain call arguments are untouched. The v3 backend already routes call arguments through the parameter type and handled this; a regression test is added there too. Cover `wrap(wrap(match ...))` in both regression tests. --- vlib/v/parser/parser.v | 52 ++++++++++++++----- ...h_as_if_expr_value_with_propagation_test.v | 20 +++++++ ...s_if_expr_value_propagation_codegen_test.v | 15 +++++- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index b5ca74cb0426ea..55c9eb7b4b7caf 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -583,18 +583,41 @@ fn (mut p Parser) mark_last_call_return_as_used(mut last_stmt ast.Stmt) { } } -// expr_is_wrapped_match_or_if reports whether an expression is a `match`/`if` -// value expression, looking through transparent `(...)`, `unsafe { }`, cast and -// `as`-cast wrappers. Used to decide whether a call argument holds a block-value -// match/if whose arm calls must be marked as return-used. -fn (p &Parser) expr_is_wrapped_match_or_if(expr ast.Expr) bool { +// expr_contains_value_match_or_if reports whether an expression is (or, through +// transparent `(...)`/`unsafe { }`/cast/`as`-cast wrappers and nested call +// arguments, contains) a `match`/`if` value expression. Used to decide whether a +// call argument holds a block-value match/if whose arm calls must be marked as +// return-used, including nested compositions like `wrap(wrap(match value { .. }))`. +fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { return match expr { - ast.MatchExpr, ast.IfExpr { true } - ast.ParExpr { p.expr_is_wrapped_match_or_if(expr.expr) } - ast.UnsafeExpr { p.expr_is_wrapped_match_or_if(expr.expr) } - ast.CastExpr { p.expr_is_wrapped_match_or_if(expr.expr) } - ast.AsCast { p.expr_is_wrapped_match_or_if(expr.expr) } - else { false } + ast.MatchExpr, ast.IfExpr { + true + } + ast.ParExpr { + p.expr_contains_value_match_or_if(expr.expr) + } + ast.UnsafeExpr { + p.expr_contains_value_match_or_if(expr.expr) + } + ast.CastExpr { + p.expr_contains_value_match_or_if(expr.expr) + } + ast.AsCast { + p.expr_contains_value_match_or_if(expr.expr) + } + ast.CallExpr { + mut found := false + for arg in expr.args { + if p.expr_contains_value_match_or_if(arg.expr) { + found = true + break + } + } + found + } + else { + false + } } } @@ -607,10 +630,11 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { mut or_block_last_stmt := expr.or_block.stmts.last() p.mark_last_call_return_as_used(mut or_block_last_stmt) } - // an argument may itself be a block-value match/if, e.g. - // `wrap(match value { First { foo()! } })`; mark its arm calls too. + // an argument may itself be (or nest) a block-value match/if, e.g. + // `wrap(match value { First { foo()! } })` or + // `wrap(wrap(match value { .. }))`; mark its arm calls too. for mut arg in expr.args { - if p.expr_is_wrapped_match_or_if(arg.expr) { + if p.expr_contains_value_match_or_if(arg.expr) { p.mark_last_call_expr_return_as_used(mut arg.expr) } } diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 122313a671f8d2..b89d82a6663d3e 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -139,6 +139,20 @@ fn select_value_callarg(node ?Node) !int { return result } +// match nested inside a call argument that is itself a call: +// `wrap(wrap(match .. { .. }))` +fn select_value_nested_callarg(node ?Node) !int { + result := if value := node { + wrap(wrap(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -273,6 +287,12 @@ fn test_call_argument_match_as_if_expr_value_with_propagation() { assert select_value_callarg(none) or { -1 } == 0 } +fn test_nested_call_argument_match_as_if_expr_value_with_propagation() { + assert select_value_nested_callarg(First{})! == 100 + assert select_value_nested_callarg(Second{})! == 200 + assert select_value_nested_callarg(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 90b52c2f636a85..5809b8d642e07e 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -136,6 +136,18 @@ fn select_value_callarg(node ?Node) !int { return result } +fn select_value_nested_callarg(node ?Node) !int { + result := if value := node { + wrap(wrap(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -193,6 +205,7 @@ fn main() { println(select_value_infix_right(First{})!) println(select_value_infix_left(Second{})!) println(select_value_callarg(Second{})!) + println(select_value_nested_callarg(First{})!) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -208,5 +221,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n6\n6\n2' } From f46183fdd884805be6f8ab6059a02573155a5f24 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 10:46:40 +0300 Subject: [PATCH 09/20] parser: recurse through infix operands inside call arguments for block-value match/if (#28000) Address PR review: a call argument that wraps the match inside an infix expression, e.g. `wrap(1 + (match value { First { lower_first(value)! } ... }))`, fell through `expr_contains_value_match_or_if` to false, so the `CallExpr` handler never traversed it and the match-arm calls stayed return-used == false, recreating the invalid empty C. Recognize `ast.InfixExpr` in the predicate, recursing through both operands, so an argument whose subtree reaches a value match/if through infix operators is marked. The marking helper already handles `InfixExpr`. The v3 backend already handled this composition; a regression test is added there too. Cover `wrap(1 + (match ...))` in both regression tests. --- vlib/v/parser/parser.v | 6 ++++++ ...h_as_if_expr_value_with_propagation_test.v | 20 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 15 +++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 55c9eb7b4b7caf..705adc6ea82f77 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -615,6 +615,12 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { } found } + ast.InfixExpr { + // e.g. `1 + (match value { .. })` as a call argument. + + p.expr_contains_value_match_or_if(expr.left) + || p.expr_contains_value_match_or_if(expr.right) + } else { false } diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index b89d82a6663d3e..04c17f735532fd 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -153,6 +153,20 @@ fn select_value_nested_callarg(node ?Node) !int { return result } +// match inside an infix expression inside a call argument: +// `wrap(1 + (match .. { .. }))` +fn select_value_callarg_infix(node ?Node) !int { + result := if value := node { + wrap(1 + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -293,6 +307,12 @@ fn test_nested_call_argument_match_as_if_expr_value_with_propagation() { assert select_value_nested_callarg(none) or { -1 } == 0 } +fn test_call_argument_infix_match_as_if_expr_value_with_propagation() { + assert select_value_callarg_infix(First{})! == 20 + assert select_value_callarg_infix(Second{})! == 30 + assert select_value_callarg_infix(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 5809b8d642e07e..6ba087d88e8833 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -148,6 +148,18 @@ fn select_value_nested_callarg(node ?Node) !int { return result } +fn select_value_callarg_infix(node ?Node) !int { + result := if value := node { + wrap(1 + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -206,6 +218,7 @@ fn main() { println(select_value_infix_left(Second{})!) println(select_value_callarg(Second{})!) println(select_value_nested_callarg(First{})!) + println(select_value_callarg_infix(First{})!) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -221,5 +234,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n6\n6\n2' } From eb4b179c58c8a133625b9d5054aff81f10859112 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 11:11:03 +0300 Subject: [PATCH 10/20] parser, checker: handle array-literal match/if block values with propagation (#28000) Address PR review: an assignment-RHS branch ending in an array literal whose element is a value match/if, e.g. `[match value { First { lower_first(value)! } ... }]`, was rejected and could recreate the invalid empty C. Two fixes were needed: - checker: in a void context (e.g. nested in an if-branch) the array element match/if was checked with a void expected type and mistyped as a statement ("invalid void array element type"). An expected-type override is unusable here (the element type is inferred and any concrete type mistypes the arms), so a new `inside_array_init_value_elem` flag is 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. - parser: with the type fixed, cgen then emitted the empty assignment (`_t = ;`) because `mark_last_call_expr_return_as_used` had no `ArrayInit` case. Recurse through array elements that contain a value match/if, and recognize `ArrayInit` in `expr_contains_value_match_or_if` so array literals nested in call arguments are covered too. The v3 backend already handled array-literal elements; a regression test is added there too. Cover `[match ...]` in both regression tests. Verified compiler_errors_test plus the array/match/if/option/result suites are unchanged. --- vlib/v/checker/checker.v | 95 ++++++++++--------- vlib/v/checker/containers.v | 15 +++ vlib/v/checker/if.v | 4 + vlib/v/checker/match.v | 5 +- vlib/v/parser/parser.v | 20 ++++ ...h_as_if_expr_value_with_propagation_test.v | 21 ++++ ...s_if_expr_value_propagation_codegen_test.v | 15 ++- 7 files changed, 126 insertions(+), 49 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index d6c06453265038..dcc3c87d63886a 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -76,53 +76,54 @@ pub mut: error_details []string should_abort bool // when too many errors are accumulated, .should_abort becomes true. It is checked in statement/expression loops, so the checker can return early, instead of wasting time. - expected_type ast.Type - expected_or_type ast.Type // fn() or { 'this type' } eg. string. expected or block type - expected_expr_type ast.Type // if/match is_expr: expected_type - mod string // current module name - has_globals_in_module bool // true if the current module has @[has_globals] attribute - strict_map_index_in_module bool // true if the current module has @[strict_map_index] attribute - const_var &ast.ConstField = unsafe { nil } // the current constant, when checking const declarations - const_deps []string - const_eval_stack []string // names of constants currently being recursively resolved (to break cycles via anon fn bodies) - const_names []string - global_names []string - locked_names []string // vars that are currently locked - rlocked_names []string // vars that are currently read-locked - in_for_count int // if checker is currently in a for loop - returns bool - scope_returns bool - is_builtin_mod bool // true inside the 'builtin', 'os' or 'strconv' modules; TODO: remove the need for special casing this - is_just_builtin_mod bool // true only inside 'builtin' - is_generated bool // true for `@[generated] module xyz` .v files - unresolved_fixed_sizes []&ast.Stmt // funcs with unresolved array fixed size e.g. fn func() [const1]int - inside_recheck bool // true when rechecking rhs assign statement - inside_unsafe bool // true inside `unsafe {}` blocks - inside_const bool // true inside `const ( ... )` blocks - inside_anon_fn bool // true inside `fn() { ... }()` - inside_lambda bool // true inside `|...| ...` - inside_ref_lit bool // true inside `a := &something` - inside_defer bool // true inside `defer {}` blocks - inside_return bool // true inside `return ...` blocks - inside_fn_arg bool // `a`, `b` in `a.f(b)` - 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 {} }` - 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)` - cur_struct_generic_types []ast.Type - cur_struct_concrete_types []ast.Type - anon_fn_generic_names []string - anon_fn_concrete_types []ast.Type - skip_flags bool // should `#flag` and `#include` be skipped - fn_level int // 0 for the top level, 1 for `fn abc() {}`, 2 for a nested fn, etc - smartcast_mut_pos token.Pos // match mut foo, if mut foo is Foo - smartcast_cond_pos token.Pos // match cond - ct_cond_stack []ast.Expr - ct_user_defines map[string]bool - ct_system_defines map[string]bool - cur_ct_id int // id counter for $if $match branches + expected_type ast.Type + expected_or_type ast.Type // fn() or { 'this type' } eg. string. expected or block type + expected_expr_type ast.Type // if/match is_expr: expected_type + mod string // current module name + has_globals_in_module bool // true if the current module has @[has_globals] attribute + strict_map_index_in_module bool // true if the current module has @[strict_map_index] attribute + const_var &ast.ConstField = unsafe { nil } // the current constant, when checking const declarations + const_deps []string + const_eval_stack []string // names of constants currently being recursively resolved (to break cycles via anon fn bodies) + const_names []string + global_names []string + locked_names []string // vars that are currently locked + rlocked_names []string // vars that are currently read-locked + in_for_count int // if checker is currently in a for loop + returns bool + scope_returns bool + is_builtin_mod bool // true inside the 'builtin', 'os' or 'strconv' modules; TODO: remove the need for special casing this + is_just_builtin_mod bool // true only inside 'builtin' + is_generated bool // true for `@[generated] module xyz` .v files + unresolved_fixed_sizes []&ast.Stmt // funcs with unresolved array fixed size e.g. fn func() [const1]int + inside_recheck bool // true when rechecking rhs assign statement + inside_unsafe bool // true inside `unsafe {}` blocks + inside_const bool // true inside `const ( ... )` blocks + inside_anon_fn bool // true inside `fn() { ... }()` + inside_lambda bool // true inside `|...| ...` + inside_ref_lit bool // true inside `a := &something` + inside_defer bool // true inside `defer {}` blocks + inside_return bool // true inside `return ...` blocks + inside_fn_arg bool // `a`, `b` in `a.f(b)` + 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_array_init_value_elem bool // true when checking a value `match`/`if` array element, e.g. `[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)` + cur_struct_generic_types []ast.Type + cur_struct_concrete_types []ast.Type + anon_fn_generic_names []string + anon_fn_concrete_types []ast.Type + skip_flags bool // should `#flag` and `#include` be skipped + fn_level int // 0 for the top level, 1 for `fn abc() {}`, 2 for a nested fn, etc + smartcast_mut_pos token.Pos // match mut foo, if mut foo is Foo + smartcast_cond_pos token.Pos // match cond + ct_cond_stack []ast.Expr + ct_user_defines map[string]bool + ct_system_defines map[string]bool + cur_ct_id int // id counter for $if $match branches mut: stmt_level int // the nesting level inside each stmts list; // .stmt_level is used to check for `evaluated but not used` ExprStmts like `1 << 1` diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index 44ea73e9a0e64d..42607f36621ea4 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -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_array_init_value_elem + && operand_is_value_match_or_if(expr) { + c.inside_array_init_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_array_init_value_elem = false + } sym := c.table.sym(expected_value_type) if sym.kind == .interface { c.type_implements(typ, expected_value_type, expr.pos()) diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index e3ad0bd4edb452..9d2eb764ec645f 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -122,6 +122,10 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type { node_is_expr = true } else if node.is_expr { node_is_expr = true + } else if c.inside_array_init_value_elem { + // a value `if` used as an array element in a void context, e.g. + // `[if cond { a } else { b }]`, must be treated as an expression. + node_is_expr = true } } if c.expected_type == ast.void_type && node_is_expr { diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index 64fa8a376f5266..956c11b044b8b7 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -7,7 +7,10 @@ import strings fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type { if !node.is_comptime { - node.is_expr = c.expected_type != ast.void_type + // `c.inside_array_init_value_elem` marks a value match used as an array + // element in a void context (`[match x { .. }]`), which must be treated as + // an expression even though the surrounding expected type is void. + node.is_expr = c.expected_type != ast.void_type || c.inside_array_init_value_elem } node.expected_type = c.expected_type if mut node.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 705adc6ea82f77..fc2eeea153bc61 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -621,6 +621,17 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.left) || p.expr_contains_value_match_or_if(expr.right) } + ast.ArrayInit { + // e.g. `[match value { .. }]` as a call argument. + mut found := false + for element in expr.exprs { + if p.expr_contains_value_match_or_if(element) { + found = true + break + } + } + found + } else { false } @@ -653,6 +664,15 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } } + ast.ArrayInit { + // last stmt on block is an array literal, e.g. `[match value { .. }]`; + // mark any element that is (or nests) a block-value match/if. + for mut element in expr.exprs { + if p.expr_contains_value_match_or_if(element) { + p.mark_last_call_expr_return_as_used(mut element) + } + } + } ast.ParExpr { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 04c17f735532fd..3f206907edf94e 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -167,6 +167,21 @@ fn select_value_callarg_infix(node ?Node) !int { return result } +// match as an array-literal element: `[match .. { .. }]` +fn select_value_arraylit(node ?Node) ![]int { + result := if value := node { + [ + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }, + ] + } else { + [0] + } + return result +} + struct Circle { r int } @@ -313,6 +328,12 @@ fn test_call_argument_infix_match_as_if_expr_value_with_propagation() { assert select_value_callarg_infix(none) or { -1 } == 0 } +fn test_array_literal_match_as_if_expr_value_with_propagation() { + assert select_value_arraylit(First{})! == [1] + assert select_value_arraylit(Second{})! == [2] + assert select_value_arraylit(none) or { [-1] } == [0] +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 6ba087d88e8833..fc7d9baa3ba9c3 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -160,6 +160,18 @@ fn select_value_callarg_infix(node ?Node) !int { return result } +fn select_value_arraylit(node ?Node) ![]int { + result := if value := node { + [match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }] + } else { + [0] + } + return result +} + struct Circle { r int } @@ -219,6 +231,7 @@ fn main() { println(select_value_callarg(Second{})!) println(select_value_nested_callarg(First{})!) println(select_value_callarg_infix(First{})!) + println(select_value_arraylit(First{})!) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -234,5 +247,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n6\n6\n2' } From 353a90ae3e82174e138e1673c52f2b77207a8d81 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 11:23:00 +0300 Subject: [PATCH 11/20] parser, checker: handle struct-field match values and scope the array-value flag (#28000) Addresses two PR review comments: 1. Struct-field composition: an assignment-RHS branch ending in a struct literal whose field is a value match/if, e.g. `Holder{ value: match node { First { lower_first(node)! } ... } }`, kept the match-arm calls return-unused and could emit the empty C assignment. Add an `ast.StructInit` case to `mark_last_call_expr_return_as_used` (recurse through `init_fields`) and to `expr_contains_value_match_or_if` (so struct literals nested in call arguments are covered). The checker already supplies the field type, so no checker change is needed here. 2. Array-value flag scoping: `inside_array_init_value_elem` stayed set for the whole recursive `c.expr` of a `[match ...]` element, so a nested *statement-level* match/if inside an arm (valid with a void/empty branch) was wrongly reclassified as a value expression ("requires an expression as the last statement of every branch"). `match_expr`/`if_expr` now capture and immediately clear the flag, so it applies only to the outer element node. The v3 backend already handled both; regression tests are added there too. Cover `Holder{ value: match ... }` and an array element whose arm holds a nested statement match/if in both regression tests. compiler_errors_test plus the struct/array/match/if suites are unchanged. --- vlib/v/checker/if.v | 9 ++- vlib/v/checker/match.v | 12 ++-- vlib/v/parser/parser.v | 21 ++++++ ...h_as_if_expr_value_with_propagation_test.v | 69 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 23 ++++++- 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index 9d2eb764ec645f..d41b22369dece6 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -115,6 +115,11 @@ 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 array-element value flag so it applies only to this outer node, + // not to nested statement-level match/if inside the branches. `[if ... ]` as a + // value array element in a void context must still be treated as an expression. + is_array_init_value_elem := c.inside_array_init_value_elem + c.inside_array_init_value_elem = false mut node_is_expr := false if node.branches.len > 0 && node.has_else { stmts := node.branches[0].stmts @@ -122,9 +127,7 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type { node_is_expr = true } else if node.is_expr { node_is_expr = true - } else if c.inside_array_init_value_elem { - // a value `if` used as an array element in a void context, e.g. - // `[if cond { a } else { b }]`, must be treated as an expression. + } else if is_array_init_value_elem { node_is_expr = true } } diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index 956c11b044b8b7..26acea9c704af7 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -6,11 +6,15 @@ import v.token import strings fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type { + // `c.inside_array_init_value_elem` marks a value match used as an array element + // in a void context (`[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_array_init_value_elem := c.inside_array_init_value_elem + c.inside_array_init_value_elem = false if !node.is_comptime { - // `c.inside_array_init_value_elem` marks a value match used as an array - // element in a void context (`[match x { .. }]`), which must be treated as - // an expression even though the surrounding expected type is void. - node.is_expr = c.expected_type != ast.void_type || c.inside_array_init_value_elem + node.is_expr = c.expected_type != ast.void_type || is_array_init_value_elem } node.expected_type = c.expected_type if mut node.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index fc2eeea153bc61..76d1c9b58d2f17 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -632,6 +632,17 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { } found } + ast.StructInit { + // e.g. `Holder{ value: match value { .. } }` as a call argument. + mut found := false + for field in expr.init_fields { + if p.expr_contains_value_match_or_if(field.expr) { + found = true + break + } + } + found + } else { false } @@ -673,6 +684,16 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } } + ast.StructInit { + // last stmt on block is a struct literal, e.g. + // `Holder{ value: match value { .. } }`; mark any field whose value + // is (or nests) a block-value match/if. + for mut field in expr.init_fields { + if p.expr_contains_value_match_or_if(field.expr) { + p.mark_last_call_expr_return_as_used(mut field.expr) + } + } + } ast.ParExpr { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 3f206907edf94e..f070de98b32060 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -182,6 +182,59 @@ fn select_value_arraylit(node ?Node) ![]int { return result } +fn side_effect() int { + return 5 +} + +// an array-element match whose arm contains a nested *statement* match/if (with a +// void/empty branch) before the propagated value. The value-element flag must not +// leak into the nested statement, which is valid as a statement, not an expression. +fn select_value_arraylit_nested_stmt(node ?Node, cond bool) ![]int { + result := if value := node { + [ + match value { + First { + match cond { + true { side_effect() } + else {} + } + if cond { + side_effect() + } + lower_first(value)! + } + Second { + lower_second(value)! + } + }, + ] + } else { + [0] + } + return result +} + +struct Holder { + value int + other int +} + +// match as a struct-literal field value: `Holder{ value: match .. { .. } }` +fn select_value_structinit(node ?Node) !Holder { + result := if value := node { + Holder{ + value: match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + other: 100 + } + } else { + Holder{} + } + return result +} + struct Circle { r int } @@ -334,6 +387,22 @@ fn test_array_literal_match_as_if_expr_value_with_propagation() { assert select_value_arraylit(none) or { [-1] } == [0] } +fn test_array_literal_match_with_nested_statement_match() { + assert select_value_arraylit_nested_stmt(First{}, true)! == [1] + assert select_value_arraylit_nested_stmt(Second{}, false)! == [2] + assert select_value_arraylit_nested_stmt(none, true) or { [-1] } == [0] +} + +fn test_struct_init_field_match_as_if_expr_value_with_propagation() { + assert select_value_structinit(First{})!.value == 1 + assert select_value_structinit(Second{})!.value == 2 + assert select_value_structinit(none) or { + Holder{ + value: -1 + } + }.value == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index fc7d9baa3ba9c3..30c55a4c91fc44 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -172,6 +172,26 @@ fn select_value_arraylit(node ?Node) ![]int { return result } +struct Holder { + value int + other int +} + +fn select_value_structinit(node ?Node) !Holder { + result := if value := node { + Holder{ + value: match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + other: 100 + } + } else { + Holder{} + } + return result +} + struct Circle { r int } @@ -232,6 +252,7 @@ fn main() { println(select_value_nested_callarg(First{})!) println(select_value_callarg_infix(First{})!) println(select_value_arraylit(First{})!) + println(select_value_structinit(First{})!.value) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -247,5 +268,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n1\n6\n6\n2' } From 23236ba7ff6c6adb58f00e3033906a71f397405b Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 11:39:35 +0300 Subject: [PATCH 12/20] parser, checker: handle map-literal match/if block values with propagation (#28000) Address PR review: an assignment-RHS branch ending in a map literal whose value is a value match/if, e.g. `{'value': match value { First { lower_first(value)! } ... }}`, was rejected and could recreate the invalid empty C. Two fixes: - checker: in a void context (nested in an if-branch) the map value match/if was checked with a void expected type and mistyped as a statement (`map[string]void`). As with array elements, an expected-type override mistypes the arms, so the existing value-element flag (renamed `inside_array_init_value_elem` -> `inside_container_value_elem`) is now also set around the first map value check in `map_init` and honored by `match_expr`/`if_expr` (which already consume it so nested statement match/if are unaffected). - parser: added an `ast.MapInit` case to `mark_last_call_expr_return_as_used` (recurse through keys and values) and to `expr_contains_value_match_or_if` (so map literals nested in call arguments are covered). The v3 backend already handled map values; a regression test is added there too. Cover `{'value': match ...}` in both regression tests. compiler_errors_test plus the map/array/match/if suites are unchanged. --- vlib/v/checker/checker.v | 96 +++++++++---------- vlib/v/checker/containers.v | 21 +++- vlib/v/checker/if.v | 13 +-- vlib/v/checker/match.v | 16 ++-- vlib/v/parser/parser.v | 34 +++++++ ...h_as_if_expr_value_with_propagation_test.v | 27 ++++++ ...s_if_expr_value_propagation_codegen_test.v | 19 +++- 7 files changed, 160 insertions(+), 66 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index dcc3c87d63886a..263114c14a71c5 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -76,54 +76,54 @@ pub mut: error_details []string should_abort bool // when too many errors are accumulated, .should_abort becomes true. It is checked in statement/expression loops, so the checker can return early, instead of wasting time. - expected_type ast.Type - expected_or_type ast.Type // fn() or { 'this type' } eg. string. expected or block type - expected_expr_type ast.Type // if/match is_expr: expected_type - mod string // current module name - has_globals_in_module bool // true if the current module has @[has_globals] attribute - strict_map_index_in_module bool // true if the current module has @[strict_map_index] attribute - const_var &ast.ConstField = unsafe { nil } // the current constant, when checking const declarations - const_deps []string - const_eval_stack []string // names of constants currently being recursively resolved (to break cycles via anon fn bodies) - const_names []string - global_names []string - locked_names []string // vars that are currently locked - rlocked_names []string // vars that are currently read-locked - in_for_count int // if checker is currently in a for loop - returns bool - scope_returns bool - is_builtin_mod bool // true inside the 'builtin', 'os' or 'strconv' modules; TODO: remove the need for special casing this - is_just_builtin_mod bool // true only inside 'builtin' - is_generated bool // true for `@[generated] module xyz` .v files - unresolved_fixed_sizes []&ast.Stmt // funcs with unresolved array fixed size e.g. fn func() [const1]int - inside_recheck bool // true when rechecking rhs assign statement - inside_unsafe bool // true inside `unsafe {}` blocks - inside_const bool // true inside `const ( ... )` blocks - inside_anon_fn bool // true inside `fn() { ... }()` - inside_lambda bool // true inside `|...| ...` - inside_ref_lit bool // true inside `a := &something` - inside_defer bool // true inside `defer {}` blocks - inside_return bool // true inside `return ...` blocks - inside_fn_arg bool // `a`, `b` in `a.f(b)` - 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_array_init_value_elem bool // true when checking a value `match`/`if` array element, e.g. `[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)` - cur_struct_generic_types []ast.Type - cur_struct_concrete_types []ast.Type - anon_fn_generic_names []string - anon_fn_concrete_types []ast.Type - skip_flags bool // should `#flag` and `#include` be skipped - fn_level int // 0 for the top level, 1 for `fn abc() {}`, 2 for a nested fn, etc - smartcast_mut_pos token.Pos // match mut foo, if mut foo is Foo - smartcast_cond_pos token.Pos // match cond - ct_cond_stack []ast.Expr - ct_user_defines map[string]bool - ct_system_defines map[string]bool - cur_ct_id int // id counter for $if $match branches + expected_type ast.Type + expected_or_type ast.Type // fn() or { 'this type' } eg. string. expected or block type + expected_expr_type ast.Type // if/match is_expr: expected_type + mod string // current module name + has_globals_in_module bool // true if the current module has @[has_globals] attribute + strict_map_index_in_module bool // true if the current module has @[strict_map_index] attribute + const_var &ast.ConstField = unsafe { nil } // the current constant, when checking const declarations + const_deps []string + const_eval_stack []string // names of constants currently being recursively resolved (to break cycles via anon fn bodies) + const_names []string + global_names []string + locked_names []string // vars that are currently locked + rlocked_names []string // vars that are currently read-locked + in_for_count int // if checker is currently in a for loop + returns bool + scope_returns bool + is_builtin_mod bool // true inside the 'builtin', 'os' or 'strconv' modules; TODO: remove the need for special casing this + is_just_builtin_mod bool // true only inside 'builtin' + is_generated bool // true for `@[generated] module xyz` .v files + unresolved_fixed_sizes []&ast.Stmt // funcs with unresolved array fixed size e.g. fn func() [const1]int + inside_recheck bool // true when rechecking rhs assign statement + inside_unsafe bool // true inside `unsafe {}` blocks + inside_const bool // true inside `const ( ... )` blocks + inside_anon_fn bool // true inside `fn() { ... }()` + inside_lambda bool // true inside `|...| ...` + inside_ref_lit bool // true inside `a := &something` + inside_defer bool // true inside `defer {}` blocks + inside_return bool // true inside `return ...` blocks + inside_fn_arg bool // `a`, `b` in `a.f(b)` + 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)` + cur_struct_generic_types []ast.Type + cur_struct_concrete_types []ast.Type + anon_fn_generic_names []string + anon_fn_concrete_types []ast.Type + skip_flags bool // should `#flag` and `#include` be skipped + fn_level int // 0 for the top level, 1 for `fn abc() {}`, 2 for a nested fn, etc + smartcast_mut_pos token.Pos // match mut foo, if mut foo is Foo + smartcast_cond_pos token.Pos // match cond + ct_cond_stack []ast.Expr + ct_user_defines map[string]bool + ct_system_defines map[string]bool + cur_ct_id int // id counter for $if $match branches mut: stmt_level int // the nesting level inside each stmts list; // .stmt_level is used to check for `evaluated but not used` ExprStmts like `1 << 1` diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index 42607f36621ea4..f6d98b637f5685 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -514,14 +514,14 @@ fn (mut c Checker) array_init(mut node ast.ArrayInit) ast.Type { // 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_array_init_value_elem + if c.expected_type == ast.void_type && !c.inside_container_value_elem && operand_is_value_match_or_if(expr) { - c.inside_array_init_value_elem = true + 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_array_init_value_elem = false + c.inside_container_value_elem = false } sym := c.table.sym(expected_value_type) if sym.kind == .interface { @@ -954,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_) { + 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() } diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index d41b22369dece6..92308c4b194622 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -115,11 +115,12 @@ 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 array-element value flag so it applies only to this outer node, - // not to nested statement-level match/if inside the branches. `[if ... ]` as a - // value array element in a void context must still be treated as an expression. - is_array_init_value_elem := c.inside_array_init_value_elem - c.inside_array_init_value_elem = false + // 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 @@ -127,7 +128,7 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type { node_is_expr = true } else if node.is_expr { node_is_expr = true - } else if is_array_init_value_elem { + } else if is_container_value_elem { node_is_expr = true } } diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index 26acea9c704af7..451f415bd775b5 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -6,15 +6,15 @@ import v.token import strings fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type { - // `c.inside_array_init_value_elem` marks a value match used as an array element - // in a void context (`[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_array_init_value_elem := c.inside_array_init_value_elem - c.inside_array_init_value_elem = false + // `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 || is_array_init_value_elem + 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 { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 76d1c9b58d2f17..67bf1568a3ebbc 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -643,6 +643,25 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { } found } + ast.MapInit { + // e.g. `{'value': match value { .. }}` as a call argument. + mut found := false + for element in expr.keys { + if p.expr_contains_value_match_or_if(element) { + found = true + break + } + } + if !found { + for element in expr.vals { + if p.expr_contains_value_match_or_if(element) { + found = true + break + } + } + } + found + } else { false } @@ -694,6 +713,21 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } } + ast.MapInit { + // last stmt on block is a map literal, e.g. + // `{'value': match value { .. }}`; mark any key/value that is (or + // nests) a block-value match/if. + for mut element in expr.keys { + if p.expr_contains_value_match_or_if(element) { + p.mark_last_call_expr_return_as_used(mut element) + } + } + for mut element in expr.vals { + if p.expr_contains_value_match_or_if(element) { + p.mark_last_call_expr_return_as_used(mut element) + } + } + } ast.ParExpr { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index f070de98b32060..ad25695112f747 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -235,6 +235,23 @@ fn select_value_structinit(node ?Node) !Holder { return result } +// match as a map-literal value: `{'value': match .. { .. }}` +fn select_value_mapinit(node ?Node) !map[string]int { + result := if value := node { + { + 'value': match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + } + } else { + { + 'value': 0 + } + } + return result +} + struct Circle { r int } @@ -403,6 +420,16 @@ fn test_struct_init_field_match_as_if_expr_value_with_propagation() { }.value == 0 } +fn test_map_init_value_match_as_if_expr_value_with_propagation() { + assert select_value_mapinit(First{})!['value'] == 1 + assert select_value_mapinit(Second{})!['value'] == 2 + assert (select_value_mapinit(none) or { + { + 'value': -1 + } + })['value'] == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 30c55a4c91fc44..c31178634eb515 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -177,6 +177,22 @@ struct Holder { other int } +fn select_value_mapinit(node ?Node) !map[int]int { + result := if value := node { + { + 7: match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + } + } else { + { + 7: 0 + } + } + return result +} + fn select_value_structinit(node ?Node) !Holder { result := if value := node { Holder{ @@ -253,6 +269,7 @@ fn main() { println(select_value_callarg_infix(First{})!) println(select_value_arraylit(First{})!) println(select_value_structinit(First{})!.value) + println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) @@ -268,5 +285,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n1\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n1\n2\n6\n6\n2' } From 92255064720dd10f12de0b8c9cc1622bfc922ef4 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 11:53:57 +0300 Subject: [PATCH 13/20] parser, checker, v3: handle prefix-expression match/if block values with propagation (#28000) Address PR review: an assignment-RHS branch ending in a prefix expression whose operand is a value match/if, e.g. `-(match value { First { lower_first(value)! } ... })`, was rejected ("value after `-` is of type `void`") and, on v3, reproduced the empty-value codegen. Three fixes: - vlib/v parser: `mark_last_call_expr_return_as_used` grouped `PrefixExpr` with the or-block-only cases and never visited `expr.right`. Split it out to recurse into the operand, and recognize `PrefixExpr` in `expr_contains_value_match_or_if` (so prefixed values nested in call arguments are covered). - vlib/v 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. The flag (renamed `inside_container_value_elem` -> `force_value_match_or_if`, now covering array elements, map values and prefix operands) is 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`). Cover `-(match ...)` in both regression tests. Verified v3 self-hosts and compiler_errors_test plus the prefix/operator/match/array suites are unchanged. --- vlib/v/checker/checker.v | 14 +++++++++++++- vlib/v/checker/containers.v | 12 ++++++------ vlib/v/checker/if.v | 14 +++++++------- vlib/v/checker/match.v | 17 +++++++++-------- vlib/v/parser/parser.v | 15 ++++++++++++++- ...h_as_if_expr_value_with_propagation_test.v | 19 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 15 ++++++++++++++- vlib/v3/transform/transform.v | 19 +++++++++++-------- 8 files changed, 93 insertions(+), 32 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 263114c14a71c5..8241928a83c79e 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -108,7 +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 { .. }}` + 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)` @@ -7939,7 +7939,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 diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index f6d98b637f5685..b2c147c747ebee 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -514,14 +514,14 @@ fn (mut c Checker) array_init(mut node ast.ArrayInit) ast.Type { // 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 + if c.expected_type == ast.void_type && !c.force_value_match_or_if && operand_is_value_match_or_if(expr) { - c.inside_container_value_elem = true + 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.inside_container_value_elem = false + c.force_value_match_or_if = false } sym := c.table.sym(expected_value_type) if sym.kind == .interface { @@ -961,14 +961,14 @@ fn (mut c Checker) map_init(mut node ast.MapInit) ast.Type { // 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 + if c.expected_type == ast.void_type && !c.force_value_match_or_if && operand_is_value_match_or_if(val_) { - c.inside_container_value_elem = true + 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.inside_container_value_elem = false + c.force_value_match_or_if = false } if node.vals[0].is_auto_deref_var() { map_val_type = map_val_type.deref() diff --git a/vlib/v/checker/if.v b/vlib/v/checker/if.v index 92308c4b194622..1d6f1ab020aa4b 100644 --- a/vlib/v/checker/if.v +++ b/vlib/v/checker/if.v @@ -115,12 +115,12 @@ 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 + // 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 @@ -128,7 +128,7 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type { node_is_expr = true } else if node.is_expr { node_is_expr = true - } else if is_container_value_elem { + } else if force_value { node_is_expr = true } } diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index 451f415bd775b5..9d5dc840965eef 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -6,15 +6,16 @@ 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 + // `c.force_value_match_or_if` marks a value match used in a value-required + // position whose surrounding expected type is void (an array element + // `[match x { .. }]`, a map value `{'k': match x { .. }}`, or a prefix operand + // `-(match x { .. })`), which must be treated as an expression. Consume the + // flag here so it applies only to this outer node, not to nested + // statement-level match/if inside the arms. + force_value := c.force_value_match_or_if + c.force_value_match_or_if = false if !node.is_comptime { - node.is_expr = c.expected_type != ast.void_type || is_container_value_elem + node.is_expr = c.expected_type != ast.void_type || force_value } node.expected_type = c.expected_type if mut node.cond is ast.ParExpr && !c.pref.translated && !c.file.is_translated { diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 67bf1568a3ebbc..baf40f4a87f829 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -621,6 +621,10 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.left) || p.expr_contains_value_match_or_if(expr.right) } + ast.PrefixExpr { + // e.g. `-(match value { .. })` as a call argument. + p.expr_contains_value_match_or_if(expr.right) + } ast.ArrayInit { // e.g. `[match value { .. }]` as a call argument. mut found := false @@ -774,7 +778,16 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { 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 { + ast.PrefixExpr { + 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 is a prefix expr with a value operand, e.g. + // `-(match value { First { bar()! } })`; recurse into the operand. + p.mark_last_call_expr_return_as_used(mut expr.right) + } + ast.ComptimeCall, ast.ComptimeSelector, ast.SelectorExpr { 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) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index ad25695112f747..ed914eaa11323a 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -252,6 +252,19 @@ fn select_value_mapinit(node ?Node) !map[string]int { return result } +// match as a prefix-expression operand: `-(match .. { .. })` +fn select_value_prefix(node ?Node) !int { + result := if value := node { + -(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + struct Circle { r int } @@ -430,6 +443,12 @@ fn test_map_init_value_match_as_if_expr_value_with_propagation() { })['value'] == 0 } +fn test_prefix_operand_match_as_if_expr_value_with_propagation() { + assert select_value_prefix(First{})! == -1 + assert select_value_prefix(Second{})! == -2 + assert select_value_prefix(none) or { 42 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index c31178634eb515..72e5a0d9c1b5e0 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -172,6 +172,18 @@ fn select_value_arraylit(node ?Node) ![]int { return result } +fn select_value_prefix(node ?Node) !int { + result := if value := node { + -(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + struct Holder { value int other int @@ -268,6 +280,7 @@ fn main() { println(select_value_nested_callarg(First{})!) println(select_value_callarg_infix(First{})!) println(select_value_arraylit(First{})!) + println(select_value_prefix(First{})!) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -285,5 +298,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index bd0c050369061f..666a049ba35936 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14515,11 +14515,12 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f }) } -// transform_infix_operand transforms an infix operand, routing a value -// `match`/`if` operand (e.g. `1 + (match x { ... })`) through -// `transform_expr_for_type` so its (possibly propagating) branch tails are -// lowered as values instead of in a value-less statement context. -fn (mut t Transformer) transform_infix_operand(id flat.NodeId) flat.NodeId { +// transform_value_operand transforms an operand of an infix/prefix expression, +// routing a value `match`/`if` operand (e.g. `1 + (match x { ... })` or +// `-(match x { ... })`) through `transform_expr_for_type` so its (possibly +// propagating) branch tails are lowered as values instead of in a value-less +// statement context. +fn (mut t Transformer) transform_value_operand(id flat.NodeId) flat.NodeId { if t.is_value_match_or_if_operand(id) { mut typ := t.node_type(id) if typ.len == 0 { @@ -14638,13 +14639,13 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat lhs_id := t.a.children[node.children_start] rhs_id := t.a.children[node.children_start + 1] pending_start := t.pending_stmts.len - new_lhs := t.transform_infix_operand(lhs_id) + new_lhs := t.transform_value_operand(lhs_id) mut lhs_pending := []flat.NodeId{} if t.pending_stmts.len > pending_start { lhs_pending = t.pending_stmts[pending_start..].clone() t.pending_stmts = t.pending_stmts[..pending_start].clone() } - new_rhs := t.transform_infix_operand(rhs_id) + new_rhs := t.transform_value_operand(rhs_id) if lhs_pending.len > 0 { rhs_pending := t.pending_stmts[pending_start..].clone() t.pending_stmts = t.pending_stmts[..pending_start].clone() @@ -16651,7 +16652,9 @@ fn (mut t Transformer) transform_prefix_expr(id flat.NodeId, node flat.Node) fla mut new_child := if node.op == .not { t.transform_expr_for_type(child_id, 'bool') } else { - t.transform_expr(child_id) + // route a value `match`/`if` operand (e.g. `-(match x { ... })`) + // through its target type so its propagating arms are lowered as values. + t.transform_value_operand(child_id) } if node.op == .not { child := t.a.nodes[int(new_child)] From 70207435608bf75695545feafacbddd8e30978e5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 12:04:59 +0300 Subject: [PATCH 14/20] parser, checker, v3: handle index-expression match/if block values with propagation (#28000) Address PR review: an assignment-RHS branch ending in an index expression whose index is a value match/if, e.g. `values[match node { First { lower_first(node)! } ... }]`, was rejected ("non-integer index `void`") and, on v3, reproduced the empty-value codegen. Three fixes: - vlib/v parser: `ast.IndexExpr` fell through both `mark_last_call_expr_return_as_used` and `expr_contains_value_match_or_if`. Handle it in both, recursing through the indexed expression (`left`) and the index operand (and its or-block). - vlib/v checker: the array/string index match/if was evaluated with a void expected type and mistyped as `void`. `index_expr` now sets the value-required flag around the (non-map) index check; the map-index path already supplies the key type. The flag is consumed by `match_expr`/`if_expr`, so nested statement match/if are unaffected. - vlib/v3: `transform_index_expr` lowered operands with plain `transform_expr`; they now go through the shared `transform_value_operand` helper. Cover `values[match ...]` in both regression tests. Verified v3 self-hosts and compiler_errors_test plus the index/array/map/match suites are unchanged. --- vlib/v/checker/checker.v | 13 ++++++++++++ vlib/v/parser/parser.v | 21 +++++++++++++++++++ ...h_as_if_expr_value_with_propagation_test.v | 20 ++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 16 +++++++++++++- vlib/v3/transform/transform.v | 4 +++- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 8241928a83c79e..fe8ee5bcd5666b 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -8557,7 +8557,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', diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index baf40f4a87f829..248c44e78aa110 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -625,6 +625,12 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { // e.g. `-(match value { .. })` as a call argument. p.expr_contains_value_match_or_if(expr.right) } + ast.IndexExpr { + // e.g. `values[match value { .. }]` as a call argument. + + p.expr_contains_value_match_or_if(expr.left) + || p.expr_contains_value_match_or_if(expr.index) + } ast.ArrayInit { // e.g. `[match value { .. }]` as a call argument. mut found := false @@ -732,6 +738,21 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } } + ast.IndexExpr { + if expr.or_expr.stmts.len > 0 { + mut or_block_last_stmt := expr.or_expr.stmts.last() + p.mark_last_call_return_as_used(mut or_block_last_stmt) + } + // last stmt on block is an index expr, e.g. `values[match value { .. }]` + // or `(match value { .. })[0]`; mark the indexed expr and the index if + // either is (or nests) a block-value match/if. + if p.expr_contains_value_match_or_if(expr.left) { + p.mark_last_call_expr_return_as_used(mut expr.left) + } + if p.expr_contains_value_match_or_if(expr.index) { + p.mark_last_call_expr_return_as_used(mut expr.index) + } + } ast.ParExpr { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index ed914eaa11323a..605e7c827f39f3 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -265,6 +265,20 @@ fn select_value_prefix(node ?Node) !int { return result } +// match as an index-expression operand: `values[match .. { .. }]` +fn select_value_index(node ?Node) !string { + values := ['a', 'b', 'c'] + result := if value := node { + values[match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }] + } else { + 'x' + } + return result +} + struct Circle { r int } @@ -449,6 +463,12 @@ fn test_prefix_operand_match_as_if_expr_value_with_propagation() { assert select_value_prefix(none) or { 42 } == 0 } +fn test_index_operand_match_as_if_expr_value_with_propagation() { + assert select_value_index(First{})! == 'b' + assert select_value_index(Second{})! == 'c' + assert select_value_index(none) or { 'z' } == 'x' +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 72e5a0d9c1b5e0..cdf0d3704311bc 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -184,6 +184,19 @@ fn select_value_prefix(node ?Node) !int { return result } +fn select_value_index(node ?Node) !int { + values := [10, 20, 30] + result := if value := node { + values[match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }] + } else { + 0 + } + return result +} + struct Holder { value int other int @@ -281,6 +294,7 @@ fn main() { println(select_value_callarg_infix(First{})!) println(select_value_arraylit(First{})!) println(select_value_prefix(First{})!) + println(select_value_index(First{})!) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -298,5 +312,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 666a049ba35936..697f41569f099f 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15104,7 +15104,9 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat mut changed := false for i in 0 .. node.children_count { child_id := t.a.child(&node, i) - mut new_child := t.transform_expr(child_id) + // route a value `match`/`if` operand (e.g. `values[match x { ... }]`) + // through its target type so its propagating arms are lowered as values. + mut new_child := t.transform_value_operand(child_id) if i == 0 { base := t.a.nodes[int(new_child)] if base.kind == .cast_expr { From d61a5dba61aa19672cc0132c199aa37062751330 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 12:19:11 +0300 Subject: [PATCH 15/20] checker, parser, v3: value-context left-hand match for membership and selector receivers (#28000) Addresses two PR review comments: 1. Infix membership left operand: a value match/if on the left of a membership operator, e.g. `(match x { First { lower_first(x)! } ... }) in [1, 2]`, was handled by checking the right operand first and imposing its full type (`[]int`) as the match's expected type. Concrete `int` arms happened to work, but context-dependent arms (e.g. enum shorthand `.red`) were rejected as needing the container type. `infix_expr` now forces the left match to be a value via the `force_value_match_or_if` flag (arms infer their own type) instead of imposing the right operand's container type. Fixes `in`, `!in`, and map membership. 2. Selector receiver: a value match/if selector receiver, e.g. `(match x { ... }).field`, was left return-unused and typed `void`. - vlib/v parser: split `SelectorExpr` out of the or-block-only group in `mark_last_call_expr_return_as_used` to recurse into the receiver, and added it to `expr_contains_value_match_or_if`. - vlib/v checker: `selector_expr` sets the value-required flag around the receiver check. - vlib/v3: `transform_selector_base_expr` routes a non-ident receiver through the shared `transform_value_operand` helper. Cover `(match ...) in [1, 2]` and `(match ...).field` in both regression tests. Verified v3 self-hosts and compiler_errors_test plus the selector/infix/enum/ membership suites are unchanged. --- vlib/v/checker/checker.v | 12 +++++ vlib/v/checker/infix.v | 24 ++++++---- vlib/v/parser/parser.v | 17 ++++++- ...h_as_if_expr_value_with_propagation_test.v | 46 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 36 ++++++++++++++- vlib/v3/transform/transform.v | 4 +- 6 files changed, 127 insertions(+), 12 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index fe8ee5bcd5666b..4c4eac7b08c4fc 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -3081,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] diff --git a/vlib/v/checker/infix.v b/vlib/v/checker/infix.v index e2e6c1518857a3..4172053c2409dc 100644 --- a/vlib/v/checker/infix.v +++ b/vlib/v/checker/infix.v @@ -125,15 +125,6 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } } } - if !check_right_type_first && c.expected_type == ast.void_type - && operand_is_value_match_or_if(node.left) { - // 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 { right_type = c.expr(mut node.right) @@ -144,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 diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 248c44e78aa110..2cd3bd5b45aa8c 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -631,6 +631,10 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.left) || p.expr_contains_value_match_or_if(expr.index) } + ast.SelectorExpr { + // e.g. `(match value { .. }).field` as a call argument. + p.expr_contains_value_match_or_if(expr.expr) + } ast.ArrayInit { // e.g. `[match value { .. }]` as a call argument. mut found := false @@ -808,7 +812,18 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { // `-(match value { First { bar()! } })`; recurse into the operand. p.mark_last_call_expr_return_as_used(mut expr.right) } - ast.ComptimeCall, ast.ComptimeSelector, ast.SelectorExpr { + ast.SelectorExpr { + 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 on block is a selector, e.g. `(match value { .. }).field`; + // recurse into the receiver if it is (or nests) a block-value match/if. + if p.expr_contains_value_match_or_if(expr.expr) { + p.mark_last_call_expr_return_as_used(mut expr.expr) + } + } + ast.ComptimeCall, ast.ComptimeSelector { 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) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 605e7c827f39f3..e986978fd9657b 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -279,6 +279,40 @@ fn select_value_index(node ?Node) !string { return result } +// match on the left of a membership operator: `(match .. { .. }) in [1, 2]` +fn select_value_membership(node ?Node) !bool { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) in [1, 2] + } else { + false + } + return result +} + +struct Boxed { + value int +} + +fn boxed(v int) Boxed { + return Boxed{v} +} + +// match as a selector receiver: `(match .. { .. }).value` +fn select_value_selector(node ?Node) !int { + result := if value := node { + (match value { + First { boxed(lower_first(value)!) } + Second { boxed(lower_second(value)!) } + }).value + } else { + 0 + } + return result +} + struct Circle { r int } @@ -469,6 +503,18 @@ fn test_index_operand_match_as_if_expr_value_with_propagation() { assert select_value_index(none) or { 'z' } == 'x' } +fn test_membership_operand_match_as_if_expr_value_with_propagation() { + assert select_value_membership(First{})! == true + assert select_value_membership(Second{})! == true + assert select_value_membership(none) or { true } == false +} + +fn test_selector_receiver_match_as_if_expr_value_with_propagation() { + assert select_value_selector(First{})! == 1 + assert select_value_selector(Second{})! == 2 + assert select_value_selector(none) or { -1 } == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index cdf0d3704311bc..877f1a96bce718 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -197,6 +197,38 @@ fn select_value_index(node ?Node) !int { return result } +fn select_value_membership(node ?Node) !bool { + result := if value := node { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) in [1, 2] + } else { + false + } + return result +} + +struct Boxed { + value int +} + +fn boxed(v int) Boxed { + return Boxed{v} +} + +fn select_value_selector(node ?Node) !int { + result := if value := node { + (match value { + First { boxed(lower_first(value)!) } + Second { boxed(lower_second(value)!) } + }).value + } else { + 0 + } + return result +} + struct Holder { value int other int @@ -295,6 +327,8 @@ fn main() { println(select_value_arraylit(First{})!) println(select_value_prefix(First{})!) println(select_value_index(First{})!) + println(select_value_membership(First{})!) + println(select_value_selector(First{})!) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -312,5 +346,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 697f41569f099f..47b1f8b24fa3e3 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15609,7 +15609,9 @@ fn (mut t Transformer) transform_selector_base_expr(id flat.NodeId) flat.NodeId // transparent parentheses (`(x).field`, `((x)).field`), where `x` is still the // direct receiver. if !t.selector_base_is_ident_receiver(id) { - return t.transform_expr(id) + // route a value `match`/`if` receiver (e.g. `(match x { ... }).field`) + // through its target type so its propagating arms are lowered as values. + return t.transform_value_operand(id) } old_in_selector_base := t.in_selector_base t.in_selector_base = true From be7a3930dc547822b8789f3243a5454b5ff2bb58 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 12:43:01 +0300 Subject: [PATCH 16/20] parser, checker: value-context map keys and array spreads for match/if block values (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two PR review comments: 1. Inferred map first key: `{(match value { First { lower_first(value)! } ... }): 'x'}` as an if-branch value failed at parse time — the `{ (` was misparsed as a block (the map-vs-block heuristic only checks `peek_token(2) == colon`, which a parenthesized key pushes past the `)`), so `(match ...)` became a discarded statement ("expression evaluated but not used"). The `stmt` `.lcbr` heuristic now scans to the matching `)` and treats it as a map when a `:` follows. With that, `map_init` forces the first key match/if to be a value (same flag as the value path), so it is not mistyped as a void key. 2. Array spread operand: `[...(match value { First { lower_first_array(value)! } ... })]` stores the match in `ArrayInit.update_expr`, which the parser traversal and the container checker ignored. `mark_last_call_expr_return_as_used` and `expr_contains_value_match_or_if` now include `update_expr`, and `array_init` forces the spread operand to be a value. Cover `{(match ...): v}` and `[...(match ...)]` in the vlib/v regression test (and the map-key spelling in the v3 test). compiler_errors_test plus the parser and map/array/block suites are unchanged. --- vlib/v/checker/containers.v | 26 ++++++++- vlib/v/parser/parser.v | 46 ++++++++++++---- ...h_as_if_expr_value_with_propagation_test.v | 54 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 15 +++++- 4 files changed, 130 insertions(+), 11 deletions(-) diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index b2c147c747ebee..c0e593ce3f37a4 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -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) @@ -949,7 +961,19 @@ 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() } diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 2cd3bd5b45aa8c..dd4c9698c93e47 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -636,12 +636,14 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.expr) } ast.ArrayInit { - // e.g. `[match value { .. }]` as a call argument. - mut found := false - for element in expr.exprs { - if p.expr_contains_value_match_or_if(element) { - found = true - break + // e.g. `[match value { .. }]` or `[...(match value { .. })]`. + mut found := expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) + if !found { + for element in expr.exprs { + if p.expr_contains_value_match_or_if(element) { + found = true + break + } } } found @@ -709,13 +711,17 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } ast.ArrayInit { - // last stmt on block is an array literal, e.g. `[match value { .. }]`; - // mark any element that is (or nests) a block-value match/if. + // last stmt on block is an array literal, e.g. `[match value { .. }]` + // or a spread `[...(match value { .. })]`; mark any element or the + // spread operand that is (or nests) a block-value match/if. for mut element in expr.exprs { if p.expr_contains_value_match_or_if(element) { p.mark_last_call_expr_return_as_used(mut element) } } + if expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) { + p.mark_last_call_expr_return_as_used(mut expr.update_expr) + } } ast.StructInit { // last stmt on block is a struct literal, e.g. @@ -1342,7 +1348,29 @@ fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { match p.tok.kind { .lcbr { mut pos := p.tok.pos() - if p.peek_token(2).kind == .colon { + mut is_map_lit := p.peek_token(2).kind == .colon + if !is_map_lit && p.peek_tok.kind == .lpar { + // A parenthesized first key, e.g. `{ (match x { .. }) : v }`, puts the + // `:` past the closing `)`, so the single-token `peek_token(2)` check + // above misses it and the `{` would be misparsed as a block. Scan to + // the matching `)` and treat it as a map literal when a `:` follows. + mut depth := 1 + for n := 2; true; n++ { + kind := p.peek_token(n).kind + if kind == .eof { + break + } else if kind == .lpar { + depth++ + } else if kind == .rpar { + depth-- + if depth == 0 { + is_map_lit = p.peek_token(n + 1).kind == .colon + break + } + } + } + } + if is_map_lit { expr := p.expr(0) // `{ 'abc' : 22 }` return ast.ExprStmt{ diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index e986978fd9657b..a6fad36e6bb30e 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -313,6 +313,44 @@ fn select_value_selector(node ?Node) !int { return result } +// match as the first (parenthesized) key of an inferred map: `{(match ..): v}` +fn select_value_mapkey(node ?Node) !map[int]string { + result := if value := node { + { + (match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }): 'x' + } + } else { + { + 0: 'x' + } + } + return result +} + +fn lower_first_array(_ First) ![]int { + return [1, 1] +} + +fn lower_second_array(_ Second) ![]int { + return [2, 2] +} + +// match as an array spread operand: `[...(match .. { .. })]` +fn select_value_spread(node ?Node) ![]int { + result := if value := node { + [...(match value { + First { lower_first_array(value)! } + Second { lower_second_array(value)! } + })] + } else { + [0] + } + return result +} + struct Circle { r int } @@ -515,6 +553,22 @@ fn test_selector_receiver_match_as_if_expr_value_with_propagation() { assert select_value_selector(none) or { -1 } == 0 } +fn test_map_key_match_as_if_expr_value_with_propagation() { + assert select_value_mapkey(First{})![1] == 'x' + assert select_value_mapkey(Second{})![2] == 'x' + assert (select_value_mapkey(none) or { + { + 9: 'z' + } + })[0] == 'x' +} + +fn test_array_spread_match_as_if_expr_value_with_propagation() { + assert select_value_spread(First{})! == [1, 1] + assert select_value_spread(Second{})! == [2, 2] + assert select_value_spread(none) or { [-1] } == [0] +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 877f1a96bce718..5f693162f65e83 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -209,6 +209,18 @@ fn select_value_membership(node ?Node) !bool { return result } +fn select_value_mapkey(node ?Node) !map[int]int { + result := if value := node { + {(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }): 100} + } else { + {0: 100} + } + return result +} + struct Boxed { value int } @@ -329,6 +341,7 @@ fn main() { println(select_value_index(First{})!) println(select_value_membership(First{})!) println(select_value_selector(First{})!) + println(select_value_mapkey(First{})![1]) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -346,5 +359,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\n1\n2\n6\n6\n2' } From 6bb72343778ef8f6798e11e5014812993bd9064b Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 13:03:04 +0300 Subject: [PATCH 17/20] parser, checker, v3: struct/map update and string-interp match/if block values (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses three PR review comments: 1. Struct update operand: `Holder{ ...(match value { First { make_first(value)! } ... }), other: 1 }` — `StructInit.update_expr` was omitted from both parser traversals. Included in `mark_last_call_expr_return_as_used` and `expr_contains_value_match_or_if` (the checker already gives the operand the struct's value context). 2. Map update operand: `{ ...(match value { First { map_first(value)! } ... }), 'k': v }` — first misparsed: `{ ...` at statement position was treated as a block. The `stmt` `.lcbr` heuristic now recognizes a leading `...` (a block cannot start with it) as a map literal. `MapInit.update_expr` is included in both parser traversals, and `map_init` forces the update operand to be a value. 3. String interpolation operand: `'x=${match value { First { stringify_first(value)! } ... }}'` — `ast.StringInterLiteral` fell through both parser traversals; now recurses through `exprs`. (The checker already types the interpolation operand.) v3: the string-interpolation operand is now routed through the shared `transform_value_operand` helper. (v3's struct/map update lower a match operand incorrectly even in the direct form — a separate pre-existing v3 gap — so the v3 regression covers the interpolation spelling.) Cover `Holder{...(match)}`, `{...(match), k: v}`, and `'${match ...}'` in the vlib/v regression test (and interpolation in the v3 test). compiler_errors_test plus the parser and map/struct/string suites are unchanged. --- vlib/v/checker/containers.v | 12 +++ vlib/v/parser/parser.v | 71 +++++++++---- ...h_as_if_expr_value_with_propagation_test.v | 99 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 15 ++- vlib/v3/transform/transform.v | 4 +- 5 files changed, 182 insertions(+), 19 deletions(-) diff --git a/vlib/v/checker/containers.v b/vlib/v/checker/containers.v index c0e593ce3f37a4..842eaf35109f0e 100644 --- a/vlib/v/checker/containers.v +++ b/vlib/v/checker/containers.v @@ -938,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) diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index dd4c9698c93e47..120bf2412eb5eb 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -649,23 +649,29 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { found } ast.StructInit { - // e.g. `Holder{ value: match value { .. } }` as a call argument. - mut found := false - for field in expr.init_fields { - if p.expr_contains_value_match_or_if(field.expr) { - found = true - break + // e.g. `Holder{ value: match value { .. } }` or + // `Holder{ ...(match value { .. }), other: 1 }` as a call argument. + mut found := expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) + if !found { + for field in expr.init_fields { + if p.expr_contains_value_match_or_if(field.expr) { + found = true + break + } } } found } ast.MapInit { - // e.g. `{'value': match value { .. }}` as a call argument. - mut found := false - for element in expr.keys { - if p.expr_contains_value_match_or_if(element) { - found = true - break + // e.g. `{'value': match value { .. }}` or + // `{ ...(match value { .. }), 'k': v }` as a call argument. + mut found := expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) + if !found { + for element in expr.keys { + if p.expr_contains_value_match_or_if(element) { + found = true + break + } } } if !found { @@ -678,6 +684,17 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { } found } + ast.StringInterLiteral { + // e.g. `'x=${match value { .. }}'` as a call argument. + mut found := false + for e in expr.exprs { + if p.expr_contains_value_match_or_if(e) { + found = true + break + } + } + found + } else { false } @@ -725,18 +742,23 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } ast.StructInit { // last stmt on block is a struct literal, e.g. - // `Holder{ value: match value { .. } }`; mark any field whose value - // is (or nests) a block-value match/if. + // `Holder{ value: match value { .. } }` or an update + // `Holder{ ...(match value { .. }), other: 1 }`; mark any field value + // or the update operand that is (or nests) a block-value match/if. for mut field in expr.init_fields { if p.expr_contains_value_match_or_if(field.expr) { p.mark_last_call_expr_return_as_used(mut field.expr) } } + if expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) { + p.mark_last_call_expr_return_as_used(mut expr.update_expr) + } } ast.MapInit { // last stmt on block is a map literal, e.g. - // `{'value': match value { .. }}`; mark any key/value that is (or - // nests) a block-value match/if. + // `{'value': match value { .. }}` or an update + // `{ ...(match value { .. }), 'k': v }`; mark any key/value or the + // update operand that is (or nests) a block-value match/if. for mut element in expr.keys { if p.expr_contains_value_match_or_if(element) { p.mark_last_call_expr_return_as_used(mut element) @@ -747,6 +769,19 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { p.mark_last_call_expr_return_as_used(mut element) } } + if expr.has_update_expr && p.expr_contains_value_match_or_if(expr.update_expr) { + p.mark_last_call_expr_return_as_used(mut expr.update_expr) + } + } + ast.StringInterLiteral { + // last stmt on block is a string interpolation, e.g. + // `'x=${match value { .. }}'`; mark any interpolated expression that is + // (or nests) a block-value match/if. + for mut e in expr.exprs { + if p.expr_contains_value_match_or_if(e) { + p.mark_last_call_expr_return_as_used(mut e) + } + } } ast.IndexExpr { if expr.or_expr.stmts.len > 0 { @@ -1348,7 +1383,9 @@ fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { match p.tok.kind { .lcbr { mut pos := p.tok.pos() - mut is_map_lit := p.peek_token(2).kind == .colon + // `{ ...m, k: v }` is a map update literal (a block cannot start with + // `...`), e.g. `{ ...(match x { .. }), 'k': v }`. + mut is_map_lit := p.peek_token(2).kind == .colon || p.peek_tok.kind == .ellipsis if !is_map_lit && p.peek_tok.kind == .lpar { // A parenthesized first key, e.g. `{ (match x { .. }) : v }`, puts the // `:` past the closing `)`, so the single-token `peek_token(2)` check diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index a6fad36e6bb30e..268cf6402c9527 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -351,6 +351,77 @@ fn select_value_spread(node ?Node) ![]int { return result } +fn make_holder_first(_ First) !Holder { + return Holder{ + value: 1 + } +} + +fn make_holder_second(_ Second) !Holder { + return Holder{ + value: 2 + } +} + +// match as a struct update operand: `Holder{ ...(match .. { .. }), other: 9 }` +fn select_value_struct_update(node ?Node) !Holder { + result := if value := node { + Holder{ + ...(match value { + First { make_holder_first(value)! } + Second { make_holder_second(value)! } + }) + other: 9 + } + } else { + Holder{} + } + return result +} + +fn map_first(_ First) !map[string]int { + return { + 'a': 1 + } +} + +fn map_second(_ Second) !map[string]int { + return { + 'a': 2 + } +} + +// match as a map update operand: `{ ...(match .. { .. }), 'b': 5 }` +fn select_value_map_update(node ?Node) !map[string]int { + result := if value := node { + { + ...(match value { + First { map_first(value)! } + Second { map_second(value)! } + }) + 'b': 5 + } + } else { + { + 'a': 0 + } + } + return result +} + +// match as a string interpolation operand: `'x=${match .. { .. }}'` +fn select_value_interp(node ?Node) !string { + result := if value := node { + 'x=${match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }}' + } else { + 'x=0' + } + return result +} + struct Circle { r int } @@ -569,6 +640,34 @@ fn test_array_spread_match_as_if_expr_value_with_propagation() { assert select_value_spread(none) or { [-1] } == [0] } +fn test_struct_update_match_as_if_expr_value_with_propagation() { + assert select_value_struct_update(First{})!.value == 1 + assert select_value_struct_update(First{})!.other == 9 + assert select_value_struct_update(Second{})!.value == 2 + assert select_value_struct_update(none) or { + Holder{ + value: -1 + } + }.value == 0 +} + +fn test_map_update_match_as_if_expr_value_with_propagation() { + assert select_value_map_update(First{})!['a'] == 1 + assert select_value_map_update(First{})!['b'] == 5 + assert select_value_map_update(Second{})!['a'] == 2 + assert (select_value_map_update(none) or { + { + 'a': -1 + } + })['a'] == 0 +} + +fn test_string_interp_match_as_if_expr_value_with_propagation() { + assert select_value_interp(First{})! == 'x=1' + assert select_value_interp(Second{})! == 'x=2' + assert select_value_interp(none) or { 'x=z' } == 'x=0' +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index 5f693162f65e83..df0d11be2dc79c 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -221,6 +221,18 @@ fn select_value_mapkey(node ?Node) !map[int]int { return result } +fn select_value_interp(node ?Node) !string { + result := if value := node { + "x=\${match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }}" + } else { + "x=0" + } + return result +} + struct Boxed { value int } @@ -342,6 +354,7 @@ fn main() { println(select_value_membership(First{})!) println(select_value_selector(First{})!) println(select_value_mapkey(First{})![1]) + println(select_value_interp(First{})!) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -359,5 +372,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\nx=1\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 47b1f8b24fa3e3..1f4173d4ec0807 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -4636,7 +4636,9 @@ fn (mut t Transformer) transform_string_interp_part(child_id flat.NodeId) flat.N t.mark_string_interp_call_part_used(expr_id) saved_in_string_interp_part := t.in_string_interp_part t.in_string_interp_part = true - mut transformed := t.transform_expr(expr_id) + // route a value `match`/`if` interpolation operand (e.g. `'${match x { ... }}'`) + // through its target type so its propagating arms are lowered as values. + mut transformed := t.transform_value_operand(expr_id) t.in_string_interp_part = saved_in_string_interp_part mut typ := t.raw_alias_type_for_expr(expr_id) if typ.len == 0 { From 58fba1406fb8ba7f9b2a429193e3218a35a51701 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 13:14:28 +0300 Subject: [PATCH 18/20] parser, checker, v3: handle dump/likely-wrapped match/if block values (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two PR review comments: 1. `dump()` operand: `dump(match value { First { lower_first(value)! } ... })` — `ast.DumpExpr` fell through both parser traversals. Handle it in both, recursing into `DumpExpr.expr` (the checker already treats the dumped operand as a value). On v3, `transform_dump_expr` routes the operand through the shared `transform_value_operand` helper. 2. `_likely_()`/`_unlikely_()` operand: `_likely_(match value { First { bool_first(value)! } ... })` — `ast.Likely` fell through both parser traversals, and the checker evaluated the operand under the branch's void context ("expects a boolean expression, instead it got `void`"). Handle `Likely` in both parser traversals, and set the value-required flag around the operand check in `checker` so the match is checked as an expression (its bool arms infer their own type). Cover `dump(match ...)` and `_likely_(match ...)` in both regression tests (dump only in vlib/v, since its stderr output would perturb the v3 exact-output comparison). Verified v3 self-hosts and compiler_errors_test plus the dump/likely/match suites are unchanged. --- vlib/v/checker/checker.v | 12 +++++ vlib/v/parser/parser.v | 18 ++++++++ ...h_as_if_expr_value_with_propagation_test.v | 46 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 23 +++++++++- vlib/v3/transform/transform.v | 4 +- 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 4c4eac7b08c4fc..304d672b4fd155 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -5305,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_' } diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 120bf2412eb5eb..79979ab4a434de 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -695,6 +695,14 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { } found } + ast.DumpExpr { + // e.g. `dump(match value { .. })` as a call argument. + p.expr_contains_value_match_or_if(expr.expr) + } + ast.Likely { + // e.g. `_likely_(match value { .. })` as a call argument. + p.expr_contains_value_match_or_if(expr.expr) + } else { false } @@ -870,6 +878,16 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { p.mark_last_call_return_as_used(mut or_block_last_stmt) } } + ast.DumpExpr { + // last stmt is `dump(match value { First { foo()! } })`; the dumped + // operand is a value, so recurse into it. + p.mark_last_call_expr_return_as_used(mut expr.expr) + } + ast.Likely { + // last stmt is `_likely_(match value { First { foo()! } })`; recurse + // into the wrapped operand. + p.mark_last_call_expr_return_as_used(mut expr.expr) + } else {} } } diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 268cf6402c9527..f5f00f7e7babc8 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -422,6 +422,40 @@ fn select_value_interp(node ?Node) !string { return result } +// match as a `dump()` operand: `dump(match .. { .. })` +fn select_value_dump(node ?Node) !int { + result := if value := node { + dump(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }) + } else { + 0 + } + return result +} + +fn bool_first(_ First) !bool { + return true +} + +fn bool_second(_ Second) !bool { + return false +} + +// match as a `_likely_()` operand: `_likely_(match .. { .. })` +fn select_value_likely(node ?Node) !bool { + result := if value := node { + _likely_(match value { + First { bool_first(value)! } + Second { bool_second(value)! } + }) + } else { + false + } + return result +} + struct Circle { r int } @@ -668,6 +702,18 @@ fn test_string_interp_match_as_if_expr_value_with_propagation() { assert select_value_interp(none) or { 'x=z' } == 'x=0' } +fn test_dump_operand_match_as_if_expr_value_with_propagation() { + assert select_value_dump(First{})! == 1 + assert select_value_dump(Second{})! == 2 + assert select_value_dump(none) or { -1 } == 0 +} + +fn test_likely_operand_match_as_if_expr_value_with_propagation() { + assert select_value_likely(First{})! == true + assert select_value_likely(Second{})! == false + assert select_value_likely(none) or { true } == false +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index df0d11be2dc79c..e8bc50704d4eec 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -233,6 +233,26 @@ fn select_value_interp(node ?Node) !string { return result } +fn bool_first(_ First) !bool { + return true +} + +fn bool_second(_ Second) !bool { + return false +} + +fn select_value_likely(node ?Node) !bool { + result := if value := node { + _likely_(match value { + First { bool_first(value)! } + Second { bool_second(value)! } + }) + } else { + false + } + return result +} + struct Boxed { value int } @@ -355,6 +375,7 @@ fn main() { println(select_value_selector(First{})!) println(select_value_mapkey(First{})![1]) println(select_value_interp(First{})!) + println(select_value_likely(First{})!) println(select_value_structinit(First{})!.value) println(select_value_mapinit(Second{})![7]) println(select_value_ascast(5)!) @@ -372,5 +393,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\nx=1\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 1f4173d4ec0807..f28f029e5eee8b 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -8018,7 +8018,9 @@ fn (mut t Transformer) transform_dump_expr(node flat.Node) flat.NodeId { if typ.len == 0 || typ == 'unknown' { typ = t.resolve_expr_type(child_id) } - child := t.transform_expr(child_id) + // route a value `match`/`if` dumped operand (e.g. `dump(match x { ... })`) + // through its target type so its propagating arms are lowered as values. + child := t.transform_value_operand(child_id) temp_name := t.new_temp('dump') t.pending_stmts << t.make_decl_assign_typed(temp_name, child, typ) if isnil(t.tc) || !t.tc.suppress_dump_output { From 4095e5f26bb9f50315f37008e72dfb2cc1fbc025 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 13:24:13 +0300 Subject: [PATCH 19/20] parser, checker: handle multi-return match/if block values with propagation (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review: a multi-return branch such as `a, b := if value := node { match value { First { lower_first(value)! } ... }, 9 } else { 0, 0 }` was rejected ("type `void` cannot be used in multi-return"). The `ConcatExpr` parser handler marked only values that were direct calls, never reaching a nested match, and `concat_expr` checked the match under the branch's void context. - vlib/v parser: `mark_last_call_expr_return_as_used`'s `ConcatExpr` case now recurses into every value via the helper (a strict superset of the previous direct-call marking), so nested/wrapped match/if/call values are marked. - vlib/v checker: `concat_expr` sets the value-required flag around each value check, so a value match/if is checked as an expression (its arms infer their own type) instead of being typed `void`. Cover `match ..., 9` in the vlib/v regression test. compiler_errors_test plus the multi-return/match/option suites are unchanged. (v3 has separate pre-existing gaps for this spelling — it mis-lowers a match in a multi-return even in the direct form, and misparses the multi-return match in an if-branch — so the v3 regression is left for a follow-up.) --- vlib/v/checker/checker.v | 12 +++++++++ vlib/v/parser/parser.v | 9 ++++--- ...h_as_if_expr_value_with_propagation_test.v | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 304d672b4fd155..cd4a54309a631d 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -6992,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 diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 79979ab4a434de..2f0106ee58f924 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -728,11 +728,12 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { } } ast.ConcatExpr { - // last stmt on block is: a, b, c := ret1(), ret2(), ret3() + // last stmt on block is a multi-return value list, e.g. + // `ret1(), ret2()` or `match value { .. }, 9`; recurse into every + // value so nested/wrapped match/if/call values are marked as + // return-used, not just direct calls. for mut val in expr.vals { - if mut val is ast.CallExpr { - val.is_return_used = true - } + p.mark_last_call_expr_return_as_used(mut val) } } ast.ArrayInit { diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index f5f00f7e7babc8..81a92cc68515a4 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -456,6 +456,19 @@ fn select_value_likely(node ?Node) !bool { return result } +// match as one value of a multi-return value list: `match .. { .. }, 9` +fn select_value_multiret(node ?Node) !(int, int) { + a, b := if value := node { + match value { + First { lower_first(value)! } + Second { lower_second(value)! } + }, 9 + } else { + 0, 0 + } + return a, b +} + struct Circle { r int } @@ -714,6 +727,18 @@ fn test_likely_operand_match_as_if_expr_value_with_propagation() { assert select_value_likely(none) or { true } == false } +fn test_multi_return_value_match_as_if_expr_value_with_propagation() { + a1, b1 := select_value_multiret(First{})! + assert a1 == 1 + assert b1 == 9 + a2, b2 := select_value_multiret(Second{})! + assert a2 == 2 + assert b2 == 9 + a3, b3 := select_value_multiret(none) or { -1, -1 } + assert a3 == 0 + assert b3 == 0 +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 From 9c8e75b12b639c6191fe4b98f75126eb87dbd2b1 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 13:37:25 +0300 Subject: [PATCH 20/20] parser, checker: method-call receiver and slice-bound match/if block values (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two PR review comments: 1. Method-call receiver: `(match value { First { make_first(value)! } ... }).get()` stores the match in `CallExpr.left`, which the `CallExpr` parser handlers traversed only for `args`. `mark_last_call_expr_return_as_used` and `expr_contains_value_match_or_if` now also recurse through a method-call receiver. (The receiver typechecks via method resolution, so no checker change was needed — only the parser marking was missing.) 2. Slice bound: `values[(match value { First { lower_first(value)! } ... })..]` stores the match in a `RangeExpr` bound, which both parser traversals fell through. Added a `RangeExpr` case to both (recursing into `low`/`high`), and `index_expr` now sets the value-required flag around each range-bound check so a value match/if is checked as an expression instead of a void index. Cover `(match ...).get()` and `values[(match ...)..]` in the vlib/v regression test (and the slice bound in the v3 test). compiler_errors_test plus the index/slice/method/call suites are unchanged. (v3's slice bound already works; v3's method-call receiver mis-lowers a match — a separate pre-existing v3 gap — so its regression is left for a follow-up.) --- vlib/v/checker/checker.v | 21 +++++++ vlib/v/parser/parser.v | 34 ++++++++++-- ...h_as_if_expr_value_with_propagation_test.v | 55 +++++++++++++++++++ ...s_if_expr_value_propagation_codegen_test.v | 16 +++++- 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index cd4a54309a631d..b667186241b80c 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -8543,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 diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index 2f0106ee58f924..5eb11b6c322c2e 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -606,11 +606,13 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.expr) } ast.CallExpr { - mut found := false - for arg in expr.args { - if p.expr_contains_value_match_or_if(arg.expr) { - found = true - break + mut found := expr.is_method && p.expr_contains_value_match_or_if(expr.left) + if !found { + for arg in expr.args { + if p.expr_contains_value_match_or_if(arg.expr) { + found = true + break + } } } found @@ -631,6 +633,12 @@ fn (p &Parser) expr_contains_value_match_or_if(expr ast.Expr) bool { p.expr_contains_value_match_or_if(expr.left) || p.expr_contains_value_match_or_if(expr.index) } + ast.RangeExpr { + // e.g. `values[(match value { .. })..]` slice bound. + + (expr.has_low && p.expr_contains_value_match_or_if(expr.low)) + || (expr.has_high && p.expr_contains_value_match_or_if(expr.high)) + } ast.SelectorExpr { // e.g. `(match value { .. }).field` as a call argument. p.expr_contains_value_match_or_if(expr.expr) @@ -726,6 +734,11 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { p.mark_last_call_expr_return_as_used(mut arg.expr) } } + // a method-call receiver may itself be a block-value match/if, e.g. + // `(match value { First { foo()! } }).get()`; mark its arm calls too. + if expr.is_method && p.expr_contains_value_match_or_if(expr.left) { + p.mark_last_call_expr_return_as_used(mut expr.left) + } } ast.ConcatExpr { // last stmt on block is a multi-return value list, e.g. @@ -807,6 +820,17 @@ fn (mut p Parser) mark_last_call_expr_return_as_used(mut expr ast.Expr) { p.mark_last_call_expr_return_as_used(mut expr.index) } } + ast.RangeExpr { + // last stmt on block is a range/slice bound, e.g. + // `values[(match value { .. })..]`; mark either bound that is (or + // nests) a block-value match/if. + if expr.has_low && p.expr_contains_value_match_or_if(expr.low) { + p.mark_last_call_expr_return_as_used(mut expr.low) + } + if expr.has_high && p.expr_contains_value_match_or_if(expr.high) { + p.mark_last_call_expr_return_as_used(mut expr.high) + } + } ast.ParExpr { // last stmt on block is parenthesized: ( match .. { a { foo() } } ) p.mark_last_call_expr_return_as_used(mut expr.expr) diff --git a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v index 81a92cc68515a4..aac61f08d2aedd 100644 --- a/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v +++ b/vlib/v/tests/match_as_if_expr_value_with_propagation_test.v @@ -469,6 +469,49 @@ fn select_value_multiret(node ?Node) !(int, int) { return a, b } +struct Getter { + value int +} + +fn (g Getter) get() int { + return g.value +} + +fn make_getter_first(_ First) !Getter { + return Getter{1} +} + +fn make_getter_second(_ Second) !Getter { + return Getter{2} +} + +// match as a method-call receiver: `(match .. { .. }).get()` +fn select_value_method_recv(node ?Node) !int { + result := if value := node { + (match value { + First { make_getter_first(value)! } + Second { make_getter_second(value)! } + }).get() + } else { + 0 + } + return result +} + +// match as a slice lower bound: `values[(match .. { .. })..]` +fn select_value_slice_bound(node ?Node) ![]int { + values := [10, 20, 30, 40] + result := if value := node { + values[(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })..] + } else { + [0] + } + return result +} + struct Circle { r int } @@ -739,6 +782,18 @@ fn test_multi_return_value_match_as_if_expr_value_with_propagation() { assert b3 == 0 } +fn test_method_receiver_match_as_if_expr_value_with_propagation() { + assert select_value_method_recv(First{})! == 1 + assert select_value_method_recv(Second{})! == 2 + assert select_value_method_recv(none) or { -1 } == 0 +} + +fn test_slice_bound_match_as_if_expr_value_with_propagation() { + assert select_value_slice_bound(First{})! == [20, 30, 40] + assert select_value_slice_bound(Second{})! == [30, 40] + assert select_value_slice_bound(none) or { [-1] } == [0] +} + fn test_match_as_if_expr_value_with_option_propagation() { assert select_opt(First{})? == 10 assert select_opt(Second{})? == 20 diff --git a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v index e8bc50704d4eec..c29b88364adbf3 100644 --- a/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -197,6 +197,19 @@ fn select_value_index(node ?Node) !int { return result } +fn select_value_slice_bound(node ?Node) ![]int { + values := [10, 20, 30, 40] + result := if value := node { + values[(match value { + First { lower_first(value)! } + Second { lower_second(value)! } + })..] + } else { + [0] + } + return result +} + fn select_value_membership(node ?Node) !bool { result := if value := node { (match value { @@ -371,6 +384,7 @@ fn main() { println(select_value_arraylit(First{})!) println(select_value_prefix(First{})!) println(select_value_index(First{})!) + println(select_value_slice_bound(First{})!) println(select_value_membership(First{})!) println(select_value_selector(First{})!) println(select_value_mapkey(First{})![1]) @@ -393,5 +407,5 @@ fn main() { run := os.execute(bin) assert run.exit_code == 0, run.output - assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2' + assert run.output.trim_space() == '1\n2\n1\n2\n1\n2\n2\n12\n20\n100\n20\n[1]\n-1\n20\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2' }