1use bynk_syntax::ast::{BaseType, CommonsItem, FnName, SourceUnit, TypeBody};
17use std::collections::HashMap;
18use std::path::PathBuf;
19
20use crate::completion::{BUILTIN_STATICS, for_each_unit};
21use crate::symbols::type_ref_str;
22
23#[derive(Debug, PartialEq, Eq)]
26pub struct CallContext {
27 pub callee: String,
28 pub active_param: usize,
29 pub open_paren: usize,
30}
31
32pub fn call_context(text: &str, offset: usize) -> Option<CallContext> {
36 let prefix = text.get(..offset)?;
37 let open = innermost_unclosed_paren(prefix)?;
38 let callee = callee_before(&prefix[..open])?;
39 let active = top_level_commas(&prefix[open + 1..]);
40 Some(CallContext {
41 callee,
42 active_param: active,
43 open_paren: open,
44 })
45}
46
47pub fn value_receiver_method(callee: &str) -> Option<(&str, &str)> {
51 let (recv, method) = callee.rsplit_once('.')?;
52 let first = recv.chars().next()?;
53 if (first.is_ascii_lowercase() || first == '_') && !recv.contains('.') {
54 Some((recv, method))
55 } else {
56 None
57 }
58}
59
60pub fn value_receiver_rewrite(
65 text: &str,
66 callee: &str,
67 open_paren: usize,
68 cursor: usize,
69) -> Option<(String, usize)> {
70 let (recv, _) = value_receiver_method(callee)?;
71 let callee_start = open_paren.checked_sub(callee.len())?;
72 let dot = callee_start + recv.len();
73 let rewritten = format!("{}{}", &text[..dot], &text[cursor..]);
74 Some((rewritten, dot.saturating_sub(1)))
75}
76
77pub fn kernel_method_signature(
79 ty: bynk_check::checker::TyId,
80 tys: &bynk_check::checker::Types,
81 method: &str,
82) -> Option<String> {
83 bynk_check::kernel_methods::methods_for(ty, tys)
84 .iter()
85 .find(|m| m.name == method)
86 .map(|m| m.signature.to_string())
87}
88
89fn innermost_unclosed_paren(prefix: &str) -> Option<usize> {
92 let b = prefix.as_bytes();
93 let mut depth = 0i32;
94 for i in (0..b.len()).rev() {
95 match b[i] {
96 b')' | b']' | b'}' => depth += 1,
97 b'(' => {
98 if depth == 0 {
99 return Some(i);
100 }
101 depth -= 1;
102 }
103 b'[' | b'{' => {
104 if depth == 0 {
105 return None;
106 }
107 depth -= 1;
108 }
109 _ => {}
110 }
111 }
112 None
113}
114
115fn top_level_commas(s: &str) -> usize {
117 let mut depth = 0i32;
118 let mut n = 0;
119 for c in s.chars() {
120 match c {
121 '(' | '[' | '{' => depth += 1,
122 ')' | ']' | '}' => depth -= 1,
123 ',' if depth == 0 => n += 1,
124 _ => {}
125 }
126 }
127 n
128}
129
130fn callee_before(s: &str) -> Option<String> {
132 let s = s.trim_end();
133 let start = s
137 .char_indices()
138 .rfind(|&(_, c)| !(c.is_alphanumeric() || c == '_' || c == '.'))
139 .map_or(0, |(i, c)| i + c.len_utf8());
140 let callee = &s[start..];
141 if callee.is_empty() || callee.starts_with('.') || callee.ends_with('.') {
142 return None;
143 }
144 Some(callee.to_string())
145}
146
147pub fn resolve_label(
150 callee: &str,
151 doc_text: &str,
152 files: Option<&HashMap<PathBuf, String>>,
153) -> Option<String> {
154 if let Some((recv, member)) = callee.rsplit_once('.') {
155 if let Some((_, statics)) = BUILTIN_STATICS.iter().find(|(n, _)| *n == recv)
157 && let Some((_, sig)) = statics.iter().find(|(n, _)| *n == member)
158 {
159 return Some((*sig).to_string());
160 }
161 return resolve_qualified(recv, member, doc_text, files);
163 }
164 match callee {
166 "Ok" => return Some("Ok(value: T) -> Result[T, E]".to_string()),
167 "Err" => return Some("Err(error: E) -> Result[T, E]".to_string()),
168 "Some" => return Some("Some(value: T) -> Option[T]".to_string()),
169 _ => {}
170 }
171 let mut found = None;
173 for_each_unit(doc_text, files, |unit| {
174 if found.is_some() {
175 return;
176 }
177 let items = match unit {
178 SourceUnit::Commons(c) => &c.items,
179 SourceUnit::Context(c) => &c.items,
180 SourceUnit::Adapter(a) => &a.items,
181 _ => return,
182 };
183 for item in items {
184 if let CommonsItem::Fn(f) = item
185 && let FnName::Free(id) = &f.name
186 && id.name == callee
187 {
188 let params: Vec<String> = f
189 .params
190 .iter()
191 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
192 .collect();
193 found = Some(format!(
194 "{callee}({}) -> {}",
195 params.join(", "),
196 type_ref_str(&f.return_type)
197 ));
198 return;
199 }
200 }
201 });
202 found
203}
204
205fn resolve_qualified(
206 recv: &str,
207 member: &str,
208 doc_text: &str,
209 files: Option<&HashMap<PathBuf, String>>,
210) -> Option<String> {
211 let mut out = None;
212 for_each_unit(doc_text, files, |unit| {
213 if out.is_some() {
214 return;
215 }
216 let items = match unit {
217 SourceUnit::Commons(c) => &c.items,
218 SourceUnit::Context(c) => &c.items,
219 SourceUnit::Adapter(a) => &a.items,
220 _ => return,
221 };
222 for item in items {
223 match item {
224 CommonsItem::Type(t)
226 if t.name.name == recv && (member == "of" || member == "unsafe") =>
227 {
228 let base = match &t.body {
229 TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => {
230 base_name(*base)
231 }
232 _ => return,
233 };
234 out = Some(if member == "of" {
235 format!("of(value: {base}) -> Result[{recv}, ValidationError]")
236 } else {
237 format!("unsafe(value: {base}) -> {recv}")
238 });
239 return;
240 }
241 CommonsItem::Capability(c) if c.name.name == recv => {
243 if let Some(op) = c.ops.iter().find(|o| o.name.name == member) {
244 let params: Vec<String> = op
245 .params
246 .iter()
247 .map(|p| format!("{}: {}", p.name.name, type_ref_str(&p.type_ref)))
248 .collect();
249 let type_params = if op.type_params.is_empty() {
251 String::new()
252 } else {
253 let names: Vec<&str> = op
254 .type_params
255 .iter()
256 .map(|tp| tp.name.name.as_str())
257 .collect();
258 format!("[{}]", names.join(", "))
259 };
260 out = Some(format!(
261 "{member}{type_params}({}) -> {}",
262 params.join(", "),
263 type_ref_str(&op.return_type)
264 ));
265 return;
266 }
267 }
268 _ => {}
269 }
270 }
271 });
272 out
273}
274
275fn base_name(b: BaseType) -> &'static str {
276 match b {
277 BaseType::Int => "Int",
278 BaseType::Float => "Float",
279 BaseType::String => "String",
280 BaseType::Bool => "Bool",
281 BaseType::Duration => "Duration",
282 BaseType::Instant => "Instant",
283 BaseType::Bytes => "Bytes",
284 }
285}
286
287pub fn param_ranges(label: &str) -> Vec<(usize, usize)> {
290 let Some(open) = label.find('(') else {
291 return Vec::new();
292 };
293 let mut ranges = Vec::new();
294 let mut depth = 0i32;
295 let mut seg_start = open + 1;
296 let bytes = label.as_bytes();
297 let mut i = open;
298 while i < bytes.len() {
299 match bytes[i] {
300 b'(' | b'[' | b'{' => depth += 1,
301 b')' | b']' | b'}' => {
302 depth -= 1;
303 if depth == 0 {
304 push_trimmed(label, seg_start, i, &mut ranges);
305 break;
306 }
307 }
308 b',' if depth == 1 => {
309 push_trimmed(label, seg_start, i, &mut ranges);
310 seg_start = i + 1;
311 }
312 _ => {}
313 }
314 i += 1;
315 }
316 ranges
317}
318
319fn push_trimmed(label: &str, start: usize, end: usize, out: &mut Vec<(usize, usize)>) {
320 let seg = &label[start..end];
321 let trimmed = seg.trim();
322 if trimmed.is_empty() {
323 return;
324 }
325 let s = start + (seg.len() - seg.trim_start().len());
326 out.push((s, s + trimmed.len()));
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn call_context_finds_callee_and_active_param() {
335 let t = " let x = f(a, b";
336 let ctx = call_context(t, t.len()).unwrap();
337 assert_eq!(ctx.callee, "f");
338 assert_eq!(ctx.active_param, 1); }
340
341 #[test]
342 fn innermost_call_wins_in_nested_calls() {
343 let t = " outer(g(x";
344 let ctx = call_context(t, t.len()).unwrap();
345 assert_eq!(ctx.callee, "g");
346 assert_eq!(ctx.active_param, 0);
347 let t2 = " outer(g(x), ";
349 let ctx2 = call_context(t2, t2.len()).unwrap();
350 assert_eq!(ctx2.callee, "outer");
351 assert_eq!(ctx2.active_param, 1);
352 }
353
354 #[test]
355 fn commas_inside_nested_brackets_dont_count() {
356 let t = " f(g(a, b), ";
357 assert_eq!(call_context(t, t.len()).unwrap().active_param, 1);
358 }
359
360 #[test]
361 fn qualified_callee_and_no_call_context() {
362 let t = " Clock.now(";
363 assert_eq!(call_context(t, t.len()).unwrap().callee, "Clock.now");
364 assert!(call_context(" let x = 1", 11).is_none()); assert!(call_context(" xs[", 4).is_none()); }
367
368 #[test]
369 fn callee_extraction_survives_a_multibyte_char_before_the_callee() {
370 assert_eq!(callee_before("\"Foo.bar"), Some("Foo.bar".to_string()));
374 assert_eq!(callee_before("€f"), Some("f".to_string()));
375 let t = " \"€greet(";
377 let _ = call_context(t, t.len());
378 }
379
380 #[test]
381 fn builtin_static_and_constructor_labels() {
382 assert_eq!(
383 resolve_label("Int.parse", "context a.b\n", None).as_deref(),
384 Some("parse(s: String) -> Option[Int]")
385 );
386 assert!(
387 resolve_label("Ok", "context a.b\n", None)
388 .unwrap()
389 .starts_with("Ok(value")
390 );
391 }
392
393 #[test]
394 fn free_fn_and_capability_op_and_refined_labels() {
395 let doc = "commons m {\n fn add(a: Int, b: Int) -> Int { a }\n}\n";
396 assert_eq!(
397 resolve_label("add", doc, None).as_deref(),
398 Some("add(a: Int, b: Int) -> Int")
399 );
400 let cap = "context a.b\n capability Timer { fn after(label: String) -> Effect[Int] }\n";
401 assert_eq!(
402 resolve_label("Timer.after", cap, None).as_deref(),
403 Some("after(label: String) -> Effect[Int]")
404 );
405 let refined = "commons m {\n type Email = String where NonEmpty\n}\n";
406 assert_eq!(
407 resolve_label("Email.of", refined, None).as_deref(),
408 Some("of(value: String) -> Result[Email, ValidationError]")
409 );
410 }
411
412 #[test]
413 fn value_receiver_callee_detection_and_rewrite() {
414 assert_eq!(value_receiver_method("xs.fold"), Some(("xs", "fold")));
415 assert_eq!(value_receiver_method("Int.parse"), None); assert_eq!(value_receiver_method("a.b.fold"), None); assert_eq!(value_receiver_method("bar"), None); let text = " let r = xs.fold(0, ";
420 let open = text.find('(').unwrap();
421 let (rw, off) = value_receiver_rewrite(text, "xs.fold", open, text.len()).unwrap();
422 assert_eq!(rw, " let r = xs", "the `.fold(0, ` is dropped");
423 assert_eq!(&text[off..=off], "s", "offset lands inside `xs`");
424 }
425
426 #[test]
427 fn kernel_method_signature_lookup() {
428 use bynk_check::checker::{Ty, Types};
429 use bynk_syntax::ast::BaseType;
430 let tys = &Types::new();
431 let list = tys.intern(Ty::List(tys.intern(Ty::Base(BaseType::Int))));
432 assert!(
433 kernel_method_signature(list, tys, "fold")
434 .unwrap()
435 .starts_with("fold(")
436 );
437 let string = tys.intern(Ty::Base(BaseType::String));
438 assert!(
439 kernel_method_signature(string, tys, "split")
440 .unwrap()
441 .starts_with("split(")
442 );
443 assert!(kernel_method_signature(string, tys, "nope").is_none());
444 }
445
446 #[test]
447 fn param_ranges_split_top_level_only() {
448 let label = "fold(init: U, step: (U, T) -> U) -> U";
449 let r = param_ranges(label);
450 assert_eq!(r.len(), 2, "two params, not split inside (U, T): {r:?}");
451 assert_eq!(&label[r[0].0..r[0].1], "init: U");
452 assert_eq!(&label[r[1].0..r[1].1], "step: (U, T) -> U");
453 }
454}