neo_solidity/ir/statements/assignments/
compound.rs

1fn lower_compound_assignment(
2    lhs: &Expression,
3    rhs: &Expression,
4    ctx: &mut LoweringContext,
5    instructions: &mut Vec<Instruction>,
6    op: BinaryOperator,
7) -> bool {
8    // Solidity compound assignment semantics:
9    // - Evaluate the LHS location once (including any array/mapping index expressions).
10    // - Compute `lhs_value op rhs_value`.
11    // - Store the result back to the same LHS.
12    // - The expression result is the stored value (so statement lowering can safely DROP it).
13
14    if let Some(mapping) = resolve_mapping_access(lhs, ctx) {
15        if !ctx.ensure_state_writable(mapping.state_index) {
16            return false;
17        }
18
19        // Evaluate keys left-to-right (Solidity semantics) and store them so we can reuse them
20        // for both the load and the store without re-evaluating side effects.
21        let tmp_id = ctx.next_label();
22        let mut key_locals: Vec<usize> = Vec::new();
23        for (index, key_expr) in mapping.key_expressions.iter().enumerate() {
24            let local = ctx.allocate_local(
25                format!("__compound_key_{tmp_id}_{index}"),
26                mapping.key_types.get(index).cloned(),
27            );
28            if !lower_expression(key_expr, ctx, instructions) {
29                ctx.record_error_with_suggestion(
30                    "failed to lower mapping key in compound assignment",
31                    "ensure the mapping key expression is a supported type (integer, string, bytes, or address)",
32                );
33                return false;
34            }
35            instructions.push(Instruction::StoreLocal(local));
36            key_locals.push(local);
37        }
38
39        // Load current value (keys must be pushed in reverse order for slot hashing).
40        for local in key_locals.iter().rev() {
41            instructions.push(Instruction::LoadLocal(*local));
42        }
43        instructions.push(Instruction::LoadMappingElement {
44            state_index: mapping.state_index,
45            key_types: mapping.key_types.clone(),
46        });
47
48        // Evaluate RHS after LHS value (left-to-right semantics for `lhs op rhs`).
49        if !lower_expression(rhs, ctx, instructions) {
50            return false;
51        }
52
53        instructions.push(Instruction::BinaryOp(op));
54
55        // Store result while preserving it as the expression value.
56        let result_local = ctx.allocate_local(format!("__compound_value_{tmp_id}"), None);
57        instructions.push(Instruction::StoreLocal(result_local));
58
59        instructions.push(Instruction::LoadLocal(result_local));
60        for local in key_locals.iter().rev() {
61            instructions.push(Instruction::LoadLocal(*local));
62        }
63        instructions.push(Instruction::StoreMappingElement {
64            state_index: mapping.state_index,
65            key_types: mapping.key_types.clone(),
66        });
67
68        instructions.push(Instruction::LoadLocal(result_local));
69        return true;
70    }
71
72    if let Some(reference) = resolve_storage_reference(lhs, ctx) {
73        if !ctx.ensure_state_writable(reference.state_index) {
74            return false;
75        }
76
77        if let Some(field) = reference.field_path.last() {
78            let field_keys: Vec<[u8; 32]> = reference.field_path.iter().map(|field| field.key).collect();
79            let tmp_id = ctx.next_label();
80            let mut key_locals: Vec<usize> = Vec::new();
81
82            for (index, key_expr) in reference.key_expressions.iter().enumerate() {
83                let local = ctx.allocate_local(
84                    format!("__compound_key_{tmp_id}_{index}"),
85                    reference.key_types.get(index).cloned(),
86                );
87                if !lower_expression(key_expr, ctx, instructions) {
88                    ctx.record_error("failed to lower storage key in compound assignment");
89                    return false;
90                }
91                instructions.push(Instruction::StoreLocal(local));
92                key_locals.push(local);
93            }
94
95            let push_keys_for_slot = |instructions: &mut Vec<Instruction>| {
96                for local in key_locals.iter().rev() {
97                    instructions.push(Instruction::LoadLocal(*local));
98                }
99            };
100
101            // Load current field value.
102            push_keys_for_slot(instructions);
103            instructions.push(Instruction::LoadStructField {
104                state_index: reference.state_index,
105                key_types: reference.key_types.clone(),
106                field_keys: field_keys.clone(),
107                field_type: field.ty.clone(),
108            });
109
110            // Evaluate RHS after LHS value (left-to-right semantics for `lhs op rhs`).
111            if !lower_expression(rhs, ctx, instructions) {
112                return false;
113            }
114
115            instructions.push(Instruction::BinaryOp(op));
116
117            // Store result while preserving it as the expression value.
118            let result_local = ctx.allocate_local(format!("__compound_value_{tmp_id}"), None);
119            instructions.push(Instruction::StoreLocal(result_local));
120
121            instructions.push(Instruction::LoadLocal(result_local));
122            push_keys_for_slot(instructions);
123            instructions.push(Instruction::StoreStructField {
124                state_index: reference.state_index,
125                key_types: reference.key_types.clone(),
126                field_keys,
127                field_type: field.ty.clone(),
128            });
129
130            instructions.push(Instruction::LoadLocal(result_local));
131            return true;
132        }
133    }
134
135    if let Expression::ArraySubscript(_, array, Some(index)) = lhs {
136        // Memory array element compound assignment: `arr[i] op= rhs`.
137        // Preserve evaluation order and avoid evaluating `array` / `index` twice.
138        let tmp_id = ctx.next_label();
139        let array_local = ctx.allocate_local(format!("__compound_arr_{tmp_id}"), None);
140        let index_local = ctx.allocate_local(format!("__compound_idx_{tmp_id}"), None);
141
142        if !lower_expression(array, ctx, instructions) {
143            return false;
144        }
145        instructions.push(Instruction::StoreLocal(array_local));
146
147        if !lower_expression(index, ctx, instructions) {
148            return false;
149        }
150        instructions.push(Instruction::StoreLocal(index_local));
151
152        // Load current element.
153        instructions.push(Instruction::LoadLocal(array_local));
154        instructions.push(Instruction::LoadLocal(index_local));
155        instructions.push(Instruction::ArrayGet);
156
157        if !lower_expression(rhs, ctx, instructions) {
158            return false;
159        }
160
161        instructions.push(Instruction::BinaryOp(op));
162
163        // Store and return the updated value.
164        let result_local = ctx.allocate_local(format!("__compound_value_{tmp_id}"), None);
165        instructions.push(Instruction::StoreLocal(result_local));
166
167        instructions.push(Instruction::LoadLocal(array_local));
168        instructions.push(Instruction::LoadLocal(index_local));
169        instructions.push(Instruction::LoadLocal(result_local));
170        instructions.push(Instruction::ArraySet);
171
172        instructions.push(Instruction::LoadLocal(result_local));
173        return true;
174    }
175
176    if let Expression::Variable(identifier) = lhs {
177        let store_instr = if let Some(local) = ctx.resolve_local(&identifier.name) {
178            Instruction::StoreLocal(local)
179        } else if let Some(state_index) = ctx.state_index_map.get(&identifier.name).copied() {
180            if !ctx.ensure_state_writable(state_index) {
181                return false;
182            }
183            Instruction::StoreState(state_index)
184        } else {
185            let local = ctx.ensure_local(&identifier.name);
186            Instruction::StoreLocal(local)
187        };
188
189        let tmp_id = ctx.next_label();
190        let result_local = ctx.allocate_local(format!("__compound_value_{tmp_id}"), None);
191
192        // Evaluate lhs then rhs (left-to-right) so BinaryOp sees [lhs, rhs].
193        if !lower_expression(lhs, ctx, instructions) {
194            return false;
195        }
196        if !lower_expression(rhs, ctx, instructions) {
197            return false;
198        }
199
200        instructions.push(Instruction::BinaryOp(op));
201        instructions.push(Instruction::StoreLocal(result_local));
202
203        instructions.push(Instruction::LoadLocal(result_local));
204        instructions.push(store_instr);
205
206        instructions.push(Instruction::LoadLocal(result_local));
207        return true;
208    }
209
210    ctx.record_error_with_suggestion(
211        "unsupported compound assignment target",
212        "compound assignment (+=, -=, etc.) is only supported for local variables, state variables, and mapping/array elements",
213    );
214    false
215}