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