Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8fd6937
v3: lower value-context match/if block operands with `!`/`?` propagat…
medvednikov Aug 2, 2026
2e31fa4
v3: route specialized infix operands through value lowering (#28000)
medvednikov Aug 2, 2026
5e2e8de
v3: route left-shift and sum-cast match/if operands through value low…
medvednikov Aug 2, 2026
800519e
v3: materialize match/if operands before cast dispatch and for `is` s…
medvednikov Aug 2, 2026
d26ae44
v3: preserve infix operand order and lower address-of match/if operan…
medvednikov Aug 2, 2026
faa61a1
v3: preserve evaluation order for match/if left-shift and index opera…
medvednikov Aug 2, 2026
319d914
v3: route gated-index match/if operands through value lowering (#28000)
medvednikov Aug 2, 2026
20f00b3
v3: lower propagating match/if values in range bounds (#28000)
medvednikov Aug 2, 2026
773d820
v3: stabilize range low bound before hoisting high bound in membershi…
medvednikov Aug 2, 2026
b7bb6a1
v3: route non-range membership and for-in containers through value lo…
medvednikov Aug 2, 2026
f378044
v3: route map-index bases and preserve needle order for match/if memb…
medvednikov Aug 2, 2026
b345126
v3: preserve array-append LHS order and lower match/if needles in con…
medvednikov Aug 2, 2026
98d026f
v3: evaluate map-membership key before hoisting the container (#28000)
medvednikov Aug 2, 2026
fd5233b
v3: lower push-many match/if append RHS as a value (#28000)
medvednikov Aug 2, 2026
595d597
v3: materialize value match/if method receivers before builtin dispat…
medvednikov Aug 2, 2026
7986db1
v3: materialize value match/if method arguments before builtin dispat…
medvednikov Aug 2, 2026
cdacd5e
v3: materialize value match/if channel-send values and order call ope…
medvednikov Aug 2, 2026
44279af
v3: preserve lvalue receivers and stabilize channel targets for match…
medvednikov Aug 2, 2026
391aee8
v3: order plain-call operands, preserve mut arg lvalues, lower array-…
medvednikov Aug 2, 2026
824b564
v3: spill non-mut lvalue args by value and order array-init len befor…
medvednikov Aug 2, 2026
f021bb9
v3: detect nested value branches when ordering call operands (#28000)
medvednikov Aug 2, 2026
bef373f
v3: detect nested value branches in append/index/array-init operand o…
medvednikov Aug 2, 2026
97e69f3
v3: detect nested value branches in infix/shift/range/membership/chan…
medvednikov Aug 2, 2026
4743d9e
v3: spill by-value method receivers and rvalue channel targets before…
medvednikov Aug 2, 2026
40591bf
v3: spill composite rvalue channel targets rooted in a call before th…
medvednikov Aug 2, 2026
49fa317
v3: snapshot value-bearing operands before a hoisted branch prelude c…
medvednikov Aug 2, 2026
aabe76a
v3: materialize branch-produced call callees and type-pattern members…
medvednikov Aug 2, 2026
ee812dc
v3: snapshot array-membership needles before a value-branch container…
medvednikov Aug 2, 2026
531e6c8
v3: snapshot range low bound and map base before a hoisted branch pre…
medvednikov Aug 2, 2026
296f7e0
v3: lower channel send targets as values and snapshot stable targets …
medvednikov Aug 2, 2026
8fc64d7
v3: stabilize non-method runtime callees before a hoisted branch argu…
medvednikov Aug 2, 2026
de6b95c
v3: snapshot gated-index bases and mutable-lvalue index components be…
medvednikov Aug 2, 2026
af10faf
v3: snapshot function-field callees instead of only their receiver (#…
medvednikov Aug 2, 2026
88c77dc
v3: capture mutable receiver storage and order struct fields before b…
medvednikov Aug 2, 2026
ef7a5a4
v3: capture the optional append target address before lowering a bran…
medvednikov Aug 2, 2026
d87e0bf
v3: preserve source order across select send cases (#28000)
medvednikov Aug 2, 2026
541fe3d
v3: snapshot nonconstant select operands and include branch-producing…
medvednikov Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,924 changes: 1,924 additions & 0 deletions vlib/v3/tests/match_as_if_expr_value_propagation_codegen_test.v

Large diffs are not rendered by default.

75 changes: 68 additions & 7 deletions vlib/v3/transform/array.v
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,37 @@ 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 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.operand_hoists_value_branch(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 {
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.
mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int')
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' {
val := t.transform_expr(t.a.child(child, 0))
mut val := t.transform_expr_for_type(t.a.child(child, 0), 'int')
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
Comment on lines 417 to 422

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stabilize len before hoisting a cap match

When len has side effects and cap is a value match/if with propagation, transforming cap queues its materialization before new_call, while the transformed len expression remains inline in that call. Thus []int{len: trace_len(), cap: match node { ... trace_cap()! ... }} evaluates trace_cap before trace_len, reversing field evaluation order. Stabilize earlier fields before a later field hoists statements and add an ordering regression.

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

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 824b564. lower_array_init_to_runtime now finds the last len/cap field whose value is a value branch, and stabilizes an earlier side-effecting len/cap field into a temp before that field hoists its materialization prelude — both are evaluated into new_call, so this preserves their source order. (init is per-element in the loop body and already ordered after.)

Regression test select_value_array_init_cap_order: []int{len: tr.tlen(), cap: match node { First { tr.tcap_first(node)! } … }} where both record into an order trace → 212 (len=1 then cap=2). On HEAD it is 221 (tcap ran before tlen). No new regressions; review_cgen passes.

} else if child.value == 'init' {
init_expr_id = t.a.child(child, 0)
Expand Down Expand Up @@ -437,7 +460,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 {
Expand Down Expand Up @@ -1167,7 +1191,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 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)
mut rhs := flat.empty_node
if !push_many {
Expand Down Expand Up @@ -1196,7 +1228,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)
Expand Down Expand Up @@ -1368,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 {
Expand Down Expand Up @@ -1395,7 +1447,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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture the optional append target before lowering the RHS

In the optional-LHS path, a push-many branch such as holder.values? << (match node { First { holder.replace(node)! } ... }) can reassign holder.values while materializing the RHS. The guard above evaluates the original selector, but lhs_addr later reuses the inline source after the RHS prelude, so the append targets the replacement optional rather than the storage selected before evaluating the RHS. Snapshot the optional source storage/address before lowering the branch and add an ordering regression.

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

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in ef7a5a4. The optional value-array address is now captured up front when the RHS hoists a value branch, instead of re-reading the inline source at lhs_addr after the RHS prelude:

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')
    ...
}

So the append target is selected before the RHS, consistent with the !source.ok guard above it.

One transparency note on the regression. I built the branch and compared v3 against mainline v across several shapes of this exact scenario — simple field (holder.values or {...} << (match ... { holder.replace()! } ...)), a side-effecting source base (ctx.get().values? << ...), an indexed source base (holders[ctx.idx].values? << ... where the arm bumps ctx.idx), and an arm that sets the source to none. In every case v3 already produced the same result as mainline (e.g. the simple field gives [100, 200, 7, 8] on both), so the append was already targeting the same storage mainline uses. 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 (source for a dynamic base is already stabilized to a single evaluation), so it's a robustness/ordering fix (selecting the target in source order, matching the guard) rather than a value change — I couldn't construct a case where the observable result diverges.

The added regression select_value_optional_append_reassign (holder.values or { ... } << (match node { First { holder.replace_first(node)! } ... }) -> [100,200,7,8] -> 500) is therefore path coverage for the reassigning-RHS push-many optional append rather than a strictly load-bearing ordering test — I flag that honestly. If you have a specific reproducer where v3 diverges from mainline here, I'll add a targeted load-bearing regression and confirm this fix addresses it. No new regressions; review_cgen passes.

}
if !push_many {
rhs = t.coerce_transformed_expr_to_type(rhs, rhs_id, elem_type)
Expand All @@ -1406,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,
Expand Down
Loading
Loading