neo_solidity/ir/context/
lowering_context.rs

1/// Structured diagnostic emitted during IR lowering.
2///
3/// Captures the originating function name, a human-readable message, and an
4/// optional actionable suggestion so that CLI consumers can render richer
5/// error output than a bare string.
6#[derive(Debug, Clone, serde::Serialize)]
7pub struct IrDiagnostic {
8    pub function_name: String,
9    pub message: String,
10    pub suggestion: Option<String>,
11    pub code: Option<String>,
12}
13
14impl IrDiagnostic {
15    /// Format as a human-readable error string.
16    pub fn display(&self) -> String {
17        let mut out = format!("function '{}': {}", self.function_name, self.message);
18        if let Some(ref suggestion) = self.suggestion {
19            out.push_str(&format!("\n  help: {}", suggestion));
20        }
21        out
22    }
23}
24
25struct LoweringContext<'a> {
26    function_name: String,
27    is_safe: bool,
28    param_index_map: HashMap<String, usize>,
29    param_types: &'a [ValueType],
30    return_slots: Vec<Option<usize>>,
31    return_types: Vec<ValueType>,
32    state_variables: &'a [StateVariableMetadata],
33    state_index_map: &'a HashMap<String, usize>,
34    state_types: &'a [ValueType],
35    /// Canonical struct type definitions available to the compilation unit.
36    ///
37    /// This enables resolving user-defined structs even when they are only used
38    /// in local variables (i.e., not present in state/param/return types).
39    defined_struct_types: &'a [ValueType],
40    event_index_map: &'a HashMap<String, usize>,
41    event_signature_map: &'a HashMap<String, Vec<ManifestType>>,
42    enum_variant_map: &'a HashMap<String, HashMap<String, u64>>,
43    contract_types: &'a HashSet<String>,
44    selector_registry: &'a SelectorRegistry,
45    function_names: &'a HashSet<String>,
46    function_overloads: &'a HashMap<(String, usize), String>,
47    /// Ordered parameter names for each function overload, keyed by (name, arg_count).
48    /// Used to reorder named function call arguments into positional order.
49    function_param_names: &'a HashMap<(String, usize), Vec<String>>,
50    /// Functions that return void (empty return_parameters). Used to avoid
51    /// emitting DROP after calling a void internal function as a statement.
52    void_functions: &'a HashSet<String>,
53    /// Mapping from original method name to renamed super-method name.
54    /// Used to resolve `super.method()` calls during IR lowering.
55    super_method_map: &'a HashMap<String, String>,
56    local_index_map: HashMap<String, Vec<usize>>,
57    local_types: HashMap<usize, ValueType>,
58    scope_stack: Vec<Vec<String>>,
59    storage_aliases: HashMap<String, StorageReference>,
60    call_data_locals: HashMap<usize, String>,
61    local_count: u16,
62    label_counter: usize,
63    loop_stack: Vec<LoopLabels>,
64    errors: Vec<IrDiagnostic>,
65}
66
67impl<'a> LoweringContext<'a> {
68    #[allow(clippy::too_many_arguments)]
69    fn new(
70        function_name: &str,
71        is_safe: bool,
72        param_index_map: HashMap<String, usize>,
73        param_types: &'a [ValueType],
74        state_variables: &'a [StateVariableMetadata],
75        state_index_map: &'a HashMap<String, usize>,
76        state_types: &'a [ValueType],
77        defined_struct_types: &'a [ValueType],
78        event_index_map: &'a HashMap<String, usize>,
79        event_signature_map: &'a HashMap<String, Vec<ManifestType>>,
80        enum_variant_map: &'a HashMap<String, HashMap<String, u64>>,
81        contract_types: &'a HashSet<String>,
82        selector_registry: &'a SelectorRegistry,
83        function_names: &'a HashSet<String>,
84        function_overloads: &'a HashMap<(String, usize), String>,
85        function_param_names: &'a HashMap<(String, usize), Vec<String>>,
86        void_functions: &'a HashSet<String>,
87        super_method_map: &'a HashMap<String, String>,
88    ) -> Self {
89        Self {
90            function_name: function_name.to_string(),
91            is_safe,
92            param_index_map,
93            param_types,
94            return_slots: Vec::new(),
95            return_types: Vec::new(),
96            state_variables,
97            state_index_map,
98            state_types,
99            defined_struct_types,
100            event_index_map,
101            event_signature_map,
102            enum_variant_map,
103            contract_types,
104            selector_registry,
105            function_names,
106            function_overloads,
107            function_param_names,
108            void_functions,
109            super_method_map,
110            local_index_map: HashMap::new(),
111            local_types: HashMap::new(),
112            scope_stack: vec![Vec::new()],
113            storage_aliases: HashMap::new(),
114            call_data_locals: HashMap::new(),
115            local_count: 0,
116            label_counter: 0,
117            loop_stack: Vec::new(),
118            errors: Vec::new(),
119        }
120    }
121
122    fn set_return_info(&mut self, slots: Vec<Option<usize>>, types: Vec<ValueType>) {
123        self.return_slots = slots;
124        self.return_types = types;
125    }
126
127    fn return_slots(&self) -> &[Option<usize>] {
128        &self.return_slots
129    }
130
131    fn return_types(&self) -> &[ValueType] {
132        &self.return_types
133    }
134
135    fn next_label(&mut self) -> usize {
136        let label = self.label_counter;
137        self.label_counter += 1;
138        label
139    }
140
141    fn push_loop(&mut self, continue_label: usize, break_label: usize) {
142        self.loop_stack.push(LoopLabels {
143            continue_label,
144            break_label,
145        });
146    }
147
148    fn pop_loop(&mut self) {
149        self.loop_stack.pop();
150    }
151
152    fn break_target(&self) -> Option<usize> {
153        self.loop_stack.last().map(|labels| labels.break_label)
154    }
155
156    fn continue_target(&self) -> Option<usize> {
157        self.loop_stack.last().map(|labels| labels.continue_label)
158    }
159
160    fn record_error(&mut self, message: impl Into<String>) {
161        self.errors.push(IrDiagnostic {
162            function_name: self.function_name.clone(),
163            message: message.into(),
164            suggestion: None,
165            code: None,
166        });
167    }
168
169    fn record_error_with_suggestion(
170        &mut self,
171        message: impl Into<String>,
172        suggestion: impl Into<String>,
173    ) {
174        self.errors.push(IrDiagnostic {
175            function_name: self.function_name.clone(),
176            message: message.into(),
177            suggestion: Some(suggestion.into()),
178            code: None,
179        });
180    }
181
182    fn set_call_data_local(&mut self, local_index: usize, method: String) {
183        self.call_data_locals.insert(local_index, method);
184    }
185
186    fn clear_call_data_local(&mut self, local_index: usize) {
187        self.call_data_locals.remove(&local_index);
188    }
189
190    fn call_data_method_for_local(&self, local_index: usize) -> Option<&str> {
191        self.call_data_locals
192            .get(&local_index)
193            .map(|method| method.as_str())
194    }
195
196    fn is_contract_type_name(&self, name: &str) -> bool {
197        self.contract_types.contains(name)
198    }
199
200    fn type_method_selectors(&self, type_name: &str, method_name: &str) -> Option<&Vec<[u8; 4]>> {
201        self.selector_registry
202            .type_method_selectors
203            .get(type_name)
204            .and_then(|methods| methods.get(method_name))
205    }
206
207    fn is_interface_type_name(&self, name: &str) -> bool {
208        self.selector_registry.interface_types.contains(name)
209    }
210
211    fn interface_id_for_type(&self, type_name: &str) -> Option<[u8; 4]> {
212        let methods = self.selector_registry.type_method_selectors.get(type_name)?;
213        let mut selectors: HashSet<[u8; 4]> = HashSet::new();
214        for overloads in methods.values() {
215            for selector in overloads {
216                selectors.insert(*selector);
217            }
218        }
219
220        let mut interface_id = [0u8; 4];
221        for selector in selectors {
222            for (idx, byte) in selector.iter().enumerate() {
223                interface_id[idx] ^= byte;
224            }
225        }
226        Some(interface_id)
227    }
228
229    fn state_type(&self, index: usize) -> Option<&ValueType> {
230        self.state_types.get(index)
231    }
232
233    fn state_metadata(&self, index: usize) -> Option<&StateVariableMetadata> {
234        self.state_variables.get(index)
235    }
236
237    fn can_write_state(&self, state_index: usize) -> bool {
238        let Some(meta) = self.state_metadata(state_index) else {
239            return true;
240        };
241
242        if !meta.is_immutable {
243            return true;
244        }
245
246        self.function_name == "constructor" || self.function_name == "_deploy"
247    }
248
249    fn ensure_state_writable(&mut self, state_index: usize) -> bool {
250        if self.can_write_state(state_index) {
251            return true;
252        }
253
254        let variable_name = self
255            .state_metadata(state_index)
256            .and_then(|meta| meta.name.as_deref())
257            .unwrap_or("<unnamed>")
258            .to_string();
259
260        self.record_error_with_suggestion(
261            format!(
262                "cannot assign to immutable state variable '{}' outside constructor/deploy initialization",
263                variable_name
264            ),
265            "initialize immutable values in the declaration or constructor only",
266        );
267        false
268    }
269
270    fn parameter_type(&self, name: &str) -> Option<&ValueType> {
271        self.param_index_map
272            .get(name)
273            .and_then(|idx| self.param_types.get(*idx))
274    }
275
276    fn local_type(&self, index: usize) -> Option<&ValueType> {
277        self.local_types.get(&index)
278    }
279
280    fn variable_type(&self, name: &str) -> Option<ValueType> {
281        if let Some(reference) = self.storage_alias(name) {
282            return Some(reference.value_type.clone());
283        }
284        if let Some(index) = self.state_index_map.get(name) {
285            if let Some(ty) = self.state_type(*index) {
286                return Some(ty.clone());
287            }
288        }
289        if let Some(ty) = self.parameter_type(name) {
290            return Some(ty.clone());
291        }
292        if let Some(local_index) = self.resolve_local(name) {
293            if let Some(ty) = self.local_type(local_index) {
294                return Some(ty.clone());
295            }
296        }
297        None
298    }
299
300    fn neo_function_name(&self, name: &str, arg_count: usize) -> Option<String> {
301        self.function_overloads
302            .get(&(name.to_string(), arg_count))
303            .cloned()
304    }
305
306    /// Returns the ordered parameter names for a function overload, if known.
307    fn get_function_param_names(&self, name: &str, arg_count: usize) -> Option<&[String]> {
308        self.function_param_names
309            .get(&(name.to_string(), arg_count))
310            .map(|v| v.as_slice())
311    }
312
313    /// Returns true if the named function returns void (no return values).
314    fn is_void_function(&self, name: &str) -> bool {
315        self.void_functions.contains(name)
316    }
317
318    /// Returns the renamed super-method name for `super.method()` resolution.
319    fn super_method_name(&self, method_name: &str) -> Option<&str> {
320        self.super_method_map.get(method_name).map(|s| s.as_str())
321    }
322
323    fn event_signature(&self, event_name: &str) -> Option<&[ManifestType]> {
324        self.event_signature_map
325            .get(event_name)
326            .map(|sig| sig.as_slice())
327    }
328
329    fn allocate_local(&mut self, name: String, value_type: Option<ValueType>) -> usize {
330        let index = self.local_count as usize;
331        self.local_count = self.local_count.checked_add(1).unwrap_or(self.local_count);
332        if let Some(scope) = self.scope_stack.last_mut() {
333            scope.push(name.clone());
334        }
335        self.local_index_map.entry(name).or_default().push(index);
336        if let Some(ty) = value_type {
337            self.local_types.insert(index, ty);
338        }
339        index
340    }
341
342    fn resolve_local(&self, name: &str) -> Option<usize> {
343        self.local_index_map
344            .get(name)
345            .and_then(|stack| stack.last().copied())
346    }
347
348    fn ensure_local(&mut self, name: &str) -> usize {
349        if let Some(index) = self.resolve_local(name) {
350            index
351        } else {
352            self.allocate_local(name.to_string(), None)
353        }
354    }
355
356    fn enter_scope(&mut self) {
357        self.scope_stack.push(Vec::new());
358    }
359
360    fn exit_scope(&mut self) {
361        if let Some(names) = self.scope_stack.pop() {
362            for name in names {
363                if let Some(stack) = self.local_index_map.get_mut(&name) {
364                    if let Some(index) = stack.pop() {
365                        self.local_types.remove(&index);
366                    }
367                    if stack.is_empty() {
368                        self.local_index_map.remove(&name);
369                    }
370                }
371                self.storage_aliases.remove(&name);
372            }
373        }
374    }
375
376    fn is_local_in_current_scope(&self, name: &str) -> bool {
377        self.scope_stack
378            .last()
379            .is_some_and(|scope| scope.iter().any(|existing| existing == name))
380    }
381
382    fn set_storage_alias(&mut self, name: String, alias: StorageReference) {
383        self.storage_aliases.insert(name, alias);
384    }
385
386    fn storage_alias(&self, name: &str) -> Option<&StorageReference> {
387        self.storage_aliases.get(name)
388    }
389}