From 8fd6937284a9efdb1f3340ee04d2a83aeace0df8 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 14:53:41 +0300 Subject: [PATCH 01/37] v3: lower value-context match/if block operands with `!`/`?` propagation (fix #28000) A `match`/`if` used as the *value* of a cast, string-interpolation part, `dump()`, prefix/infix operand, index expression, selector receiver, or `as` operand must have its (possibly propagating) branch tails lowered as values. Previously they were lowered in a value-less statement context, emitting an empty ternary / expression. Adds `transform_value_operand` + `is_value_match_or_if_operand` helpers (the latter looks through `(...)`, `unsafe { }` and trailing `expr_stmt` wrappers) and routes the affected operands through target-typed lowering. Regression test: vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v --- ...s_if_expr_value_propagation_codegen_test.v | 411 ++++++++++++++++++ vlib/v3/transform/sum.v | 25 ++ vlib/v3/transform/transform.v | 76 +++- 3 files changed, 504 insertions(+), 8 deletions(-) create mode 100644 vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v 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..c29b88364adbf3 --- /dev/null +++ b/vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v @@ -0,0 +1,411 @@ +// 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 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 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 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 +} + +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 +} + +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 +} + +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 +} + +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 +} + +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 +} + +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 +} + +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 +} + +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 +} + +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 { + First { lower_first(value)! } + Second { lower_second(value)! } + }) in [1, 2] + } else { + false + } + 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 +} + +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 +} + +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 +} + +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 +} + +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{ + value: match value { + First { lower_first(value)! } + Second { lower_second(value)! } + } + other: 100 + } + } else { + Holder{} + } + 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 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)! } + Second { lower_second(node)! } + } +} + +fn main() { + println(select_value(First{})!) + println(select_value(Second{})!) + println(select_value_paren(First{})!) + 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_callarg(Second{})!) + 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_index(First{})!) + println(select_value_slice_bound(First{})!) + println(select_value_membership(First{})!) + 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)!) + println(select_value_ascast_unsafe(5)!) + 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\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' +} diff --git a/vlib/v3/transform/sum.v b/vlib/v3/transform/sum.v index ece6e68cd598fd..2da9beeb9a85bd 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 8b0dd4b8cad34c..c3028dce1dfb40 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -4635,7 +4635,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 { @@ -8017,7 +8019,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 { @@ -11053,7 +11057,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 @@ -14561,6 +14569,24 @@ fn (mut t Transformer) transform_children_expr(id flat.NodeId, node flat.Node) f }) } +// 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 { + 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 { @@ -14667,13 +14693,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_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_expr(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() @@ -15142,7 +15168,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 { @@ -15645,7 +15673,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 @@ -16697,7 +16727,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)] @@ -17160,6 +17192,28 @@ fn (mut t Transformer) transform_postfix_expr(id flat.NodeId, node flat.Node) fl }) } +// 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 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] +} + // 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 { @@ -17341,6 +17395,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 2e31fa40883598d5232e82f589c776f9f32f8d94 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 15:23:41 +0300 Subject: [PATCH 02/37] v3: route specialized infix operands through value lowering (#28000) Addresses review feedback on #28011. The type-specialized infix handlers (`transform_infix_string_ops`, `_array_ops`, `_interface_ops`, `_sum_ops`, `_struct_ops`) run before the generic fallthrough and lower their operands with plain `transform_expr`. A value-context `match`/`if` operand routed to one of them (e.g. `(match x { First { get_a()! } else { get_b()! } }) + suffix`, which enters the string handler) therefore had its propagating branch tails lowered in a value-less statement context, reproducing the empty-expression bug this PR fixes for the fallthrough path. Materialize value-branch `match`/`if` operands into value temps at the dispatch point, before the specialized handlers run, then re-dispatch over the rewritten node so every handler (and the fallthrough) sees a plain, typed operand. Only the value-branch operand is materialized; the other side keeps its original node and is transformed exactly once by the re-dispatch. --- vlib/v3/transform/transform.v | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index c3028dce1dfb40..61a98bfe71df69 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14669,6 +14669,46 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat t.annotate_left_shift(new_id) return new_id } + // A value-context `match`/`if` operand (e.g. `(match x { First { get_a()! } + // else { get_b()! } }) + suffix`) must be materialized as a value before the + // type-specialized handlers below dispatch on operand type. Those handlers + // (string/array/map/interface/sum/struct ops) lower their operands with plain + // `transform_expr`, which would lower the (possibly propagating) branch tails + // in a value-less statement context and emit an empty expression. Materialize + // only the value-branch operand(s) into value temps here, then re-dispatch over + // the rewritten node so every handler sees a plain, typed operand. The other + // operand is left as its original node so it is transformed exactly once. + infix_lhs_id := t.a.children[node.children_start] + infix_rhs_id := t.a.children[node.children_start + 1] + lhs_is_value_branch := t.is_value_match_or_if_operand(infix_lhs_id) + rhs_is_value_branch := t.is_value_match_or_if_operand(infix_rhs_id) + if lhs_is_value_branch || rhs_is_value_branch { + new_lhs := if lhs_is_value_branch { + t.transform_value_operand(infix_lhs_id) + } else { + infix_lhs_id + } + new_rhs := if rhs_is_value_branch { + t.transform_value_operand(infix_rhs_id) + } else { + infix_rhs_id + } + if new_lhs != infix_lhs_id || new_rhs != infix_rhs_id { + start := t.a.children.len + t.a.children << new_lhs + t.a.children << new_rhs + new_id := t.a.add_node(flat.Node{ + kind: .infix + op: node.op + children_start: start + children_count: 2 + pos: node.pos + value: node.value + typ: node.typ + }) + return t.transform_infix_expr(new_id, t.a.nodes[int(new_id)]) + } + } if str_result := t.transform_infix_string_ops(id, node) { return str_result } From 5e2e8dee39aa5afebf2e006a5de0b86bdf689554 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 15:44:39 +0300 Subject: [PATCH 03/37] v3: route left-shift and sum-cast match/if operands through value lowering (#28000) Addresses review feedback on #28011. Two more value-context `match`/`if` operand paths bypassed the value-aware lowering: - Left-shift: the early `.left_shift` branch lowered its operands with plain `transform_expr` when `rhs_target_type` is empty, so a numeric shift such as `1 << (match x { First { get_a()! } else { get_b()! } })` (and the symmetric `(match ...) << 2`) kept the empty-expression failure. Use `transform_value_operand` for both operands (a no-op for non-branch operands; the array-append RHS keeps its `transform_expr_for_type` element-typed path). - Sum-type cast: `transform_cast_expr` returns via the `is_sum_type_name` branch into `wrap_sum_value`, which lowers the operand with plain `transform_expr`, before reaching the value-aware cast loop. Materialize a value `match`/`if` operand through the target sum type first (mirrors the `as`-cast path), e.g. `Shape(match x { ... make_circle()! ... })`. --- vlib/v3/transform/transform.v | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 61a98bfe71df69..ffc75459adb7e8 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14648,11 +14648,17 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat rhs_target_type = elem_type } } - new_lhs := t.transform_expr(lhs_id) + // Route `match`/`if` value operands through value lowering: a numeric shift + // such as `1 << (match x { First { get_a()! } else { get_b()! } })` leaves + // `rhs_target_type` empty, so a propagating branch tail would otherwise be + // lowered with plain `transform_expr` in a value-less statement context and + // emit an empty expression. `transform_value_operand` is a no-op for the + // common non-branch operands. + new_lhs := t.transform_value_operand(lhs_id) new_rhs := if rhs_target_type.len > 0 { t.transform_expr_for_type(rhs_id, rhs_target_type) } else { - t.transform_expr(rhs_id) + t.transform_value_operand(rhs_id) } start := t.a.children.len t.a.children << new_lhs @@ -17384,7 +17390,17 @@ fn (mut t Transformer) transform_cast_expr(id flat.NodeId, node flat.Node) flat. return t.make_optional_some(expr, optional_target) } if t.is_sum_type_name(target_type) { - return t.wrap_sum_value(t.a.child(&node, 0), target_type) + sum_child_id := t.a.child(&node, 0) + if t.is_value_match_or_if_operand(sum_child_id) { + // A `match`/`if` operand of a sum-type cast (e.g. + // `Shape(match x { First { make_circle()! } else { make_square()! } })`) is a + // value expression whose (possibly propagating) branch tails must be lowered + // as values. `wrap_sum_value` would lower them with plain `transform_expr` in + // a value-less statement context and emit an empty expression, so route the + // operand through the target sum type instead (mirrors the `as`-cast path). + return t.transform_expr_for_type(sum_child_id, target_type) + } + return t.wrap_sum_value(sum_child_id, target_type) } // An explicit cast to an interface (`Animal(dog)`, `&PRNG(rng)`) boxes the // concrete value into the interface representation, just like an implicit From 800519e77d5d2ddf74421d4686017e3000d6cba3 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 16:09:18 +0300 Subject: [PATCH 04/37] v3: materialize match/if operands before cast dispatch and for `is` subjects (#28000) Addresses review feedback on #28011. - transform_cast_expr: type-specific cast paths return before the value-aware guard, so a value `match`/`if` operand of an optional-sum cast (`?Shape(match ...)`), interface cast (`Animal(match ...)`) or pointer-to-sum cast (`&Shape(match ...)`) was lowered by its helper (`wrap_sum_value` / interface boxing) with plain `transform_expr`, reproducing the empty propagating branch-tail expression. Materialize a value `match`/`if` cast operand into a value temp once, before cast-type dispatch, then re-dispatch over the rewritten operand so every path (optional/interface/pointer-sum/sum/ generic) sees a plain, typed operand. This subsumes the per-branch sum and generic guards added earlier, which are consolidated away. - transform_is_expr: the `is` subject was lowered with plain `transform_expr` (sum.v:773), so `(match n { ... make_circle()! ... }) is Circle` reproduced the same empty expression. Route the subject through value-aware lowering before building the tag check. `transform_value_operand` is a no-op for the common non-branch operands. --- vlib/v3/transform/sum.v | 6 ++++- vlib/v3/transform/transform.v | 48 ++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/vlib/v3/transform/sum.v b/vlib/v3/transform/sum.v index 2da9beeb9a85bd..1bfbf8025ab40f 100644 --- a/vlib/v3/transform/sum.v +++ b/vlib/v3/transform/sum.v @@ -770,7 +770,11 @@ fn (mut t Transformer) transform_is_expr(id flat.NodeId, node flat.Node) flat.No if clean_type.len == 0 || resolved_clean_type !in t.sum_types { return t.make_bool_literal(true) } - new_expr := t.transform_expr(expr_id) + // Route a value-context `match`/`if` subject (e.g. `(match n { First { make_circle()! + // } else { make_square()! } }) is Circle`) through value lowering so a propagating + // branch tail is materialized as a value instead of in a value-less statement context. + // `transform_value_operand` is a no-op for the common non-branch subjects. + new_expr := t.transform_value_operand(expr_id) // Mutable array/map loop bindings are storage pointers, but their rvalue // transform above already loads the sum value. Build the tag/path checks from // the transformed storage type so the value is not dereferenced twice. diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index ffc75459adb7e8..2a82db11885a5b 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -17267,6 +17267,34 @@ fn (mut t Transformer) transform_cast_expr(id flat.NodeId, node flat.Node) flat. return id } target_type := t.normalize_type_alias(node.value) + // Materialize a value-context `match`/`if` cast operand into a value temp before + // the type-specific dispatch below. Several cast paths return early into helpers + // that lower the operand with plain `transform_expr` — the optional-sum branch + // (`?Shape(match ...)`), interface boxing (`Animal(match ...)`), the pointer-to-sum + // branch (`&Shape(match ...)`) and the sum branch (`Shape(match ...)`) — which would + // lower a propagating branch tail in a value-less statement context and emit an + // empty expression. Re-dispatch over the rewritten temp so every path sees a plain, + // typed operand. `transform_value_operand` is a no-op for the common non-branch operands. + if node.children_count == 1 { + match_cast_child := t.a.child(&node, 0) + if t.is_value_match_or_if_operand(match_cast_child) { + value := t.transform_value_operand(match_cast_child) + if value != match_cast_child { + start := t.a.children.len + t.a.children << value + new_id := t.a.add_node(flat.Node{ + kind: .cast_expr + op: node.op + children_start: start + children_count: 1 + pos: node.pos + value: node.value + typ: node.typ + }) + return t.transform_cast_expr(new_id, t.a.nodes[int(new_id)]) + } + } + } if target_type.starts_with('&') && t.is_interface_type(target_type) { child := t.a.child_node(&node, 0) if child.kind == .call && child.children_count > 0 { @@ -17390,17 +17418,9 @@ fn (mut t Transformer) transform_cast_expr(id flat.NodeId, node flat.Node) flat. return t.make_optional_some(expr, optional_target) } if t.is_sum_type_name(target_type) { - sum_child_id := t.a.child(&node, 0) - if t.is_value_match_or_if_operand(sum_child_id) { - // A `match`/`if` operand of a sum-type cast (e.g. - // `Shape(match x { First { make_circle()! } else { make_square()! } })`) is a - // value expression whose (possibly propagating) branch tails must be lowered - // as values. `wrap_sum_value` would lower them with plain `transform_expr` in - // a value-less statement context and emit an empty expression, so route the - // operand through the target sum type instead (mirrors the `as`-cast path). - return t.transform_expr_for_type(sum_child_id, target_type) - } - return t.wrap_sum_value(sum_child_id, target_type) + // A value `match`/`if` operand here has already been materialized into a value + // temp by the pre-dispatch guard above, so `wrap_sum_value` sees a plain operand. + return t.wrap_sum_value(t.a.child(&node, 0), target_type) } // An explicit cast to an interface (`Animal(dog)`, `&PRNG(rng)`) boxes the // concrete value into the interface representation, just like an implicit @@ -17451,12 +17471,6 @@ 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 d26ae44058b4f8db2055274ae2db6311636869fe Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 16:34:07 +0300 Subject: [PATCH 05/37] v3: preserve infix operand order and lower address-of match/if operands (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - transform_infix_expr: when only one operand was a value `match`/`if`, the pre-dispatch guard materialized it (emitting its prelude to pending_stmts) before the other, untouched operand was evaluated — so `mark('L') + (match x { ... mark_result('R')! ... })` ran the RHS prelude before the LHS call, reversing observable evaluation order. Evaluate operands left-to-right with the same LHS-before-RHS pending ordering as the fallthrough, spilling a non-stable (side-effecting) non-branch operand to a temp so its evaluation precedes the branch prelude; stable operands are left untouched. - transform_prefix_expr: the `.amp` (address-of) branch lowered its operand with plain `transform_expr` and returned before the generic value-aware path, so `&(match ...)` lowered a propagating branch tail in statement context. Route it through value lowering. Regression tests (added to the existing codegen test, whose size avoids a separate pre-existing parallel late-scan crash): `select_value_infix_order` encodes the sum and the recorded L-before-R evaluation order (1112; a reversed order would be 1121), and `select_value_addr` takes the address of a struct-typed value match and reads a field through it. Both fail on HEAD without these fixes. --- ...s_if_expr_value_propagation_codegen_test.v | 56 ++++++++++++++++++- vlib/v3/transform/transform.v | 34 ++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) 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 c29b88364adbf3..10ca6a6fa9fa97 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 @@ -369,6 +369,58 @@ fn direct_match(node Node) !int { } } +struct Tracer { +mut: + order []int +} + +fn (mut tr Tracer) lhs() int { + tr.order << 1 + return 1 +} + +fn (mut tr Tracer) rf(_ First) !int { + tr.order << 2 + return 10 +} + +fn (mut tr Tracer) rs(_ Second) !int { + tr.order << 2 + return 20 +} + +// The LHS call must be evaluated before the RHS match materialization prelude. +// Encodes sum (11) and the recorded order ([1,2] = LHS then RHS) as 1112; a +// reversed order would yield 1121. +fn select_value_infix_order(node ?Node) !int { + mut tr := Tracer{} + sum := if value := node { + tr.lhs() + (match value { + First { tr.rf(value)! } + Second { tr.rs(value)! } + }) + } else { + 0 + } + return sum * 100 + tr.order[0] * 10 + tr.order[1] +} + +// Address-of a value match (the checker permits `&` on a struct-typed match): +// the propagating branch tail is materialized to a value temp whose address is +// taken, then a field is read through it. +fn select_value_addr(node ?Node) !int { + result := if value := node { + p := &(match value { + First { boxed(lower_first(value)!) } + Second { boxed(lower_second(value)!) } + }) + p.value + } else { + 0 + } + return result +} + fn main() { println(select_value(First{})!) println(select_value(Second{})!) @@ -395,6 +447,8 @@ fn main() { println(select_value_ascast(5)!) println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) + println(select_value_infix_order(First{})!) + println(select_value_addr(First{})!) } ') or { panic(err) @@ -407,5 +461,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\n[20, 30, 40]\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\n1112\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 2a82db11885a5b..0836521b7bb625 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14689,16 +14689,44 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat lhs_is_value_branch := t.is_value_match_or_if_operand(infix_lhs_id) rhs_is_value_branch := t.is_value_match_or_if_operand(infix_rhs_id) if lhs_is_value_branch || rhs_is_value_branch { + // Evaluate operands left-to-right so their materialization statements land in + // `pending_stmts` in source order (LHS before RHS). Materializing only one side + // would emit its prelude before the other operand is evaluated — e.g. + // `mark('L') + (match x { ... mark_result('R')! ... })` would run the RHS prelude + // before the LHS call, reversing observable evaluation order. When one side is a + // value branch, spill a non-stable (side-effecting) other operand to a temp first + // so its evaluation still precedes the branch's prelude; stable operands + // (idents/literals) are left untouched for the re-dispatch to transform once. + pending_start := t.pending_stmts.len new_lhs := if lhs_is_value_branch { t.transform_value_operand(infix_lhs_id) + } else if rhs_is_value_branch && !t.is_stable_expr_for_reuse(infix_lhs_id) { + t.stable_expr_for_reuse(infix_lhs_id) } else { infix_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 := if rhs_is_value_branch { t.transform_value_operand(infix_rhs_id) + } else if lhs_is_value_branch && !t.is_stable_expr_for_reuse(infix_rhs_id) { + t.stable_expr_for_reuse(infix_rhs_id) } else { infix_rhs_id } + if lhs_pending.len > 0 { + rhs_pending := t.pending_stmts[pending_start..].clone() + t.pending_stmts = t.pending_stmts[..pending_start].clone() + for stmt in lhs_pending { + t.pending_stmts << stmt + } + for stmt in rhs_pending { + t.pending_stmts << stmt + } + } if new_lhs != infix_lhs_id || new_rhs != infix_rhs_id { start := t.a.children.len t.a.children << new_lhs @@ -16753,7 +16781,11 @@ fn (mut t Transformer) transform_prefix_expr(id flat.NodeId, node flat.Node) fla t.set_node_typ(int(addr), node.typ) return addr } - value := t.transform_expr(child_id) + // Route a value-context `match`/`if` operand (e.g. `&(match x { First { get_a()! + // } else { get_b()! } })`) through value lowering so a propagating branch tail is + // materialized as a value here instead of in a value-less statement context. + // `transform_value_operand` is a no-op for the common non-branch operands. + value := t.transform_value_operand(child_id) if !t.expr_can_take_address(value) { mut value_type := t.node_type(child_id) if value_type.len == 0 { From faa61a1bd2bb3386f308cc010a56540d112ff5b0 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 16:49:30 +0300 Subject: [PATCH 06/37] v3: preserve evaluation order for match/if left-shift and index operands (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - Left-shift fast path: when the RHS is a value `match`/`if` whose materialization queues propagation prelude statements and the LHS has side effects, the LHS was left inline and thus evaluated after the RHS prelude — `mark_lhs() << (match x { ... mark_rhs()! ... })` ran mark_rhs before mark_lhs. Stabilize a non-stable LHS before materializing the RHS (numeric shift only; an array-append LHS is a mutated lvalue and must not be spilled). - Index/slice operands: when a later child (index or slice bound) is a value `match`/`if` that hoists statements while an earlier child has side effects, the earlier child ran after the hoisted prelude — `make_values(mut tr)[match n { ... tr.index_result()! ... }]` ran the index prelude before make_values. Stabilize preceding non-stable children once a later child hoists. Only rvalue reads (`.index`) reach here; lvalue targets are the separate `.index_assign` kind. Regression tests `select_value_shift_order` (102412 vs reversed 102421) and `select_value_index_order` (1012 vs reversed 1021) encode both the computed value and the recorded L-before-R order; both fail on HEAD without these fixes. --- ...s_if_expr_value_propagation_codegen_test.v | 48 ++++++++++++++++++- vlib/v3/transform/transform.v | 33 ++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) 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 10ca6a6fa9fa97..21c1bb264cb5ce 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 @@ -405,6 +405,50 @@ fn select_value_infix_order(node ?Node) !int { return sum * 100 + tr.order[0] * 10 + tr.order[1] } +fn (mut tr Tracer) shift_lhs() int { + tr.order << 1 + return 1 +} + +// Left-shift ordering: the side-effecting LHS must run before the RHS match +// prelude. Encodes shift result (1 << 10 = 1024) and order ([1,2]) as 102412; a +// reversed order would be 102421. +fn select_value_shift_order(node Node) !int { + mut tr := Tracer{} + sum := tr.shift_lhs() << (match node { + First { tr.rf(node)! } + Second { tr.rs(node)! } + }) + return sum * 100 + tr.order[0] * 10 + tr.order[1] +} + +fn (mut tr Tracer) base_values() []int { + tr.order << 1 + return [10, 20, 30] +} + +fn (mut tr Tracer) idx_first(_ First) !int { + tr.order << 2 + return 0 +} + +fn (mut tr Tracer) idx_second(_ Second) !int { + tr.order << 2 + return 1 +} + +// Index ordering: the side-effecting base must run before the match-index prelude. +// Encodes indexed value ([10,20,30][0] = 10) and order ([1,2]) as 1012; a reversed +// order would be 1021. +fn select_value_index_order(node Node) !int { + mut tr := Tracer{} + val := tr.base_values()[match node { + First { tr.idx_first(node)! } + Second { tr.idx_second(node)! } + }] + return val * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -448,6 +492,8 @@ fn main() { println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) println(select_value_infix_order(First{})!) + println(select_value_shift_order(First{})!) + println(select_value_index_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -461,5 +507,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1' + 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\n1112\n102412\n1012\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 0836521b7bb625..80c42e691bfbdc 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14654,7 +14654,19 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // lowered with plain `transform_expr` in a value-less statement context and // emit an empty expression. `transform_value_operand` is a no-op for the // common non-branch operands. - new_lhs := t.transform_value_operand(lhs_id) + // Preserve LHS-before-RHS evaluation order: for a numeric shift whose RHS is a + // value branch (its materialization below queues prelude statements), stabilize a + // side-effecting LHS first so it runs before that prelude, e.g. + // `mark_lhs() << (match x { ... mark_rhs()! ... })`. An array-append LHS + // (`rhs_target_type` set) is a mutated lvalue and must not be spilled; a value-branch + // LHS is already materialized in order by `transform_value_operand`. + rhs_is_value_branch := t.is_value_match_or_if_operand(rhs_id) + new_lhs := if rhs_target_type.len == 0 && rhs_is_value_branch + && !t.is_value_match_or_if_operand(lhs_id) && !t.is_stable_expr_for_reuse(lhs_id) { + t.stable_expr_for_reuse(lhs_id) + } else { + t.transform_value_operand(lhs_id) + } new_rhs := if rhs_target_type.len > 0 { t.transform_expr_for_type(rhs_id, rhs_target_type) } else { @@ -15238,13 +15250,30 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat if lowered := t.lower_gated_scalar_index(node) { return t.lower_owned_array_index_move(id, lowered) } + // A later child (index / slice bound) that is a value `match`/`if` hoists its + // propagation prelude into `pending_stmts`; a preceding side-effecting child left + // inline would then run after that prelude. Find the last hoisting child so earlier + // children can be stabilized first, preserving left-to-right evaluation order, e.g. + // `make_values(mut tr)[match n { ... tr.index_result()! ... }]`. Index reads only + // reach here (`.index`); lvalue targets are the separate `.index_assign` kind. + mut last_value_branch := -1 + for i in 0 .. node.children_count { + if t.is_value_match_or_if_operand(t.a.child(&node, i)) { + last_value_branch = i + } + } mut new_children := []flat.NodeId{cap: int(node.children_count)} mut changed := false for i in 0 .. node.children_count { child_id := t.a.child(&node, i) // 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) + mut new_child := if i < last_value_branch && !t.is_value_match_or_if_operand(child_id) + && !t.is_stable_expr_for_reuse(child_id) { + t.stable_expr_for_reuse(child_id) + } else { + t.transform_value_operand(child_id) + } if i == 0 { base := t.a.nodes[int(new_child)] if base.kind == .cast_expr { From 319d914eb5342cf1a0f8c02a3b06db3af90f719c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 17:01:14 +0300 Subject: [PATCH 07/37] v3: route gated-index match/if operands through value lowering (#28000) Addresses review feedback on #28011. `transform_index_expr` returns through `lower_gated_scalar_index` before the value-branch scan, and that helper lowered its base and index with `stable_expr_for_reuse` (plain `transform_expr`), so a gated index such as `values#[match n { First { get_index()! } else { other_index()! } }]` lowered the propagating match tail in a value-less statement context and emitted an empty generated expression. Route a value `match`/`if` base/index through `transform_value_operand`, which materializes it into a value temp (already stable for the helper's multiple uses); non-branch operands keep `stable_expr_for_reuse`. The base is still evaluated before the index, preserving base-before-index order. Regression test `select_value_gated_index_order` exercises a gated negative index (`[10,20,30]#[-1]` = 30) driven by a propagating match, encoding value and order as 3012. Without the fix the generated C fails to compile (empty expression). --- ...s_if_expr_value_propagation_codegen_test.v | 26 ++++++++++++++++++- vlib/v3/transform/transform.v | 20 ++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) 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 21c1bb264cb5ce..84475b94bb3132 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 @@ -449,6 +449,29 @@ fn select_value_index_order(node Node) !int { return val * 100 + tr.order[0] * 10 + tr.order[1] } +fn (mut tr Tracer) gated_first(_ First) !int { + tr.order << 2 + return -1 +} + +fn (mut tr Tracer) gated_second(_ Second) !int { + tr.order << 2 + return -2 +} + +// Gated index (`#[]`) with a propagating value match: the match tail must be +// lowered as a value (the gated helper otherwise lowers it with plain +// `transform_expr`). Encodes the gated value ([10,20,30]#[-1] = 30) and order +// ([1,2]) as 3012. +fn select_value_gated_index_order(node Node) !int { + mut tr := Tracer{} + val := tr.base_values()#[match node { + First { tr.gated_first(node)! } + Second { tr.gated_second(node)! } + }] + return val * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -494,6 +517,7 @@ fn main() { println(select_value_infix_order(First{})!) println(select_value_shift_order(First{})!) println(select_value_index_order(First{})!) + println(select_value_gated_index_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -507,5 +531,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n1' + 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\n1112\n102412\n1012\n3012\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 80c42e691bfbdc..14fde0a587891d 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15192,8 +15192,24 @@ fn (mut t Transformer) lower_gated_scalar_index(node flat.Node) ?flat.NodeId { return none } base_child := t.a.child(&node, 0) - base := t.stable_expr_for_reuse(base_child) - idx := t.stable_expr_for_reuse(t.a.child(&node, 1)) + idx_child := t.a.child(&node, 1) + // Route value `match`/`if` operands through value lowering: `stable_expr_for_reuse` + // lowers via plain `transform_expr`, which would lower a propagating branch tail in a + // value-less statement context and emit an empty expression, e.g. + // `values#[match n { First { get_index()! } else { other_index()! } }]`. + // `transform_value_operand` materializes such an operand into a value temp (already + // stable for the multiple uses below); non-branch operands keep `stable_expr_for_reuse`. + // The base is evaluated before the index, preserving base-before-index order. + base := if t.is_value_match_or_if_operand(base_child) { + t.transform_value_operand(base_child) + } else { + t.stable_expr_for_reuse(base_child) + } + idx := if t.is_value_match_or_if_operand(idx_child) { + t.transform_value_operand(idx_child) + } else { + t.stable_expr_for_reuse(idx_child) + } mut base_type := t.node_type(base) if base_type.len == 0 { base_type = t.node_type(base_child) From 20f00b3b387c4024e91804bf57c038ddc53d5d9d Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 17:12:08 +0300 Subject: [PATCH 08/37] v3: lower propagating match/if values in range bounds (#28000) Addresses review feedback on #28011. Range bounds bypassed the value-aware helper: `lower_range_for_in` sent both bounds through `stable_expr_for_reuse` and `transform_in_expr`'s range branch lowered `low_id`/`high_id` with plain `transform_expr`. So a bound such as `for i in (match node { First { lower_first(node)! } Second { lower_second(node)! } }) .. 10` lowered the propagating arm tail in a value-less statement context and emitted an empty expression. Route value `match`/`if` range bounds (and, in the membership form, the tested value) through `transform_value_operand`, which materializes them into value temps (stable for the loop condition / two comparisons); non-branch operands keep `stable_expr_for_reuse`. The low bound is still evaluated before the high bound. Regression tests: `select_value_range_low` (`for i in (match ...) .. 4`, sum 6) and `select_value_range_membership` (`3 in (match ...) .. 4`, true). Both fail to compile on HEAD (empty expression) without these fixes. --- ...s_if_expr_value_propagation_codegen_test.v | 26 ++++++++++++++++++- vlib/v3/transform/expr.v | 15 ++++++++--- vlib/v3/transform/for.v | 18 +++++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) 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 84475b94bb3132..49436881585c52 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 @@ -472,6 +472,28 @@ fn select_value_gated_index_order(node Node) !int { return val * 100 + tr.order[0] * 10 + tr.order[1] } +// A propagating value match as the low bound of a `for in` range loop: the bound +// must be lowered as a value. First -> low 1, sum of 1..4 = 6. +fn select_value_range_low(node Node) !int { + mut sum := 0 + for i in (match node { + First { lower_first(node)! } + Second { lower_second(node)! } + }) .. 4 { + sum += i + } + return sum +} + +// A propagating value match as the low bound of an `x in low..high` membership +// test. First -> low 1, so `3 in 1..4` is true. +fn select_value_range_membership(node Node) !bool { + return 3 in (match node { + First { lower_first(node)! } + Second { lower_second(node)! } + }) .. 4 +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -518,6 +540,8 @@ fn main() { println(select_value_shift_order(First{})!) println(select_value_index_order(First{})!) println(select_value_gated_index_order(First{})!) + println(select_value_range_low(First{})!) + println(select_value_range_membership(First{})!) println(select_value_addr(First{})!) } ') or { @@ -531,5 +555,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 5d38df09752766..7abebec15c1f04 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1875,11 +1875,20 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No if rhs.kind == .range { // x in low..high -> x >= low && x < high if rhs.children_count >= 2 { - new_lhs := t.stable_expr_for_reuse(lhs_id) + // Route value `match`/`if` operands (the tested value and the range bounds) + // through value lowering so a propagating branch tail is materialized as a + // value instead of in a value-less statement context, e.g. + // `x in (match node { ... lower(node)! ... }) .. 10`. `transform_value_operand` + // is a no-op for the common non-branch operands. + new_lhs := if t.is_value_match_or_if_operand(lhs_id) { + t.transform_value_operand(lhs_id) + } else { + t.stable_expr_for_reuse(lhs_id) + } low_id := t.a.children[rhs.children_start] high_id := t.a.children[rhs.children_start + 1] - new_low := t.transform_expr(low_id) - new_high := t.transform_expr(high_id) + new_low := t.transform_value_operand(low_id) + new_high := t.transform_value_operand(high_id) ge_cmp := t.make_infix(.ge, new_lhs, new_low) lt_cmp := t.make_infix(.lt, new_lhs, new_high) diff --git a/vlib/v3/transform/for.v b/vlib/v3/transform/for.v index 76b34208ce6f3a..529990c1c14dce 100644 --- a/vlib/v3/transform/for.v +++ b/vlib/v3/transform/for.v @@ -800,8 +800,22 @@ fn (mut t Transformer) lower_range_for_in(id flat.NodeId, node flat.Node, key_id return arr1(id) } range_type := t.range_loop_var_type_name(low_id) - low := t.stable_expr_for_reuse(low_id) - high := t.stable_expr_for_reuse(high_id) + // Route value `match`/`if` range bounds through value lowering (e.g. + // `for i in (match node { First { lower_first(node)! } ... }) .. 10`); otherwise a + // propagating branch tail is lowered in a value-less statement context and emits an + // empty expression. `transform_value_operand` materializes such a bound into a value + // temp (stable for reuse in the loop condition); non-branch bounds keep + // `stable_expr_for_reuse`. The low bound is evaluated before the high bound. + low := if t.is_value_match_or_if_operand(low_id) { + t.transform_value_operand(low_id) + } else { + t.stable_expr_for_reuse(low_id) + } + high := if t.is_value_match_or_if_operand(high_id) { + t.transform_value_operand(high_id) + } else { + t.stable_expr_for_reuse(high_id) + } loop_name := if key.value == '_' { '__discard_${int(key_id)}' } else { key.value } t.set_var_type(loop_name, range_type) mut prefix := []flat.NodeId{} From 773d8208d4924ada6c2ef84dd119f94d65c12505 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 17:23:56 +0300 Subject: [PATCH 09/37] v3: stabilize range low bound before hoisting high bound in membership (#28000) Addresses review feedback on #28011. In `transform_in_expr`'s `x in low..high` branch, both bounds went through `transform_value_operand`, which leaves a non-branch side-effecting low bound inline. When the high bound is a value `match`/`if`, its materialization queues prelude statements, so `x in low_with_effect() .. (match node { ... high_with_effect()! ... })` ran the high-bound prelude before the low bound, reversing source order. Stabilize a side-effecting low bound to a temp before materializing a hoisting high bound (a value-branch low is already materialized in order by `transform_value_operand`; the `for in` range path already used `stable_expr_for_reuse` and was unaffected). Regression test `select_value_range_order`: `5 in tr.range_low() .. (match node { ... })` records order into a trace and encodes membership + order as 112 (a reversed order would be 121). It fails on HEAD without the fix. --- ...s_if_expr_value_propagation_codegen_test.v | 31 ++++++++++++++++++- vlib/v3/transform/expr.v | 12 ++++++- 2 files changed, 41 insertions(+), 2 deletions(-) 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 49436881585c52..f63313b25a8770 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 @@ -494,6 +494,34 @@ fn select_value_range_membership(node Node) !bool { }) .. 4 } +fn (mut tr Tracer) range_low() int { + tr.order << 1 + return 0 +} + +fn (mut tr Tracer) range_high_first(_ First) !int { + tr.order << 2 + return 10 +} + +fn (mut tr Tracer) range_high_second(_ Second) !int { + tr.order << 2 + return 20 +} + +// Membership range ordering: a side-effecting low bound must run before the +// hoisting match high bound. `5 in 0..10` is true; order [1,2] -> 112 (a reversed +// order would be 121). +fn select_value_range_order(node Node) !int { + mut tr := Tracer{} + inside := 5 in tr.range_low() .. (match node { + First { tr.range_high_first(node)! } + Second { tr.range_high_second(node)! } + }) + flag := if inside { 1 } else { 0 } + return flag * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -542,6 +570,7 @@ fn main() { println(select_value_gated_index_order(First{})!) println(select_value_range_low(First{})!) println(select_value_range_membership(First{})!) + println(select_value_range_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -555,5 +584,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 7abebec15c1f04..ed37bbd5eb9147 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1887,7 +1887,17 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } low_id := t.a.children[rhs.children_start] high_id := t.a.children[rhs.children_start + 1] - new_low := t.transform_value_operand(low_id) + // If the high bound is a value branch, its materialization below queues prelude + // statements; stabilize a side-effecting low bound first so it evaluates before + // them, preserving low-before-high order, e.g. + // `x in low_with_effect() .. (match node { ... high_with_effect()! ... })`. + // A value-branch low is materialized in order by `transform_value_operand`. + new_low := if !t.is_value_match_or_if_operand(low_id) + && t.is_value_match_or_if_operand(high_id) && !t.is_stable_expr_for_reuse(low_id) { + t.stable_expr_for_reuse(low_id) + } else { + t.transform_value_operand(low_id) + } new_high := t.transform_value_operand(high_id) ge_cmp := t.make_infix(.ge, new_lhs, new_low) From b7bb6a16a3fba9e7f3a13b74d7e411da54b61e07 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 17:41:31 +0300 Subject: [PATCH 10/37] v3: route non-range membership and for-in containers through value lowering (#28000) Addresses review feedback on #28011. The value-aware routing only covered range bounds, so a value `match`/`if` used as the *container* still lowered its propagating arm tail in a value-less statement context (empty expression): - Membership (`transform_in_expr`): the dynamic-array, fixed-array, string, unknown-ident and fallback branches lowered the RHS container with plain `transform_expr`, and `stable_array_expr_for_membership` (semantic array membership) and `lower_map_membership_expr` did the same. Route the container through `transform_value_operand`. - for-in (`transform_for_in_body` / `lower_indexed_for_in` / `lower_iterator_for_in`): the indexed, iterator and map/rebuild paths lowered the container with `stable_expr_for_reuse` / `transform_expr`. Materialize a value-branch container into a value temp (stable for the loop's repeated use); non-branch containers keep the existing lowering. Regression tests: `select_value_membership_container` (`20 in (match node { ... make_values(node)! ... })` -> true) and `select_value_forin_container` (`for v in (match ...)` summed -> 60). Both fail to compile on HEAD (empty expression) without these fixes. --- ...s_if_expr_value_propagation_codegen_test.v | 34 ++++++++++++++++++- vlib/v3/transform/expr.v | 17 ++++++---- vlib/v3/transform/for.v | 19 +++++++++-- vlib/v3/transform/map.v | 8 ++++- 4 files changed, 68 insertions(+), 10 deletions(-) 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 f63313b25a8770..62b3360769685a 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 @@ -522,6 +522,36 @@ fn select_value_range_order(node Node) !int { return flag * 100 + tr.order[0] * 10 + tr.order[1] } +fn make_values_first(_ First) ![]int { + return [10, 20, 30] +} + +fn make_values_second(_ Second) ![]int { + return [40, 50] +} + +// Membership over a value-match container (dynamic array): the propagating match +// tail must be lowered as a value. First -> [10,20,30], so `20 in ...` is true. +fn select_value_membership_container(node Node) !bool { + return 20 in (match node { + First { make_values_first(node)! } + Second { make_values_second(node)! } + }) +} + +// for-in over a value-match container: the propagating match tail must be lowered +// as a value. First -> [10,20,30] -> sum 60. +fn select_value_forin_container(node Node) !int { + mut sum := 0 + for v in (match node { + First { make_values_first(node)! } + Second { make_values_second(node)! } + }) { + sum += v + } + return sum +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -571,6 +601,8 @@ fn main() { println(select_value_range_low(First{})!) println(select_value_range_membership(First{})!) println(select_value_range_order(First{})!) + println(select_value_membership_container(First{})!) + println(select_value_forin_container(First{})!) println(select_value_addr(First{})!) } ') or { @@ -584,5 +616,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index ed37bbd5eb9147..7685c5729a7b15 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1941,7 +1941,8 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No result = lowered } else { // dynamic array membership -> array_contains_int/string(arr, val) - mut new_rhs := t.transform_expr(rhs_id) + // (value-aware so a `match`/`if` container is materialized as a value) + mut new_rhs := t.transform_value_operand(rhs_id) if rhs_is_ptr_array { new_rhs = t.make_prefix(.mul, new_rhs) } @@ -1955,7 +1956,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } } else if rhs.kind in [.ident, .selector] && (rhs_type.len == 0 || rhs_type == 'unknown') { new_lhs := t.transform_expr(lhs_id) - new_rhs := t.transform_expr(rhs_id) + new_rhs := t.transform_value_operand(rhs_id) mut elem := t.node_type(lhs_id) lhs := t.a.nodes[int(lhs_id)] if elem.len == 0 && lhs.kind == .selector { @@ -1974,7 +1975,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } else { // fixed array membership -> fixed_array_contains_int/string(arr, len, val) new_lhs := t.transform_expr(lhs_id) - new_rhs := t.transform_expr(rhs_id) + new_rhs := t.transform_value_operand(rhs_id) elem := fixed_array_elem_type(clean_rhs_type) fn_name := fixed_array_contains_fn_name(elem) len_expr := t.make_fixed_array_len_expr(clean_rhs_type) @@ -1982,7 +1983,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } } else if clean_rhs_type == 'string' { new_lhs := t.transform_expr(lhs_id) - new_rhs := t.transform_expr(rhs_id) + new_rhs := t.transform_value_operand(rhs_id) fn_name := if t.node_type(lhs_id) in ['u8', 'byte'] { 'string__contains_u8' } else { @@ -1997,7 +1998,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // Unknown containment is kept as in_expr so the backend can reject or // handle genuinely unresolved cases. new_lhs := t.transform_expr(lhs_id) - new_rhs := t.transform_expr(rhs_id) + new_rhs := t.transform_value_operand(rhs_id) in_start := t.a.children.len t.a.children << new_lhs t.a.children << new_rhs @@ -2393,7 +2394,11 @@ fn (mut t Transformer) lower_array_last_index_expr(base_id flat.NodeId, needle_i // stable_array_expr_for_membership // supports helper handling in transform. fn (mut t Transformer) stable_array_expr_for_membership(id flat.NodeId, raw_type string, clean_type string) flat.NodeId { - mut expr := t.transform_expr(id) + // Route a value `match`/`if` container through value lowering (e.g. + // `needle in (match node { ... get_values(node)! ... })`); otherwise the propagating + // arm tail is lowered in a value-less statement context and emits an empty expression. + // `transform_value_operand` is a no-op for the common non-branch containers. + mut expr := t.transform_value_operand(id) if t.membership_container_is_pointer_array(raw_type) { expr = t.make_prefix(.mul, expr) } diff --git a/vlib/v3/transform/for.v b/vlib/v3/transform/for.v index 529990c1c14dce..8b16de6a358597 100644 --- a/vlib/v3/transform/for.v +++ b/vlib/v3/transform/for.v @@ -445,7 +445,15 @@ fn (mut t Transformer) rebuild_for_in_stmt(_id flat.NodeId, node flat.Node) []fl body_ids := t.a.children_of(&node)[header_count..].clone() source_is_owned_temporary := !raw_iter_type.starts_with('&') && !t.expr_can_take_address(container_id) - mut new_container := if map_iter_type.starts_with('map[') && source_is_owned_temporary { + // Route a value `match`/`if` for-in container through value lowering (e.g. + // `for value in (match node { First { get_values(node)! } ... })`); otherwise the + // propagating arm tail is lowered in a value-less statement context and emits an + // empty expression. `transform_value_operand` materializes it into a value temp + // (stable for the loop's repeated use); non-branch containers keep the existing + // `stable_expr_for_reuse` / `transform_expr` lowering. + mut new_container := if t.is_value_match_or_if_operand(container_id) { + t.transform_value_operand(container_id) + } else if map_iter_type.starts_with('map[') && source_is_owned_temporary { t.stable_expr_for_reuse(container_id) } else { t.transform_expr(container_id) @@ -861,7 +869,9 @@ fn (mut t Transformer) lower_iterator_for_in(id flat.NodeId, node flat.Node, key } iter_name := t.new_temp('iter') next_name := t.new_temp('iter_next') - iter_expr := t.transform_expr(container_id) + // Route a value `match`/`if` iterator container through value lowering so a + // propagating arm tail is materialized as a value (no-op for non-branch containers). + iter_expr := t.transform_value_operand(container_id) mut prefix := []flat.NodeId{} t.drain_pending(mut prefix) prefix << t.make_decl_assign_typed(iter_name, iter_expr, iter_type) @@ -925,6 +935,11 @@ fn (mut t Transformer) lower_indexed_for_in(id flat.NodeId, node flat.Node, key_ direct_map_index_container := node.op == .amp && container_node.kind == .index mut container := if direct_map_index_container { container_id + } else if t.is_value_match_or_if_operand(container_id) { + // Route a value `match`/`if` container through value lowering so a propagating + // arm tail is materialized into a value temp (stable for the loop's repeated use); + // non-branch containers keep `stable_expr_for_reuse`. + t.transform_value_operand(container_id) } else { t.stable_expr_for_reuse(container_id) } diff --git a/vlib/v3/transform/map.v b/vlib/v3/transform/map.v index 5b3b84ad72450e..3339245c72b537 100644 --- a/vlib/v3/transform/map.v +++ b/vlib/v3/transform/map.v @@ -289,7 +289,13 @@ fn (mut t Transformer) lower_map_membership_expr(map_id flat.NodeId, key_id flat return none } map_source_id := t.const_expr_for_ident(map_id) or { map_id } - map_expr := t.stable_expr_for_reuse(map_source_id) + // Route a value `match`/`if` map container through value lowering so a propagating + // arm tail is materialized as a value (no-op for the common non-branch containers). + map_expr := if t.is_value_match_or_if_operand(map_source_id) { + t.transform_value_operand(map_source_id) + } else { + t.stable_expr_for_reuse(map_source_id) + } key_name := t.new_temp('map_key') t.pending_stmts << t.make_decl_assign_typed(key_name, t.transform_expr_for_type(key_id, key_type), t.map_key_storage_type(key_type)) From f3780447fe09f68f41cb929b8beeb5362759b300 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 18:02:46 +0300 Subject: [PATCH 11/37] v3: route map-index bases and preserve needle order for match/if membership (#28000) Addresses review feedback on #28011. - Map index base (try_lower_map_index_expr): a value `match`/`if` map base returned through this helper before the value-branch scan and was lowered with `stable_expr_for_reuse` (plain transform_expr), so `(match n { First { make_map_first(n)! } ... })['key']` lowered the propagating arm in statement context (empty expression). Route it through `transform_value_operand`. - Membership needle order (transform_in_expr): in the inline dynamic-array, fixed-array, string and unknown branches the needle stayed inline while `transform_value_operand(rhs_id)` hoisted a value-branch container's prelude, so `tr.needle() in (match n { ... tr.text(n)! ... })` ran the container before the needle. Stabilize a side-effecting needle before materializing a value-branch container (the semantic-array path via lower_array_membership_expr already stabilized the needle first and was unaffected). Regression tests: select_value_map_index (`(match ...)['b']` -> 2, fails to compile on HEAD) and select_value_string_membership_order (`tr.needle_str() in (match ...)` -> 512, reversed 521 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 53 ++++++++++++++++++- vlib/v3/transform/expr.v | 40 +++++++++++--- vlib/v3/transform/map.v | 11 +++- 3 files changed, 94 insertions(+), 10 deletions(-) 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 62b3360769685a..46a6f8808facf7 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 @@ -552,6 +552,55 @@ fn select_value_forin_container(node Node) !int { return sum } +fn make_map_first(_ First) !map[string]int { + return { + "a": 1 + "b": 2 + } +} + +fn make_map_second(_ Second) !map[string]int { + return { + "a": 3 + } +} + +// Map index whose base is a value match: the propagating arm tail must be lowered +// as a value. First -> {a:1, b:2}, so ["b"] is 2. +fn select_value_map_index(node Node) !int { + return (match node { + First { make_map_first(node)! } + Second { make_map_second(node)! } + })["b"] +} + +fn (mut tr Tracer) needle_str() string { + tr.order << 1 + return "lo" +} + +fn (mut tr Tracer) text_first(_ First) !string { + tr.order << 2 + return "hello" +} + +fn (mut tr Tracer) text_second(_ Second) !string { + tr.order << 2 + return "world" +} + +// String-membership ordering: the side-effecting needle must run before the +// hoisting match container. "lo" in "hello" is true; order [1,2] -> 512 (a +// reversed order would be 521). +fn select_value_string_membership_order(node Node) !int { + mut tr := Tracer{} + inside := tr.needle_str() in (match node { + First { tr.text_first(node)! } + Second { tr.text_second(node)! } + }) + return (if inside { 500 } else { 0 }) + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -603,6 +652,8 @@ fn main() { println(select_value_range_order(First{})!) println(select_value_membership_container(First{})!) println(select_value_forin_container(First{})!) + println(select_value_map_index(First{})!) + println(select_value_string_membership_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -616,5 +667,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 7685c5729a7b15..9f4388e24f95ae 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1942,15 +1942,22 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } else { // dynamic array membership -> array_contains_int/string(arr, val) // (value-aware so a `match`/`if` container is materialized as a value) - mut new_rhs := t.transform_value_operand(rhs_id) - if rhs_is_ptr_array { - new_rhs = t.make_prefix(.mul, new_rhs) - } mut elem := if clean_rhs_type.starts_with('[]') { clean_rhs_type[2..] } else { '' } if elem.len == 0 { elem = t.node_type(lhs_id) } - new_lhs := t.transform_expr_for_type(lhs_id, elem) + // Evaluate the needle before materializing a value-branch container so a + // side-effecting needle precedes the container's hoisted prelude. + new_lhs := if t.is_value_match_or_if_operand(rhs_id) { + t.stable_transformed_expr_for_reuse(t.transform_expr_for_type(lhs_id, elem), + elem, 'in_lhs') + } else { + t.transform_expr_for_type(lhs_id, elem) + } + mut new_rhs := t.transform_value_operand(rhs_id) + if rhs_is_ptr_array { + new_rhs = t.make_prefix(.mul, new_rhs) + } fn_name := array_contains_fn_name(elem) result = t.make_call_typed(fn_name, arr2(new_rhs, new_lhs), 'bool') } @@ -1974,7 +1981,12 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No result = lowered } else { // fixed array membership -> fixed_array_contains_int/string(arr, len, val) - new_lhs := t.transform_expr(lhs_id) + // stabilize a side-effecting needle before a value-branch container hoists + new_lhs := if t.is_value_match_or_if_operand(rhs_id) { + t.stable_expr_for_reuse(lhs_id) + } else { + t.transform_expr(lhs_id) + } new_rhs := t.transform_value_operand(rhs_id) elem := fixed_array_elem_type(clean_rhs_type) fn_name := fixed_array_contains_fn_name(elem) @@ -1982,7 +1994,14 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No result = t.make_call_typed(fn_name, arr3(new_rhs, len_expr, new_lhs), 'bool') } } else if clean_rhs_type == 'string' { - new_lhs := t.transform_expr(lhs_id) + // If the container is a value branch, its materialization below hoists a + // prelude; stabilize a side-effecting needle first so it evaluates before it, + // e.g. `tr.needle() in (match n { First { tr.text_first(n)! } ... })`. + new_lhs := if t.is_value_match_or_if_operand(rhs_id) { + t.stable_expr_for_reuse(lhs_id) + } else { + t.transform_expr(lhs_id) + } new_rhs := t.transform_value_operand(rhs_id) fn_name := if t.node_type(lhs_id) in ['u8', 'byte'] { 'string__contains_u8' @@ -1997,7 +2016,12 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } else { // Unknown containment is kept as in_expr so the backend can reject or // handle genuinely unresolved cases. - new_lhs := t.transform_expr(lhs_id) + // stabilize a side-effecting needle before a value-branch container hoists + new_lhs := if t.is_value_match_or_if_operand(rhs_id) { + t.stable_expr_for_reuse(lhs_id) + } else { + t.transform_expr(lhs_id) + } new_rhs := t.transform_value_operand(rhs_id) in_start := t.a.children.len t.a.children << new_lhs diff --git a/vlib/v3/transform/map.v b/vlib/v3/transform/map.v index 3339245c72b537..9024c6aec0b0cd 100644 --- a/vlib/v3/transform/map.v +++ b/vlib/v3/transform/map.v @@ -332,7 +332,16 @@ fn (mut t Transformer) try_lower_map_index_expr(id flat.NodeId, node flat.Node) source_is_owned_temporary := !isnil(t.tc) && t.tc.ownership_type_requires_destruction(t.tc.parse_type(map_type)) && !base_type.starts_with('&') && !t.expr_can_take_address(map_source_id) - map_expr := t.stable_expr_for_reuse(map_source_id) + // Route a value `match`/`if` map-index base through value lowering (e.g. + // `(match n { First { make_map_first(n)! } ... })['key']`); otherwise the propagating + // arm tail is lowered in a value-less statement context and emits an empty expression. + // `transform_value_operand` materializes it into a value temp (stable for the repeated + // use below); non-branch bases keep `stable_expr_for_reuse`. + map_expr := if t.is_value_match_or_if_operand(map_source_id) { + t.transform_value_operand(map_source_id) + } else { + t.stable_expr_for_reuse(map_source_id) + } key_name := t.new_temp('map_key') t.pending_stmts << t.make_decl_assign_typed(key_name, t.transform_expr_for_type(key_id, key_type), t.map_key_storage_type(key_type)) From b3451265108c25d66e1bd641ebf9a316baa760b9 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 18:22:15 +0300 Subject: [PATCH 12/37] v3: preserve array-append LHS order and lower match/if needles in const-array membership (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - Array-append LHS order: for an append whose RHS is a value `match`/`if` that hoists a prelude, the LHS lvalue's dynamic base/index components are now stabilized into temps first (via stabilize_transformed_lvalue_for_reuse, which preserves the lvalue shape and does not spill the mutated array value), so a side-effecting index runs before the RHS prelude — `arrays[next(mut trace)] << (match node { First { make_value(node)! } ... })`. A statement append is lowered by `try_lower_array_append_stmt` (where `transform_lvalue` left the index inline); the guard is also applied in the `transform_infix_expr` left-shift path the review cited, for any append reaching it. Gated on a value-branch RHS, so normal appends are unchanged. - Const-string-array membership needle: `lower_const_string_array_membership_expr` runs before the value-aware needle path and lowered its needle with plain `transform_expr`, so `(match node { First { get_first(node)! } ... }) in allowed_words` lowered the propagating arm in statement context (empty expression). Route the needle through `transform_expr_for_type(.., 'string')`. Regression tests: select_value_append_order (`arrays[tr.next_index()] << (match ...)` -> 712, reversed 721 on HEAD) and select_value_const_membership (`(match ...) in allowed_words` -> true, fails to compile on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 52 ++++++++++++++++++- vlib/v3/transform/array.v | 10 +++- vlib/v3/transform/expr.v | 6 ++- vlib/v3/transform/transform.v | 10 +++- 4 files changed, 74 insertions(+), 4 deletions(-) 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 46a6f8808facf7..8df5235ddb83ec 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 @@ -601,6 +601,54 @@ fn select_value_string_membership_order(node Node) !int { return (if inside { 500 } else { 0 }) + tr.order[0] * 10 + tr.order[1] } +fn (mut tr Tracer) next_index() int { + tr.order << 1 + return 0 +} + +fn (mut tr Tracer) append_val_first(_ First) !int { + tr.order << 2 + return 7 +} + +fn (mut tr Tracer) append_val_second(_ Second) !int { + tr.order << 2 + return 8 +} + +// Array-append ordering: a side-effecting LHS index must run before the RHS match +// prelude, while the mutated array is not spilled. First -> arrays[0] << 7, order +// [1,2] -> 712 (a reversed order would be 721). +fn select_value_append_order(node Node) !int { + mut tr := Tracer{} + mut arrays := [[]int{}, []int{}] + arrays[tr.next_index()] << (match node { + First { tr.append_val_first(node)! } + Second { tr.append_val_second(node)! } + }) + return arrays[0][0] * 100 + tr.order[0] * 10 + tr.order[1] +} + +const allowed_words = ["alpha", "beta"] + +fn get_first(_ First) !string { + return "alpha" +} + +fn get_second(_ Second) !string { + return "gamma" +} + +// Value-match needle in a constant string array (membership shortcut): the +// propagating arm tail must be lowered as a value. First -> "alpha" in +// ["alpha", "beta"] -> true. +fn select_value_const_membership(node Node) !bool { + return (match node { + First { get_first(node)! } + Second { get_second(node)! } + }) in allowed_words +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -654,6 +702,8 @@ fn main() { println(select_value_forin_container(First{})!) println(select_value_map_index(First{})!) println(select_value_string_membership_order(First{})!) + println(select_value_append_order(First{})!) + println(select_value_const_membership(First{})!) println(select_value_addr(First{})!) } ') or { @@ -667,5 +717,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 422d079829aeef..838a227513cb53 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -1167,7 +1167,15 @@ fn (mut t Transformer) try_lower_array_append_stmt(id flat.NodeId) ?[]flat.NodeI } mut result := []flat.NodeId{} - lhs := t.transform_lvalue(lhs_id) + mut lhs := t.transform_lvalue(lhs_id) + // For an append whose RHS is a value `match`/`if` that hoists a prelude, stabilize the + // LHS lvalue's dynamic base/index components into temps first — without spilling the + // mutated array value — so a side-effecting index (e.g. + // `arrays[next(mut trace)] << (match ...)`) evaluates before the RHS prelude below, + // preserving source order. + if t.is_value_match_or_if_operand(rhs_id) { + lhs = t.stabilize_transformed_lvalue_for_reuse(lhs) + } t.drain_pending(mut result) mut rhs := flat.empty_node if !push_many { diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 9f4388e24f95ae..53b62cb4eaa00c 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -2110,7 +2110,11 @@ fn (mut t Transformer) lower_const_string_array_membership_expr(base_id flat.Nod return none } } - needle := t.transform_expr(needle_id) + // Route the needle through typed value lowering (the container is a string array), + // so a value `match`/`if` needle materializes its propagating arm as a value instead + // of in a value-less statement context, e.g. + // `(match node { First { get_first(node)! } ... }) in allowed_words`. + needle := t.transform_expr_for_type(needle_id, 'string') base_value := t.transform_expr(base_id) base_data := t.make_cast('&string', t.make_selector(base_value, 'data', 'voidptr'), '&string') len_expr := t.make_int_literal(expr.children_count) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 14fde0a587891d..fa18121eaf2453 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14661,12 +14661,20 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // (`rhs_target_type` set) is a mutated lvalue and must not be spilled; a value-branch // LHS is already materialized in order by `transform_value_operand`. rhs_is_value_branch := t.is_value_match_or_if_operand(rhs_id) - new_lhs := if rhs_target_type.len == 0 && rhs_is_value_branch + mut new_lhs := if rhs_target_type.len == 0 && rhs_is_value_branch && !t.is_value_match_or_if_operand(lhs_id) && !t.is_stable_expr_for_reuse(lhs_id) { t.stable_expr_for_reuse(lhs_id) } else { t.transform_value_operand(lhs_id) } + // For an array append (`rhs_target_type` set) whose RHS is a value branch that + // hoists a prelude, stabilize the LHS lvalue's dynamic base/index components into + // temps first — without spilling the mutated array value — so a side-effecting + // index (e.g. `arrays[next(mut trace)] << (match ...)`) evaluates before the RHS + // prelude, preserving source order. + if rhs_target_type.len > 0 && rhs_is_value_branch { + new_lhs = t.stabilize_transformed_lvalue_for_reuse(new_lhs) + } new_rhs := if rhs_target_type.len > 0 { t.transform_expr_for_type(rhs_id, rhs_target_type) } else { From 98d026f508f1b838ddd36c08fffe255b7928739e Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 18:32:59 +0300 Subject: [PATCH 13/37] v3: evaluate map-membership key before hoisting the container (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. In lower_map_membership_expr (`key in map`), the container (map_expr) was materialized before the key was spilled, so when the RHS map is a value `match`/`if`, its propagation prelude ran before a side-effecting key — `tr.key() in (match node { First { tr.map_first(node)! } ... })` evaluated the match arm before key(), reversing source order. Spill the typed key to its temp first, then materialize the container, so the key evaluates before a value-branch map's hoisted prelude (key and container are independent, so the reorder is safe; non-branch maps are unaffected). Regression test select_value_map_membership_order: `tr.map_key() in (match node { First { tr.map_first(node)! } ... })` records order into a trace -> 612 (order key=1 then container=2). It fails on HEAD (produces 621 - reversed). --- ...s_if_expr_value_propagation_codegen_test.v | 35 ++++++++++++++++++- vlib/v3/transform/map.v | 9 +++-- 2 files changed, 40 insertions(+), 4 deletions(-) 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 8df5235ddb83ec..63b638cd45d4d7 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 @@ -649,6 +649,38 @@ fn select_value_const_membership(node Node) !bool { }) in allowed_words } +fn (mut tr Tracer) map_key() string { + tr.order << 1 + return "b" +} + +fn (mut tr Tracer) map_first(_ First) !map[string]int { + tr.order << 2 + return { + "a": 1 + "b": 2 + } +} + +fn (mut tr Tracer) map_second(_ Second) !map[string]int { + tr.order << 2 + return { + "a": 3 + } +} + +// Map-membership ordering: a side-effecting key must run before the hoisting match +// container. `"b" in {a:1, b:2}` is true; order [1,2] -> 612 (a reversed order +// would be 621). +fn select_value_map_membership_order(node Node) !int { + mut tr := Tracer{} + inside := tr.map_key() in (match node { + First { tr.map_first(node)! } + Second { tr.map_second(node)! } + }) + return (if inside { 600 } else { 0 }) + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -704,6 +736,7 @@ fn main() { println(select_value_string_membership_order(First{})!) println(select_value_append_order(First{})!) println(select_value_const_membership(First{})!) + println(select_value_map_membership_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -717,5 +750,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n1' } diff --git a/vlib/v3/transform/map.v b/vlib/v3/transform/map.v index 9024c6aec0b0cd..e380f9a237beef 100644 --- a/vlib/v3/transform/map.v +++ b/vlib/v3/transform/map.v @@ -289,6 +289,12 @@ fn (mut t Transformer) lower_map_membership_expr(map_id flat.NodeId, key_id flat return none } map_source_id := t.const_expr_for_ident(map_id) or { map_id } + // Spill the typed key before materializing the container so a side-effecting key + // evaluates before a value-branch map's hoisted propagation prelude, preserving + // source order, e.g. `tr.key() in (match node { First { tr.map_first(node)! } ... })`. + key_name := t.new_temp('map_key') + t.pending_stmts << t.make_decl_assign_typed(key_name, t.transform_expr_for_type(key_id, + key_type), t.map_key_storage_type(key_type)) // Route a value `match`/`if` map container through value lowering so a propagating // arm tail is materialized as a value (no-op for the common non-branch containers). map_expr := if t.is_value_match_or_if_operand(map_source_id) { @@ -296,9 +302,6 @@ fn (mut t Transformer) lower_map_membership_expr(map_id flat.NodeId, key_id flat } else { t.stable_expr_for_reuse(map_source_id) } - key_name := t.new_temp('map_key') - t.pending_stmts << t.make_decl_assign_typed(key_name, t.transform_expr_for_type(key_id, - key_type), t.map_key_storage_type(key_type)) exists := t.make_map_exists_expr(map_expr, map_type, key_name) cleanup_key := !isnil(t.tc) && t.map_key_expr_creates_owned_value(key_id, key_type) && t.tc.ownership_type_requires_destruction(t.tc.parse_type(key_type)) From fd5233b64472ba3239d44b8812b93a9ca3703ce1 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 18:45:11 +0300 Subject: [PATCH 14/37] v3: lower push-many match/if append RHS as a value (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. When an array-append RHS is an array-producing value `match`/`if`, `array_append_rhs_is_push_many` selects the push-many branch, which lowered `rhs_id` with plain `transform_expr` — so the propagating arm tail stayed in a value-less statement context and emitted an empty expression, e.g. `out << (match node { First { values_first(node)! } else { values_second(node)! } })`. Route the push-many RHS through `transform_value_operand` (a no-op for the common non-branch operands) in both `try_lower_array_append_stmt` and `try_lower_optional_array_append_stmt`. Regression test select_value_push_many: `out << (match node { First { make_values_first(node)! } ... })` appends [10,20,30] to [1] -> sum 61. It fails to compile on HEAD (empty expression) without the fix. --- ...as_if_expr_value_propagation_codegen_test.v | 18 +++++++++++++++++- vlib/v3/transform/array.v | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) 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 63b638cd45d4d7..880add34e2ae14 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 @@ -681,6 +681,21 @@ fn select_value_map_membership_order(node Node) !int { return (if inside { 600 } else { 0 }) + tr.order[0] * 10 + tr.order[1] } +// Push-many append whose RHS is a value match producing an array: the propagating +// arm tail must be lowered as a value. First -> [1] << [10,20,30] -> sum 61. +fn select_value_push_many(node Node) !int { + mut out := [1] + out << (match node { + First { make_values_first(node)! } + Second { make_values_second(node)! } + }) + mut sum := 0 + for v in out { + sum += v + } + return sum +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -737,6 +752,7 @@ fn main() { println(select_value_append_order(First{})!) println(select_value_const_membership(First{})!) println(select_value_map_membership_order(First{})!) + println(select_value_push_many(First{})!) println(select_value_addr(First{})!) } ') or { @@ -750,5 +766,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 838a227513cb53..0031a61c2e9bb0 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -1204,7 +1204,12 @@ fn (mut t Transformer) try_lower_array_append_stmt(id flat.NodeId) ?[]flat.NodeI } } } else { - rhs = t.transform_expr(rhs_id) + // Route a value `match`/`if` push-many RHS (an array-producing match, e.g. + // `out << (match node { First { values_first(node)! } ... })`) through value + // lowering so its propagating arm tail is materialized as a value instead of in a + // value-less statement context. `transform_value_operand` is a no-op for the + // common non-branch push-many operands. + rhs = t.transform_value_operand(rhs_id) } if !push_many { rhs = t.coerce_transformed_expr_to_type(rhs, rhs_id, elem_type) @@ -1403,7 +1408,12 @@ fn (mut t Transformer) try_lower_optional_array_append_stmt(_node flat.Node, lhs } } } else { - rhs = t.transform_expr(rhs_id) + // Route a value `match`/`if` push-many RHS (an array-producing match, e.g. + // `out << (match node { First { values_first(node)! } ... })`) through value + // lowering so its propagating arm tail is materialized as a value instead of in a + // value-less statement context. `transform_value_operand` is a no-op for the + // common non-branch push-many operands. + rhs = t.transform_value_operand(rhs_id) } if !push_many { rhs = t.coerce_transformed_expr_to_type(rhs, rhs_id, elem_type) From 595d597a942fac2fce2ab5697f32fa7dfaad16d5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 18:58:50 +0300 Subject: [PATCH 15/37] v3: materialize value match/if method receivers before builtin dispatch (#28000) Addresses review feedback on #28011. The value-aware receiver path only runs when a selector reaches transform_selector_expr; builtin method calls dispatch earlier through transform_call_expr / try_lower_array_method_call. So `(match node { First { make_values_first(node)! } ... }).clone()` reached make_array_clone_call, which lowered the receiver with plain transform_expr (array.v:272), leaving the propagating arm tail in a value-less statement context (empty expression). Materialize a value match/if method receiver into a value temp at the top of transform_call_expr, before any builtin/method dispatch: rebuild the selector/call over the materialized receiver and re-dispatch. Gated on a value-branch receiver, so all non-branch method calls are unchanged; covers builtin and user method receivers uniformly. Regression test select_value_method_receiver: `(match node { First { make_values_first(node)! } ... }).clone()` -> sum 60 + len 3 = 63. It fails to compile on HEAD (empty expression) without the fix. --- ...s_if_expr_value_propagation_codegen_test.v | 18 ++++++- vlib/v3/transform/transform.v | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) 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 880add34e2ae14..0f4821e48184df 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 @@ -696,6 +696,21 @@ fn select_value_push_many(node Node) !int { return sum } +// Builtin method call on a value match receiver: the receiver must be materialized +// as a value before builtin dispatch (`.clone()` -> make_array_clone_call). First -> +// [10,20,30].clone() -> sum 60 + len 3 = 63. +fn select_value_method_receiver(node Node) !int { + cloned := (match node { + First { make_values_first(node)! } + Second { make_values_second(node)! } + }).clone() + mut sum := 0 + for v in cloned { + sum += v + } + return sum + cloned.len +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -753,6 +768,7 @@ fn main() { println(select_value_const_membership(First{})!) println(select_value_map_membership_order(First{})!) println(select_value_push_many(First{})!) + println(select_value_method_receiver(First{})!) println(select_value_addr(First{})!) } ') or { @@ -766,5 +782,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index fa18121eaf2453..cd8b1182978643 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14838,6 +14838,53 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if node.value.len > 0 && node.value == '__v_compile_error' { t.record_selected_compile_error_call(node) } + // Materialize a value `match`/`if` method receiver before builtin/method dispatch, so + // builtin lowerings (e.g. `(match node { ... }).clone()` -> make_array_clone_call, which + // lowers the receiver with plain `transform_expr`) receive a plain value temp rather than + // lowering the propagating arm tail in a value-less statement context. Rebuild the + // selector/call over the materialized receiver and re-dispatch; a no-op for the common + // non-branch receivers. + if node.children_count > 0 { + recv_fn_id := t.a.children[node.children_start] + recv_fn := t.a.nodes[int(recv_fn_id)] + if recv_fn.kind == .selector && recv_fn.children_count > 0 { + recv_id := t.a.children[recv_fn.children_start] + if t.is_value_match_or_if_operand(recv_id) { + new_recv := t.transform_value_operand(recv_id) + if new_recv != recv_id { + sel_start := t.a.children.len + t.a.children << new_recv + for i in 1 .. recv_fn.children_count { + t.a.children << t.a.child(&recv_fn, i) + } + new_fn_id := t.a.add_node(flat.Node{ + kind: .selector + op: recv_fn.op + value: recv_fn.value + typ: recv_fn.typ + children_start: sel_start + children_count: recv_fn.children_count + pos: recv_fn.pos + }) + call_start := t.a.children.len + t.a.children << new_fn_id + for i in 1 .. node.children_count { + t.a.children << t.a.child(&node, i) + } + new_call_id := t.a.add_node(flat.Node{ + kind: .call + op: node.op + value: node.value + typ: node.typ + children_start: call_start + children_count: node.children_count + pos: node.pos + }) + return t.transform_call_expr(new_call_id, t.a.nodes[int(new_call_id)]) + } + } + } + } if lowered := t.try_lower_bound_method_array_call(node) { return lowered } From 7986db14035d3c670dc9bc01a5d8bb2229dc7d56 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 19:13:23 +0300 Subject: [PATCH 16/37] v3: materialize value match/if method arguments before builtin dispatch (#28000) Addresses review feedback on #28011. Last commit materialized value match/if method receivers before builtin dispatch, but a value match passed as a builtin method argument was still lowered in statement context: e.g. `values.index(match node { First { lower_first(node)! } ... })` reaches lower_array_index_expr, whose needle is lowered through stable_expr_for_reuse (expr.v:2317; last_index at expr.v:2382) -> plain transform_expr -> empty expression. Extend the transform_call_expr materialization to also route value match/if arguments through transform_value_operand (receiver first, then args, in source order), rebuilding the selector/call over the materialized operands and re-dispatching. Gated on a value-branch receiver or argument, so all non-branch method calls are unchanged. Regression test select_value_method_arg: `values.index(match node { First { idx_needle_first(node)! } ... })` -> index of 20 in [10,20,30] = 1, encoded 1*1000 + len 3 = 1003. It fails to compile on HEAD (empty expression) without the fix. --- ...s_if_expr_value_propagation_codegen_test.v | 23 +++++- vlib/v3/transform/transform.v | 79 +++++++++++++------ 2 files changed, 78 insertions(+), 24 deletions(-) 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 0f4821e48184df..ac69afb8139206 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 @@ -711,6 +711,26 @@ fn select_value_method_receiver(node Node) !int { return sum + cloned.len } +fn idx_needle_first(_ First) !int { + return 20 +} + +fn idx_needle_second(_ Second) !int { + return 30 +} + +// Builtin method argument that is a value match: the needle must be materialized as +// a value before builtin dispatch (`values.index(match ...)` -> lower_array_index_expr). +// First -> index of 20 in [10,20,30] = 1, encoded as 1*1000 + len 3 = 1003. +fn select_value_method_arg(node Node) !int { + values := [10, 20, 30] + idx := values.index(match node { + First { idx_needle_first(node)! } + Second { idx_needle_second(node)! } + }) + return idx * 1000 + values.len +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -769,6 +789,7 @@ fn main() { println(select_value_map_membership_order(First{})!) println(select_value_push_many(First{})!) println(select_value_method_receiver(First{})!) + println(select_value_method_arg(First{})!) println(select_value_addr(First{})!) } ') or { @@ -782,5 +803,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index cd8b1182978643..36200ceea5ad19 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14838,38 +14838,71 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if node.value.len > 0 && node.value == '__v_compile_error' { t.record_selected_compile_error_call(node) } - // Materialize a value `match`/`if` method receiver before builtin/method dispatch, so - // builtin lowerings (e.g. `(match node { ... }).clone()` -> make_array_clone_call, which - // lowers the receiver with plain `transform_expr`) receive a plain value temp rather than + // Materialize value `match`/`if` method receivers and arguments before builtin/method + // dispatch, so builtin lowerings (e.g. `(match ...).clone()` -> make_array_clone_call, or + // `values.index(match ...)` -> lower_array_index_expr, which lower the receiver/needle with + // plain `transform_expr` / `stable_expr_for_reuse`) receive plain value temps rather than // lowering the propagating arm tail in a value-less statement context. Rebuild the - // selector/call over the materialized receiver and re-dispatch; a no-op for the common - // non-branch receivers. + // selector/call over the materialized operands and re-dispatch; a no-op for the common + // non-branch receivers/arguments. if node.children_count > 0 { recv_fn_id := t.a.children[node.children_start] recv_fn := t.a.nodes[int(recv_fn_id)] if recv_fn.kind == .selector && recv_fn.children_count > 0 { recv_id := t.a.children[recv_fn.children_start] - if t.is_value_match_or_if_operand(recv_id) { - new_recv := t.transform_value_operand(recv_id) - if new_recv != recv_id { - sel_start := t.a.children.len - t.a.children << new_recv - for i in 1 .. recv_fn.children_count { - t.a.children << t.a.child(&recv_fn, i) + recv_is_branch := t.is_value_match_or_if_operand(recv_id) + mut has_branch_arg := false + for i in 1 .. node.children_count { + if t.is_value_match_or_if_operand(t.a.child(&node, i)) { + has_branch_arg = true + break + } + } + if recv_is_branch || has_branch_arg { + // Evaluate the receiver before the arguments (source order), so their + // materialization preludes are queued in order. + mut changed := false + new_recv := if recv_is_branch { + r := t.transform_value_operand(recv_id) + if r != recv_id { + changed = true } - new_fn_id := t.a.add_node(flat.Node{ - kind: .selector - op: recv_fn.op - value: recv_fn.value - typ: recv_fn.typ - children_start: sel_start - children_count: recv_fn.children_count - pos: recv_fn.pos - }) + r + } else { + recv_id + } + sel_start := t.a.children.len + t.a.children << new_recv + for i in 1 .. recv_fn.children_count { + t.a.children << t.a.child(&recv_fn, i) + } + new_fn_id := t.a.add_node(flat.Node{ + kind: .selector + op: recv_fn.op + value: recv_fn.value + typ: recv_fn.typ + children_start: sel_start + children_count: recv_fn.children_count + pos: recv_fn.pos + }) + mut new_args := []flat.NodeId{cap: int(node.children_count)} + for i in 1 .. node.children_count { + arg_id := t.a.child(&node, i) + if t.is_value_match_or_if_operand(arg_id) { + na := t.transform_value_operand(arg_id) + if na != arg_id { + changed = true + } + new_args << na + } else { + new_args << arg_id + } + } + if changed { call_start := t.a.children.len t.a.children << new_fn_id - for i in 1 .. node.children_count { - t.a.children << t.a.child(&node, i) + for a in new_args { + t.a.children << a } new_call_id := t.a.add_node(flat.Node{ kind: .call From cdacd5e490f613bac9a76454831350a4f625db16 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 19:38:04 +0300 Subject: [PATCH 17/37] v3: materialize value match/if channel-send values and order call operands (#28000) Addresses review feedback on #28011. - Channel send with `or {}`: the `.arrow` fast path lowered the sent value with plain transform_expr, so `ch <- (match node { First { get_first(node)! } ... }) or { return }` lowered the propagating arm in statement context (empty expression). Route it through transform_value_operand, and detach the sent value's materialization prelude before transforming the `or {}` handler (so it isn't captured into the handler body) then re-queue it, so it is emitted before the channel send. - Call operand order: the transform_call_expr materialization guard hoisted a value-branch argument while leaving a preceding side-effecting receiver/argument inline, so `make_values(mut tr).index(match ...)` ran the needle prelude before make_values after re-dispatch. Stabilize each preceding non-stable operand (via stable_expr_for_reuse) before materializing a later branch operand, preserving source order; stable operands are left for the re-dispatch to transform once. Regression tests: select_value_channel_send (`ch <- (match ...) or { return -1 }` -> 42; fails to compile on HEAD) and select_value_call_operand_order (`make_index_values().index(match ...)` -> 2012, reversed 2021 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 51 +++++++++++++++- vlib/v3/transform/transform.v | 58 ++++++++++++++----- 2 files changed, 92 insertions(+), 17 deletions(-) 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 ac69afb8139206..a84e0d290822d6 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 @@ -731,6 +731,53 @@ fn select_value_method_arg(node Node) !int { return idx * 1000 + values.len } +fn ch_first(_ First) !int { + return 42 +} + +fn ch_second(_ Second) !int { + return 43 +} + +// Channel send with an `or {}` handler whose sent value is a value match: the +// propagating arm tail must be lowered as a value inside the `.arrow` fast path. +// First -> send 42, receive 42. +fn select_value_channel_send(node Node) !int { + ch := chan int{cap: 1} + ch <- (match node { + First { ch_first(node)! } + Second { ch_second(node)! } + }) or { return -1 } + return <-ch +} + +fn (mut tr Tracer) make_index_values() []int { + tr.order << 1 + return [10, 20, 30] +} + +fn (mut tr Tracer) needle_first(_ First) !int { + tr.order << 2 + return 30 +} + +fn (mut tr Tracer) needle_second(_ Second) !int { + tr.order << 2 + return 20 +} + +// Call operand ordering: a side-effecting receiver must run before the branch +// argument hoisted prelude. First -> index of 30 in [10,20,30] = 2, order [1,2] +// -> 2012 (a reversed order would be 2021). +fn select_value_call_operand_order(node Node) !int { + mut tr := Tracer{} + idx := tr.make_index_values().index(match node { + First { tr.needle_first(node)! } + Second { tr.needle_second(node)! } + }) + return idx * 1000 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -790,6 +837,8 @@ fn main() { println(select_value_push_many(First{})!) println(select_value_method_receiver(First{})!) println(select_value_method_arg(First{})!) + println(select_value_channel_send(First{})!) + println(select_value_call_operand_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -803,5 +852,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 36200ceea5ad19..08e798daca42b9 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14599,11 +14599,27 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat t.mark_fn_used('sync__Channel__try_push_priv') t.mark_fn_used('sync__Channel__closed_error') lhs := t.transform_expr(t.a.child(&node, 0)) - value := t.transform_expr(t.a.child(&rhs, 0)) + // Route a value `match`/`if` sent value through value lowering so its propagating + // arm tail is materialized as a value, e.g. + // `ch <- (match node { First { get_first(node)! } ... }) or { return }`. + // `transform_value_operand` is a no-op for the common non-branch sent values. + value_pending_start := t.pending_stmts.len + value := t.transform_value_operand(t.a.child(&rhs, 0)) + // Detach the sent value's materialization prelude so transforming the `or {}` + // handler below does not capture it into the handler body; re-queue it afterwards + // so it is emitted before the channel send. + mut value_prelude := []flat.NodeId{} + if t.pending_stmts.len > value_pending_start { + value_prelude = t.pending_stmts[value_pending_start..].clone() + t.pending_stmts = t.pending_stmts[..value_pending_start].clone() + } saved_var_types := t.var_types.clone() t.set_implicit_err_var_type() body := t.transform_expr(t.a.child(&rhs, 1)) t.restore_var_types(saved_var_types) + for stmt in value_prelude { + t.pending_stmts << stmt + } or_start := t.a.children.len t.a.children << value t.a.children << body @@ -14850,24 +14866,32 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. recv_fn := t.a.nodes[int(recv_fn_id)] if recv_fn.kind == .selector && recv_fn.children_count > 0 { recv_id := t.a.children[recv_fn.children_start] - recv_is_branch := t.is_value_match_or_if_operand(recv_id) - mut has_branch_arg := false + // Position of the last value-branch operand (0 = receiver, 1.. = arguments). + mut last_branch := if t.is_value_match_or_if_operand(recv_id) { 0 } else { -1 } for i in 1 .. node.children_count { if t.is_value_match_or_if_operand(t.a.child(&node, i)) { - has_branch_arg = true - break + last_branch = i } } - if recv_is_branch || has_branch_arg { - // Evaluate the receiver before the arguments (source order), so their - // materialization preludes are queued in order. + if last_branch >= 0 { + // Evaluate operands in source order (receiver, then arguments). A value branch + // is materialized into a value temp; a non-stable operand that precedes a later + // branch is stabilized to a temp first, so its side effects run before that + // branch's hoisted prelude (e.g. `make_values(mut tr).index(match ...)`). Stable + // operands are left for the re-dispatch to transform once. mut changed := false - new_recv := if recv_is_branch { + new_recv := if t.is_value_match_or_if_operand(recv_id) { r := t.transform_value_operand(recv_id) if r != recv_id { changed = true } r + } else if last_branch > 0 && !t.is_stable_expr_for_reuse(recv_id) { + r := t.stable_expr_for_reuse(recv_id) + if r != recv_id { + changed = true + } + r } else { recv_id } @@ -14888,15 +14912,17 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. mut new_args := []flat.NodeId{cap: int(node.children_count)} for i in 1 .. node.children_count { arg_id := t.a.child(&node, i) - if t.is_value_match_or_if_operand(arg_id) { - na := t.transform_value_operand(arg_id) - if na != arg_id { - changed = true - } - new_args << na + na := if t.is_value_match_or_if_operand(arg_id) { + t.transform_value_operand(arg_id) + } else if i < last_branch && !t.is_stable_expr_for_reuse(arg_id) { + t.stable_expr_for_reuse(arg_id) } else { - new_args << arg_id + arg_id + } + if na != arg_id { + changed = true } + new_args << na } if changed { call_start := t.a.children.len From 44279af4d5390581f8ead7bbabe1f2f2f8ea9c86 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 19:59:17 +0300 Subject: [PATCH 18/37] v3: preserve lvalue receivers and stabilize channel targets for match/if operands (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - Mutable receiver identity: the transform_call_expr materialization guard stabilized a non-stable receiver via stable_expr_for_reuse, which declares a temp holding the receiver's *value* — so a mutable receiver like `items[next()].update(match ...)` mutated the copy and lost its state change. Add stabilize_original_lvalue_receiver, which spills only the receiver lvalue's dynamic base/index components (on the untransformed node, so the re-dispatch transforms it once) while preserving the lvalue shape; a non-lvalue (rvalue call) receiver is still spilled by value. - Channel target order: in the `<- ... or {}` fast path, a side-effecting channel target was left inline while the sent value's prelude was hoisted, so `channels[next(mut trace)] <- (match ...) or {}` ran the value prelude before the target index. Stabilize the target's dynamic base/index components (via stabilize_transformed_lvalue_for_reuse) before materializing the sent value, and detach the whole send prelude (target + value) before transforming the `or {}` handler so it is not captured into the handler body, re-queuing it before the send. Regression tests: select_value_mut_receiver (`items[tr.pick_index()].add(match ...)` -> 4512; mutation lost is 4012 on HEAD) and select_value_channel_target_order (`channels[tr.ch_index()] <- (match ...) or {}` -> 9912; reversed 9921 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 75 ++++++++++- vlib/v3/transform/transform.v | 122 ++++++++++++++++-- 2 files changed, 184 insertions(+), 13 deletions(-) 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 a84e0d290822d6..9be68e876412ea 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 @@ -778,6 +778,77 @@ fn select_value_call_operand_order(node Node) !int { return idx * 1000 + tr.order[0] * 10 + tr.order[1] } +struct MutItem { +mut: + v int +} + +fn (mut m MutItem) add(x int) { + m.v += x +} + +fn (mut tr Tracer) pick_index() int { + tr.order << 1 + return 0 +} + +fn (mut tr Tracer) add_first(_ First) !int { + tr.order << 2 + return 5 +} + +fn (mut tr Tracer) add_second(_ Second) !int { + tr.order << 2 + return 7 +} + +// Mutable receiver with a value-match argument: the receiver lvalue identity must +// be preserved (only its index is stabilized), so the mutation reaches items[0], and +// the index runs before the argument prelude. First -> items[0].v = 40 + 5 = 45, +// order [1,2] -> 4512 (mutation lost would be 4012; reversed order 4521). +fn select_value_mut_receiver(node Node) !int { + mut tr := Tracer{} + mut items := [MutItem{ + v: 40 + }, MutItem{ + v: 50 + }] + items[tr.pick_index()].add(match node { + First { tr.add_first(node)! } + Second { tr.add_second(node)! } + }) + return items[0].v * 100 + tr.order[0] * 10 + tr.order[1] +} + +fn (mut tr Tracer) ch_index() int { + tr.order << 1 + return 0 +} + +fn (mut tr Tracer) ch_rhs_first(_ First) !int { + tr.order << 2 + return 99 +} + +fn (mut tr Tracer) ch_rhs_second(_ Second) !int { + tr.order << 2 + return 88 +} + +// Channel target with a side-effecting index and a value-match sent value: the +// target index must run before the sent value prelude. First -> send 99, order [1,2] +// -> 9912 (reversed order 9921). +fn select_value_channel_target_order(node Node) !int { + mut tr := Tracer{} + mut channels := [chan int{cap: 1}, chan int{cap: 1}] + channels[tr.ch_index()] <- (match node { + First { tr.ch_rhs_first(node)! } + Second { tr.ch_rhs_second(node)! } + }) or { return -1 } + got := <-channels[0] + return got * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -839,6 +910,8 @@ fn main() { println(select_value_method_arg(First{})!) println(select_value_channel_send(First{})!) println(select_value_call_operand_order(First{})!) + println(select_value_mut_receiver(First{})!) + println(select_value_channel_target_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -852,5 +925,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 08e798daca42b9..024fdfabd4998d 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9933,6 +9933,88 @@ fn (mut t Transformer) rebuild_transformed_lvalue(node flat.Node, children []fla }) } +// stabilize_original_lvalue_receiver spills the non-stable dynamic index/base components of +// an *untransformed* lvalue receiver into temps while preserving the lvalue shape and its +// untransformed base, so the caller's re-dispatch transforms the receiver exactly once and a +// mutable receiver keeps its identity (e.g. `items[next()].update(...)` still mutates +// `items[next()]`). Returns none for a non-lvalue (rvalue) receiver, which the caller spills +// by value instead. +fn (mut t Transformer) stabilize_original_lvalue_receiver(id flat.NodeId) ?flat.NodeId { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return none + } + node := t.a.nodes[int(id)] + match node.kind { + .ident { + return id + } + .paren { + if node.children_count == 0 { + return none + } + inner := t.stabilize_original_lvalue_receiver(t.a.child(&node, 0))? + return t.rebuild_transformed_lvalue(node, arr1(inner)) + } + .prefix { + if node.op != .mul || node.children_count == 0 { + return none + } + child_id := t.a.child(&node, 0) + new_child := if t.is_stable_expr_for_reuse(child_id) { + child_id + } else { + t.spill_original_lvalue_component(child_id, 'recv_deref') + } + return t.rebuild_transformed_lvalue(node, arr1(new_child)) + } + .selector { + if node.children_count == 0 { + return none + } + base := t.stabilize_original_lvalue_receiver(t.a.child(&node, 0))? + mut children := [base] + for i in 1 .. node.children_count { + children << t.a.child(&node, i) + } + return t.rebuild_transformed_lvalue(node, children) + } + .index { + if node.children_count == 0 { + return none + } + base := t.stabilize_original_lvalue_receiver(t.a.child(&node, 0))? + mut children := [base] + for i in 1 .. node.children_count { + comp_id := t.a.child(&node, i) + children << if t.is_stable_expr_for_reuse(comp_id) { + comp_id + } else { + t.spill_original_lvalue_component(comp_id, 'recv_index') + } + } + return t.rebuild_transformed_lvalue(node, children) + } + else { + return none + } + } +} + +fn (mut t Transformer) spill_original_lvalue_component(id flat.NodeId, prefix string) flat.NodeId { + transformed := t.transform_expr(id) + tmp_name := t.new_temp(prefix) + mut typ := t.node_type(transformed) + if typ.len == 0 { + typ = t.node_type(id) + } + if typ.len > 0 { + t.pending_stmts << t.make_decl_assign_typed(tmp_name, transformed, typ) + } else { + t.pending_stmts << t.make_decl_assign(tmp_name, transformed) + } + return t.make_ident(tmp_name) +} + fn (mut t Transformer) invalidate_smartcast_for_lvalue(id flat.NodeId) { key := t.expr_key(id) if key.len == 0 || t.smartcast_stack.len == 0 { @@ -14598,26 +14680,34 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat if rhs.kind == .or_expr && rhs.children_count >= 2 { t.mark_fn_used('sync__Channel__try_push_priv') t.mark_fn_used('sync__Channel__closed_error') - lhs := t.transform_expr(t.a.child(&node, 0)) + send_prelude_start := t.pending_stmts.len + mut lhs := t.transform_expr(t.a.child(&node, 0)) + sent_value_id := t.a.child(&rhs, 0) + // Stabilize the channel target's dynamic base/index components before materializing + // a value-branch sent value, so a side-effecting target index (e.g. + // `channels[next(mut trace)] <- (match ...) or {}`) evaluates before the sent value's + // hoisted prelude. Preserves the lvalue shape without spilling the channel value. + if t.is_value_match_or_if_operand(sent_value_id) { + lhs = t.stabilize_transformed_lvalue_for_reuse(lhs) + } // Route a value `match`/`if` sent value through value lowering so its propagating // arm tail is materialized as a value, e.g. // `ch <- (match node { First { get_first(node)! } ... }) or { return }`. // `transform_value_operand` is a no-op for the common non-branch sent values. - value_pending_start := t.pending_stmts.len - value := t.transform_value_operand(t.a.child(&rhs, 0)) - // Detach the sent value's materialization prelude so transforming the `or {}` - // handler below does not capture it into the handler body; re-queue it afterwards - // so it is emitted before the channel send. - mut value_prelude := []flat.NodeId{} - if t.pending_stmts.len > value_pending_start { - value_prelude = t.pending_stmts[value_pending_start..].clone() - t.pending_stmts = t.pending_stmts[..value_pending_start].clone() + value := t.transform_value_operand(sent_value_id) + // Detach the channel target + sent value materialization prelude so transforming the + // `or {}` handler below does not capture it into the handler body; re-queue it + // afterwards so it is emitted before the channel send (target index before value). + mut send_prelude := []flat.NodeId{} + if t.pending_stmts.len > send_prelude_start { + send_prelude = t.pending_stmts[send_prelude_start..].clone() + t.pending_stmts = t.pending_stmts[..send_prelude_start].clone() } saved_var_types := t.var_types.clone() t.set_implicit_err_var_type() body := t.transform_expr(t.a.child(&rhs, 1)) t.restore_var_types(saved_var_types) - for stmt in value_prelude { + for stmt in send_prelude { t.pending_stmts << stmt } or_start := t.a.children.len @@ -14887,7 +14977,15 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. } r } else if last_branch > 0 && !t.is_stable_expr_for_reuse(recv_id) { - r := t.stable_expr_for_reuse(recv_id) + // Stabilize a side-effecting receiver before a later branch argument hoists its + // prelude. Preserve the lvalue's identity by spilling only its dynamic base/index + // components (so a mutable receiver like `items[next()].update(match ...)` still + // mutates `items[next()]`); a non-lvalue (rvalue call) receiver is spilled by value. + r := if stabilized := t.stabilize_original_lvalue_receiver(recv_id) { + stabilized + } else { + t.stable_expr_for_reuse(recv_id) + } if r != recv_id { changed = true } From 391aee81634462034c7585c1c2a5bc15b9bc14bf Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 20:17:08 +0300 Subject: [PATCH 19/37] v3: order plain-call operands, preserve mut arg lvalues, lower array-init fields (#28000) Addresses review feedback on #28011. - Plain function calls: the pre-dispatch ordering guard only handled selector callees, so a plain call with a side-effecting argument before a value-branch argument left the earlier argument inline while the branch queued its prelude (`combine(tr.first(), match ...)` ran the match arm before first()). Apply the same source-order stabilization to non-method calls. - Mutable argument lvalues: a non-stable argument preceding a value-branch argument was spilled by value, so a `mut` lvalue argument like `helper.apply(mut items[next()], match ...)` passed a copy and lost mutations. Stabilize only the lvalue dynamic components (as for the receiver) and preserve is_mut when rebuilding the lvalue, so the mutation still reaches items[next()]. - Array initializer fields: lower_array_init_to_runtime lowered len/cap/init with plain transform_expr, so `[]int{len: match ...}` lowered the propagating arm in statement context (empty expression). Route them through transform_expr_for_type (typed value lowering). Regression tests: select_value_plain_call_order (3412; reversed 3421 on HEAD), select_value_mut_arg (7612; mutation lost 7012 on HEAD), and select_value_array_init (4004; fails to compile on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 99 ++++++++++++++++- vlib/v3/transform/array.v | 10 +- vlib/v3/transform/transform.v | 104 +++++++++--------- 3 files changed, 160 insertions(+), 53 deletions(-) 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 9be68e876412ea..c8aed0024ae286 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 @@ -849,6 +849,100 @@ fn select_value_channel_target_order(node Node) !int { return got * 100 + tr.order[0] * 10 + tr.order[1] } +fn combine(a int, b int) int { + return a * 10 + b +} + +fn (mut tr Tracer) first_arg() int { + tr.order << 1 + return 3 +} + +fn (mut tr Tracer) second_arg_first(_ First) !int { + tr.order << 2 + return 4 +} + +fn (mut tr Tracer) second_arg_second(_ Second) !int { + tr.order << 2 + return 5 +} + +// Plain function call ordering: a side-effecting first argument must run before the +// value-match second argument prelude. First -> combine(3, 4) = 34, order [1,2] -> 3412 +// (a reversed order would be 3421). +fn select_value_plain_call_order(node Node) !int { + mut tr := Tracer{} + r := combine(tr.first_arg(), match node { + First { tr.second_arg_first(node)! } + Second { tr.second_arg_second(node)! } + }) + return r * 100 + tr.order[0] * 10 + tr.order[1] +} + +struct Holder2 { +mut: + v int +} + +struct Helper {} + +fn (h Helper) apply(mut item Holder2, x int) { + item.v += x +} + +fn (mut tr Tracer) which_index() int { + tr.order << 1 + return 0 +} + +fn (mut tr Tracer) delta_first(_ First) !int { + tr.order << 2 + return 6 +} + +fn (mut tr Tracer) delta_second(_ Second) !int { + tr.order << 2 + return 8 +} + +// Mutable argument lvalue before a value-match argument: the mut lvalue identity must +// be preserved (only its index stabilized), so the mutation reaches holders[0], and the +// index runs before the argument prelude. First -> holders[0].v = 70 + 6 = 76, order +// [1,2] -> 7612 (mutation lost would be 7012; reversed order 7621). +fn select_value_mut_arg(node Node) !int { + mut tr := Tracer{} + helper := Helper{} + mut holders := [Holder2{ + v: 70 + }, Holder2{ + v: 80 + }] + helper.apply(mut holders[tr.which_index()], match node { + First { tr.delta_first(node)! } + Second { tr.delta_second(node)! } + }) + return holders[0].v * 100 + tr.order[0] * 10 + tr.order[1] +} + +fn len_first(_ First) !int { + return 4 +} + +fn len_second(_ Second) !int { + return 6 +} + +// Array initializer field that is a value match: the len field must be lowered as a +// value. First -> []int{len: 4}, so arr.len = 4, encoded 4*1000 + 4 = 4004. +fn select_value_array_init(node Node) !int { + arr := []int{len: match node { + First { len_first(node)! } + Second { len_second(node)! } + }} + return arr.len * 1000 + arr.len +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -912,6 +1006,9 @@ fn main() { println(select_value_call_operand_order(First{})!) println(select_value_mut_receiver(First{})!) println(select_value_channel_target_order(First{})!) + println(select_value_plain_call_order(First{})!) + println(select_value_mut_arg(First{})!) + println(select_value_array_init(First{})!) println(select_value_addr(First{})!) } ') or { @@ -925,5 +1022,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n7612\n4004\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 0031a61c2e9bb0..193cd0ecffa673 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -392,10 +392,13 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod child := t.a.child_node(&node, i) if child.kind == .field_init && child.children_count > 0 { if child.value == 'len' { - val := t.transform_expr(t.a.child(child, 0)) + // Typed value lowering so a value `match`/`if` len field (e.g. + // `[]int{len: match node { ... lower(node)! ... }}`) is materialized as a + // value instead of lowering its propagating arm in a statement context. + val := t.transform_expr_for_type(t.a.child(child, 0), 'int') len_expr = val } else if child.value == 'cap' { - val := t.transform_expr(t.a.child(child, 0)) + val := t.transform_expr_for_type(t.a.child(child, 0), 'int') cap_expr = val } else if child.value == 'init' { init_expr_id = t.a.child(child, 0) @@ -437,7 +440,8 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod saved_pending := t.pending_stmts.clone() t.pending_stmts.clear() indexed_init := t.substitute_ident_expr(init_expr_id, 'index', t.make_ident(idx_name)) - init_expr = t.transform_expr(indexed_init) + // Typed value lowering so a value `match`/`if` init field is materialized as a value. + init_expr = t.transform_expr_for_type(indexed_init, elem_type) init_pending := t.pending_stmts.clone() t.pending_stmts = saved_pending for stmt in init_pending { diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 024fdfabd4998d..e2f9d377667720 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9930,6 +9930,7 @@ fn (mut t Transformer) rebuild_transformed_lvalue(node flat.Node, children []fla pos: node.pos value: node.value typ: node.typ + is_mut: node.is_mut }) } @@ -14954,22 +14955,27 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if node.children_count > 0 { recv_fn_id := t.a.children[node.children_start] recv_fn := t.a.nodes[int(recv_fn_id)] - if recv_fn.kind == .selector && recv_fn.children_count > 0 { - recv_id := t.a.children[recv_fn.children_start] - // Position of the last value-branch operand (0 = receiver, 1.. = arguments). - mut last_branch := if t.is_value_match_or_if_operand(recv_id) { 0 } else { -1 } - for i in 1 .. node.children_count { - if t.is_value_match_or_if_operand(t.a.child(&node, i)) { - last_branch = i - } - } - if last_branch >= 0 { - // Evaluate operands in source order (receiver, then arguments). A value branch - // is materialized into a value temp; a non-stable operand that precedes a later - // branch is stabilized to a temp first, so its side effects run before that - // branch's hoisted prelude (e.g. `make_values(mut tr).index(match ...)`). Stable - // operands are left for the re-dispatch to transform once. - mut changed := false + is_method := recv_fn.kind == .selector && recv_fn.children_count > 0 + recv_id := if is_method { t.a.children[recv_fn.children_start] } else { flat.empty_node } + // Position of the last value-branch operand (0 = method receiver, 1.. = arguments). + mut last_branch := if is_method && t.is_value_match_or_if_operand(recv_id) { 0 } else { -1 } + for i in 1 .. node.children_count { + if t.is_value_match_or_if_operand(t.a.child(&node, i)) { + last_branch = i + } + } + if last_branch >= 0 { + // Evaluate operands in source order (method receiver, then arguments). A value branch + // is materialized into a value temp; a non-stable operand that precedes a later branch + // is stabilized first so its side effects run before that branch's hoisted prelude. + // Stabilization preserves an lvalue's identity (spilling only its dynamic base/index + // components, so `mut`/mutable operands still mutate through, e.g. + // `items[next()].update(match ...)` or `apply(mut items[next()], match ...)`); an rvalue + // is spilled by value. Applies to method calls and plain function calls alike. Stable + // operands are left for the re-dispatch to transform once. + mut changed := false + mut new_fn_id := recv_fn_id + if is_method { new_recv := if t.is_value_match_or_if_operand(recv_id) { r := t.transform_value_operand(recv_id) if r != recv_id { @@ -14977,10 +14983,6 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. } r } else if last_branch > 0 && !t.is_stable_expr_for_reuse(recv_id) { - // Stabilize a side-effecting receiver before a later branch argument hoists its - // prelude. Preserve the lvalue's identity by spilling only its dynamic base/index - // components (so a mutable receiver like `items[next()].update(match ...)` still - // mutates `items[next()]`); a non-lvalue (rvalue call) receiver is spilled by value. r := if stabilized := t.stabilize_original_lvalue_receiver(recv_id) { stabilized } else { @@ -14998,7 +15000,7 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. for i in 1 .. recv_fn.children_count { t.a.children << t.a.child(&recv_fn, i) } - new_fn_id := t.a.add_node(flat.Node{ + new_fn_id = t.a.add_node(flat.Node{ kind: .selector op: recv_fn.op value: recv_fn.value @@ -15007,38 +15009,42 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. children_count: recv_fn.children_count pos: recv_fn.pos }) - mut new_args := []flat.NodeId{cap: int(node.children_count)} - for i in 1 .. node.children_count { - arg_id := t.a.child(&node, i) - na := if t.is_value_match_or_if_operand(arg_id) { - t.transform_value_operand(arg_id) - } else if i < last_branch && !t.is_stable_expr_for_reuse(arg_id) { - t.stable_expr_for_reuse(arg_id) + } + mut new_args := []flat.NodeId{cap: int(node.children_count)} + for i in 1 .. node.children_count { + arg_id := t.a.child(&node, i) + na := if t.is_value_match_or_if_operand(arg_id) { + t.transform_value_operand(arg_id) + } else if i < last_branch && !t.is_stable_expr_for_reuse(arg_id) { + if stabilized := t.stabilize_original_lvalue_receiver(arg_id) { + stabilized } else { - arg_id - } - if na != arg_id { - changed = true + t.stable_expr_for_reuse(arg_id) } - new_args << na + } else { + arg_id } - if changed { - call_start := t.a.children.len - t.a.children << new_fn_id - for a in new_args { - t.a.children << a - } - new_call_id := t.a.add_node(flat.Node{ - kind: .call - op: node.op - value: node.value - typ: node.typ - children_start: call_start - children_count: node.children_count - pos: node.pos - }) - return t.transform_call_expr(new_call_id, t.a.nodes[int(new_call_id)]) + if na != arg_id { + changed = true + } + new_args << na + } + if changed { + call_start := t.a.children.len + t.a.children << new_fn_id + for a in new_args { + t.a.children << a } + new_call_id := t.a.add_node(flat.Node{ + kind: .call + op: node.op + value: node.value + typ: node.typ + children_start: call_start + children_count: node.children_count + pos: node.pos + }) + return t.transform_call_expr(new_call_id, t.a.nodes[int(new_call_id)]) } } } From 824b56491fe0b27e3db9267789ed0869449aacf5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 20:33:44 +0300 Subject: [PATCH 20/37] v3: spill non-mut lvalue args by value and order array-init len before cap (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - Non-mut lvalue arguments: the argument-stabilization path used lvalue-preserving stabilization (spilling only the dynamic components) for every non-stable argument, so an ordinary non-`mut` argument like `items[next()]` had its value loaded at the call — after a later value-branch prelude that mutates `items`, the callee observed the mutated value rather than the source-order value. Reserve lvalue-preserving stabilization for `mut` arguments (they must mutate through); spill ordinary arguments by value so their value is read in source order. - Array initializer field order: `lower_array_init_to_runtime` transformed `len` and `cap` into `new_call`, so a value-`match`/`if` `cap` hoisted its prelude before an inline side-effecting `len` (`[]int{len: trace_len(), cap: match ...}` ran trace_cap before trace_len). Stabilize an earlier len/cap field to a temp before a later len/cap field hoists its prelude. Regression tests: select_value_nonmut_arg_value (`take(m.items[m.idx()], match ... { mutate } ...)` -> 507; leaked-mutation 99907 on HEAD) and select_value_array_init_cap_order (`[]int{len: tr.tlen(), cap: match ...}` -> 212; reversed 221 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 67 ++++++++++++++++++- vlib/v3/transform/array.v | 23 ++++++- vlib/v3/transform/transform.v | 12 +++- 3 files changed, 97 insertions(+), 5 deletions(-) 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 c8aed0024ae286..6cb9e909985ca6 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 @@ -943,6 +943,69 @@ fn select_value_array_init(node Node) !int { return arr.len * 1000 + arr.len } +struct Mutator { +mut: + items []int +} + +fn (mut m Mutator) idx() int { + return 0 +} + +fn (mut m Mutator) bump_first(_ First) !int { + m.items[0] = 999 + return 7 +} + +fn (mut m Mutator) bump_second(_ Second) !int { + m.items[0] = 888 + return 8 +} + +fn take(a int, b int) int { + return a * 100 + b +} + +// Ordinary (non-mut) lvalue argument (non-stable index) before a value-match argument +// whose prelude mutates the container: the argument value must be read in source order, +// before the mutation. First -> take(items[idx()]=5, 7) = 507 (if the mutated 999 leaked +// in it would be 99907). +fn select_value_nonmut_arg_value(node Node) !int { + mut m := Mutator{ + items: [5, 6] + } + return take(m.items[m.idx()], match node { + First { m.bump_first(node)! } + Second { m.bump_second(node)! } + }) +} + +fn (mut tr Tracer) tlen() int { + tr.order << 1 + return 2 +} + +fn (mut tr Tracer) tcap_first(_ First) !int { + tr.order << 2 + return 4 +} + +fn (mut tr Tracer) tcap_second(_ Second) !int { + tr.order << 2 + return 6 +} + +// Array initializer field ordering: a side-effecting len must run before a value-match +// cap prelude. First -> len 2, order [1,2] -> 212 (a reversed order would be 221). +fn select_value_array_init_cap_order(node Node) !int { + mut tr := Tracer{} + arr := []int{len: tr.tlen(), cap: match node { + First { tr.tcap_first(node)! } + Second { tr.tcap_second(node)! } + }} + return arr.len * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -1009,6 +1072,8 @@ fn main() { println(select_value_plain_call_order(First{})!) println(select_value_mut_arg(First{})!) println(select_value_array_init(First{})!) + println(select_value_nonmut_arg_value(First{})!) + println(select_value_array_init_cap_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1022,5 +1087,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n7612\n4004\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n7612\n4004\n507\n212\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 193cd0ecffa673..5e6d8f8645ae1b 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -388,6 +388,19 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod mut cap_expr := t.make_int_literal(0) mut init_expr := flat.empty_node mut init_expr_id := flat.empty_node + // Source (child) position of the last `len`/`cap` field whose value is a value branch, + // so an earlier side-effecting `len`/`cap` field can be stabilized before that field + // hoists its materialization prelude, preserving field evaluation order (both are + // evaluated into `new_call` below; `init` is per-element in the loop body). + mut last_lencap_branch := -1 + for i in 0 .. node.children_count { + child := t.a.child_node(&node, i) + if child.kind == .field_init && child.children_count > 0 && child.value in ['len', 'cap'] { + if t.is_value_match_or_if_operand(t.a.child(child, 0)) { + last_lencap_branch = i + } + } + } for i in 0 .. node.children_count { child := t.a.child_node(&node, i) if child.kind == .field_init && child.children_count > 0 { @@ -395,10 +408,16 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod // Typed value lowering so a value `match`/`if` len field (e.g. // `[]int{len: match node { ... lower(node)! ... }}`) is materialized as a // value instead of lowering its propagating arm in a statement context. - val := t.transform_expr_for_type(t.a.child(child, 0), 'int') + mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int') + if i < last_lencap_branch && !t.is_stable_expr_for_reuse(val) { + val = t.stable_transformed_expr_for_reuse(val, 'int', 'arr_len') + } len_expr = val } else if child.value == 'cap' { - val := t.transform_expr_for_type(t.a.child(child, 0), 'int') + mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int') + if i < last_lencap_branch && !t.is_stable_expr_for_reuse(val) { + val = t.stable_transformed_expr_for_reuse(val, 'int', 'arr_cap') + } cap_expr = val } else if child.value == 'init' { init_expr_id = t.a.child(child, 0) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index e2f9d377667720..3b5f426eefeeeb 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15016,8 +15016,16 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. na := if t.is_value_match_or_if_operand(arg_id) { t.transform_value_operand(arg_id) } else if i < last_branch && !t.is_stable_expr_for_reuse(arg_id) { - if stabilized := t.stabilize_original_lvalue_receiver(arg_id) { - stabilized + // A `mut` argument keeps its lvalue identity (only its dynamic base/index + // components are spilled) so it still mutates through. An ordinary argument is + // spilled by value, so its value is read in source order — a later branch + // prelude that mutates its container cannot change the observed value. + if t.a.nodes[int(arg_id)].is_mut { + if stabilized := t.stabilize_original_lvalue_receiver(arg_id) { + stabilized + } else { + t.stable_expr_for_reuse(arg_id) + } } else { t.stable_expr_for_reuse(arg_id) } From f021bb90e0a3e4f18b854f8e3018d884bde9b449 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 20:50:41 +0300 Subject: [PATCH 21/37] v3: detect nested value branches when ordering call operands (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. The last_branch scan that decides which preceding call operands to stabilize for source order used is_value_match_or_if_operand, which only looks through paren/unsafe/ block wrappers and stops at the outer expression. When a later argument buries the value branch inside a compound expression — e.g. combine(tr.first_arg(), 1 + (match node { ... })) or i64(match ...) — the scan left last_branch at -1, so the guard did not fire: the compound argument still materialized the inner branch into pending_stmts while the first argument stayed inline, so the second argument prelude ran before first_arg(). Add operand_hoists_value_branch, which recursively reports whether lowering an operand can hoist a value match/if (directly or nested in a compound operand), stopping at closure/lambda/spawn boundaries that materialize into their own scope. Over-detection is safe: it only spills an extra preceding operand to a temp, which is order-preserving. Use it in the operand-ordering scan. Regression test select_value_nested_branch_arg_order: combine(tr.first_arg(), 1 + (match node { First { tr.second_arg_first(node)! } ... })) -> 3512 (order [1,2]); on HEAD the nested match prelude runs first -> 3521. --- ...s_if_expr_value_propagation_codegen_test.v | 17 ++++++++- vlib/v3/transform/transform.v | 37 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) 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 6cb9e909985ca6..3c98b1fa35def4 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 @@ -880,6 +880,20 @@ fn select_value_plain_call_order(node Node) !int { return r * 100 + tr.order[0] * 10 + tr.order[1] } +// Plain call ordering with a *nested* value branch: the match is buried inside a compound +// second argument (`1 + (match ...)`), which still materializes the branch prelude into +// pending_stmts. The side-effecting first argument must run before that prelude. +// First -> combine(3, 1 + 4) = combine(3, 5) = 35, order [1,2] -> 3512 +// (a reversed order, the match prelude before first_arg, would be 3521). +fn select_value_nested_branch_arg_order(node Node) !int { + mut tr := Tracer{} + r := combine(tr.first_arg(), 1 + (match node { + First { tr.second_arg_first(node)! } + Second { tr.second_arg_second(node)! } + })) + return r * 100 + tr.order[0] * 10 + tr.order[1] +} + struct Holder2 { mut: v int @@ -1070,6 +1084,7 @@ fn main() { println(select_value_mut_receiver(First{})!) println(select_value_channel_target_order(First{})!) println(select_value_plain_call_order(First{})!) + println(select_value_nested_branch_arg_order(First{})!) println(select_value_mut_arg(First{})!) println(select_value_array_init(First{})!) println(select_value_nonmut_arg_value(First{})!) @@ -1087,5 +1102,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n7612\n4004\n507\n212\n1' + 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\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n3512\n7612\n4004\n507\n212\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 3b5f426eefeeeb..00cfbeb454ab36 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14957,10 +14957,14 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. recv_fn := t.a.nodes[int(recv_fn_id)] is_method := recv_fn.kind == .selector && recv_fn.children_count > 0 recv_id := if is_method { t.a.children[recv_fn.children_start] } else { flat.empty_node } - // Position of the last value-branch operand (0 = method receiver, 1.. = arguments). + // Position of the last operand that hoists a value branch (0 = method receiver, + // 1.. = arguments). An argument counts even when the branch is nested inside a + // compound expression (`1 + (match ...)`, `i64(match ...)`): lowering it still + // materializes the inner branch into pending_stmts, so an earlier operand must be + // stabilized to keep source order. mut last_branch := if is_method && t.is_value_match_or_if_operand(recv_id) { 0 } else { -1 } for i in 1 .. node.children_count { - if t.is_value_match_or_if_operand(t.a.child(&node, i)) { + if t.operand_hoists_value_branch(t.a.child(&node, i)) { last_branch = i } } @@ -17563,6 +17567,35 @@ fn (t &Transformer) is_value_match_or_if_operand(id flat.NodeId) bool { return node.kind in [.match_stmt, .if_expr] } +// operand_hoists_value_branch reports whether lowering `id` as a call operand (receiver or +// argument) can materialize a value `match`/`if` into pending_stmts — either directly, or +// nested inside a compound expression such as an infix, cast, index, prefix, nested call or +// composite literal (`1 + (match ...)`, `i64(match ...)`, `arr[match ...]`). The `last_branch` +// scan uses this to detect an operand that hoists a prelude so preceding operands can be +// stabilized for source order; `is_value_match_or_if_operand` alone stops at the outer +// wrapper and misses a branch buried inside such a compound operand. Recursion stops at +// constructs that lower into their own scope — a nested closure/lambda/spawn body materializes +// into that body, not the current pending. Over-detection is safe here: it only spills an +// extra preceding operand to a temp, which is always order-preserving. +fn (t &Transformer) operand_hoists_value_branch(id flat.NodeId) bool { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return false + } + node := t.a.nodes[int(id)] + if node.kind in [.match_stmt, .if_expr] { + return true + } + if node.kind in [.fn_literal, .lambda_expr, .spawn_expr] { + return false + } + for i in 0 .. node.children_count { + if t.operand_hoists_value_branch(t.a.child(&node, i)) { + return true + } + } + return false +} + // 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 { From bef373fa080dcc845b0f5da0dbe009faa778750a Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 21:05:03 +0300 Subject: [PATCH 22/37] v3: detect nested value branches in append/index/array-init operand ordering (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. Three operand-ordering scans still used is_value_match_or_if_operand, which only looks through paren/unsafe/block wrappers and stops at the outer expression, so a value branch buried inside a compound operand was missed while lowering it still hoisted the branch prelude into pending_stmts — reversing evaluation order against a preceding inline side effect. Switch each to the recursive operand_hoists_value_branch (added in f021bb9): - array.v array-append: `arrays[next()] << wrap(match ...)` — the wrapped RHS left the LHS index unstabilized, so it ran after the RHS prelude. - transform.v index read: `make_values()[1 + (match ...)]` — the compound index left last_value_branch unset, so the base was not stabilized before the index prelude. - array.v array-init len/cap: `[]int{len: trace_len(), cap: 1 + (match ...)}` — the compound cap left last_lencap_branch unset, so len stayed inline and ran after cap. Regression tests: select_value_nested_index_order (base[1 + (match ...)] -> 2012; reversed 2021), select_value_nested_append_order (arrays[i] << wrap_append(match ...) -> 812; reversed 821), select_value_nested_cap_order ([]int{len:.., cap: 1 + (match ...)} -> 312; reversed 321). Each fails with the reversed value when its scan is reverted. --- ...s_if_expr_value_propagation_codegen_test.v | 54 ++++++++++++++++++- vlib/v3/transform/array.v | 23 ++++---- vlib/v3/transform/transform.v | 7 +-- 3 files changed, 69 insertions(+), 15 deletions(-) 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 3c98b1fa35def4..6ecae1b2f6275b 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 @@ -449,6 +449,19 @@ fn select_value_index_order(node Node) !int { return val * 100 + tr.order[0] * 10 + tr.order[1] } +// Index ordering with a *nested* branch: the match is buried inside a compound index +// (`base[1 + (match ...)]`), whose infix lowering still hoists the match prelude. The +// side-effecting base must run before that prelude. First -> base_values()[1 + 0] = 20, +// order [1,2] -> 2012 (a reversed order would be 2021). +fn select_value_nested_index_order(node Node) !int { + mut tr := Tracer{} + val := tr.base_values()[1 + (match node { + First { tr.idx_first(node)! } + Second { tr.idx_second(node)! } + })] + return val * 100 + tr.order[0] * 10 + tr.order[1] +} + fn (mut tr Tracer) gated_first(_ First) !int { tr.order << 2 return -1 @@ -629,6 +642,24 @@ fn select_value_append_order(node Node) !int { return arrays[0][0] * 100 + tr.order[0] * 10 + tr.order[1] } +fn wrap_append(x int) int { + return x + 1 +} + +// Array-append ordering with a *nested* RHS branch: the match is wrapped inside a call +// (`arrays[i] << wrap_append(match ...)`), which still hoists the match prelude. The +// side-effecting LHS index must run before that prelude while the array is not spilled. +// First -> arrays[0] << 8, order [1,2] -> 812 (a reversed order would be 821). +fn select_value_nested_append_order(node Node) !int { + mut tr := Tracer{} + mut arrays := [[]int{}, []int{}] + arrays[tr.next_index()] << wrap_append(match node { + First { tr.append_val_first(node)! } + Second { tr.append_val_second(node)! } + }) + return arrays[0][0] * 100 + tr.order[0] * 10 + tr.order[1] +} + const allowed_words = ["alpha", "beta"] fn get_first(_ First) !string { @@ -1020,6 +1051,24 @@ fn select_value_array_init_cap_order(node Node) !int { return arr.len * 100 + tr.order[0] * 10 + tr.order[1] } +fn (mut tr Tracer) tlen2() int { + tr.order << 1 + return 3 +} + +// Array initializer field ordering with a *nested* cap branch: the match is buried inside a +// compound cap (`cap: 1 + (match ...)`), which still hoists the prelude ahead of the +// allocation call. A side-effecting len must still run before it. First -> len 3, order +// [1,2] -> 312 (a reversed order would be 321). +fn select_value_nested_cap_order(node Node) !int { + mut tr := Tracer{} + arr := []int{len: tr.tlen2(), cap: 1 + (match node { + First { tr.tcap_first(node)! } + Second { tr.tcap_second(node)! } + })} + return arr.len * 100 + tr.order[0] * 10 + tr.order[1] +} + // Address-of a value match (the checker permits `&` on a struct-typed match): // the propagating branch tail is materialized to a value temp whose address is // taken, then a field is read through it. @@ -1065,6 +1114,7 @@ fn main() { println(select_value_infix_order(First{})!) println(select_value_shift_order(First{})!) println(select_value_index_order(First{})!) + println(select_value_nested_index_order(First{})!) println(select_value_gated_index_order(First{})!) println(select_value_range_low(First{})!) println(select_value_range_membership(First{})!) @@ -1074,6 +1124,7 @@ fn main() { println(select_value_map_index(First{})!) println(select_value_string_membership_order(First{})!) println(select_value_append_order(First{})!) + println(select_value_nested_append_order(First{})!) println(select_value_const_membership(First{})!) println(select_value_map_membership_order(First{})!) println(select_value_push_many(First{})!) @@ -1089,6 +1140,7 @@ fn main() { println(select_value_array_init(First{})!) println(select_value_nonmut_arg_value(First{})!) println(select_value_array_init_cap_order(First{})!) + println(select_value_nested_cap_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1102,5 +1154,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n3512\n7612\n4004\n507\n212\n1' + 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\n1112\n102412\n1012\n2012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n3512\n7612\n4004\n507\n212\n312\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 5e6d8f8645ae1b..258671f76c88b0 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -388,15 +388,16 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod mut cap_expr := t.make_int_literal(0) mut init_expr := flat.empty_node mut init_expr_id := flat.empty_node - // Source (child) position of the last `len`/`cap` field whose value is a value branch, - // so an earlier side-effecting `len`/`cap` field can be stabilized before that field - // hoists its materialization prelude, preserving field evaluation order (both are - // evaluated into `new_call` below; `init` is per-element in the loop body). + // Source (child) position of the last `len`/`cap` field whose value hoists a value branch + // — directly or nested inside a compound field value (`cap: 1 + (match ...)`) — so an + // earlier side-effecting `len`/`cap` field can be stabilized before that field hoists its + // materialization prelude, preserving field evaluation order (both are evaluated into + // `new_call` below; `init` is per-element in the loop body). mut last_lencap_branch := -1 for i in 0 .. node.children_count { child := t.a.child_node(&node, i) if child.kind == .field_init && child.children_count > 0 && child.value in ['len', 'cap'] { - if t.is_value_match_or_if_operand(t.a.child(child, 0)) { + if t.operand_hoists_value_branch(t.a.child(child, 0)) { last_lencap_branch = i } } @@ -1191,12 +1192,12 @@ fn (mut t Transformer) try_lower_array_append_stmt(id flat.NodeId) ?[]flat.NodeI mut result := []flat.NodeId{} mut lhs := t.transform_lvalue(lhs_id) - // For an append whose RHS is a value `match`/`if` that hoists a prelude, stabilize the - // LHS lvalue's dynamic base/index components into temps first — without spilling the - // mutated array value — so a side-effecting index (e.g. - // `arrays[next(mut trace)] << (match ...)`) evaluates before the RHS prelude below, - // preserving source order. - if t.is_value_match_or_if_operand(rhs_id) { + // For an append whose RHS hoists a value `match`/`if` prelude — directly or nested inside + // a compound RHS (`arrays[next(mut trace)] << wrap(match ...)`) — stabilize the LHS + // lvalue's dynamic base/index components into temps first — without spilling the mutated + // array value — so a side-effecting index (e.g. `arrays[next(mut trace)] << (match ...)`) + // evaluates before the RHS prelude below, preserving source order. + if t.operand_hoists_value_branch(rhs_id) { lhs = t.stabilize_transformed_lvalue_for_reuse(lhs) } t.drain_pending(mut result) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 00cfbeb454ab36..ca5934ec22c11c 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15496,15 +15496,16 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat if lowered := t.lower_gated_scalar_index(node) { return t.lower_owned_array_index_move(id, lowered) } - // A later child (index / slice bound) that is a value `match`/`if` hoists its - // propagation prelude into `pending_stmts`; a preceding side-effecting child left + // A later child (index / slice bound) that hoists a value `match`/`if` — directly or + // nested inside a compound child (`make_values(mut tr)[1 + (match n { ... })]`) — lifts + // its propagation prelude into `pending_stmts`; a preceding side-effecting child left // inline would then run after that prelude. Find the last hoisting child so earlier // children can be stabilized first, preserving left-to-right evaluation order, e.g. // `make_values(mut tr)[match n { ... tr.index_result()! ... }]`. Index reads only // reach here (`.index`); lvalue targets are the separate `.index_assign` kind. mut last_value_branch := -1 for i in 0 .. node.children_count { - if t.is_value_match_or_if_operand(t.a.child(&node, i)) { + if t.operand_hoists_value_branch(t.a.child(&node, i)) { last_value_branch = i } } From 97e69f376db8d1d47faefa1ece1053a11447e42f Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 21:23:54 +0300 Subject: [PATCH 23/37] v3: detect nested value branches in infix/shift/range/membership/channel ordering (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. Five more operand-ordering guards still used is_value_match_or_if_operand, which only looks through paren/unsafe/block wrappers and stops at the outer expression, so a value branch buried inside a compound operand was missed while lowering it still hoisted the branch prelude into pending_stmts — reversing evaluation order against a preceding inline side effect. Switch each to the recursive operand_hoists_value_branch (added in f021bb9): - transform.v general infix (both operands): `trace_left() + (1 + (match ...))` left the block unentered, so trace_left stayed inline and the RHS prelude ran first. - transform.v numeric shift: `trace_left() << (1 + (match ...))` left rhs_is_value_branch false, so the side-effecting LHS was not stabilized. - transform.v channel send: `channels[next()] <- wrap(match ...) or {}` left the target index inline, so the sent value ran before next(). - expr.v range high bound: `x in trace_low() .. (1 + (match ...))` left trace_low inline, so trace_high ran first. - expr.v string membership container: `trace_needle() in wrap(match ...)` left the needle inline, so the container prelude ran first. Regression tests (each fails with the reversed value when its predicate is reverted): select_value_nested_infix_order 1212/1221, select_value_nested_shift_order 204812/204821, select_value_nested_range_order 112/121, select_value_nested_string_membership_order 512/521, select_value_nested_channel_target_order 9912/9921. --- ...s_if_expr_value_propagation_codegen_test.v | 83 ++++++++++++++++++- vlib/v3/transform/expr.v | 10 ++- vlib/v3/transform/transform.v | 26 ++++-- 3 files changed, 105 insertions(+), 14 deletions(-) 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 6ecae1b2f6275b..2cc2190c3fd375 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 @@ -405,6 +405,19 @@ fn select_value_infix_order(node ?Node) !int { return sum * 100 + tr.order[0] * 10 + tr.order[1] } +// Infix ordering with a *nested* branch: the match is buried inside a compound RHS +// (`lhs() + (1 + (match ...))`), whose inner infix still hoists the match prelude. The +// side-effecting LHS must run before that prelude. First -> 1 + (1 + 10) = 12, order [1,2] +// -> 1212 (a reversed order would be 1221). +fn select_value_nested_infix_order(node Node) !int { + mut tr := Tracer{} + sum := tr.lhs() + (1 + (match node { + First { tr.rf(node)! } + Second { tr.rs(node)! } + })) + return sum * 100 + tr.order[0] * 10 + tr.order[1] +} + fn (mut tr Tracer) shift_lhs() int { tr.order << 1 return 1 @@ -422,6 +435,19 @@ fn select_value_shift_order(node Node) !int { return sum * 100 + tr.order[0] * 10 + tr.order[1] } +// Left-shift ordering with a *nested* RHS branch: the match is buried inside a compound +// shift RHS (`shift_lhs() << (1 + (match ...))`), which still hoists the match prelude. The +// side-effecting LHS must run before it. First -> 1 << (1 + 10) = 1 << 11 = 2048, order +// [1,2] -> 204812 (a reversed order would be 204821). +fn select_value_nested_shift_order(node Node) !int { + mut tr := Tracer{} + sum := tr.shift_lhs() << (1 + (match node { + First { tr.rf(node)! } + Second { tr.rs(node)! } + })) + return sum * 100 + tr.order[0] * 10 + tr.order[1] +} + fn (mut tr Tracer) base_values() []int { tr.order << 1 return [10, 20, 30] @@ -535,6 +561,20 @@ fn select_value_range_order(node Node) !int { return flag * 100 + tr.order[0] * 10 + tr.order[1] } +// Membership-range ordering with a *nested* high bound: the match is buried inside a +// compound high bound (`.. (1 + (match ...))`), which still hoists the match prelude. The +// side-effecting low bound must run before it. First -> `5 in 0 .. (1 + 10)` = true, order +// [1,2] -> 112 (a reversed order would be 121). +fn select_value_nested_range_order(node Node) !int { + mut tr := Tracer{} + inside := 5 in tr.range_low() .. (1 + (match node { + First { tr.range_high_first(node)! } + Second { tr.range_high_second(node)! } + })) + flag := if inside { 1 } else { 0 } + return flag * 100 + tr.order[0] * 10 + tr.order[1] +} + fn make_values_first(_ First) ![]int { return [10, 20, 30] } @@ -614,6 +654,23 @@ fn select_value_string_membership_order(node Node) !int { return (if inside { 500 } else { 0 }) + tr.order[0] * 10 + tr.order[1] } +fn wrap_str(s string) string { + return s +} + +// String-membership ordering with a *nested* container: the match is wrapped inside a call +// (`needle in wrap_str(match ...)`), which still hoists the match prelude. The side-effecting +// needle must run before it. First -> "lo" in "hello" = true, order [1,2] -> 512 (a reversed +// order would be 521). +fn select_value_nested_string_membership_order(node Node) !int { + mut tr := Tracer{} + inside := tr.needle_str() in wrap_str(match node { + First { tr.text_first(node)! } + Second { tr.text_second(node)! } + }) + return (if inside { 500 } else { 0 }) + tr.order[0] * 10 + tr.order[1] +} + fn (mut tr Tracer) next_index() int { tr.order << 1 return 0 @@ -880,6 +937,25 @@ fn select_value_channel_target_order(node Node) !int { return got * 100 + tr.order[0] * 10 + tr.order[1] } +fn wrap_send(x int) int { + return x +} + +// Channel-send ordering with a *nested* sent value: the match is wrapped inside a call +// (`channels[i] <- wrap_send(match ...) or {}`), which still hoists the match prelude. The +// side-effecting target index must run before it. First -> send 99, order [1,2] -> 9912 (a +// reversed order would be 9921). +fn select_value_nested_channel_target_order(node Node) !int { + mut tr := Tracer{} + mut channels := [chan int{cap: 1}, chan int{cap: 1}] + channels[tr.ch_index()] <- wrap_send(match node { + First { tr.ch_rhs_first(node)! } + Second { tr.ch_rhs_second(node)! } + }) or { return -1 } + got := <-channels[0] + return got * 100 + tr.order[0] * 10 + tr.order[1] +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1112,17 +1188,21 @@ fn main() { println(select_value_ascast_unsafe(5)!) println(direct_match(Second{})!) println(select_value_infix_order(First{})!) + println(select_value_nested_infix_order(First{})!) println(select_value_shift_order(First{})!) + println(select_value_nested_shift_order(First{})!) println(select_value_index_order(First{})!) println(select_value_nested_index_order(First{})!) println(select_value_gated_index_order(First{})!) println(select_value_range_low(First{})!) println(select_value_range_membership(First{})!) println(select_value_range_order(First{})!) + println(select_value_nested_range_order(First{})!) println(select_value_membership_container(First{})!) println(select_value_forin_container(First{})!) println(select_value_map_index(First{})!) println(select_value_string_membership_order(First{})!) + println(select_value_nested_string_membership_order(First{})!) println(select_value_append_order(First{})!) println(select_value_nested_append_order(First{})!) println(select_value_const_membership(First{})!) @@ -1134,6 +1214,7 @@ fn main() { println(select_value_call_operand_order(First{})!) println(select_value_mut_receiver(First{})!) println(select_value_channel_target_order(First{})!) + println(select_value_nested_channel_target_order(First{})!) println(select_value_plain_call_order(First{})!) println(select_value_nested_branch_arg_order(First{})!) println(select_value_mut_arg(First{})!) @@ -1154,5 +1235,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n102412\n1012\n2012\n3012\n6\ntrue\n112\ntrue\n60\n2\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n3412\n3512\n7612\n4004\n507\n212\n312\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n9912\n3412\n3512\n7612\n4004\n507\n212\n312\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 53b62cb4eaa00c..396b46f7a22674 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1887,13 +1887,14 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No } low_id := t.a.children[rhs.children_start] high_id := t.a.children[rhs.children_start + 1] - // If the high bound is a value branch, its materialization below queues prelude + // If the high bound hoists a value branch — directly or nested inside a compound + // bound (`.. (1 + (match ...))`) — its materialization below queues prelude // statements; stabilize a side-effecting low bound first so it evaluates before // them, preserving low-before-high order, e.g. // `x in low_with_effect() .. (match node { ... high_with_effect()! ... })`. // A value-branch low is materialized in order by `transform_value_operand`. new_low := if !t.is_value_match_or_if_operand(low_id) - && t.is_value_match_or_if_operand(high_id) && !t.is_stable_expr_for_reuse(low_id) { + && t.operand_hoists_value_branch(high_id) && !t.is_stable_expr_for_reuse(low_id) { t.stable_expr_for_reuse(low_id) } else { t.transform_value_operand(low_id) @@ -1994,10 +1995,11 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No result = t.make_call_typed(fn_name, arr3(new_rhs, len_expr, new_lhs), 'bool') } } else if clean_rhs_type == 'string' { - // If the container is a value branch, its materialization below hoists a + // If the container hoists a value branch — directly or nested inside a compound + // container (`... in wrap(match ...)`) — its materialization below hoists a // prelude; stabilize a side-effecting needle first so it evaluates before it, // e.g. `tr.needle() in (match n { First { tr.text_first(n)! } ... })`. - new_lhs := if t.is_value_match_or_if_operand(rhs_id) { + new_lhs := if t.operand_hoists_value_branch(rhs_id) { t.stable_expr_for_reuse(lhs_id) } else { t.transform_expr(lhs_id) diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index ca5934ec22c11c..6a5dc183937de9 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14685,10 +14685,12 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat mut lhs := t.transform_expr(t.a.child(&node, 0)) sent_value_id := t.a.child(&rhs, 0) // Stabilize the channel target's dynamic base/index components before materializing - // a value-branch sent value, so a side-effecting target index (e.g. - // `channels[next(mut trace)] <- (match ...) or {}`) evaluates before the sent value's - // hoisted prelude. Preserves the lvalue shape without spilling the channel value. - if t.is_value_match_or_if_operand(sent_value_id) { + // a sent value that hoists a value branch — directly or nested inside a compound + // sent value (`channels[next()] <- wrap(match ...) or {}`) — so a side-effecting + // target index (e.g. `channels[next(mut trace)] <- (match ...) or {}`) evaluates + // before the sent value's hoisted prelude. Preserves the lvalue shape without + // spilling the channel value. + if t.operand_hoists_value_branch(sent_value_id) { lhs = t.stabilize_transformed_lvalue_for_reuse(lhs) } // Route a value `match`/`if` sent value through value lowering so its propagating @@ -14761,13 +14763,14 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // lowered with plain `transform_expr` in a value-less statement context and // emit an empty expression. `transform_value_operand` is a no-op for the // common non-branch operands. - // Preserve LHS-before-RHS evaluation order: for a numeric shift whose RHS is a - // value branch (its materialization below queues prelude statements), stabilize a + // Preserve LHS-before-RHS evaluation order: for a numeric shift whose RHS hoists a + // value branch — directly or nested inside a compound RHS (`mark_lhs() << (1 + + // (match ...))`) — its materialization below queues prelude statements, so stabilize a // side-effecting LHS first so it runs before that prelude, e.g. // `mark_lhs() << (match x { ... mark_rhs()! ... })`. An array-append LHS // (`rhs_target_type` set) is a mutated lvalue and must not be spilled; a value-branch // LHS is already materialized in order by `transform_value_operand`. - rhs_is_value_branch := t.is_value_match_or_if_operand(rhs_id) + rhs_is_value_branch := t.operand_hoists_value_branch(rhs_id) mut new_lhs := if rhs_target_type.len == 0 && rhs_is_value_branch && !t.is_value_match_or_if_operand(lhs_id) && !t.is_stable_expr_for_reuse(lhs_id) { t.stable_expr_for_reuse(lhs_id) @@ -14813,8 +14816,13 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // operand is left as its original node so it is transformed exactly once. infix_lhs_id := t.a.children[node.children_start] infix_rhs_id := t.a.children[node.children_start + 1] - lhs_is_value_branch := t.is_value_match_or_if_operand(infix_lhs_id) - rhs_is_value_branch := t.is_value_match_or_if_operand(infix_rhs_id) + // Detect a value branch that either side hoists — directly or nested inside a compound + // operand (`trace_left() + (1 + (match ...))`) — so the other, side-effecting operand is + // stabilized before that operand's materialization prelude, preserving left-to-right order. + // A directly-branch operand is materialized in order by `transform_value_operand` below; a + // nested one is materialized by its `transform_expr` recursion. + lhs_is_value_branch := t.operand_hoists_value_branch(infix_lhs_id) + rhs_is_value_branch := t.operand_hoists_value_branch(infix_rhs_id) if lhs_is_value_branch || rhs_is_value_branch { // Evaluate operands left-to-right so their materialization statements land in // `pending_stmts` in source order (LHS before RHS). Materializing only one side From 4743d9e7defea30b6961b242e3390d0cbe337ca7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 21:42:48 +0300 Subject: [PATCH 24/37] v3: spill by-value method receivers and rvalue channel targets before branch operands (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. Both fixes distinguish reference (lvalue-preserving) from by-value (spill-by-value) stabilization when an operand later hoists a value branch: - Method receiver: when a non-mut *value*-receiver method is called on an lvalue and a later argument hoists a value branch, the receiver path unconditionally used lvalue-preserving stabilization (spilling only the dynamic base/index components) and re-dispatched the original lvalue, so the receiver value was loaded at the call — after a branch prelude that mutates the container, so `items[next()].read(match ... { mutate(mut items)! } ...)` observed the mutated value. Add method_receiver_is_reference (mirroring method_value_has_pointer_receiver): reserve lvalue-preserving stabilization for mut/reference receivers (they must mutate through); spill ordinary value receivers by value so the value is read in source order. - Channel target: stabilize_transformed_lvalue_for_reuse only rewrites lvalue shapes and returns a non-lvalue rvalue unchanged, so a side-effecting rvalue target such as `get_channel(mut trace) <- (match ...) or {}` stayed inline and ran after the sent value prelude. Detect the unchanged (non-lvalue) result and spill it by value with stable_transformed_expr_for_reuse, so the target evaluates before the sent branch. Regression tests: select_value_value_receiver (`vh.items[vh.at()].read(match ... { overwrite(mut vh.items)! } ...)` -> 5002; leaked-mutation 9002 on HEAD) and select_value_rvalue_channel_target (`c.get_channel() <- (match ...) or {}` -> 7712; reversed 7721 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 90 ++++++++++++++++++- vlib/v3/transform/transform.v | 57 ++++++++++-- 2 files changed, 137 insertions(+), 10 deletions(-) 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 2cc2190c3fd375..65b76876712226 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 @@ -908,6 +908,55 @@ fn select_value_mut_receiver(node Node) !int { return items[0].v * 100 + tr.order[0] * 10 + tr.order[1] } +struct ValItem { + v int +} + +fn (vi ValItem) read(x int) int { + return vi.v * 1000 + x +} + +struct ValHolder { +mut: + items []ValItem +} + +fn (mut vh ValHolder) at() int { + return 0 +} + +fn (mut vh ValHolder) overwrite_first(_ First) !int { + vh.items[0] = ValItem{ + v: 9 + } + return 2 +} + +fn (mut vh ValHolder) overwrite_second(_ Second) !int { + vh.items[0] = ValItem{ + v: 8 + } + return 3 +} + +// A non-mut *value*-receiver method called on an lvalue element, with a later argument whose +// match arm mutates that element: the receiver value must be read in source order (before the +// mutation), not reloaded after the branch prelude. First -> items[0] (ValItem{5}).read(2) = +// 5 * 1000 + 2 = 5002 (if the mutation leaked, the reloaded receiver would give 9002). +fn select_value_value_receiver(node Node) !int { + mut vh := ValHolder{ + items: [ValItem{ + v: 5 + }, ValItem{ + v: 6 + }] + } + return vh.items[vh.at()].read(match node { + First { vh.overwrite_first(node)! } + Second { vh.overwrite_second(node)! } + }) +} + fn (mut tr Tracer) ch_index() int { tr.order << 1 return 0 @@ -956,6 +1005,43 @@ fn select_value_nested_channel_target_order(node Node) !int { return got * 100 + tr.order[0] * 10 + tr.order[1] } +struct ChanHolder { +mut: + order []int + ch chan int +} + +fn (mut c ChanHolder) get_channel() chan int { + c.order << 1 + return c.ch +} + +fn (mut c ChanHolder) sent_first(_ First) !int { + c.order << 2 + return 77 +} + +fn (mut c ChanHolder) sent_second(_ Second) !int { + c.order << 2 + return 66 +} + +// A side-effecting *rvalue* channel target (a method call, not an lvalue) with a value-match +// sent value: the target call must run before the hoisted prelude of the sent value. Since it +// is not an lvalue shape it is spilled by value. First -> send 77, order [1,2] -> 7712 (a +// reversed order would be 7721). +fn select_value_rvalue_channel_target(node Node) !int { + mut c := ChanHolder{ + ch: chan int{cap: 1} + } + c.get_channel() <- (match node { + First { c.sent_first(node)! } + Second { c.sent_second(node)! } + }) or { return -1 } + got := <-c.ch + return got * 100 + c.order[0] * 10 + c.order[1] +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1213,8 +1299,10 @@ fn main() { println(select_value_channel_send(First{})!) println(select_value_call_operand_order(First{})!) println(select_value_mut_receiver(First{})!) + println(select_value_value_receiver(First{})!) println(select_value_channel_target_order(First{})!) println(select_value_nested_channel_target_order(First{})!) + println(select_value_rvalue_channel_target(First{})!) println(select_value_plain_call_order(First{})!) println(select_value_nested_branch_arg_order(First{})!) println(select_value_mut_arg(First{})!) @@ -1235,5 +1323,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n9912\n9912\n3412\n3512\n7612\n4004\n507\n212\n312\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n3412\n3512\n7612\n4004\n507\n212\n312\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 6a5dc183937de9..ebb4eb3efafeff 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -6534,6 +6534,26 @@ fn (t &Transformer) method_value_has_pointer_receiver(id flat.NodeId) bool { return t.tc.mut_receiver_methods[method_name] } +// method_receiver_is_reference reports whether the method `method` resolved on `base_id` +// takes its receiver by reference (a `mut` or `&` receiver). Such a receiver must keep its +// lvalue identity when stabilized (only its dynamic base/index components spilled) so the +// call still mutates through the lvalue; an ordinary by-value receiver is spilled by value so +// its value is read in source order — a later branch prelude that mutates its container cannot +// then change the observed receiver value. +fn (t &Transformer) method_receiver_is_reference(base_id flat.NodeId, method string) bool { + if isnil(t.tc) { + return false + } + method_name := t.resolve_receiver_method_name(base_id, method) + if method_name.len == 0 { + return false + } + if params := t.tc.fn_param_types[method_name] { + return params.len > 0 && params[0] is types.Pointer + } + return t.tc.mut_receiver_methods[method_name] +} + fn (mut t Transformer) mark_callback_method_value_receiver_escape(id flat.NodeId, amp_sources map[string][]string, ptr_aliases map[string]string, local_stack_names map[string]bool) { if int(id) < 0 || int(id) >= t.a.nodes.len { return @@ -14684,14 +14704,23 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat send_prelude_start := t.pending_stmts.len mut lhs := t.transform_expr(t.a.child(&node, 0)) sent_value_id := t.a.child(&rhs, 0) - // Stabilize the channel target's dynamic base/index components before materializing - // a sent value that hoists a value branch — directly or nested inside a compound - // sent value (`channels[next()] <- wrap(match ...) or {}`) — so a side-effecting - // target index (e.g. `channels[next(mut trace)] <- (match ...) or {}`) evaluates - // before the sent value's hoisted prelude. Preserves the lvalue shape without - // spilling the channel value. + // Stabilize the channel target before materializing a sent value that hoists a value + // branch — directly or nested inside a compound sent value (`channels[next()] <- + // wrap(match ...) or {}`) — so a side-effecting target (e.g. + // `channels[next(mut trace)] <- (match ...) or {}`) evaluates before the sent value's + // hoisted prelude. An lvalue target has only its dynamic base/index components + // spilled (preserving the lvalue shape without spilling the channel value); a + // non-lvalue rvalue target (e.g. `get_channel(mut trace) <- ...`), which + // `stabilize_transformed_lvalue_for_reuse` returns unchanged, is spilled by value. if t.operand_hoists_value_branch(sent_value_id) { - lhs = t.stabilize_transformed_lvalue_for_reuse(lhs) + stabilized := t.stabilize_transformed_lvalue_for_reuse(lhs) + lhs = if stabilized != lhs { + stabilized + } else if !t.is_stable_expr_for_reuse(lhs) { + t.stable_transformed_expr_for_reuse(lhs, t.node_type(lhs), 'chan_target') + } else { + lhs + } } // Route a value `match`/`if` sent value through value lowering so its propagating // arm tail is materialized as a value, e.g. @@ -14995,8 +15024,18 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. } r } else if last_branch > 0 && !t.is_stable_expr_for_reuse(recv_id) { - r := if stabilized := t.stabilize_original_lvalue_receiver(recv_id) { - stabilized + // A `mut`/reference receiver keeps its lvalue identity (only its dynamic + // base/index components are spilled) so the call still mutates through the + // lvalue. An ordinary by-value receiver is spilled by value, so its value is + // read in source order — a later branch prelude that mutates its container + // (e.g. `items[next()].read(match ... { mutate(mut items)! } ...)`) cannot + // then change the observed receiver value. + r := if t.method_receiver_is_reference(recv_id, recv_fn.value) { + if stabilized := t.stabilize_original_lvalue_receiver(recv_id) { + stabilized + } else { + t.stable_expr_for_reuse(recv_id) + } } else { t.stable_expr_for_reuse(recv_id) } From 40591bf6d3970abcb070c9bc75b7f37d88a91dd7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 22:00:22 +0300 Subject: [PATCH 25/37] v3: spill composite rvalue channel targets rooted in a call before the sent value (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. The prior channel-target fix decided between lvalue-preserving stabilization and a by-value spill by comparing the stabilized node id to the original (stabilized != lhs). That is fooled by a composite lvalue rooted in a side-effecting rvalue such as make_channels(mut trace)[0]: stabilize_transformed_lvalue_for_reuse spills the index components but recurses into the index *base* and returns the non-lvalue call base unchanged, then rebuilds the outer index into a new node id. The new id != lhs, so the old check treated the target as stabilized and used it with the call still inline — the sent value prelude then ran before make_channels(), reversing source order. Decide on the actual root instead: transformed_lvalue_root_needs_value_spill walks the lvalue spine (index/selector/paren base, deref operand is spilled as a component so it is safe) to the root leaf. If the leaf is a non-lvalue rvalue that is not stable, the whole target is spilled by value; otherwise the stable-rooted lvalue keeps only its dynamic components spilled. This subsumes the bare-call case (get_channel() <- ...) and adds the composite case. Deciding before stabilizing also avoids pushing orphan component temps. Regression test select_value_composite_rvalue_channel_target (`make_channels()[0] <- (match ...) or {}` -> 5512; reversed 5521 with the old id-comparison heuristic). --- ...s_if_expr_value_propagation_codegen_test.v | 40 ++++++++++++- vlib/v3/transform/transform.v | 57 ++++++++++++++++--- 2 files changed, 87 insertions(+), 10 deletions(-) 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 65b76876712226..a3165e09c156a1 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 @@ -1042,6 +1042,43 @@ fn select_value_rvalue_channel_target(node Node) !int { return got * 100 + c.order[0] * 10 + c.order[1] } +struct ChanFactory { +mut: + order []int + chans []chan int +} + +fn (mut f ChanFactory) make_channels() []chan int { + f.order << 1 + return f.chans +} + +fn (mut f ChanFactory) fsent_first(_ First) !int { + f.order << 2 + return 55 +} + +fn (mut f ChanFactory) fsent_second(_ Second) !int { + f.order << 2 + return 44 +} + +// A channel target that is an index into a side-effecting *rvalue* base (`make_channels()[0]`, +// not an lvalue): the lvalue stabilizer rebuilds the outer index but leaves the base call +// inline, so the whole target must be spilled by value to keep the base call ahead of the sent +// value prelude. First -> send 55, order [1,2] -> 5512 (a reversed order would be 5521). +fn select_value_composite_rvalue_channel_target(node Node) !int { + mut f := ChanFactory{ + chans: [chan int{cap: 1}, chan int{cap: 1}] + } + f.make_channels()[0] <- (match node { + First { f.fsent_first(node)! } + Second { f.fsent_second(node)! } + }) or { return -1 } + got := <-f.chans[0] + return got * 100 + f.order[0] * 10 + f.order[1] +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1303,6 +1340,7 @@ fn main() { println(select_value_channel_target_order(First{})!) println(select_value_nested_channel_target_order(First{})!) println(select_value_rvalue_channel_target(First{})!) + println(select_value_composite_rvalue_channel_target(First{})!) println(select_value_plain_call_order(First{})!) println(select_value_nested_branch_arg_order(First{})!) println(select_value_mut_arg(First{})!) @@ -1323,5 +1361,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n3412\n3512\n7612\n4004\n507\n212\n312\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index ebb4eb3efafeff..4878ccd9352c10 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9923,6 +9923,45 @@ fn (mut t Transformer) stabilize_transformed_lvalue_for_reuse(id flat.NodeId) fl } } +// transformed_lvalue_root_needs_value_spill reports whether stabilize_transformed_lvalue_for_reuse +// would leave a side-effecting subexpression inline in `id`. That stabilizer spills index/selector +// *components* and deref operands, but for an index/selector it recurses into the *base* and +// returns a non-lvalue base (a call or other rvalue) unchanged. So a target rooted in a +// side-effecting rvalue (e.g. `make_channels()[0]`) is not made reusable by that stabilization — +// only its outer shape is rebuilt (a new node id) while the rvalue base still runs at use time. +// Walk the lvalue spine to the root leaf: if that leaf is a non-lvalue rvalue that is not stable, +// the whole target must instead be spilled by value. +fn (t &Transformer) transformed_lvalue_root_needs_value_spill(id flat.NodeId) bool { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return false + } + node := t.a.nodes[int(id)] + match node.kind { + .ident { + return false + } + .selector, .index, .paren { + if node.children_count == 0 { + return false + } + return t.transformed_lvalue_root_needs_value_spill(t.a.child(&node, 0)) + } + .prefix { + // A deref (`*p`) has its pointer operand spilled as a component, so it is safe; + // any other prefix is not an lvalue root. + if node.op == .mul { + return false + } + return !t.is_stable_expr_for_reuse(id) + } + else { + // A non-lvalue root (call, etc.): the stabilizer leaves it inline, so if it is + // side-effecting (not stable) the whole target must be spilled by value. + return !t.is_stable_expr_for_reuse(id) + } + } +} + fn (mut t Transformer) stabilize_transformed_lvalue_component(id flat.NodeId, prefix string) flat.NodeId { if t.is_stable_expr_for_reuse(id) { return id @@ -14708,18 +14747,18 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // branch — directly or nested inside a compound sent value (`channels[next()] <- // wrap(match ...) or {}`) — so a side-effecting target (e.g. // `channels[next(mut trace)] <- (match ...) or {}`) evaluates before the sent value's - // hoisted prelude. An lvalue target has only its dynamic base/index components - // spilled (preserving the lvalue shape without spilling the channel value); a - // non-lvalue rvalue target (e.g. `get_channel(mut trace) <- ...`), which - // `stabilize_transformed_lvalue_for_reuse` returns unchanged, is spilled by value. + // hoisted prelude. A stable-rooted lvalue target has only its dynamic base/index + // components spilled (preserving the lvalue shape without spilling the channel value). + // A target rooted in a side-effecting rvalue — a bare call (`get_channel(mut trace) + // <- ...`) or one under an index/selector (`make_channels(mut trace)[0] <- ...`), + // which the lvalue stabilizer would leave inline — is spilled whole by value. if t.operand_hoists_value_branch(sent_value_id) { - stabilized := t.stabilize_transformed_lvalue_for_reuse(lhs) - lhs = if stabilized != lhs { - stabilized - } else if !t.is_stable_expr_for_reuse(lhs) { + lhs = if t.is_stable_expr_for_reuse(lhs) { + lhs + } else if t.transformed_lvalue_root_needs_value_spill(lhs) { t.stable_transformed_expr_for_reuse(lhs, t.node_type(lhs), 'chan_target') } else { - lhs + t.stabilize_transformed_lvalue_for_reuse(lhs) } } // Route a value `match`/`if` sent value through value lowering so its propagating From 49fa317b594b562c81f225af879df1786fae125d Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 23:36:41 +0300 Subject: [PATCH 26/37] v3: snapshot value-bearing operands before a hoisted branch prelude can mutate them (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. The operand-ordering guards spilled a preceding operand only when !is_stable_expr_for_reuse, leaving an identifier / selector / stable index inline because it is cheap to re-evaluate. But a later value-branch operand's hoisted prelude can mutate that storage, so re-reading it after the prelude yields a different value than source order — e.g. y := x + (match node { First { change(mut x)! } ... }) read the updated x. Add is_pure_constant_expr (stricter than is_stable_expr_for_reuse: false for value-bearing lvalue reads) and snapshot_*_for_reuse, and route the value-read ordering guards through them: infix LHS, numeric-shift LHS, call receiver and preceding args, index children, range tested value and low bound, string/fixed/dynamic/unknown membership needle, and array-init len/cap. A pure constant is still left inline. Append and channel targets are lvalue/reference mutation targets (not value reads) and are unchanged. To keep the call/infix/index re-dispatch terminating (a snapshot temp is a non-pure ident that would otherwise be re-snapshotted on each pass with a nested-branch operand), record snapshot temp names in ordering_snapshot_names and treat them as already-captured via operand_needs_ordering_snapshot / is_ordering_snapshot_temp. Regression tests: select_value_stable_lhs_snapshot (c.v + (match { c.bump()! }) -> 6100; leaked mutation 105100 on HEAD) and select_value_stable_arg_snapshot (take2(c.v, match { c.bump()! }) -> 1005; leaked 100005 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 45 ++++++- vlib/v3/transform/array.v | 8 +- vlib/v3/transform/expr.v | 125 ++++++++++++++++-- vlib/v3/transform/transform.v | 25 ++-- 4 files changed, 176 insertions(+), 27 deletions(-) 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 a3165e09c156a1..111f9f34139145 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 @@ -1079,6 +1079,47 @@ fn select_value_composite_rvalue_channel_target(node Node) !int { return got * 100 + f.order[0] * 10 + f.order[1] } +struct Counter { +mut: + v int +} + +fn (mut c Counter) bump() !int { + c.v = 100 + return 5 +} + +// A syntactically stable LHS lvalue (a struct-field read) whose value the RHS branch prelude +// mutates: the infix must read the LHS source-order value, not the updated value. First -> +// c.v (1) + 5 = 6, then c.v is 100 -> 6 * 1000 + 100 = 6100 (a leaked mutation gives 105100). +fn select_value_stable_lhs_snapshot(node Node) !int { + mut c := Counter{ + v: 1 + } + y := c.v + (match node { + First { c.bump()! } + Second { c.bump()! } + }) + return y * 1000 + c.v +} + +fn take2(a int, b int) int { + return a * 1000 + b +} + +// A stable field-read argument whose value the prelude of a later value-branch argument +// mutates: the argument must be read in source order. First -> take2(1, 5) = 1005 (a leaked +// mutation into the first argument gives 100005). +fn select_value_stable_arg_snapshot(node Node) !int { + mut c := Counter{ + v: 1 + } + return take2(c.v, match node { + First { c.bump()! } + Second { c.bump()! } + }) +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1348,6 +1389,8 @@ fn main() { println(select_value_nonmut_arg_value(First{})!) println(select_value_array_init_cap_order(First{})!) println(select_value_nested_cap_order(First{})!) + println(select_value_stable_lhs_snapshot(First{})!) + println(select_value_stable_arg_snapshot(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1361,5 +1404,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index 258671f76c88b0..beb1c49bd70a23 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -410,14 +410,14 @@ fn (mut t Transformer) lower_array_init_to_runtime(id flat.NodeId, node flat.Nod // `[]int{len: match node { ... lower(node)! ... }}`) is materialized as a // value instead of lowering its propagating arm in a statement context. mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int') - if i < last_lencap_branch && !t.is_stable_expr_for_reuse(val) { - val = t.stable_transformed_expr_for_reuse(val, 'int', 'arr_len') + if i < last_lencap_branch && t.operand_needs_ordering_snapshot(val) { + val = t.snapshot_transformed_expr_for_reuse(val, 'int', 'arr_len') } len_expr = val } else if child.value == 'cap' { mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int') - if i < last_lencap_branch && !t.is_stable_expr_for_reuse(val) { - val = t.stable_transformed_expr_for_reuse(val, 'int', 'arr_cap') + if i < last_lencap_branch && t.operand_needs_ordering_snapshot(val) { + val = t.snapshot_transformed_expr_for_reuse(val, 'int', 'arr_cap') } cap_expr = val } else if child.value == 'init' { diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index 396b46f7a22674..dbc16664aff24b 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -1880,22 +1880,29 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // value instead of in a value-less statement context, e.g. // `x in (match node { ... lower(node)! ... }) .. 10`. `transform_value_operand` // is a no-op for the common non-branch operands. + low_id := t.a.children[rhs.children_start] + high_id := t.a.children[rhs.children_start + 1] + // The tested value is evaluated first; if either bound hoists a value branch whose + // prelude could mutate it, snapshot its source-order value before that prelude. + bound_hoists := t.operand_hoists_value_branch(low_id) + || t.operand_hoists_value_branch(high_id) new_lhs := if t.is_value_match_or_if_operand(lhs_id) { t.transform_value_operand(lhs_id) + } else if bound_hoists && t.operand_needs_ordering_snapshot(lhs_id) { + t.snapshot_expr_for_reuse(lhs_id) } else { t.stable_expr_for_reuse(lhs_id) } - low_id := t.a.children[rhs.children_start] - high_id := t.a.children[rhs.children_start + 1] // If the high bound hoists a value branch — directly or nested inside a compound // bound (`.. (1 + (match ...))`) — its materialization below queues prelude - // statements; stabilize a side-effecting low bound first so it evaluates before - // them, preserving low-before-high order, e.g. + // statements; snapshot a value-bearing low bound first so it evaluates before them, + // preserving low-before-high order, e.g. // `x in low_with_effect() .. (match node { ... high_with_effect()! ... })`. // A value-branch low is materialized in order by `transform_value_operand`. new_low := if !t.is_value_match_or_if_operand(low_id) - && t.operand_hoists_value_branch(high_id) && !t.is_stable_expr_for_reuse(low_id) { - t.stable_expr_for_reuse(low_id) + && t.operand_hoists_value_branch(high_id) + && t.operand_needs_ordering_snapshot(low_id) { + t.snapshot_expr_for_reuse(low_id) } else { t.transform_value_operand(low_id) } @@ -1950,7 +1957,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // Evaluate the needle before materializing a value-branch container so a // side-effecting needle precedes the container's hoisted prelude. new_lhs := if t.is_value_match_or_if_operand(rhs_id) { - t.stable_transformed_expr_for_reuse(t.transform_expr_for_type(lhs_id, elem), + t.snapshot_transformed_expr_for_reuse(t.transform_expr_for_type(lhs_id, elem), elem, 'in_lhs') } else { t.transform_expr_for_type(lhs_id, elem) @@ -1984,7 +1991,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // fixed array membership -> fixed_array_contains_int/string(arr, len, val) // stabilize a side-effecting needle before a value-branch container hoists new_lhs := if t.is_value_match_or_if_operand(rhs_id) { - t.stable_expr_for_reuse(lhs_id) + t.snapshot_expr_for_reuse(lhs_id) } else { t.transform_expr(lhs_id) } @@ -2000,7 +2007,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // prelude; stabilize a side-effecting needle first so it evaluates before it, // e.g. `tr.needle() in (match n { First { tr.text_first(n)! } ... })`. new_lhs := if t.operand_hoists_value_branch(rhs_id) { - t.stable_expr_for_reuse(lhs_id) + t.snapshot_expr_for_reuse(lhs_id) } else { t.transform_expr(lhs_id) } @@ -2020,7 +2027,7 @@ fn (mut t Transformer) transform_in_expr(id flat.NodeId, node flat.Node) flat.No // handle genuinely unresolved cases. // stabilize a side-effecting needle before a value-branch container hoists new_lhs := if t.is_value_match_or_if_operand(rhs_id) { - t.stable_expr_for_reuse(lhs_id) + t.snapshot_expr_for_reuse(lhs_id) } else { t.transform_expr(lhs_id) } @@ -3383,6 +3390,68 @@ fn (mut t Transformer) stable_transformed_expr_for_reuse(expr flat.NodeId, typ s return t.make_ident(tmp_name) } +// snapshot_expr_for_reuse materializes `id` into a temp holding its current value, unless it is +// a pure constant (which cannot change, so needs no snapshot). Ordering guards use it to capture +// the source-order value of an operand that precedes a value branch whose hoisted prelude might +// mutate that operand's storage. Unlike stable_expr_for_reuse it does snapshot value-bearing +// lvalues (idents/selectors/indexes) rather than leaving them inline. +fn (mut t Transformer) snapshot_expr_for_reuse(id flat.NodeId) flat.NodeId { + if t.is_ordering_snapshot_temp(id) { + return id + } + expr := if _ := t.generated_variant_access_type(id) { + id + } else { + t.transform_expr(id) + } + if t.is_pure_constant_expr(expr) || t.is_ordering_snapshot_temp(expr) { + return expr + } + tmp_name := t.new_temp('order_snapshot') + mut tmp_typ := t.node_type(expr) + if tmp_typ.len == 0 { + tmp_typ = t.node_type(id) + } + decl := t.make_decl_assign(tmp_name, expr) + if tmp_typ.len > 0 { + t.set_node_typ(int(decl), tmp_typ) + t.set_var_type(tmp_name, tmp_typ) + } + t.ordering_snapshot_names[tmp_name] = true + t.pending_stmts << decl + return t.make_ident(tmp_name) +} + +// snapshot_transformed_expr_for_reuse is snapshot_expr_for_reuse for an already-transformed +// expression of known type. +fn (mut t Transformer) snapshot_transformed_expr_for_reuse(expr flat.NodeId, typ string, prefix string) flat.NodeId { + if t.is_pure_constant_expr(expr) || t.is_ordering_snapshot_temp(expr) { + return expr + } + tmp_name := t.new_temp(prefix) + t.ordering_snapshot_names[tmp_name] = true + t.pending_stmts << t.make_decl_assign_typed(tmp_name, expr, typ) + return t.make_ident(tmp_name) +} + +// is_ordering_snapshot_temp reports whether `id` is an identifier naming a temp already created +// by a snapshot_*_for_reuse call. Such a temp holds a captured source-order value that no branch +// prelude mutates, so it must not be snapshotted again (which would recurse on a re-dispatch). +fn (t &Transformer) is_ordering_snapshot_temp(id flat.NodeId) bool { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return false + } + node := t.a.nodes[int(id)] + return node.kind == .ident && node.value in t.ordering_snapshot_names +} + +// operand_needs_ordering_snapshot reports whether a preceding operand must be snapshotted to +// preserve its source-order value before a later value branch's hoisted prelude runs: it is a +// value-bearing lvalue read (not a pure constant) and is not already a snapshot temp. +fn (t &Transformer) operand_needs_ordering_snapshot(id flat.NodeId) bool { + return !t.is_pure_constant_expr(id) && !t.is_ordering_snapshot_temp(id) +} + // is_stable_expr_for_reuse reports whether is stable expr for reuse applies in transform. fn (t &Transformer) is_stable_expr_for_reuse(id flat.NodeId) bool { if int(id) < 0 { @@ -3426,6 +3495,42 @@ fn (t &Transformer) is_stable_expr_for_reuse(id flat.NodeId) bool { } } +// is_pure_constant_expr reports whether `id`'s value cannot be changed by a later mutation of +// any variable — a literal, enum value, `sizeof`/`typeof`, or a cast/paren/struct made only of +// such. Unlike is_stable_expr_for_reuse it returns false for value-bearing lvalue reads +// (idents, selectors, indexes): those are cheap to re-evaluate, but a hoisted branch prelude +// can mutate their storage, so reading them after the prelude yields a different value. Ordering +// guards use this to decide whether a preceding operand must be snapshotted to preserve its +// source-order value. +fn (t &Transformer) is_pure_constant_expr(id flat.NodeId) bool { + if int(id) < 0 { + return true + } + node := t.a.nodes[int(id)] + return match node.kind { + .int_literal, .float_literal, .bool_literal, .char_literal, .string_literal, .nil_literal, + .none_expr, .enum_val, .sizeof_expr, .typeof_expr { + true + } + .cast_expr, .paren { + node.children_count == 0 || t.is_pure_constant_expr(t.a.children[node.children_start]) + } + .struct_init, .field_init { + mut pure := true + for i in 0 .. node.children_count { + if !t.is_pure_constant_expr(t.a.child(&node, i)) { + pure = false + break + } + } + pure + } + else { + false + } + } +} + // transform_fixed_array_len transforms transform fixed array len data for transform. fn (mut t Transformer) transform_fixed_array_len(_id flat.NodeId, node flat.Node) ?flat.NodeId { if node.value != 'len' || node.children_count == 0 { diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 4878ccd9352c10..28f9dbe452715b 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -152,6 +152,7 @@ mut: mut_param_values map[string]bool fixed_array_param_values map[string]bool mut_value_ident_nodes map[int]bool + ordering_snapshot_names map[string]bool pointer_value_lvalues map[string]bool pointer_value_rvalues map[string]bool addr_lvalue_pointer_locals map[string]bool @@ -14840,8 +14841,8 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat // LHS is already materialized in order by `transform_value_operand`. rhs_is_value_branch := t.operand_hoists_value_branch(rhs_id) mut new_lhs := if rhs_target_type.len == 0 && rhs_is_value_branch - && !t.is_value_match_or_if_operand(lhs_id) && !t.is_stable_expr_for_reuse(lhs_id) { - t.stable_expr_for_reuse(lhs_id) + && !t.is_value_match_or_if_operand(lhs_id) && t.operand_needs_ordering_snapshot(lhs_id) { + t.snapshot_expr_for_reuse(lhs_id) } else { t.transform_value_operand(lhs_id) } @@ -14903,8 +14904,8 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat pending_start := t.pending_stmts.len new_lhs := if lhs_is_value_branch { t.transform_value_operand(infix_lhs_id) - } else if rhs_is_value_branch && !t.is_stable_expr_for_reuse(infix_lhs_id) { - t.stable_expr_for_reuse(infix_lhs_id) + } else if rhs_is_value_branch && t.operand_needs_ordering_snapshot(infix_lhs_id) { + t.snapshot_expr_for_reuse(infix_lhs_id) } else { infix_lhs_id } @@ -15062,7 +15063,7 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. changed = true } r - } else if last_branch > 0 && !t.is_stable_expr_for_reuse(recv_id) { + } else if last_branch > 0 && t.operand_needs_ordering_snapshot(recv_id) { // A `mut`/reference receiver keeps its lvalue identity (only its dynamic // base/index components are spilled) so the call still mutates through the // lvalue. An ordinary by-value receiver is spilled by value, so its value is @@ -15073,10 +15074,10 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if stabilized := t.stabilize_original_lvalue_receiver(recv_id) { stabilized } else { - t.stable_expr_for_reuse(recv_id) + t.snapshot_expr_for_reuse(recv_id) } } else { - t.stable_expr_for_reuse(recv_id) + t.snapshot_expr_for_reuse(recv_id) } if r != recv_id { changed = true @@ -15105,7 +15106,7 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. arg_id := t.a.child(&node, i) na := if t.is_value_match_or_if_operand(arg_id) { t.transform_value_operand(arg_id) - } else if i < last_branch && !t.is_stable_expr_for_reuse(arg_id) { + } else if i < last_branch && t.operand_needs_ordering_snapshot(arg_id) { // A `mut` argument keeps its lvalue identity (only its dynamic base/index // components are spilled) so it still mutates through. An ordinary argument is // spilled by value, so its value is read in source order — a later branch @@ -15114,10 +15115,10 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if stabilized := t.stabilize_original_lvalue_receiver(arg_id) { stabilized } else { - t.stable_expr_for_reuse(arg_id) + t.snapshot_expr_for_reuse(arg_id) } } else { - t.stable_expr_for_reuse(arg_id) + t.snapshot_expr_for_reuse(arg_id) } } else { arg_id @@ -15602,8 +15603,8 @@ fn (mut t Transformer) transform_index_expr(id flat.NodeId, node flat.Node) flat // 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 := if i < last_value_branch && !t.is_value_match_or_if_operand(child_id) - && !t.is_stable_expr_for_reuse(child_id) { - t.stable_expr_for_reuse(child_id) + && t.operand_needs_ordering_snapshot(child_id) { + t.snapshot_expr_for_reuse(child_id) } else { t.transform_value_operand(child_id) } From aabe76a8ce1e84545829d252c7db785f078a8ee8 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 2 Aug 2026 23:57:37 +0300 Subject: [PATCH 27/37] v3: materialize branch-produced call callees and type-pattern membership subjects (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. - Call callee: the operand-materialization scan only set last_branch for a method receiver (child 0 when is_method) and arguments (1..), so a plain call whose callee is itself a value branch — (match node { First { make_cb(node)! } ... })() — never materialized child 0. transform_call_args then lowered it with plain transform_expr (fn.v: transformed_callee := ... t.transform_expr(callee_id)), leaving a propagating branch tail in a value-less statement context and emitting an empty callee. Detect a non-selector value-branch callee (callee_is_value_branch), count it as operand 0, and materialize it via transform_value_operand, symmetric with receiver/argument handling. - Type-pattern membership subject: lower_type_pattern_membership spilled the subject with stable_expr_for_reuse (plain transform_expr), so a value-branch subject ((match ...) in [Foo1, Foo3]) would lower its propagating arms as an empty expression. Route a value-branch subject through typed value lowering (transform_expr_for_type(lhs_id, sum_name)), consistent with the range/needle paths. Regression: select_value_branch_callee ((match node { First { make_cb_first(node)!.f } ... })() -> 41) exercises the immediately-invoked value-branch callee end to end. (A strictly load-bearing variant with a bare propagating callable arm tail and the direct type-pattern-membership syntax are currently blocked by separate v3 limitations — a bare function value wrapped in a Result mis-lowers, and the checker rejects x in [Type, ...]; see the PR thread replies.) --- ...s_if_expr_value_propagation_codegen_test.v | 39 ++++++++++++++++++- vlib/v3/transform/expr.v | 11 +++++- vlib/v3/transform/transform.v | 24 ++++++++++-- 3 files changed, 68 insertions(+), 6 deletions(-) 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 111f9f34139145..f49464fac5d3af 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 @@ -1120,6 +1120,42 @@ fn select_value_stable_arg_snapshot(node Node) !int { }) } +type IntFn = fn () int + +struct FnBox { + f IntFn +} + +fn cb_a() int { + return 41 +} + +fn cb_b() int { + return 52 +} + +fn make_cb_first(_ First) !FnBox { + return FnBox{ + f: cb_a + } +} + +fn make_cb_second(_ Second) !FnBox { + return FnBox{ + f: cb_b + } +} + +// The call target itself is a value match (with propagating arms) producing a function value, +// immediately invoked: child 0 (the callee) must be materialized as a value, not lowered with +// plain transform_expr into an empty callee. First -> cb_a() = 41. +fn select_value_branch_callee(node Node) !int { + return (match node { + First { make_cb_first(node)!.f } + Second { make_cb_second(node)!.f } + })() +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1391,6 +1427,7 @@ fn main() { println(select_value_nested_cap_order(First{})!) println(select_value_stable_lhs_snapshot(First{})!) println(select_value_stable_arg_snapshot(First{})!) + println(select_value_branch_callee(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1404,5 +1441,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n41\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index dbc16664aff24b..de956740f37a0a 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -2167,7 +2167,16 @@ fn (mut t Transformer) lower_type_pattern_membership(lhs_id flat.NodeId, rhs fla if !t.is_sum_type_name(sum_name) { return none } - base := t.stable_expr_for_reuse(lhs_id) + // A value-branch subject (`(match node { First { make_foo(node)! } ... }) in [Foo1, Foo3]`) + // must be lowered as a typed value so its propagating arms are materialized into a temp; + // plain `stable_expr_for_reuse` would lower it with `transform_expr` in a value-less + // statement context and emit an empty expression. + base := if t.is_value_match_or_if_operand(lhs_id) { + t.stable_transformed_expr_for_reuse(t.transform_expr_for_type(lhs_id, sum_name), sum_name, + 'in_lhs') + } else { + t.stable_expr_for_reuse(lhs_id) + } // A non-trivial lhs is materialized as a value temp above. Use that temp's // storage type for the tag checks; retaining the source pointer type here // makes the generated checks dereference the value temp a second time. diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 28f9dbe452715b..426921753e26c8 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15034,12 +15034,22 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. recv_fn := t.a.nodes[int(recv_fn_id)] is_method := recv_fn.kind == .selector && recv_fn.children_count > 0 recv_id := if is_method { t.a.children[recv_fn.children_start] } else { flat.empty_node } - // Position of the last operand that hoists a value branch (0 = method receiver, - // 1.. = arguments). An argument counts even when the branch is nested inside a - // compound expression (`1 + (match ...)`, `i64(match ...)`): lowering it still + // A plain (non-method) call whose callee is itself a value branch — + // `(match node { ... make_cb(node)! ... })()` — must materialize operand 0 too; + // otherwise transform_call_args lowers child 0 with plain transform_expr and leaves the + // propagating branch tail in a value-less statement context, emitting an empty callee. + callee_is_value_branch := !is_method && t.is_value_match_or_if_operand(recv_fn_id) + // Position of the last operand that hoists a value branch (0 = method receiver or a + // branch callee, 1.. = arguments). An argument counts even when the branch is nested + // inside a compound expression (`1 + (match ...)`, `i64(match ...)`): lowering it still // materializes the inner branch into pending_stmts, so an earlier operand must be // stabilized to keep source order. - mut last_branch := if is_method && t.is_value_match_or_if_operand(recv_id) { 0 } else { -1 } + mut last_branch := if (is_method && t.is_value_match_or_if_operand(recv_id)) + || callee_is_value_branch { + 0 + } else { + -1 + } for i in 1 .. node.children_count { if t.operand_hoists_value_branch(t.a.child(&node, i)) { last_branch = i @@ -15100,6 +15110,12 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. children_count: recv_fn.children_count pos: recv_fn.pos }) + } else if callee_is_value_branch { + r := t.transform_value_operand(recv_fn_id) + if r != recv_fn_id { + new_fn_id = r + changed = true + } } mut new_args := []flat.NodeId{cap: int(node.children_count)} for i in 1 .. node.children_count { From ee812dc717ae754ed3da94d7a550751793d3d170 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 00:07:59 +0300 Subject: [PATCH 28/37] v3: snapshot array-membership needles before a value-branch container hoists (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. In `x in container`, lower_array_membership_expr lowers the needle before the container (needle-first, receiver_first=false) but via stable_transformed_expr_for_reuse, which leaves a syntactically stable needle (an ident/selector) inline. When the container is a value branch whose prelude mutates that needle (`x in (match node { First { change(mut x)! } ... })`), the container prelude drains after the needle is left inline, so the generated membership loop reads the needle only after the mutation — not in source order. Snapshot the needle whenever the array container hoists a value branch (operand_hoists_value_branch(base_id) -> snapshot_transformed_expr_for_reuse), capturing its source-order value before the container prelude. The receiver-first path (arr.contains(x)) is unchanged: there the container is evaluated first in source order, so reading the needle after it is correct. Regression test select_value_membership_needle_snapshot: `c.v in (match node { First { c.arr_first(node)! } ... })` where the arm sets c.v = 100 -> `5 in [1, 5, 9]` = true; on HEAD the mutated needle leaks in (`100 in [1, 5, 9]` = false). --- ...s_if_expr_value_propagation_codegen_test.v | 27 ++++++++++++++++++- vlib/v3/transform/expr.v | 12 +++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) 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 f49464fac5d3af..21074f8bffb772 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 @@ -1089,6 +1089,30 @@ fn (mut c Counter) bump() !int { return 5 } +fn (mut c Counter) arr_first(_ First) ![]int { + c.v = 100 + return [1, 5, 9] +} + +fn (mut c Counter) arr_second(_ Second) ![]int { + c.v = 200 + return [2, 6] +} + +// A syntactically stable needle (a struct-field read) whose value the prelude of the +// value-branch container mutates: the membership loop must read the needle in source order, +// before the mutation. First -> c.v (5) in [1, 5, 9] -> true (a leaked mutation to 100 gives +// false). +fn select_value_membership_needle_snapshot(node Node) !bool { + mut c := Counter{ + v: 5 + } + return c.v in (match node { + First { c.arr_first(node)! } + Second { c.arr_second(node)! } + }) +} + // A syntactically stable LHS lvalue (a struct-field read) whose value the RHS branch prelude // mutates: the infix must read the LHS source-order value, not the updated value. First -> // c.v (1) + 5 = 6, then c.v is 100 -> 6 * 1000 + 100 = 6100 (a leaked mutation gives 105100). @@ -1427,6 +1451,7 @@ fn main() { println(select_value_nested_cap_order(First{})!) println(select_value_stable_lhs_snapshot(First{})!) println(select_value_stable_arg_snapshot(First{})!) + println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_addr(First{})!) } @@ -1441,5 +1466,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n41\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\ntrue\n41\n1' } diff --git a/vlib/v3/transform/expr.v b/vlib/v3/transform/expr.v index de956740f37a0a..aec83cc659c012 100644 --- a/vlib/v3/transform/expr.v +++ b/vlib/v3/transform/expr.v @@ -2270,8 +2270,16 @@ fn (mut t Transformer) lower_array_membership_expr(base_id flat.NodeId, needle_i elem_type, 'contains_needle') t.drain_pending(mut prefix) } else { - needle = t.stable_transformed_expr_for_reuse(t.transform_expr_for_type(needle_id, elem_type), - elem_type, 'contains_needle') + // `needle in container`: the needle is evaluated before the container in source order. + // If the container hoists a value branch whose prelude can mutate a syntactically stable + // needle (`x in (match node { First { change(mut x)! } ... })`), snapshot the needle's + // source-order value so the membership loop reads it before that prelude runs. + transformed_needle := t.transform_expr_for_type(needle_id, elem_type) + needle = if t.operand_hoists_value_branch(base_id) { + t.snapshot_transformed_expr_for_reuse(transformed_needle, elem_type, 'contains_needle') + } else { + t.stable_transformed_expr_for_reuse(transformed_needle, elem_type, 'contains_needle') + } t.drain_pending(mut prefix) base = t.stable_array_expr_for_membership(base_id, base_type, clean_base_type) t.drain_pending(mut prefix) From 531e6c89831ba4ddfb1242e2f53066d68f50669e Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 00:18:53 +0300 Subject: [PATCH 29/37] v3: snapshot range low bound and map base before a hoisted branch prelude (#28000) Addresses review feedback on #28011. Two more value-read ordering guards left a syntactically stable operand inline via stable_expr_for_reuse while lowering a later branch operand queued its prelude: - for-in range: lower_range_for_in lowers the low bound before the high bound. When the high bound hoists a value branch whose prelude mutates a stable low bound (`for i in low .. (match node { First { change_low(mut low)! } ... })`), the queued prelude runs before the loop initializer reads the inline low bound, so iteration starts at the mutated value. Snapshot the low bound when the high bound hoists a value branch. - map index: try_lower_map_index_expr leaves a stable map base inline, then lowers the key. When the key hoists a value branch whose prelude reassigns the base (`items[match node { First { replace(mut items)! } ... }]`), the lookup uses the replacement map instead of the map evaluated before the key. Snapshot the map base when the key hoists a value branch. Regression tests: select_value_range_low_snapshot (`for i in c.v .. (match { rng_hi })` where the arm sets c.v = 100 -> sum 1+2 = 3; leaked mutation gives an empty range and 0) and select_value_map_base_snapshot (`items[match { replace_map(mut items)! } ...]` -> original items["x"] = 5; leaked reassignment gives 999). --- ...s_if_expr_value_propagation_codegen_test.v | 52 ++++++++++++++++++- vlib/v3/transform/for.v | 7 ++- vlib/v3/transform/map.v | 8 ++- 3 files changed, 64 insertions(+), 3 deletions(-) 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 21074f8bffb772..d8b36a8a356354 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 @@ -1144,6 +1144,54 @@ fn select_value_stable_arg_snapshot(node Node) !int { }) } +fn (mut c Counter) rng_hi_first(_ First) !int { + c.v = 100 + return 3 +} + +fn (mut c Counter) rng_hi_second(_ Second) !int { + c.v = 200 + return 5 +} + +// A stable range low bound whose value the high-bound branch prelude mutates: the loop must +// start at the source-order low, not the mutated value. First -> `for i in 1 .. 3` sums 1+2 = +// 3 (a leaked mutation of the low bound to 100 gives an empty range and sum 0). +fn select_value_range_low_snapshot(node Node) !int { + mut c := Counter{ + v: 1 + } + mut sum := 0 + for i in c.v .. (match node { + First { c.rng_hi_first(node)! } + Second { c.rng_hi_second(node)! } + }) { + sum += i + } + return sum +} + +fn replace_map(mut m map[string]int) !string { + m = { + "x": 999 + } + return "x" +} + +// A stable map base whose variable the key branch prelude reassigns: the lookup must use the +// map evaluated before the key, not the replacement. First -> original items["x"] = 5 (a +// leaked reassignment gives the replacement value 999). +fn select_value_map_base_snapshot(node Node) !int { + mut items := { + "x": 5 + "y": 7 + } + return items[match node { + First { replace_map(mut items)! } + Second { "y" } + }] +} + type IntFn = fn () int struct FnBox { @@ -1451,6 +1499,8 @@ fn main() { println(select_value_nested_cap_order(First{})!) println(select_value_stable_lhs_snapshot(First{})!) println(select_value_stable_arg_snapshot(First{})!) + println(select_value_range_low_snapshot(First{})!) + println(select_value_map_base_snapshot(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_addr(First{})!) @@ -1466,5 +1516,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\ntrue\n41\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n1' } diff --git a/vlib/v3/transform/for.v b/vlib/v3/transform/for.v index 8b16de6a358597..30902d9a1fc32d 100644 --- a/vlib/v3/transform/for.v +++ b/vlib/v3/transform/for.v @@ -813,9 +813,14 @@ fn (mut t Transformer) lower_range_for_in(id flat.NodeId, node flat.Node, key_id // propagating branch tail is lowered in a value-less statement context and emits an // empty expression. `transform_value_operand` materializes such a bound into a value // temp (stable for reuse in the loop condition); non-branch bounds keep - // `stable_expr_for_reuse`. The low bound is evaluated before the high bound. + // `stable_expr_for_reuse`. The low bound is evaluated before the high bound: if the high + // bound hoists a value branch whose prelude can mutate a syntactically stable low bound + // (`for i in low .. (match node { First { change_low(mut low)! } ... })`), snapshot the + // low bound's source-order value so the loop initializer reads it before that prelude. low := if t.is_value_match_or_if_operand(low_id) { t.transform_value_operand(low_id) + } else if t.operand_hoists_value_branch(high_id) && t.operand_needs_ordering_snapshot(low_id) { + t.snapshot_expr_for_reuse(low_id) } else { t.stable_expr_for_reuse(low_id) } diff --git a/vlib/v3/transform/map.v b/vlib/v3/transform/map.v index e380f9a237beef..4c7afc697c92db 100644 --- a/vlib/v3/transform/map.v +++ b/vlib/v3/transform/map.v @@ -339,9 +339,15 @@ fn (mut t Transformer) try_lower_map_index_expr(id flat.NodeId, node flat.Node) // `(match n { First { make_map_first(n)! } ... })['key']`); otherwise the propagating // arm tail is lowered in a value-less statement context and emits an empty expression. // `transform_value_operand` materializes it into a value temp (stable for the repeated - // use below); non-branch bases keep `stable_expr_for_reuse`. + // use below); non-branch bases keep `stable_expr_for_reuse`. The base is evaluated before + // the key: if the key hoists a value branch whose prelude can reassign a syntactically + // stable base (`items[match node { First { replace(mut items)! } ... }]`), snapshot the + // base's source-order value so the lookup uses the map evaluated before that prelude. map_expr := if t.is_value_match_or_if_operand(map_source_id) { t.transform_value_operand(map_source_id) + } else if t.operand_hoists_value_branch(key_id) + && t.operand_needs_ordering_snapshot(map_source_id) { + t.snapshot_expr_for_reuse(map_source_id) } else { t.stable_expr_for_reuse(map_source_id) } From 296f7e0d9a4dc662e8a7e74dd9c01c17a66e6d25 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 00:30:59 +0300 Subject: [PATCH 30/37] v3: lower channel send targets as values and snapshot stable targets (#28000) Addresses review feedback on #28011. In the `<- ... or {}` fast path, only the sent value received value-aware lowering; the target (child 0) still used plain transform_expr and was stabilized as an lvalue: - A value `match`/`if` channel target (`(match node { First { channel_first(node)! } ... }) <- 1 or { return }`) had its propagating arm tail lowered in a value-less statement context, emitting an empty channel expression (the generated C failed with "expected expression"). Lower the target through transform_value_operand so a branch target is materialized into a value temp. - A stable target was left inline, so a sent-value branch that reassigns it (`target <- (match node { First { retarget(mut target)! } ... }) or {}`) sent to the replacement channel instead of the target evaluated before the RHS. A send target is a channel reference handle and does not need lvalue identity, so snapshot the target value (snapshot_transformed_expr_for_reuse) when the sent value hoists a branch. This subsumes the earlier index-component / rvalue-root cases (still verified by the existing channel-order regressions), so the bespoke transformed_lvalue_root_needs_value_spill helper is removed. Regression tests: select_value_branch_channel_target (`(match ...) <- 7` -> 7; empty callee C error on HEAD) and select_value_channel_target_reassign (`c.target <- (match { c.retarget(node)! } ...)` where the arm sets c.target = c.ch2 -> 7 on the original ch1; leaked reassignment sends to ch2 -> 107). --- ...s_if_expr_value_propagation_codegen_test.v | 66 ++++++++++++++++- vlib/v3/transform/transform.v | 74 +++++-------------- 2 files changed, 82 insertions(+), 58 deletions(-) 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 d8b36a8a356354..fd699316903b81 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 @@ -1079,6 +1079,68 @@ fn select_value_composite_rvalue_channel_target(node Node) !int { return got * 100 + f.order[0] * 10 + f.order[1] } +struct ChanPick { +mut: + ch1 chan int + ch2 chan int +} + +fn (mut c ChanPick) pick_first(_ First) !chan int { + return c.ch1 +} + +fn (mut c ChanPick) pick_second(_ Second) !chan int { + return c.ch2 +} + +// The channel target is itself a value match producing a channel: it must be materialized as a +// value, not lowered as an empty channel. First -> send 7 to ch1 -> received 7. +fn select_value_branch_channel_target(node Node) !int { + mut c := ChanPick{ + ch1: chan int{cap: 1} + ch2: chan int{cap: 1} + } + (match node { + First { c.pick_first(node)! } + Second { c.pick_second(node)! } + }) <- 7 or { return -1 } + return <-c.ch1 +} + +struct ChanReassign { +mut: + ch1 chan int + ch2 chan int + target chan int +} + +fn (mut c ChanReassign) retarget(_ First) !int { + c.target = c.ch2 + return 7 +} + +// A stable channel target whose variable the sent-value branch reassigns: the value must be +// sent to the channel evaluated before the RHS, not the replacement. First -> 7 lands on the +// original target ch1 -> 7 (a leaked reassignment sends to ch2 -> 107). +fn select_value_channel_target_reassign(node Node) !int { + mut c := ChanReassign{ + ch1: chan int{cap: 1} + ch2: chan int{cap: 1} + } + c.target = c.ch1 + c.target <- (match node { + First { c.retarget(node)! } + Second { 1 } + }) or { return -1 } + return if c.ch1.len == 1 { + <-c.ch1 + } else if c.ch2.len == 1 { + 100 + <-c.ch2 + } else { + -1 + } +} + struct Counter { mut: v int @@ -1490,6 +1552,8 @@ fn main() { println(select_value_nested_channel_target_order(First{})!) println(select_value_rvalue_channel_target(First{})!) println(select_value_composite_rvalue_channel_target(First{})!) + println(select_value_branch_channel_target(First{})!) + println(select_value_channel_target_reassign(First{})!) println(select_value_plain_call_order(First{})!) println(select_value_nested_branch_arg_order(First{})!) println(select_value_mut_arg(First{})!) @@ -1516,5 +1580,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 426921753e26c8..c021317031663c 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9924,45 +9924,6 @@ fn (mut t Transformer) stabilize_transformed_lvalue_for_reuse(id flat.NodeId) fl } } -// transformed_lvalue_root_needs_value_spill reports whether stabilize_transformed_lvalue_for_reuse -// would leave a side-effecting subexpression inline in `id`. That stabilizer spills index/selector -// *components* and deref operands, but for an index/selector it recurses into the *base* and -// returns a non-lvalue base (a call or other rvalue) unchanged. So a target rooted in a -// side-effecting rvalue (e.g. `make_channels()[0]`) is not made reusable by that stabilization — -// only its outer shape is rebuilt (a new node id) while the rvalue base still runs at use time. -// Walk the lvalue spine to the root leaf: if that leaf is a non-lvalue rvalue that is not stable, -// the whole target must instead be spilled by value. -fn (t &Transformer) transformed_lvalue_root_needs_value_spill(id flat.NodeId) bool { - if int(id) < 0 || int(id) >= t.a.nodes.len { - return false - } - node := t.a.nodes[int(id)] - match node.kind { - .ident { - return false - } - .selector, .index, .paren { - if node.children_count == 0 { - return false - } - return t.transformed_lvalue_root_needs_value_spill(t.a.child(&node, 0)) - } - .prefix { - // A deref (`*p`) has its pointer operand spilled as a component, so it is safe; - // any other prefix is not an lvalue root. - if node.op == .mul { - return false - } - return !t.is_stable_expr_for_reuse(id) - } - else { - // A non-lvalue root (call, etc.): the stabilizer leaves it inline, so if it is - // side-effecting (not stable) the whole target must be spilled by value. - return !t.is_stable_expr_for_reuse(id) - } - } -} - fn (mut t Transformer) stabilize_transformed_lvalue_component(id flat.NodeId, prefix string) flat.NodeId { if t.is_stable_expr_for_reuse(id) { return id @@ -14742,25 +14703,24 @@ fn (mut t Transformer) transform_infix_expr(id flat.NodeId, node flat.Node) flat t.mark_fn_used('sync__Channel__try_push_priv') t.mark_fn_used('sync__Channel__closed_error') send_prelude_start := t.pending_stmts.len - mut lhs := t.transform_expr(t.a.child(&node, 0)) + target_id := t.a.child(&node, 0) + // Route a value `match`/`if` channel target through value lowering so its propagating + // arm tail is materialized as a value temp, e.g. + // `(match node { First { channel_first(node)! } ... }) <- 1 or { return }`; otherwise + // it is lowered in a value-less statement context and emits an empty channel + // expression. `transform_value_operand` is a no-op for the common non-branch targets. + mut lhs := t.transform_value_operand(target_id) sent_value_id := t.a.child(&rhs, 0) - // Stabilize the channel target before materializing a sent value that hoists a value - // branch — directly or nested inside a compound sent value (`channels[next()] <- - // wrap(match ...) or {}`) — so a side-effecting target (e.g. - // `channels[next(mut trace)] <- (match ...) or {}`) evaluates before the sent value's - // hoisted prelude. A stable-rooted lvalue target has only its dynamic base/index - // components spilled (preserving the lvalue shape without spilling the channel value). - // A target rooted in a side-effecting rvalue — a bare call (`get_channel(mut trace) - // <- ...`) or one under an index/selector (`make_channels(mut trace)[0] <- ...`), - // which the lvalue stabilizer would leave inline — is spilled whole by value. - if t.operand_hoists_value_branch(sent_value_id) { - lhs = if t.is_stable_expr_for_reuse(lhs) { - lhs - } else if t.transformed_lvalue_root_needs_value_spill(lhs) { - t.stable_transformed_expr_for_reuse(lhs, t.node_type(lhs), 'chan_target') - } else { - t.stabilize_transformed_lvalue_for_reuse(lhs) - } + // A send target is a channel reference handle, not an lvalue that must be written + // through, so when the sent value hoists a value branch, snapshot the target's channel + // value before that branch's prelude. This captures the source-order channel even if + // the prelude reassigns a stable target + // (`target <- (match ... { retarget(mut target)! } ...)`) or mutates a side-effecting + // target's components (`channels[next()] <- (match ...)`). A value-branch target is + // already materialized into a temp above. + if t.operand_hoists_value_branch(sent_value_id) + && !t.is_value_match_or_if_operand(target_id) { + lhs = t.snapshot_transformed_expr_for_reuse(lhs, t.node_type(lhs), 'chan_target') } // Route a value `match`/`if` sent value through value lowering so its propagating // arm tail is materialized as a value, e.g. From 8fc64d7dd1be576b2c5834b3e5379ca01706c12a Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 00:42:44 +0300 Subject: [PATCH 31/37] v3: stabilize non-method runtime callees before a hoisted branch argument (#28000) Addresses review feedback on #28011. The operand-ordering path materialized a direct value-branch callee (callee_is_value_branch) but never stabilized any other plain callee, so a non-method runtime callee preceding a later branch argument stayed inline. For `make_cb(mut trace)(match node { First { trace.arg(node)! } ... })`, the argument prelude was queued before the final call evaluated make_cb, reversing callee-before-argument order (and a function-valued variable could be reassigned by the prelude before it is read). Treat the non-method callee as operand position 0: when a later argument hoists a value branch, snapshot the callee via callee_needs_ordering_snapshot, which stabilizes a runtime callee expression (a call/index/... that is not pure-constant) and a function-valued local variable, but leaves a plain top-level function-name ident inline (name-based call dispatch relies on it) and skips already-snapshotted temps. Regression test select_value_runtime_callee_order: `make_adder_cb(mut tr)(match node { First { tr.cbarg_first(node)! } ... })` where both record into an order trace -> 712 (the callee runs first); a reversed order gives 721 (verified: disabling the snapshot reproduces 721). --- ...s_if_expr_value_propagation_codegen_test.v | 36 ++++++++++++++++++- vlib/v3/transform/transform.v | 31 ++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) 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 fd699316903b81..3d66f8085be1fd 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 @@ -1290,6 +1290,39 @@ fn select_value_branch_callee(node Node) !int { })() } +type ArgFn = fn (int) int + +fn adder(x int) int { + return x +} + +fn make_adder_cb(mut tr Tracer) ArgFn { + tr.order << 1 + return adder +} + +fn (mut tr Tracer) cbarg_first(_ First) !int { + tr.order << 2 + return 7 +} + +fn (mut tr Tracer) cbarg_second(_ Second) !int { + tr.order << 2 + return 9 +} + +// A non-method runtime callee (a call returning a function value) with a value-match argument: +// the callee must evaluate before the argument prelude. First -> make_adder_cb (order 1) then +// arg (order 2), adder(7) = 7 -> 712 (a reversed order would be 721). +fn select_value_runtime_callee_order(node Node) !int { + mut tr := Tracer{} + r := make_adder_cb(mut tr)(match node { + First { tr.cbarg_first(node)! } + Second { tr.cbarg_second(node)! } + }) + return r * 100 + tr.order[0] * 10 + tr.order[1] +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1567,6 +1600,7 @@ fn main() { println(select_value_map_base_snapshot(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) + println(select_value_runtime_callee_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1580,5 +1614,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n712\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index c021317031663c..1a9c0ee5100f8f 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -15076,6 +15076,15 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. new_fn_id = r changed = true } + } else if last_branch > 0 && t.callee_needs_ordering_snapshot(recv_fn_id) { + // A non-method runtime callee (make_cb(mut trace)(match ...), or a function-valued + // variable a branch could reassign) must evaluate before a later branch argument's + // hoisted prelude, so snapshot it in source order. + r := t.snapshot_expr_for_reuse(recv_fn_id) + if r != recv_fn_id { + new_fn_id = r + changed = true + } } mut new_args := []flat.NodeId{cap: int(node.children_count)} for i in 1 .. node.children_count { @@ -20485,6 +20494,28 @@ fn (t &Transformer) is_local_fn_value_call(node flat.Node) bool { return local_type.starts_with('fn ') || t.is_fn_pointer_type_name(local_type) } +// callee_needs_ordering_snapshot reports whether a plain-call callee (operand 0) must be +// snapshotted to keep callee-before-argument order before a later branch argument's hoisted +// prelude. A runtime callee expression (`make_cb(mut trace)(match ...)`) must evaluate once, +// in source order; a function-valued local variable can be reassigned by the prelude, so it is +// snapshotted too. A plain top-level function-name ident is a constant reference that +// name-based call dispatch relies on, so it is left inline. Already-snapshotted temps are not +// re-snapshotted. +fn (t &Transformer) callee_needs_ordering_snapshot(id flat.NodeId) bool { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return false + } + node := t.a.nodes[int(id)] + if node.kind == .ident { + if t.is_ordering_snapshot_temp(id) { + return false + } + local_type := t.var_type(node.value) + return local_type.starts_with('fn ') || t.is_fn_pointer_type_name(local_type) + } + return t.operand_needs_ordering_snapshot(id) +} + // const_type_name supports const type name handling for Transformer. fn (t &Transformer) const_type_name(name string) ?string { if isnil(t.tc) || name.len == 0 { From de6b95cdaafc4a71eade5cfcd614a0fb28a29eab Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 00:54:17 +0300 Subject: [PATCH 32/37] v3: snapshot gated-index bases and mutable-lvalue index components before branch operands (#28000) Addresses review feedback on #28011. - Gated index: lower_gated_scalar_index lowers the base before the index but via stable_expr_for_reuse, which leaves a stable base inline. When the index hoists a value branch whose prelude reassigns the base (`values#[match n { First { replace(mut values)! } ... }]`), the gated access indexes the replacement array instead of the array evaluated before the index. Snapshot the base when the index hoists a value branch. - Mutable-lvalue index components: stabilize_original_lvalue_receiver (used for mut receivers and `mut` arguments) preserved a stable index/deref component inline via is_stable_expr_for_reuse. When a later branch argument mutates that index (`items[idx].update(match ... { change_idx(mut idx)! } ...)`), the mutation then targets the element at the new index. Snapshot value-bearing index/deref components (is_pure_constant_expr gate) into temps while keeping the surrounding lvalue identity, so the element at the source-order index is mutated. This helper only runs in value-branch operand contexts, so the extra spill is scoped to those. Regression tests: select_value_gated_base_snapshot (`values#[match { replace_arr(mut values)! } ...]` -> original values#[0] = 5; leaked reassignment indexes [100,200,300] -> 100) and select_value_mut_receiver_index_snapshot (`items[c.v].add(match { c.idx_bump_first(node)! } ...)` where the arm sets c.v = 1 -> items[0].v = 45 -> 4550; mutated index updates items[1] -> 4055). --- ...s_if_expr_value_propagation_codegen_test.v | 49 ++++++++++++++++++- vlib/v3/transform/transform.v | 16 ++++-- 2 files changed, 61 insertions(+), 4 deletions(-) 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 3d66f8085be1fd..171a438a528288 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 @@ -1254,6 +1254,51 @@ fn select_value_map_base_snapshot(node Node) !int { }] } +fn replace_arr(mut a []int) !int { + a = [100, 200, 300] + return 0 +} + +// A stable gated-index base whose variable the index branch prelude reassigns: the gated access +// must index the array evaluated before the index, not the replacement. First -> original +// values#[0] = 5 (a leaked reassignment indexes [100, 200, 300] -> 100). +fn select_value_gated_base_snapshot(node Node) !int { + mut values := [5, 6, 7] + return values#[match node { + First { replace_arr(mut values)! } + Second { 1 } + }] +} + +fn (mut c Counter) idx_bump_first(_ First) !int { + c.v = 1 + return 5 +} + +fn (mut c Counter) idx_bump_second(_ Second) !int { + c.v = 1 + return 6 +} + +// A mutable receiver whose index (a stable field read) the value-branch argument mutates: the +// index value must be captured in source order, so the method updates the original element. +// First -> items[c.v=0].add(5) -> items[0].v = 45 (a mutated index updates items[1] -> 4055). +fn select_value_mut_receiver_index_snapshot(node Node) !int { + mut c := Counter{ + v: 0 + } + mut items := [MutItem{ + v: 40 + }, MutItem{ + v: 50 + }] + items[c.v].add(match node { + First { c.idx_bump_first(node)! } + Second { c.idx_bump_second(node)! } + }) + return items[0].v * 100 + items[1].v +} + type IntFn = fn () int struct FnBox { @@ -1598,6 +1643,8 @@ fn main() { println(select_value_stable_arg_snapshot(First{})!) println(select_value_range_low_snapshot(First{})!) println(select_value_map_base_snapshot(First{})!) + println(select_value_gated_base_snapshot(First{})!) + println(select_value_mut_receiver_index_snapshot(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) @@ -1614,5 +1661,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\ntrue\n41\n712\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\ntrue\n41\n712\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 1a9c0ee5100f8f..c284ac12771dfb 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9982,7 +9982,7 @@ fn (mut t Transformer) stabilize_original_lvalue_receiver(id flat.NodeId) ?flat. return none } child_id := t.a.child(&node, 0) - new_child := if t.is_stable_expr_for_reuse(child_id) { + new_child := if t.is_pure_constant_expr(child_id) { child_id } else { t.spill_original_lvalue_component(child_id, 'recv_deref') @@ -10008,7 +10008,11 @@ fn (mut t Transformer) stabilize_original_lvalue_receiver(id flat.NodeId) ?flat. mut children := [base] for i in 1 .. node.children_count { comp_id := t.a.child(&node, i) - children << if t.is_stable_expr_for_reuse(comp_id) { + // Snapshot a value-bearing index component (an ident/selector a later branch + // prelude could mutate) into a temp while keeping the surrounding lvalue shape, + // so `items[idx].update(match ... { change(mut idx)! } ...)` mutates the element + // at the source-order index. A pure constant index needs no snapshot. + children << if t.is_pure_constant_expr(comp_id) { comp_id } else { t.spill_original_lvalue_component(comp_id, 'recv_index') @@ -15501,9 +15505,15 @@ fn (mut t Transformer) lower_gated_scalar_index(node flat.Node) ?flat.NodeId { // `values#[match n { First { get_index()! } else { other_index()! } }]`. // `transform_value_operand` materializes such an operand into a value temp (already // stable for the multiple uses below); non-branch operands keep `stable_expr_for_reuse`. - // The base is evaluated before the index, preserving base-before-index order. + // The base is evaluated before the index: if the index hoists a value branch whose prelude + // can reassign a syntactically stable base (`values#[match n { First { replace(mut values)! + // } ... }]`), snapshot the base's source-order value so the gated access reads it before + // that prelude. base := if t.is_value_match_or_if_operand(base_child) { t.transform_value_operand(base_child) + } else if t.operand_hoists_value_branch(idx_child) + && t.operand_needs_ordering_snapshot(base_child) { + t.snapshot_expr_for_reuse(base_child) } else { t.stable_expr_for_reuse(base_child) } From af10fafbc4e40ec071720f8afa3f5128e24f4670 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 01:05:22 +0300 Subject: [PATCH 33/37] v3: snapshot function-field callees instead of only their receiver (#28000) Addresses review feedback on #28011. The operand-ordering path classified any selector callee as a method (is_method := recv_fn.kind == .selector), so a function-valued field callee like `p.callback(match ...)` took the method path and stabilized only the receiver `p`. For a reference-backed holder, a hoisted argument prelude could then replace `p.callback` before the rebuilt `p.callback` selector was read, invoking the new callback instead of the callee evaluated before the arguments. Distinguish a function-field callee via the existing receiver_selector_is_fn_field: such a selector is not a method, so it falls into the non-method callee path and is snapshotted whole (callee_needs_ordering_snapshot -> snapshot_expr_for_reuse captures the field value before the arguments). A real method selector still stabilizes only its receiver, since a method name cannot be reassigned. Only value-branch operand contexts reach this block, so the reclassification is scoped to them. Regression test select_value_fn_field_callee_order: `p.callback(match node { First { p.install_new(node)! } ... })` on `mut p := &CbHolder{callback: cb_orig}`, where the arm sets `p.callback = cb_new` -> cb_orig(3) = 30; on HEAD the replaced field is invoked -> cb_new(3) = 300. --- ...s_if_expr_value_propagation_codegen_test.v | 35 ++++++++++++++++++- vlib/v3/transform/transform.v | 16 +++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) 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 171a438a528288..72572de18b8b53 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 @@ -1368,6 +1368,38 @@ fn select_value_runtime_callee_order(node Node) !int { return r * 100 + tr.order[0] * 10 + tr.order[1] } +fn cb_orig(x int) int { + return x * 10 +} + +fn cb_new(x int) int { + return x * 100 +} + +struct CbHolder { +mut: + callback ArgFn +} + +fn (mut h CbHolder) install_new(_ First) !int { + h.callback = cb_new + return 3 +} + +// The callee is a function-valued field on a reference-backed holder; the value-match argument +// prelude replaces the field. The call must invoke the callback evaluated before the arguments +// (snapshotting the whole callee), not the replacement. First -> cb_orig(3) = 30 (a replaced +// field would invoke cb_new(3) = 300). +fn select_value_fn_field_callee_order(node Node) !int { + mut p := &CbHolder{ + callback: cb_orig + } + return p.callback(match node { + First { p.install_new(node)! } + Second { 3 } + }) +} + fn combine(a int, b int) int { return a * 10 + b } @@ -1648,6 +1680,7 @@ fn main() { println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) + println(select_value_fn_field_callee_order(First{})!) println(select_value_addr(First{})!) } ') or { @@ -1661,5 +1694,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\ntrue\n41\n712\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\ntrue\n41\n712\n30\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index c284ac12771dfb..f7d6b3b4170b71 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14996,8 +14996,20 @@ fn (mut t Transformer) transform_call_expr(id flat.NodeId, node flat.Node) flat. if node.children_count > 0 { recv_fn_id := t.a.children[node.children_start] recv_fn := t.a.nodes[int(recv_fn_id)] - is_method := recv_fn.kind == .selector && recv_fn.children_count > 0 - recv_id := if is_method { t.a.children[recv_fn.children_start] } else { flat.empty_node } + is_selector_call := recv_fn.kind == .selector && recv_fn.children_count > 0 + recv_sel_base_id := if is_selector_call { + t.a.children[recv_fn.children_start] + } else { + flat.empty_node + } + // A function-valued field callee (`p.callback(...)`) is a selector but not a method call: + // the field holds a function value that an argument prelude can replace (via a + // reference-backed holder), so it must be snapshotted whole like any other runtime callee + // rather than treated as a method that only stabilizes its receiver. + is_fn_field_callee := is_selector_call + && t.receiver_selector_is_fn_field(t.normalize_type_alias(t.trim_pointer_type(t.lvalue_type(recv_sel_base_id))), recv_fn.value) + is_method := is_selector_call && !is_fn_field_callee + recv_id := if is_method { recv_sel_base_id } else { flat.empty_node } // A plain (non-method) call whose callee is itself a value branch — // `(match node { ... make_cb(node)! ... })()` — must materialize operand 0 too; // otherwise transform_call_args lowers child 0 with plain transform_expr and leaves the From 88c77dc88a7d3bce13179a63109e6454125b5bf7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 01:26:26 +0300 Subject: [PATCH 34/37] v3: capture mutable receiver storage and order struct fields before branch operands (#28000) Addresses review feedback on #28011. - Mutable receiver storage: stabilize_original_lvalue_receiver preserved a receiver lvalue by returning its base identifier unchanged, which does not stabilize a receiver reached through a reassignable pointer or container. A later branch argument prelude could then reassign the indirection before the rebuilt lvalue is read, so the method mutated the replacement. Capture such storage in source order: a pointer-valued receiver (`holder.ptr.update(match ... { retarget(mut holder)! } ...)`) is snapshotted whole (a reference handle needs no lvalue identity), and a reassignable array/map container base (`items[i].update(match ... { replace(mut items)! } ...)`) is snapshotted so the element mutation still reaches the source-order backing storage. - Struct field order: transform_struct_fields drains each field value prelude into the shared prelude emitted before the struct initializer, so a later field whose value hoists a nested block/if/match branch ran its prelude while earlier inline field values were read afterward (`Pair{a: tr.first(), b: if c { match ... }}` ran second before first). Snapshot a preceding field value before a later field hoists its branch prelude. Regression tests: select_value_pointer_receiver_capture (16000; retargeted 11005), select_value_array_base_capture (2500; retargeted 2005), select_value_struct_field_order (3412; reversed 3421). --- ...s_if_expr_value_propagation_codegen_test.v | 112 +++++++++++++++++- vlib/v3/transform/struct.v | 21 ++++ vlib/v3/transform/transform.v | 22 +++- 3 files changed, 153 insertions(+), 2 deletions(-) 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 72572de18b8b53..b88be292aba202 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 @@ -1299,6 +1299,113 @@ fn select_value_mut_receiver_index_snapshot(node Node) !int { return items[0].v * 100 + items[1].v } +struct PtrObj { +mut: + v int +} + +fn (mut o PtrObj) add(x int) { + o.v += x +} + +struct PtrHolder { +mut: + ptr &PtrObj +} + +fn (mut h PtrHolder) retarget_first(_ First) !int { + h.ptr = &PtrObj{ + v: 1000 + } + return 5 +} + +fn (mut h PtrHolder) retarget_second(_ Second) !int { + h.ptr = &PtrObj{ + v: 2000 + } + return 6 +} + +// A pointer-field mutable receiver whose pointer a value-branch argument reassigns: the method +// must mutate the object selected in source order, not the replacement. First -> orig.v = 15, +// then h.ptr points to the replacement (v 1000) -> 15 * 1000 + 1000 = 16000 (a retargeted call +// gives 11005). +fn select_value_pointer_receiver_capture(node Node) !int { + orig := &PtrObj{ + v: 10 + } + mut h := PtrHolder{ + ptr: orig + } + h.ptr.add(match node { + First { h.retarget_first(node)! } + Second { h.retarget_second(node)! } + }) + return orig.v * 1000 + h.ptr.v +} + +fn replace_cells(mut a []PtrObj) !int { + a = [PtrObj{ + v: 1000 + }, PtrObj{ + v: 2000 + }] + return 5 +} + +// An indexed mutable receiver whose array base a value-branch argument replaces: the method +// must mutate the element in the source-order array. First -> orig[0].v = 15, items reassigned +// -> orig[0].v * 100 + items[0].v = 1500 + 1000 = 2500 (a retargeted call gives 2005). +fn select_value_array_base_capture(node Node) !int { + mut items := [PtrObj{ + v: 10 + }, PtrObj{ + v: 20 + }] + orig := items + items[0].add(match node { + First { replace_cells(mut items)! } + Second { 0 } + }) + return orig[0].v * 100 + items[0].v +} + +fn (mut tr Tracer) sfirst() int { + tr.order << 1 + return 3 +} + +fn (mut tr Tracer) ssecond(_ First) !int { + tr.order << 2 + return 4 +} + +struct SPair { + a int + b int +} + +// A later struct field whose value is a nested block/if with a propagating match tail hoists a +// prelude; earlier field values must be snapshotted so fields evaluate in source order. First +// -> a=3, b=4, order [1,2] -> 3 * 1000 + 4 * 100 + 12 = 3412 (a reversed order gives 3421). +fn select_value_struct_field_order(node Node) !int { + mut tr := Tracer{} + cond := true + p := SPair{ + a: tr.sfirst() + b: if cond { + match node { + First { tr.ssecond(node)! } + Second { 0 } + } + } else { + 0 + } + } + return p.a * 1000 + p.b * 100 + tr.order[0] * 10 + tr.order[1] +} + type IntFn = fn () int struct FnBox { @@ -1677,6 +1784,9 @@ fn main() { println(select_value_map_base_snapshot(First{})!) println(select_value_gated_base_snapshot(First{})!) println(select_value_mut_receiver_index_snapshot(First{})!) + println(select_value_pointer_receiver_capture(First{})!) + println(select_value_array_base_capture(First{})!) + println(select_value_struct_field_order(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) @@ -1694,5 +1804,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\ntrue\n41\n712\n30\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\ntrue\n41\n712\n30\n1' } diff --git a/vlib/v3/transform/struct.v b/vlib/v3/transform/struct.v index 7cb35b489c8d0d..4c863ce3e42ba5 100644 --- a/vlib/v3/transform/struct.v +++ b/vlib/v3/transform/struct.v @@ -50,6 +50,18 @@ fn (mut t Transformer) transform_struct_fields(id flat.NodeId, node flat.Node) f mut promoted_paths := map[string][]FieldInfo{} mut prelude := []flat.NodeId{} t.drain_pending(mut prelude) + // Source (child) position of the last field whose value hoists a value branch. A preceding + // field value is snapshotted before that field materializes its branch prelude, so struct + // fields evaluate in source order — a nested block/`if`/`match` tail otherwise runs its + // prelude before the struct initializer while earlier field values stay inline. + mut last_hoisting_field := -1 + for i in 0 .. node.children_count { + child := t.a.nodes[int(t.a.child(&node, i))] + if child.kind == .field_init && child.children_count > 0 + && t.operand_hoists_value_branch(t.a.child(&child, 0)) { + last_hoisting_field = i + } + } for i in 0 .. node.children_count { child_id := t.a.child(&node, i) child := t.a.nodes[int(child_id)] @@ -114,6 +126,15 @@ fn (mut t Transformer) transform_struct_fields(id flat.NodeId, node flat.Node) f if sum_field_type.len == 0 && field_type.len > 0 { new_val = t.coerce_transformed_expr_to_type(new_val, val_id, field_type) } + // Snapshot a preceding field value before a later field hoists its branch prelude, + // so this value is read in source order rather than after that prelude. + if i < last_hoisting_field && !t.is_pure_constant_expr(new_val) { + mut snap_typ := t.node_type(new_val) + if snap_typ.len == 0 { + snap_typ = field_type + } + new_val = t.snapshot_transformed_expr_for_reuse(new_val, snap_typ, 'struct_field') + } t.drain_pending(mut prelude) if int(child_id) in t.local_closure_field_cleanups { mut closure_type := field_type diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index f7d6b3b4170b71..53860a59209bda 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -9965,6 +9965,14 @@ fn (mut t Transformer) stabilize_original_lvalue_receiver(id flat.NodeId) ?flat. if int(id) < 0 || int(id) >= t.a.nodes.len { return none } + // A receiver/argument whose value is a pointer reaches its target through that pointer; a + // later branch prelude can reassign the pointer (`holder.ptr.update(match ... { + // retarget(mut holder)! } ...)`) before the rebuilt lvalue is read, retargeting the + // mutation. A pointer is a reference handle that needs no lvalue identity, so capture its + // value in source order. + if t.lvalue_type(id).starts_with('&') { + return t.snapshot_expr_for_reuse(id) + } node := t.a.nodes[int(id)] match node.kind { .ident { @@ -10004,7 +10012,19 @@ fn (mut t Transformer) stabilize_original_lvalue_receiver(id flat.NodeId) ?flat. if node.children_count == 0 { return none } - base := t.stabilize_original_lvalue_receiver(t.a.child(&node, 0))? + base_child := t.a.child(&node, 0) + // If the container base is a reassignable array/map, snapshot it so a later branch + // prelude that replaces the container (`items[i].update(match ... { replace(mut + // items)! } ...)`) cannot retarget the in-place mutation — the snapshot shares the + // original backing storage, so the element mutation still reaches the source-order + // container. (A pointer base is captured by the top-level check above.) + base_type := t.normalize_type_alias(t.trim_pointer_type(t.lvalue_type(base_child))) + base := if (base_type.starts_with('[]') || base_type.starts_with('map[')) + && !t.is_pure_constant_expr(base_child) { + t.snapshot_expr_for_reuse(base_child) + } else { + t.stabilize_original_lvalue_receiver(base_child)? + } mut children := [base] for i in 1 .. node.children_count { comp_id := t.a.child(&node, i) From ef7a5a4aaf904620ac1f11bf5b4b45e3cb5f85be Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 01:45:49 +0300 Subject: [PATCH 35/37] v3: capture the optional append target address before lowering a branch RHS (#28000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on #28011. In the optional-LHS array append (try_lower_optional_array_append_stmt), lhs_addr was computed from the inline transformed source (`&source.value`) after the RHS prelude, so a value-branch RHS that reassigns the optional source (`holder.values or { ... } << (match ... { holder.replace()! } ...)`) had the append target re-read from the source after the RHS rather than selected before it (the `!source.ok` guard already evaluates the source before the RHS). When the RHS hoists a value branch, capture the optional value-array address up front via stable_transformed_expr_for_reuse and reuse it for the append, so the target is selected in source order and consistent with the guard. Verified against mainline V: v3 already matched mainline across the scenarios I could construct (simple field, side-effecting base, indexed base, none-reassignment), and this change keeps that parity — for the cases that reach this path the value-array slot address is identical whether captured before or after the RHS, so it is a robustness/ordering fix with no behavioral divergence rather than a value change. Regression select_value_optional_append_reassign covers the reassigning-RHS push-many optional append (`holder.values or { ... } << (match ... { holder.replace_first()! } ...)` -> 500). --- ...s_if_expr_value_propagation_codegen_test.v | 29 ++++++++++++++++++- vlib/v3/transform/array.v | 21 +++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) 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 b88be292aba202..d092759b2c2e35 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 @@ -1406,6 +1406,32 @@ fn select_value_struct_field_order(node Node) !int { return p.a * 1000 + p.b * 100 + tr.order[0] * 10 + tr.order[1] } +struct OptHolder { +mut: + values ?[]int +} + +fn (mut h OptHolder) replace_first(_ First) ![]int { + h.values = [100, 200] + return [7, 8] +} + +// A push-many optional-LHS append whose value-branch RHS reassigns the optional source: the +// append targets the value-array storage selected before the RHS (captured up front), matching +// mainline. First -> [100, 200] << [7, 8] = [100, 200, 7, 8], len 4, first 100 -> 4 * 100 + 100 +// = 500. +fn select_value_optional_append_reassign(node Node) !int { + mut h := OptHolder{ + values: [1, 2] + } + h.values or { return error("none") } << (match node { + First { h.replace_first(node)! } + Second { [9] } + }) + got := h.values or { []int{} } + return got.len * 100 + got[0] +} + type IntFn = fn () int struct FnBox { @@ -1787,6 +1813,7 @@ fn main() { println(select_value_pointer_receiver_capture(First{})!) println(select_value_array_base_capture(First{})!) println(select_value_struct_field_order(First{})!) + println(select_value_optional_append_reassign(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) @@ -1804,5 +1831,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\ntrue\n41\n712\n30\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\n500\ntrue\n41\n712\n30\n1' } diff --git a/vlib/v3/transform/array.v b/vlib/v3/transform/array.v index beb1c49bd70a23..2fb122ff880624 100644 --- a/vlib/v3/transform/array.v +++ b/vlib/v3/transform/array.v @@ -1405,6 +1405,21 @@ fn (mut t Transformer) try_lower_optional_array_append_stmt(_node flat.Node, lhs source) result << t.make_if(not_ok, t.make_block(guard_stmts), t.make_empty()) + // If the RHS hoists a value branch whose prelude can reassign the optional source + // (`holder.values? << (match ... { holder.replace()! } ...)`), capture the optional's + // value-array address before lowering the RHS, so the append targets the storage selected in + // source order (consistent with the guard above) instead of re-reading the inline source + // after the RHS prelude. + mut captured_lhs_addr := flat.empty_node + mut has_captured_addr := false + if t.operand_hoists_value_branch(rhs_id) { + addr := t.runtime_addr(t.make_selector(source, 'value', array_type), array_type) + captured_lhs_addr = t.stable_transformed_expr_for_reuse(addr, '&${array_type}', + 'opt_append_target') + has_captured_addr = true + t.drain_pending(mut result) + } + mut rhs := flat.empty_node if !push_many { if !rhs_is_sum_variant { @@ -1448,7 +1463,11 @@ fn (mut t Transformer) try_lower_optional_array_append_stmt(_node flat.Node, lhs push_many = t.array_append_rhs_is_push_many(lhs_id, rhs_id, rhs_type, elem_type) } - lhs_addr := t.runtime_addr(t.make_selector(source, 'value', array_type), array_type) + lhs_addr := if has_captured_addr { + captured_lhs_addr + } else { + t.runtime_addr(t.make_selector(source, 'value', array_type), array_type) + } if push_many { call := if t.is_fixed_array_type(rhs_type) { t.make_call_typed('array_push_many_ptr', arr3(lhs_addr, rhs, From d87e0bfd0980f38d5adec207f3523fa6ca4efe65 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 02:07:33 +0300 Subject: [PATCH 36/37] v3: preserve source order across select send cases (#28000) Addresses review feedback on #28011. When a later select send-case value contains a propagating value branch, its materialization prelude stayed in the transformer global pending_stmts; transform_select_expr does not drain per branch, so the caller drains it before the whole select while gen_select evaluates earlier case values during select setup. Thus `select { ch1 <- tr.first() {} ch2 <- (match node { First { tr.second(node)! } ... }) {} }` ran second before first (order [2,1]), and such a prelude could also mutate an earlier case channel before it is read. When any select case hoists a value branch (select_case_hoists_value_branch), transform_select_branch now captures each case in source order: transform_select_send_ordered snapshots the channel and send value into temps, and transform_select_recv_ordered snapshots the receive channel. The snapshots land in pending_stmts in case order, so they drain before the select in source order and gen_select reads the temps. Selects with no hoisting case are unchanged. Regression test select_value_select_case_order: `select { ch1 <- tr.sel_first() {} ch2 <- (match node { First { tr.sel_second(node)! } ... }) {} }` -> order [1,2] -> 12; on HEAD the branch prelude runs first -> [2,1] -> 21. --- ...s_if_expr_value_propagation_codegen_test.v | 31 +++++- vlib/v3/transform/transform.v | 95 ++++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) 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 d092759b2c2e35..c6745c3836aa9a 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 @@ -1432,6 +1432,34 @@ fn select_value_optional_append_reassign(node Node) !int { return got.len * 100 + got[0] } +fn (mut tr Tracer) sel_first() int { + tr.order << 1 + return 3 +} + +fn (mut tr Tracer) sel_second(_ First) !int { + tr.order << 2 + return 4 +} + +// A select whose later send-case value is a value branch: all case values are evaluated during +// select setup in source order, so the branch prelude must not be drained before the whole +// select. First -> ch1 <- sel_first (order 1) then ch2 <- (match -> sel_second) (order 2) -> 12 +// (a reversed order would be 21). +fn select_value_select_case_order(node Node) !int { + mut tr := Tracer{} + ch1 := chan int{cap: 1} + ch2 := chan int{cap: 1} + select { + ch1 <- tr.sel_first() {} + ch2 <- (match node { + First { tr.sel_second(node)! } + Second { 0 } + }) {} + } + return tr.order[0] * 10 + tr.order[1] +} + type IntFn = fn () int struct FnBox { @@ -1814,6 +1842,7 @@ fn main() { println(select_value_array_base_capture(First{})!) println(select_value_struct_field_order(First{})!) println(select_value_optional_append_reassign(First{})!) + println(select_value_select_case_order(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) @@ -1831,5 +1860,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\n500\ntrue\n41\n712\n30\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\n500\n12\ntrue\n41\n712\n30\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index 53860a59209bda..ae9049c7643eac 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14389,10 +14389,23 @@ fn (mut t Transformer) transform_select_expr(node flat.Node) flat.NodeId { return t.make_block(body) } } + // A later send case whose value hoists a value branch materializes its prelude into + // pending_stmts, which is drained before the whole select while gen_select evaluates + // earlier case values during select setup — so `second` would run before `first` (and the + // prelude could mutate an earlier case's channel first). When any case hoists, capture each + // case's channel and send value into temps in case order so the preludes land in + // pending_stmts in source order. + mut order_cases := false + for i in 0 .. node.children_count { + if t.select_case_hoists_value_branch(t.a.child(&node, i)) { + order_cases = true + break + } + } mut branches := []flat.NodeId{cap: int(node.children_count)} if t.smartcast_stack.len == 0 { for i in 0 .. node.children_count { - branches << t.transform_select_branch(t.a.child(&node, i)) + branches << t.transform_select_branch(t.a.child(&node, i), order_cases) } } else { base_smartcasts := t.smartcast_stack.clone() @@ -14401,7 +14414,7 @@ fn (mut t Transformer) transform_select_expr(node flat.Node) flat.NodeId { for i in 0 .. node.children_count { t.smartcast_stack = base_smartcasts.clone() t.invalidated_smartcasts = base_invalidated.clone() - branches << t.transform_select_branch(t.a.child(&node, i)) + branches << t.transform_select_branch(t.a.child(&node, i), order_cases) for key, invalidated in t.invalidated_smartcasts { if invalidated { merged_invalidated[key] = true @@ -14424,7 +14437,7 @@ fn (mut t Transformer) transform_select_expr(node flat.Node) flat.NodeId { }) } -fn (mut t Transformer) transform_select_branch(id flat.NodeId) flat.NodeId { +fn (mut t Transformer) transform_select_branch(id flat.NodeId, order_cases bool) flat.NodeId { if int(id) < 0 || int(id) >= t.a.nodes.len { return id } @@ -14462,10 +14475,19 @@ fn (mut t Transformer) transform_select_branch(id flat.NodeId) flat.NodeId { mut children := []flat.NodeId{cap: int(branch.children_count)} for i in 0 .. body_start { child_id := t.a.child(&branch, i) + child := t.a.nodes[int(child_id)] children << if branch.value == 'recv_assign' && body_start == 2 && i == 0 { t.transform_lvalue_without_smartcast(child_id) } else if body_start == 2 && i == 0 { t.transform_lvalue(child_id) + } else if order_cases && child.kind == .infix && child.op == .arrow + && child.children_count >= 2 { + // Send case `ch <- value`: capture channel and value in source order. + t.transform_select_send_ordered(child) + } else if order_cases && child.kind == .prefix && child.op == .arrow + && child.children_count > 0 { + // Receive case `<-ch`: capture the channel in source order. + t.transform_select_recv_ordered(child) } else { t.transform_expr(child_id) } @@ -14534,6 +14556,73 @@ fn (mut t Transformer) transform_select_branch(id flat.NodeId) flat.NodeId { }) } +// select_case_hoists_value_branch reports whether a select case's send value hoists a value +// `match`/`if` whose materialization prelude would otherwise be drained before the whole select. +fn (t &Transformer) select_case_hoists_value_branch(id flat.NodeId) bool { + if int(id) < 0 || int(id) >= t.a.nodes.len { + return false + } + branch := t.a.nodes[int(id)] + if branch.kind != .select_branch || branch.children_count == 0 { + return false + } + first := t.a.nodes[int(t.a.child(&branch, 0))] + if first.kind == .infix && first.op == .arrow && first.children_count >= 2 { + return t.operand_hoists_value_branch(t.a.child(&first, 1)) + } + return false +} + +// transform_select_send_ordered lowers a select send case `ch <- value` capturing the channel +// and the send value into temps (in source order) so their evaluation lands in pending_stmts +// before a later case's hoisted prelude, matching gen_select's per-case setup order. +fn (mut t Transformer) transform_select_send_ordered(infix flat.Node) flat.NodeId { + channel_id := t.a.child(&infix, 0) + value_id := t.a.child(&infix, 1) + mut chan_expr := t.transform_expr(channel_id) + if !t.is_stable_expr_for_reuse(chan_expr) { + chan_expr = t.snapshot_transformed_expr_for_reuse(chan_expr, t.node_type(chan_expr), + 'select_chan') + } + mut val_expr := t.transform_value_operand(value_id) + if !t.is_stable_expr_for_reuse(val_expr) { + val_expr = t.snapshot_transformed_expr_for_reuse(val_expr, t.node_type(val_expr), + 'select_send_val') + } + start := t.a.children.len + t.a.children << chan_expr + t.a.children << val_expr + return t.a.add_node(flat.Node{ + kind: .infix + op: .arrow + children_start: start + children_count: 2 + pos: infix.pos + typ: infix.typ + }) +} + +// transform_select_recv_ordered lowers a select receive case `<-ch` capturing the channel into a +// temp so a later case's hoisted prelude cannot change the channel before it is read. +fn (mut t Transformer) transform_select_recv_ordered(prefix flat.Node) flat.NodeId { + channel_id := t.a.child(&prefix, 0) + mut chan_expr := t.transform_expr(channel_id) + if !t.is_stable_expr_for_reuse(chan_expr) { + chan_expr = t.snapshot_transformed_expr_for_reuse(chan_expr, t.node_type(chan_expr), + 'select_chan') + } + start := t.a.children.len + t.a.children << chan_expr + return t.a.add_node(flat.Node{ + kind: .prefix + op: .arrow + children_start: start + children_count: 1 + pos: prefix.pos + typ: prefix.typ + }) +} + fn smartcasts_without_binding(contexts []SmartcastContext, name string) []SmartcastContext { mut keep := []SmartcastContext{cap: contexts.len} for sc in contexts { From 541fe3db6b2b91e564b067f7dc529eae19e3cf26 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 3 Aug 2026 02:22:36 +0300 Subject: [PATCH 37/37] v3: snapshot nonconstant select operands and include branch-producing channels (#28000) Addresses review feedback on #28011. - The ordered select helpers snapshotted only operands classified as unstable (!is_stable_expr_for_reuse), so a stable value-bearing identifier in an earlier case that a later branch prelude mutates was left inline until select setup (`select { ch1 <- x {} ch2 <- (match ... { change(mut x)! } ...) {} }` read the mutated x). Snapshot nonconstant channel and send values via operand_needs_ordering_snapshot (the shared snapshot gate), so value-bearing operands are captured in source order and only pure constants stay inline. - The order_cases scan examined only a send case value, so a send or receive case whose channel is a value branch left order_cases false and the channel was lowered with plain transform_expr (`select { ch1 <- first() {} (match ... { make_channel()! } ...) <- 1 {} }` queued the channel prelude before the whole select while first() stayed in setup). Include channel operands (send + receive) in the scan and lower them through the value-aware path. Both paths share a new snapshot_select_operand helper: a value-branch operand is materialized via transform_value_operand; a nonconstant operand is snapshotted; a pure constant is left inline. Regression tests: select_value_select_stable_operand (`ch1 <- c.v` before `ch2 <- (match { c.sel_change()! })` -> ch1 gets 5; mutated 100 on HEAD) and select_value_select_branch_channel (`(match { c.c_make()! }) <- 1` channel case -> order [1,2] -> 12; reversed 21 on HEAD). --- ...s_if_expr_value_propagation_codegen_test.v | 62 ++++++++++++++++++- vlib/v3/transform/transform.v | 58 ++++++++++------- 2 files changed, 98 insertions(+), 22 deletions(-) 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 c6745c3836aa9a..780a6ec7b22599 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 @@ -1460,6 +1460,64 @@ fn select_value_select_case_order(node Node) !int { return tr.order[0] * 10 + tr.order[1] } +fn (mut c Counter) sel_change(_ First) !int { + c.v = 100 + return 0 +} + +// A stable value operand in an earlier select send case that a later branch prelude mutates: it +// must be captured in source order, before the prelude. First -> ch1 gets c.v in source order +// (5), not the mutated 100 (an unbuffered ch2 with no receiver forces case 1). +fn select_value_select_stable_operand(node Node) !int { + mut c := Counter{ + v: 5 + } + ch1 := chan int{cap: 1} + ch2 := chan int{} + select { + ch1 <- c.v {} + ch2 <- (match node { + First { c.sel_change(node)! } + Second { 0 } + }) {} + } + return <-ch1 +} + +struct ChanCtx { +mut: + order []int + ch2 chan int +} + +fn (mut c ChanCtx) c_first() int { + c.order << 1 + return 3 +} + +fn (mut c ChanCtx) c_make(_ First) !chan int { + c.order << 2 + return c.ch2 +} + +// A later select case whose channel is a value branch: case operands must evaluate in source +// order during select setup. First -> c_first (order 1) then the channel match -> c_make (order +// 2) -> 12 (a reversed order would be 21). +fn select_value_select_branch_channel(node Node) !int { + mut c := ChanCtx{ + ch2: chan int{cap: 1} + } + ch1 := chan int{cap: 1} + select { + ch1 <- c.c_first() {} + (match node { + First { c.c_make(node)! } + Second { c.ch2 } + }) <- 1 {} + } + return c.order[0] * 10 + c.order[1] +} + type IntFn = fn () int struct FnBox { @@ -1843,6 +1901,8 @@ fn main() { println(select_value_struct_field_order(First{})!) println(select_value_optional_append_reassign(First{})!) println(select_value_select_case_order(First{})!) + println(select_value_select_stable_operand(First{})!) + println(select_value_select_branch_channel(First{})!) println(select_value_membership_needle_snapshot(First{})!) println(select_value_branch_callee(First{})!) println(select_value_runtime_callee_order(First{})!) @@ -1860,5 +1920,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\n[20, 30, 40]\ntrue\n1\n100\nx=1\ntrue\n1\n2\n6\n6\n2\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\n500\n12\ntrue\n41\n712\n30\n1' + 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\n1112\n1212\n102412\n204812\n1012\n2012\n3012\n6\ntrue\n112\n112\ntrue\n60\n2\n512\n512\n712\n812\ntrue\n612\n61\n63\n1003\n42\n2012\n4512\n5002\n9912\n9912\n7712\n5512\n7\n7\n3412\n3512\n7612\n4004\n507\n212\n312\n6100\n1005\n3\n5\n5\n4550\n16000\n2500\n3412\n500\n12\n5\n12\ntrue\n41\n712\n30\n1' } diff --git a/vlib/v3/transform/transform.v b/vlib/v3/transform/transform.v index ae9049c7643eac..a6f8b4a245fd30 100644 --- a/vlib/v3/transform/transform.v +++ b/vlib/v3/transform/transform.v @@ -14556,8 +14556,9 @@ fn (mut t Transformer) transform_select_branch(id flat.NodeId, order_cases bool) }) } -// select_case_hoists_value_branch reports whether a select case's send value hoists a value -// `match`/`if` whose materialization prelude would otherwise be drained before the whole select. +// select_case_hoists_value_branch reports whether a select case's channel or send value hoists +// a value `match`/`if` whose materialization prelude would otherwise be drained before the whole +// select. It covers a send case's channel and value, and a receive case's channel. fn (t &Transformer) select_case_hoists_value_branch(id flat.NodeId) bool { if int(id) < 0 || int(id) >= t.a.nodes.len { return false @@ -14567,28 +14568,48 @@ fn (t &Transformer) select_case_hoists_value_branch(id flat.NodeId) bool { return false } first := t.a.nodes[int(t.a.child(&branch, 0))] + // Send case `ch <- value`: either the channel or the value can hoist a branch. if first.kind == .infix && first.op == .arrow && first.children_count >= 2 { - return t.operand_hoists_value_branch(t.a.child(&first, 1)) + return t.operand_hoists_value_branch(t.a.child(&first, 0)) + || t.operand_hoists_value_branch(t.a.child(&first, 1)) + } + // Receive case `<-ch`: the channel can hoist a branch. + if first.kind == .prefix && first.op == .arrow && first.children_count > 0 { + return t.operand_hoists_value_branch(t.a.child(&first, 0)) + } + // Receive-assign case `x := <-ch`: child 1 is the receive prefix. + if branch.children_count >= 2 { + second := t.a.nodes[int(t.a.child(&branch, 1))] + if second.kind == .prefix && second.op == .arrow && second.children_count > 0 { + return t.operand_hoists_value_branch(t.a.child(&second, 0)) + } } return false } +// snapshot_select_operand lowers a select-case channel or send value in source order. A value +// `match`/`if` operand (directly or nested) is materialized through the value-aware path; a +// nonconstant (value-bearing) operand is snapshotted into a temp so a later case's hoisted +// prelude cannot mutate a stable identifier before select setup reads it; a pure constant is +// left inline. +fn (mut t Transformer) snapshot_select_operand(id flat.NodeId, prefix string) flat.NodeId { + val := t.transform_value_operand(id) + if t.is_value_match_or_if_operand(id) { + // Already materialized into a value temp above. + return val + } + if t.operand_needs_ordering_snapshot(val) { + return t.snapshot_transformed_expr_for_reuse(val, t.node_type(val), prefix) + } + return val +} + // transform_select_send_ordered lowers a select send case `ch <- value` capturing the channel // and the send value into temps (in source order) so their evaluation lands in pending_stmts // before a later case's hoisted prelude, matching gen_select's per-case setup order. fn (mut t Transformer) transform_select_send_ordered(infix flat.Node) flat.NodeId { - channel_id := t.a.child(&infix, 0) - value_id := t.a.child(&infix, 1) - mut chan_expr := t.transform_expr(channel_id) - if !t.is_stable_expr_for_reuse(chan_expr) { - chan_expr = t.snapshot_transformed_expr_for_reuse(chan_expr, t.node_type(chan_expr), - 'select_chan') - } - mut val_expr := t.transform_value_operand(value_id) - if !t.is_stable_expr_for_reuse(val_expr) { - val_expr = t.snapshot_transformed_expr_for_reuse(val_expr, t.node_type(val_expr), - 'select_send_val') - } + chan_expr := t.snapshot_select_operand(t.a.child(&infix, 0), 'select_chan') + val_expr := t.snapshot_select_operand(t.a.child(&infix, 1), 'select_send_val') start := t.a.children.len t.a.children << chan_expr t.a.children << val_expr @@ -14605,12 +14626,7 @@ fn (mut t Transformer) transform_select_send_ordered(infix flat.Node) flat.NodeI // transform_select_recv_ordered lowers a select receive case `<-ch` capturing the channel into a // temp so a later case's hoisted prelude cannot change the channel before it is read. fn (mut t Transformer) transform_select_recv_ordered(prefix flat.Node) flat.NodeId { - channel_id := t.a.child(&prefix, 0) - mut chan_expr := t.transform_expr(channel_id) - if !t.is_stable_expr_for_reuse(chan_expr) { - chan_expr = t.snapshot_transformed_expr_for_reuse(chan_expr, t.node_type(chan_expr), - 'select_chan') - } + chan_expr := t.snapshot_select_operand(t.a.child(&prefix, 0), 'select_chan') start := t.a.children.len t.a.children << chan_expr return t.a.add_node(flat.Node{