1use crate::ast::{Expr, FnBody, Stmt};
2
3pub trait CallLowerCtx {
9 fn is_local_value(&self, name: &str) -> bool;
12
13 fn is_user_type(&self, name: &str) -> bool;
15
16 fn resolve_module_call<'a>(&self, dotted: &'a str) -> Option<(&'a str, &'a str)>;
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum WrapperKind {
23 ResultOk,
24 ResultErr,
25 OptionSome,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum CallPlan {
30 Dynamic,
32 Function(String),
34 Builtin(String),
36 Wrapper(WrapperKind),
38 NoneValue,
40 TypeConstructor {
42 qualified_type_name: String,
43 variant_name: String,
44 },
45}
46
47pub type SemanticCallee = CallPlan;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum ForwardArg {
51 Local(String),
52 Slot(u16),
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ForwardCallPlan {
57 pub target: CallPlan,
58 pub args: Vec<ForwardArg>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum TailCallPlan {
63 SelfCall,
64 KnownFunction(String),
65 Unknown(String),
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum SemanticConstructor {
70 Wrapper(WrapperKind),
71 NoneValue,
72 TypeConstructor {
73 qualified_type_name: String,
74 variant_name: String,
75 },
76 Unknown(String),
77}
78
79pub fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
80 match expr {
81 Expr::Ident(name) => Some(name.clone()),
82 Expr::Attr(obj, field) => {
83 let head = expr_to_dotted_name(obj)?;
84 Some(format!("{head}.{field}"))
85 }
86 _ => None,
87 }
88}
89
90pub fn is_builtin_namespace(name: &str) -> bool {
91 matches!(
92 name,
93 "Args"
94 | "Bool"
95 | "Byte"
96 | "Char"
97 | "Console"
98 | "Disk"
99 | "Env"
100 | "Float"
101 | "Http"
102 | "HttpServer"
103 | "Int"
104 | "List"
105 | "Map"
106 | "Option"
107 | "Random"
108 | "Result"
109 | "SelfHostRuntime"
110 | "String"
111 | "Tcp"
112 | "Terminal"
113 | "Time"
114 | "Vector"
115 )
116}
117
118pub fn classify_call_plan(expr: &Expr, ctx: &impl CallLowerCtx) -> CallPlan {
119 match expr {
120 Expr::Resolved(_) => CallPlan::Dynamic,
121 Expr::Ident(name) => match ctx.is_local_value(name) {
122 true => CallPlan::Dynamic,
123 false => classify_named_callee(name, None, ctx),
124 },
125 Expr::Attr(_, _) => {
126 let Some(dotted) = expr_to_dotted_name(expr) else {
127 return CallPlan::Dynamic;
128 };
129 match dotted.chars().next().is_some_and(|c| c.is_uppercase()) {
130 false => CallPlan::Dynamic,
131 true => {
132 let module_split = ctx.resolve_module_call(&dotted);
133 match module_split {
134 Some((prefix, suffix)) => {
135 classify_named_callee(&dotted, Some((prefix, suffix)), ctx)
136 }
137 None => classify_named_callee(&dotted, None, ctx),
138 }
139 }
140 }
141 }
142 _ => CallPlan::Dynamic,
143 }
144}
145
146pub fn classify_callee(expr: &Expr, ctx: &impl CallLowerCtx) -> SemanticCallee {
147 classify_call_plan(expr, ctx)
148}
149
150pub fn classify_forward_call_plan(expr: &Expr, ctx: &impl CallLowerCtx) -> Option<ForwardCallPlan> {
151 let Expr::FnCall(fn_expr, args) = expr else {
152 return None;
153 };
154 classify_forward_call_parts(fn_expr, args, ctx)
155}
156
157pub fn classify_forward_call_parts(
158 fn_expr: &Expr,
159 args: &[Expr],
160 ctx: &impl CallLowerCtx,
161) -> Option<ForwardCallPlan> {
162 let target = classify_call_plan(fn_expr, ctx);
163 if matches!(target, CallPlan::Dynamic)
164 || matches!(target, CallPlan::NoneValue)
165 || matches!(target, CallPlan::Wrapper(_) if args.len() != 1)
166 {
167 return None;
168 }
169
170 let args = args
171 .iter()
172 .map(|arg| classify_forward_arg(arg, ctx))
173 .collect::<Option<Vec<_>>>()?;
174
175 Some(ForwardCallPlan { target, args })
176}
177
178pub fn classify_forward_fn_body(body: &FnBody, ctx: &impl CallLowerCtx) -> Option<ForwardCallPlan> {
179 let [Stmt::Expr(expr)] = body.stmts() else {
180 return None;
181 };
182 classify_forward_call_plan(expr, ctx)
183}
184
185pub fn classify_tail_call_plan(
186 target: &str,
187 current_fn: &str,
188 ctx: &impl CallLowerCtx,
189) -> TailCallPlan {
190 if target == current_fn {
191 return TailCallPlan::SelfCall;
192 }
193
194 let module_split = if target.chars().next().is_some_and(|c| c.is_uppercase()) {
195 ctx.resolve_module_call(target)
196 } else {
197 None
198 };
199
200 match classify_named_callee(target, module_split, ctx) {
201 CallPlan::Function(name) => TailCallPlan::KnownFunction(name),
202 _ => TailCallPlan::Unknown(target.to_string()),
203 }
204}
205
206pub fn classify_constructor_name(name: &str, ctx: &impl CallLowerCtx) -> SemanticConstructor {
207 match name {
208 "None" | "Option.None" => return SemanticConstructor::NoneValue,
209 "Ok" | "Result.Ok" => return SemanticConstructor::Wrapper(WrapperKind::ResultOk),
210 "Err" | "Result.Err" => return SemanticConstructor::Wrapper(WrapperKind::ResultErr),
211 "Some" | "Option.Some" => return SemanticConstructor::Wrapper(WrapperKind::OptionSome),
212 _ => {}
213 }
214
215 let module_split = if name.chars().next().is_some_and(|c| c.is_uppercase()) {
216 ctx.resolve_module_call(name)
217 } else {
218 None
219 };
220
221 let bare = match module_split {
222 Some((_, suffix)) => suffix,
223 None => name,
224 };
225
226 if let Some((type_name, variant_name)) = bare.rsplit_once('.')
227 && ctx.is_user_type(type_name)
228 {
229 let qualified_type_name = match module_split {
230 Some((prefix, _)) => format!("{prefix}.{type_name}"),
231 None => type_name.to_string(),
232 };
233 return SemanticConstructor::TypeConstructor {
234 qualified_type_name,
235 variant_name: variant_name.to_string(),
236 };
237 }
238
239 SemanticConstructor::Unknown(name.to_string())
240}
241
242fn classify_forward_arg(expr: &Expr, ctx: &impl CallLowerCtx) -> Option<ForwardArg> {
243 match expr {
244 Expr::Resolved(slot) => Some(ForwardArg::Slot(*slot)),
245 Expr::Ident(name) if ctx.is_local_value(name) => Some(ForwardArg::Local(name.clone())),
246 _ => None,
247 }
248}
249
250fn classify_named_callee(
251 full_name: &str,
252 module_split: Option<(&str, &str)>,
253 ctx: &impl CallLowerCtx,
254) -> CallPlan {
255 let bare = match module_split {
256 Some((_, suffix)) => suffix,
257 None => full_name,
258 };
259
260 if module_split.is_none() {
261 match bare {
262 "Option.None" => return CallPlan::NoneValue,
263 "Result.Ok" => return CallPlan::Wrapper(WrapperKind::ResultOk),
264 "Result.Err" => return CallPlan::Wrapper(WrapperKind::ResultErr),
265 "Option.Some" => return CallPlan::Wrapper(WrapperKind::OptionSome),
266 _ => {}
267 }
268
269 if let Some((ns, _)) = bare.split_once('.')
270 && is_builtin_namespace(ns)
271 {
272 return CallPlan::Builtin(bare.to_string());
273 }
274 }
275
276 if let Some((type_name, variant_name)) = bare.rsplit_once('.')
277 && ctx.is_user_type(type_name)
278 {
279 let qualified_type_name = match module_split {
280 Some((prefix, _)) => format!("{prefix}.{type_name}"),
281 None => type_name.to_string(),
282 };
283 return CallPlan::TypeConstructor {
284 qualified_type_name,
285 variant_name: variant_name.to_string(),
286 };
287 }
288
289 CallPlan::Function(full_name.to_string())
290}