-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
v3: lower value-context match/if block operands with !/? propagation (fix #28000)
#28011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 34 commits
8fd6937
2e31fa4
5e2e8de
800519e
d26ae44
faa61a1
319d914
20f00b3
773d820
b7bb6a1
f378044
b345126
98d026f
fd5233b
595d597
7986db1
cdacd5e
44279af
391aee8
824b564
f021bb9
bef373f
97e69f3
4743d9e
40591bf
49fa317
aabe76a
ee812dc
531e6c8
296f7e0
8fc64d7
de6b95c
af10faf
88c77dc
ef7a5a4
d87e0bf
541fe3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } else if child.value == 'init' { | ||
| init_expr_id = t.a.child(child, 0) | ||
|
|
@@ -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 { | ||
|
|
@@ -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 { | ||
|
|
@@ -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) | ||
|
|
@@ -1395,7 +1432,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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the optional-LHS path, a push-many branch such as AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 One transparency note on the regression. I built the branch and compared v3 against mainline The added regression |
||
| } | ||
| if !push_many { | ||
| rhs = t.coerce_transformed_expr_to_type(rhs, rhs_id, elem_type) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1875,11 +1875,38 @@ 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. | ||
| 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) | ||
| // 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) | ||
|
Comment on lines
+1889
to
+1890
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This value-aware membership handling only covers the range branch; for AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in aabe76a. One thing to note: I couldn't add a runtime regression because this path isn't reachable in v3 yet — the checker rejects So the fix is applied for correctness/consistency and will be exercised once the v3 checker admits type-pattern membership, but a value-propagation regression can't compile under v3 today. I can fold in the checker support separately if you'd like it wired up end to end. No new regressions; |
||
| } 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) | ||
|
Comment on lines
+1889
to
+1894
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the RHS container is itself a value AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b7bb6a1. All non-range membership container lowerings now go through
Regression test |
||
| } | ||
| // If the high bound hoists a value branch — directly or nested inside a compound | ||
| // bound (`.. (1 + (match ...))`) — its materialization below queues prelude | ||
| // 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.operand_needs_ordering_snapshot(low_id) { | ||
| t.snapshot_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) | ||
| lt_cmp := t.make_infix(.lt, new_lhs, new_high) | ||
|
|
@@ -1922,21 +1949,29 @@ 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) | ||
| if rhs_is_ptr_array { | ||
| new_rhs = t.make_prefix(.mul, new_rhs) | ||
| } | ||
| // (value-aware so a `match`/`if` container is materialized as a value) | ||
| 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.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) | ||
|
Comment on lines
+1959
to
+1963
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This value-aware needle lowering is never reached for membership in a constant string array because AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b345126. Regression test |
||
| } | ||
| 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') | ||
| } | ||
| } 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 { | ||
|
|
@@ -1954,16 +1989,29 @@ 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) | ||
| new_rhs := t.transform_expr(rhs_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.snapshot_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) | ||
| len_expr := t.make_fixed_array_len_expr(clean_rhs_type) | ||
| 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) | ||
| new_rhs := t.transform_expr(rhs_id) | ||
| // 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.operand_hoists_value_branch(rhs_id) { | ||
| t.snapshot_expr_for_reuse(lhs_id) | ||
| } else { | ||
| t.transform_expr(lhs_id) | ||
| } | ||
| new_rhs := t.transform_value_operand(rhs_id) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a string membership container is a value AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f378044. In the inline membership branches (dynamic-array, fixed-array, string, and the unknown fallback), a side-effecting needle is now stabilized to a temp before a value-branch container is materialized, so it evaluates first — preserving needle-before-container source order. For the dynamic-array branch (which transformed the container first) the needle is now evaluated before the container; the others simply gate Regression test |
||
| fn_name := if t.node_type(lhs_id) in ['u8', 'byte'] { | ||
| 'string__contains_u8' | ||
| } else { | ||
|
|
@@ -1977,8 +2025,13 @@ 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) | ||
| new_rhs := t.transform_expr(rhs_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.snapshot_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 | ||
| t.a.children << new_rhs | ||
|
|
@@ -2066,7 +2119,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) | ||
|
|
@@ -2110,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. | ||
|
|
@@ -2204,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) | ||
|
|
@@ -2374,7 +2448,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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a propagating value-branch container mutates a syntactically stable needle, this materialization queues the container prelude after AGENTS.md reference: AGENTS.md:L657-L658 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in ee812dc. In The needle-first branch now snapshots the needle when the array container hoists a value branch — Regression test |
||
| if t.membership_container_is_pointer_array(raw_type) { | ||
| expr = t.make_prefix(.mul, expr) | ||
| } | ||
|
|
@@ -3329,6 +3407,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 { | ||
|
|
@@ -3372,6 +3512,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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
lenhas side effects andcapis a valuematch/ifwith propagation, transformingcapqueues its materialization beforenew_call, while the transformedlenexpression remains inline in that call. Thus[]int{len: trace_len(), cap: match node { ... trace_cap()! ... }}evaluatestrace_capbeforetrace_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 👍 / 👎.
There was a problem hiding this comment.
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_runtimenow finds the lastlen/capfield whose value is a value branch, and stabilizes an earlier side-effectinglen/capfield into a temp before that field hoists its materialization prelude — both are evaluated intonew_call, so this preserves their source order. (initis 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 (tcapran beforetlen). No new regressions;review_cgenpasses.