neo_solidity/ir/statements/dispatch/
try_catch.rs

1fn value_type_to_catch_guard(value_type: &ValueType) -> Option<ConvertTarget> {
2    match value_type {
3        ValueType::Any => None,
4        ValueType::Boolean => Some(ConvertTarget::Boolean),
5        ValueType::Integer { .. } => Some(ConvertTarget::Integer),
6        ValueType::String | ValueType::Address | ValueType::ByteArray { .. } => {
7            Some(ConvertTarget::ByteArray)
8        }
9        ValueType::Array(_) | ValueType::Struct { .. } => Some(ConvertTarget::Array),
10        ValueType::Mapping { .. } => Some(ConvertTarget::Map),
11    }
12}
13
14fn catch_clause_param(clause: &solang_parser::pt::CatchClause) -> Option<&solang_parser::pt::Parameter> {
15    match clause {
16        solang_parser::pt::CatchClause::Simple(_, param, _) => param.as_ref(),
17        solang_parser::pt::CatchClause::Named(_, _, param, _) => Some(param),
18    }
19}
20
21fn catch_clause_statement(clause: &solang_parser::pt::CatchClause) -> &Statement {
22    match clause {
23        solang_parser::pt::CatchClause::Simple(_, _, stmt) => stmt,
24        solang_parser::pt::CatchClause::Named(_, _, _, stmt) => stmt,
25    }
26}
27
28fn is_bare_catch_clause(clause: &solang_parser::pt::CatchClause) -> bool {
29    matches!(clause, solang_parser::pt::CatchClause::Simple(_, None, _))
30}
31
32fn catch_clause_guard_target(
33    clause: &solang_parser::pt::CatchClause,
34    ctx: &mut LoweringContext,
35) -> Option<ConvertTarget> {
36    if let Some(parameter) = catch_clause_param(clause) {
37        return infer_type_from_expression(&parameter.ty, ctx)
38            .as_ref()
39            .and_then(value_type_to_catch_guard);
40    }
41
42    if let solang_parser::pt::CatchClause::Named(_, ident, _, _) = clause {
43        if ident.name == "Panic" {
44            return Some(ConvertTarget::Integer);
45        }
46    }
47
48    None
49}
50
51fn bind_catch_clause_parameter(
52    clause: &solang_parser::pt::CatchClause,
53    catch_local: usize,
54    ctx: &mut LoweringContext,
55    instructions: &mut Vec<Instruction>,
56) {
57    let Some(parameter) = catch_clause_param(clause) else {
58        return;
59    };
60
61    let Some(name) = parameter.name.as_ref().map(|id| id.name.clone()) else {
62        return;
63    };
64
65    let inferred = infer_type_from_expression(&parameter.ty, ctx).unwrap_or(ValueType::Any);
66    let slot = ctx.allocate_local(name, Some(inferred));
67    instructions.push(Instruction::LoadLocal(catch_local));
68    instructions.push(Instruction::StoreLocal(slot));
69}
70
71fn lower_try_statement(
72    expr: &Expression,
73    handler: &Option<(solang_parser::pt::ParameterList, Box<Statement>)>,
74    catches: &[solang_parser::pt::CatchClause],
75    ctx: &mut LoweringContext,
76    instructions: &mut Vec<Instruction>,
77) -> bool {
78    // Solidity try/catch is only defined for external calls and contract creation.
79    // NeoVM supports structured exception handling via TRY/ENDTRY/ENDFINALLY, so we can
80    // map it directly.
81
82    let catch_label = ctx.next_label();
83    let success_label = ctx.next_label();
84    let end_label = ctx.next_label();
85
86    let (call_expr, inline_success) = match expr {
87        Expression::FunctionCallBlock(_, call, block) => (call.as_ref(), Some(block.as_ref())),
88        _ => (expr, None),
89    };
90
91    let (handler_params, handler_stmt) = handler
92        .as_ref()
93        .map(|(params, stmt)| (params.as_slice(), Some(stmt.as_ref())))
94        .unwrap_or((&[][..], None));
95
96    let success_stmt = handler_stmt.or(inline_success);
97
98    if catches.is_empty() {
99        ctx.record_error_with_suggestion(
100            "try statement without catch clause is not supported",
101            "add a catch clause: try expr { ... } catch { ... }",
102        );
103        if lower_expression(call_expr, ctx, instructions) {
104            instructions.push(Instruction::Drop(ValueType::Any));
105        }
106        if let Some(success_stmt) = success_stmt {
107            let _ = lower_statement(success_stmt, ctx, instructions);
108        }
109        return false;
110    }
111
112    // Lower the call expression inside a TRY, then ENDTRY to the success label.
113    instructions.push(Instruction::Try {
114        catch_target: catch_label,
115    });
116
117    let mut try_return_slot: Option<(usize, ValueType)> = None;
118    if lower_expression(call_expr, ctx, instructions) {
119        if handler_params.len() == 1 {
120            if let Some(param) = handler_params[0].1.as_ref() {
121                let inferred_type =
122                    infer_type_from_expression(&param.ty, ctx).unwrap_or(ValueType::Any);
123                let tmp = ctx.allocate_local("__try_ret".to_string(), Some(inferred_type.clone()));
124                instructions.push(Instruction::StoreLocal(tmp));
125                try_return_slot = Some((tmp, inferred_type));
126            } else {
127                ctx.record_error_with_suggestion(
128                    "try returns(...) parameter is missing",
129                    "specify a return parameter: try func() returns (uint256 result) { ... }",
130                );
131                instructions.push(Instruction::Drop(ValueType::Any));
132            }
133        } else {
134            if !handler_params.is_empty() {
135                ctx.record_error_with_suggestion(
136                    "try returns(...) with multiple values is not supported",
137                    "use a single return value or a struct to aggregate multiple values",
138                );
139            }
140            instructions.push(Instruction::Drop(ValueType::Any));
141        }
142    } else if !handler_params.is_empty() {
143        ctx.record_error("try returns(...) expects a return value");
144    }
145
146    instructions.push(Instruction::EndTry {
147        target: success_label,
148    });
149
150    // Catch handler: bind or drop the thrown value, then execute the matching catch body.
151    instructions.push(Instruction::Label(catch_label));
152    ctx.enter_scope();
153
154    if catches.len() == 1 && is_bare_catch_clause(&catches[0]) {
155        // Preserve existing compact lowering for a lone `catch { ... }`.
156        instructions.push(Instruction::Drop(ValueType::Any));
157        let _ = lower_statement(catch_clause_statement(&catches[0]), ctx, instructions);
158    } else {
159        let catch_local = ctx.allocate_local("__catch_exception".to_string(), None);
160        instructions.push(Instruction::StoreLocal(catch_local));
161
162        let mut fallback_clause: Option<&solang_parser::pt::CatchClause> = None;
163
164        for clause in catches {
165            if is_bare_catch_clause(clause) {
166                if fallback_clause.is_none() {
167                    fallback_clause = Some(clause);
168                }
169                continue;
170            }
171
172            let next_clause_label = ctx.next_label();
173            if let Some(guard_target) = catch_clause_guard_target(clause, ctx) {
174                instructions.push(Instruction::LoadLocal(catch_local));
175                instructions.push(Instruction::IsType {
176                    target: guard_target,
177                });
178                instructions.push(Instruction::JumpIf {
179                    target: next_clause_label,
180                });
181            }
182
183            if let solang_parser::pt::CatchClause::Named(_, ident, _, _) = clause {
184                if ident.name == "Panic" {
185                    eprintln!(
186                        "warning: catch Panic(uint256) uses NeoVM stack-item integer checks; EVM panic code semantics do not fully apply on Neo N3"
187                    );
188                }
189            }
190
191            ctx.enter_scope();
192            bind_catch_clause_parameter(clause, catch_local, ctx, instructions);
193            let _ = lower_statement(catch_clause_statement(clause), ctx, instructions);
194            ctx.exit_scope();
195            instructions.push(Instruction::Jump { target: end_label });
196            instructions.push(Instruction::Label(next_clause_label));
197        }
198
199        if let Some(clause) = fallback_clause {
200            ctx.enter_scope();
201            let _ = lower_statement(catch_clause_statement(clause), ctx, instructions);
202            ctx.exit_scope();
203        } else {
204            // No fallback clause and no type guard matched: rethrow the original exception.
205            instructions.push(Instruction::LoadLocal(catch_local));
206            instructions.push(Instruction::Throw);
207        }
208    }
209
210    ctx.exit_scope();
211    instructions.push(Instruction::EndTry { target: end_label });
212
213    // Success handler: bind return values (if present) and run handler block.
214    instructions.push(Instruction::Label(success_label));
215    if let Some(success_stmt) = success_stmt {
216        ctx.enter_scope();
217
218        if handler_params.len() == 1 {
219            if let Some(param) = handler_params[0].1.as_ref() {
220                if let Some(name) = param.name.as_ref().map(|id| id.name.clone()) {
221                    let inferred = try_return_slot
222                        .as_ref()
223                        .map(|(_, ty)| ty.clone())
224                        .unwrap_or(ValueType::Any);
225                    let slot = ctx.allocate_local(name, Some(inferred.clone()));
226                    if let Some((tmp, _)) = try_return_slot {
227                        instructions.push(Instruction::LoadLocal(tmp));
228                        instructions.push(Instruction::StoreLocal(slot));
229                    } else {
230                        push_default_for_value_type(&inferred, ctx, instructions);
231                        instructions.push(Instruction::StoreLocal(slot));
232                    }
233                }
234            }
235        }
236
237        let _ = lower_statement(success_stmt, ctx, instructions);
238        ctx.exit_scope();
239    }
240
241    instructions.push(Instruction::Label(end_label));
242    false
243}