1use bynk_check::checker::{TyId, Types};
43use bynk_check::expr_types::type_at_span;
44use bynk_check::locals::{LocalBinding, locals_at};
45use bynk_check::requirements::Requirement;
46use bynk_syntax::ast::*;
47use bynk_syntax::lexer::tokenize;
48use bynk_syntax::parser::parse_unit_with_recovery;
49use bynk_syntax::span::Span;
50use std::collections::HashMap;
51use tower_lsp::lsp_types::*;
52
53pub fn extract_variable(
57 text: &str,
58 requested: Span,
59 uri: &Url,
60 version: Option<i32>,
61) -> Vec<CodeActionOrCommand> {
62 if requested.start == requested.end {
63 return Vec::new();
64 }
65 let Ok(tokens) = tokenize(text) else {
66 return Vec::new();
67 };
68 let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, text) else {
69 return Vec::new();
70 };
71 let Some(site) = find_site(&unit, requested) else {
72 return Vec::new();
73 };
74 let mut read = Vec::new();
82 collect_idents(site.expr, &mut read);
83 if read.iter().any(|id| site.binders.binds(&id.name)) {
84 return Vec::new();
85 }
86
87 let selected = &text[site.expr_span.start..site.expr_span.end];
88 let name = fresh_name(text);
89 let indent = line_indent(text, site.insertion_offset);
90 let insert_pos = crate::position::offset_to_position(text, site.insertion_offset);
91
92 let edits = vec![
93 OneOf::Left(TextEdit {
94 range: Range::new(insert_pos, insert_pos),
95 new_text: format!("let {name} = {selected}\n{indent}"),
96 }),
97 OneOf::Left(TextEdit {
98 range: crate::position::span_to_range(text, site.expr_span),
99 new_text: name.clone(),
100 }),
101 ];
102 vec![CodeActionOrCommand::CodeAction(CodeAction {
103 title: format!("Extract variable `{name}`"),
104 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
105 edit: Some(WorkspaceEdit {
106 changes: None,
107 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
108 text_document: OptionalVersionedTextDocumentIdentifier {
109 uri: uri.clone(),
110 version,
111 },
112 edits,
113 }])),
114 change_annotations: None,
115 }),
116 ..Default::default()
117 })]
118}
119
120#[cfg(test)]
166#[allow(clippy::too_many_arguments)]
167pub fn extract_function(
168 text: &str,
169 requested: Span,
170 uri: &Url,
171 version: Option<i32>,
172 requirements: &[Requirement],
173 locals: &[LocalBinding],
174 expr_types: &[(Span, TyId)],
175 tys: &Types,
176) -> Vec<CodeActionOrCommand> {
177 let Ok(plan) = plan_function(text, requested, requirements, locals, expr_types, tys) else {
178 return Vec::new();
179 };
180 let edits = plan
181 .edits
182 .iter()
183 .map(|(span, new_text)| {
184 OneOf::Left(TextEdit {
185 range: crate::position::span_to_range(text, *span),
186 new_text: new_text.clone(),
187 })
188 })
189 .collect();
190 vec![CodeActionOrCommand::CodeAction(CodeAction {
191 title: format!("Extract function `{}`", plan.name),
192 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
193 edit: Some(WorkspaceEdit {
194 changes: None,
195 document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit {
196 text_document: OptionalVersionedTextDocumentIdentifier {
197 uri: uri.clone(),
198 version,
199 },
200 edits,
201 }])),
202 change_annotations: None,
203 }),
204 ..Default::default()
205 })]
206}
207
208pub struct FunctionPlan {
211 pub name: String,
212 pub edits: Vec<(Span, String)>,
213}
214
215#[derive(Debug)]
220pub struct FunctionRefusal {
221 pub reason: Option<String>,
222}
223
224impl FunctionRefusal {
225 fn silent() -> Self {
226 FunctionRefusal { reason: None }
227 }
228}
229
230pub fn plan_function(
236 text: &str,
237 requested: Span,
238 requirements: &[Requirement],
239 locals: &[LocalBinding],
240 expr_types: &[(Span, TyId)],
241 tys: &Types,
242) -> Result<FunctionPlan, FunctionRefusal> {
243 let silent = || FunctionRefusal::silent();
244 if requested.start == requested.end {
245 return Err(silent());
246 }
247 let tokens = tokenize(text).map_err(|_| silent())?;
248 let (Some(unit), _errs) = parse_unit_with_recovery(&tokens, text) else {
249 return Err(silent());
250 };
251 let site = find_function_site(&unit, requested, text).ok_or_else(silent)?;
252 let mut caps: Vec<&str> = requirements
253 .iter()
254 .filter(|r| contains(site.span, r.site))
255 .map(|r| r.capability.as_str())
256 .collect();
257 if !caps.is_empty() {
258 caps.sort_unstable();
259 caps.dedup();
260 let names = caps
261 .iter()
262 .map(|c| format!("`{c}`"))
263 .collect::<Vec<_>>()
264 .join(", ");
265 return Err(FunctionRefusal {
266 reason: Some(format!(
267 "the selection uses {names}; an extracted function stays capability-free, \
268 since a `fn` has no `given` clause to carry {}",
269 if caps.len() == 1 { "it" } else { "them" }
270 )),
271 });
272 }
273 if expr_types
274 .iter()
275 .any(|(s, t)| contains(site.span, *s) && t.is_error(tys))
276 {
277 return Err(silent());
278 }
279
280 let mut exprs: Vec<&Expr> = Vec::new();
281 let (ret_ty_display, call_site_form): (String, CallSiteForm) = match &site.selection {
282 FunctionSelection::Expr(expr) => {
283 let Some(ret_ty) = type_at_span(expr_types, site.span) else {
284 return Err(silent());
285 };
286 exprs.push(expr);
287 (ret_ty.display(tys), CallSiteForm::Bare)
288 }
289 FunctionSelection::Stmts {
290 stmts,
291 tail,
292 remainder,
293 } => {
294 if stmts_contain_assign_stmt(stmts) {
298 return Err(silent());
299 }
300 for s in *stmts {
301 statement_exprs(s, &mut exprs);
302 }
303 match tail {
304 Some(t) => {
305 let Some(ret_ty) = type_at_span(expr_types, t.span) else {
306 return Err(silent());
307 };
308 exprs.push(t);
309 (ret_ty.display(tys), CallSiteForm::Bare)
310 }
311 None => {
312 if leaks_a_binding(stmts, remainder, locals, site.span) {
313 return Err(silent());
314 }
315 if stmts_contain_effect_stmt(stmts) {
316 ("Effect[()]".to_string(), CallSiteForm::Do)
317 } else {
318 ("()".to_string(), CallSiteForm::Discard)
319 }
320 }
321 }
322 }
323 };
324
325 if exprs.iter().any(|e| has_early_return(e)) {
328 return Err(FunctionRefusal {
329 reason: Some(
330 "the selection contains `?`, which would return from the extracted \
331 function instead of the one it is written in"
332 .to_string(),
333 ),
334 });
335 }
336
337 let mut idents = Vec::new();
338 for e in &exprs {
339 collect_idents(e, &mut idents);
340 }
341 let mut free: Vec<(&str, Span, &str)> = Vec::new();
342 for id in &idents {
343 let Some(binding) = locals_at(locals, id.span.start)
344 .into_iter()
345 .find(|b| b.name == id.name)
346 else {
347 continue;
348 };
349 if contains(site.span, binding.def_span) {
350 continue; }
352 free.push((id.name.as_str(), binding.def_span, binding.ty.as_str()));
353 }
354 let mut def_by_name: HashMap<&str, Span> = HashMap::new();
355 for (name, def_span, _) in &free {
356 match def_by_name.get(name) {
357 Some(prev) if *prev != *def_span => return Err(silent()),
358 _ => {
359 def_by_name.insert(name, *def_span);
360 }
361 }
362 }
363 let mut params: Vec<(&str, &str)> = Vec::new();
364 for (name, _, ty) in &free {
365 if !params.iter().any(|(n, _)| n == name) {
366 params.push((name, ty));
367 }
368 }
369
370 let fn_name = fresh_word(text, "extractedFn");
371 let param_list = params
372 .iter()
373 .map(|(n, t)| format!("{n}: {t}"))
374 .collect::<Vec<_>>()
375 .join(", ");
376 let arg_list = params
377 .iter()
378 .map(|(n, _)| n.to_string())
379 .collect::<Vec<_>>()
380 .join(", ");
381 let selected = &text[site.span.start..site.span.end];
382 let new_fn = format!("fn {fn_name}({param_list}) -> {ret_ty_display} {{\n {selected}\n}}\n\n");
383
384 let call_expr = format!("{fn_name}({arg_list})");
390 let call_site_text = match call_site_form {
391 CallSiteForm::Bare => call_expr,
392 CallSiteForm::Discard => format!("let _ = {call_expr}"),
393 CallSiteForm::Do => format!("do {call_expr}"),
394 };
395
396 Ok(FunctionPlan {
397 name: fn_name,
398 edits: vec![
399 (Span::new(site.item_start, site.item_start), new_fn),
400 (site.span, call_site_text),
401 ],
402 })
403}
404
405const FUNCTION_DATA_TAG: &str = "bynk.extractFunction";
406
407pub fn function_listed(
412 planned: &Result<FunctionPlan, FunctionRefusal>,
413 requested: Span,
414 uri: &Url,
415 version: Option<i32>,
416 disabled_support: bool,
417) -> Option<CodeActionOrCommand> {
418 match planned {
419 Ok(plan) => Some(CodeActionOrCommand::CodeAction(CodeAction {
420 title: format!("Extract function `{}`", plan.name),
421 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
422 data: Some(serde_json::json!({
423 "kind": FUNCTION_DATA_TAG,
424 "uri": uri.as_str(),
425 "version": version,
426 "start": requested.start,
427 "end": requested.end,
428 })),
429 ..Default::default()
430 })),
431 Err(FunctionRefusal {
432 reason: Some(reason),
433 }) if disabled_support => Some(CodeActionOrCommand::CodeAction(CodeAction {
434 title: "Extract function".to_string(),
435 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
436 disabled: Some(CodeActionDisabled {
437 reason: reason.clone(),
438 }),
439 ..Default::default()
440 })),
441 Err(_) => None,
442 }
443}
444
445pub struct FunctionTarget {
447 pub uri: Url,
448 pub version: Option<i32>,
449 pub requested: Span,
450}
451
452impl FunctionTarget {
453 pub fn of(action: &CodeAction) -> Option<Self> {
454 let data = action.data.as_ref()?;
455 if data.get("kind")?.as_str()? != FUNCTION_DATA_TAG {
456 return None;
457 }
458 Some(FunctionTarget {
459 uri: Url::parse(data.get("uri")?.as_str()?).ok()?,
460 version: data
461 .get("version")
462 .and_then(serde_json::Value::as_i64)
463 .and_then(|v| i32::try_from(v).ok()),
464 requested: Span::new(
465 usize::try_from(data.get("start")?.as_u64()?).ok()?,
466 usize::try_from(data.get("end")?.as_u64()?).ok()?,
467 ),
468 })
469 }
470}
471
472fn has_early_return(e: &Expr) -> bool {
475 match &e.kind {
476 ExprKind::Question(_) => true,
477 ExprKind::Lambda(_) => false,
478 _ => expr_children(e).into_iter().any(has_early_return),
479 }
480}
481
482struct Site<'a> {
488 insertion_offset: usize,
489 expr_span: Span,
490 expr: &'a Expr,
491 binders: Binders<'a>,
497}
498
499#[derive(Default)]
502struct Binders<'a> {
503 names: Vec<&'a str>,
504 opaque: bool,
508}
509
510impl<'a> Binders<'a> {
511 fn extend(&mut self, names: impl IntoIterator<Item = &'a str>) {
512 self.names.extend(names);
513 }
514
515 fn binds(&self, name: &str) -> bool {
516 self.opaque || self.names.contains(&name)
517 }
518}
519
520fn is_bindings(e: &Expr) -> Vec<&str> {
524 match &e.kind {
525 ExprKind::Is { pattern, .. } => pattern
526 .bound_names()
527 .into_iter()
528 .map(|id| id.name.as_str())
529 .collect(),
530 ExprKind::BinOp(BinOp::And | BinOp::Implies, l, r) => {
531 let mut names = is_bindings(l);
532 names.extend(is_bindings(r));
533 names
534 }
535 ExprKind::Paren(inner) => is_bindings(inner),
536 _ => Vec::new(),
537 }
538}
539
540fn contains(outer: Span, inner: Span) -> bool {
542 outer.start <= inner.start && inner.end <= outer.end
543}
544
545fn find_site(unit: &SourceUnit, target: Span) -> Option<Site<'_>> {
554 find_site_any(unit, target).filter(|site| !site.expr.is_let_else_match())
555}
556
557fn find_site_any(unit: &SourceUnit, target: Span) -> Option<Site<'_>> {
558 match unit {
559 SourceUnit::Commons(c) => find_in_items(&c.items, target),
560 SourceUnit::Context(c) => find_in_items(&c.items, target),
561 SourceUnit::Adapter(_) => None,
563 SourceUnit::Suite(t) => t
564 .cases
565 .iter()
566 .find(|case| contains(case.body.span, target))
567 .and_then(|case| find_in_block(&case.body, target)),
568 }
569}
570
571fn find_in_items(items: &[CommonsItem], target: Span) -> Option<Site<'_>> {
572 for item in items {
573 match item {
574 CommonsItem::Fn(f) if contains(f.body.span, target) => {
575 return find_in_block(&f.body, target);
576 }
577 CommonsItem::Provider(p) => {
578 if let Some(op) = p.ops.iter().find(|op| contains(op.body.span, target)) {
579 return find_in_block(&op.body, target);
580 }
581 }
582 CommonsItem::Service(s) => {
583 if let Some(h) = s.handlers.iter().find(|h| contains(h.body.span, target)) {
584 return find_in_block(&h.body, target);
585 }
586 }
587 CommonsItem::Agent(a) => {
588 if let Some(h) = a.handlers.iter().find(|h| contains(h.body.span, target)) {
589 return find_in_block(&h.body, target);
590 }
591 }
592 _ => {}
593 }
594 }
595 None
596}
597
598fn find_in_block(block: &Block, target: Span) -> Option<Site<'_>> {
603 for stmt in &block.statements {
604 let mut exprs = Vec::new();
608 statement_exprs(stmt, &mut exprs);
609 if let Some(e) = exprs.into_iter().find(|e| contains(e.span, target)) {
610 return Some(locate(e, target, stmt.span().start, Binders::default()));
611 }
612 }
613 if contains(block.tail.span, target) {
614 return Some(locate(
615 &block.tail,
616 target,
617 block.tail.span.start,
618 Binders::default(),
619 ));
620 }
621 None
622}
623
624fn locate<'a>(
636 expr: &'a Expr,
637 target: Span,
638 insertion_offset: usize,
639 mut binders: Binders<'a>,
640) -> Site<'a> {
641 match &expr.kind {
642 ExprKind::Block(b) => find_in_block(b, target).unwrap_or(Site {
643 insertion_offset,
644 expr_span: expr.span,
645 expr,
646 binders,
647 }),
648 ExprKind::If {
649 cond,
650 then_block,
651 else_block,
652 } => {
653 if contains(cond.span, target) {
654 return locate(cond, target, insertion_offset, binders);
655 }
656 if contains(then_block.span, target)
657 && let Some(site) = find_in_block(then_block, target)
658 {
659 return site;
660 }
661 if contains(else_block.span, target)
662 && let Some(site) = find_in_block(else_block, target)
663 {
664 return site;
665 }
666 Site {
667 insertion_offset,
668 expr_span: expr.span,
669 expr,
670 binders,
671 }
672 }
673 ExprKind::Match { discriminant, arms } => {
674 if contains(discriminant.span, target) {
675 return locate(discriminant, target, insertion_offset, binders);
676 }
677 for arm in arms {
684 if !contains(arm.body.span(), target) {
685 continue;
686 }
687 return match &arm.body {
688 MatchBody::Expr(e) => {
689 binders.extend(arm.pattern.bound_names().iter().map(|id| id.name.as_str()));
693 locate(e, target, insertion_offset, binders)
694 }
695 MatchBody::Block(b) => find_in_block(b, target).unwrap_or(Site {
696 insertion_offset,
697 expr_span: expr.span,
698 expr,
699 binders,
700 }),
701 };
702 }
703 Site {
704 insertion_offset,
705 expr_span: expr.span,
706 expr,
707 binders,
708 }
709 }
710 ExprKind::IntLit { .. }
722 | ExprKind::FloatLit { .. }
723 | ExprKind::DurationLit { .. }
724 | ExprKind::StrLit(_)
725 | ExprKind::InterpStr(_)
726 | ExprKind::BoolLit(_)
727 | ExprKind::Ident(_)
728 | ExprKind::Call { .. }
729 | ExprKind::Lambda(_)
730 | ExprKind::BinOp(..)
731 | ExprKind::UnaryOp(..)
732 | ExprKind::Paren(_)
733 | ExprKind::Ok(_)
734 | ExprKind::Err(_)
735 | ExprKind::Question(_)
736 | ExprKind::RecordConstruction { .. }
737 | ExprKind::FieldAccess { .. }
738 | ExprKind::MethodCall { .. }
739 | ExprKind::Is { .. }
740 | ExprKind::Some(_)
741 | ExprKind::None
742 | ExprKind::UnitLit
743 | ExprKind::RecordSpread { .. }
744 | ExprKind::EffectPure(_)
745 | ExprKind::Expect(_)
746 | ExprKind::Val { .. }
747 | ExprKind::Wire(_)
748 | ExprKind::ListLit(_)
749 | ExprKind::Observation(_)
750 | ExprKind::Faults(_)
751 | ExprKind::Trace { .. } => {
752 let children = expr_children(expr);
753 match children.into_iter().find(|c| contains(c.span, target)) {
754 Some(child) => {
755 if let ExprKind::Lambda(l) = &expr.kind {
758 binders.extend(l.params.iter().map(|p| p.name.name.as_str()));
759 }
760 if let ExprKind::BinOp(BinOp::And | BinOp::Implies, lhs, rhs) = &expr.kind
763 && std::ptr::eq(child, rhs.as_ref())
764 {
765 binders.extend(is_bindings(lhs));
766 }
767 if let ExprKind::Observation(o) = &expr.kind
768 && let ObservationMatcher::Called {
769 with_pred: Some(pred),
770 ..
771 } = &o.matcher
772 && std::ptr::eq(child, pred.as_ref())
773 {
774 binders.opaque = true;
775 }
776 locate(child, target, insertion_offset, binders)
777 }
778 None => Site {
779 insertion_offset,
780 expr_span: expr.span,
781 expr,
782 binders,
783 },
784 }
785 }
786 }
787}
788
789fn fresh_name(text: &str) -> String {
794 fresh_word(text, "extracted")
795}
796
797fn fresh_word(text: &str, base: &str) -> String {
802 let is_ident_char = |c: char| c.is_alphanumeric() || c == '_';
803 let occurs_as_word = |candidate: &str| {
804 text.match_indices(candidate).any(|(i, _)| {
805 let before_ok = text[..i]
806 .chars()
807 .next_back()
808 .is_none_or(|c| !is_ident_char(c));
809 let after_ok = text[i + candidate.len()..]
810 .chars()
811 .next()
812 .is_none_or(|c| !is_ident_char(c));
813 before_ok && after_ok
814 })
815 };
816 let mut n = 1;
817 loop {
818 let candidate = if n == 1 {
819 base.to_string()
820 } else {
821 format!("{base}{n}")
822 };
823 if !occurs_as_word(&candidate) {
824 return candidate;
825 }
826 n += 1;
827 }
828}
829
830fn item_span(item: &CommonsItem) -> Option<Span> {
834 match item {
835 CommonsItem::Fn(f) => Some(f.span),
836 CommonsItem::Provider(p) => Some(p.span),
837 CommonsItem::Service(s) => Some(s.span),
838 CommonsItem::Agent(a) => Some(a.span),
839 _ => None,
840 }
841}
842
843enum FunctionSelection<'a> {
846 Expr(&'a Expr),
848 Stmts {
856 stmts: &'a [Statement],
857 tail: Option<&'a Expr>,
858 remainder: Vec<&'a Expr>,
859 },
860}
861
862enum CallSiteForm {
867 Bare,
870 Discard,
872 Do,
875}
876
877struct FunctionSite<'a> {
881 item_start: usize,
882 span: Span,
885 selection: FunctionSelection<'a>,
886}
887
888fn find_function_site<'a>(
894 unit: &'a SourceUnit,
895 target: Span,
896 text: &str,
897) -> Option<FunctionSite<'a>> {
898 let items = match unit {
899 SourceUnit::Commons(c) => &c.items,
900 SourceUnit::Context(c) => &c.items,
901 SourceUnit::Adapter(_) | SourceUnit::Suite(_) => return None,
902 };
903 let item = items
904 .iter()
905 .find(|it| item_span(it).is_some_and(|s| contains(s, target)))?;
906 let item_start = item_span(item)?.start;
907
908 let trimmed = trim_span(text, target);
909 if trimmed.start < trimmed.end
910 && let Some((stmts, tail, remainder)) = find_multi_stmt_in_item(item, trimmed)
911 {
912 return Some(FunctionSite {
913 item_start,
914 span: trimmed,
915 selection: FunctionSelection::Stmts {
916 stmts,
917 tail,
918 remainder,
919 },
920 });
921 }
922
923 let site = find_in_items(std::slice::from_ref(item), target)?;
924 Some(FunctionSite {
925 item_start,
926 span: site.expr_span,
927 selection: FunctionSelection::Expr(site.expr),
928 })
929}
930
931fn trim_span(text: &str, span: Span) -> Span {
936 let s = &text[span.start..span.end];
937 let start = span.start + (s.len() - s.trim_start().len());
938 let trimmed_len = s.trim().len();
939 Span::new(start, start + trimmed_len)
940}
941
942type StmtRun<'a> = (&'a [Statement], Option<&'a Expr>, Vec<&'a Expr>);
946
947fn find_multi_stmt_in_item(item: &CommonsItem, target: Span) -> Option<StmtRun<'_>> {
951 match item {
952 CommonsItem::Fn(f) if contains(f.body.span, target) => find_stmt_run(&f.body, target),
953 CommonsItem::Provider(p) => p
954 .ops
955 .iter()
956 .find(|op| contains(op.body.span, target))
957 .and_then(|op| find_stmt_run(&op.body, target)),
958 CommonsItem::Service(s) => s
959 .handlers
960 .iter()
961 .find(|h| contains(h.body.span, target))
962 .and_then(|h| find_stmt_run(&h.body, target)),
963 CommonsItem::Agent(a) => a
964 .handlers
965 .iter()
966 .find(|h| contains(h.body.span, target))
967 .and_then(|h| find_stmt_run(&h.body, target)),
968 _ => None,
969 }
970}
971
972fn find_stmt_run(block: &Block, target: Span) -> Option<StmtRun<'_>> {
982 for stmt in &block.statements {
983 let mut values = Vec::new();
984 statement_exprs(stmt, &mut values);
985 for v in values {
986 if let Some(found) = find_stmt_run_in_expr(v, target) {
987 return Some(found);
988 }
989 }
990 }
991 if let Some(found) = find_stmt_run_in_expr(&block.tail, target) {
992 return Some(found);
993 }
994 align_stmt_run(block, target)
995}
996
997fn find_stmt_run_in_expr(e: &Expr, target: Span) -> Option<StmtRun<'_>> {
1002 if !contains(e.span, target) {
1003 return None;
1004 }
1005 match &e.kind {
1006 ExprKind::Block(b) => find_stmt_run(b, target),
1007 ExprKind::If {
1008 cond,
1009 then_block,
1010 else_block,
1011 } => find_stmt_run_in_expr(cond, target)
1012 .or_else(|| find_stmt_run(then_block, target))
1013 .or_else(|| find_stmt_run(else_block, target)),
1014 ExprKind::Match { discriminant, arms } => find_stmt_run_in_expr(discriminant, target)
1017 .or_else(|| {
1018 arms.iter().find_map(|arm| {
1019 arm.guard
1020 .as_ref()
1021 .and_then(|g| find_stmt_run_in_expr(g, target))
1022 .or_else(|| match &arm.body {
1023 MatchBody::Expr(e) => find_stmt_run_in_expr(e, target),
1024 MatchBody::Block(b) => find_stmt_run(b, target),
1025 })
1026 })
1027 }),
1028 ExprKind::IntLit { .. }
1037 | ExprKind::FloatLit { .. }
1038 | ExprKind::DurationLit { .. }
1039 | ExprKind::StrLit(_)
1040 | ExprKind::InterpStr(_)
1041 | ExprKind::BoolLit(_)
1042 | ExprKind::Ident(_)
1043 | ExprKind::Call { .. }
1044 | ExprKind::Lambda(_)
1045 | ExprKind::BinOp(..)
1046 | ExprKind::UnaryOp(..)
1047 | ExprKind::Paren(_)
1048 | ExprKind::Ok(_)
1049 | ExprKind::Err(_)
1050 | ExprKind::Question(_)
1051 | ExprKind::RecordConstruction { .. }
1052 | ExprKind::FieldAccess { .. }
1053 | ExprKind::MethodCall { .. }
1054 | ExprKind::Is { .. }
1055 | ExprKind::Some(_)
1056 | ExprKind::None
1057 | ExprKind::UnitLit
1058 | ExprKind::RecordSpread { .. }
1059 | ExprKind::EffectPure(_)
1060 | ExprKind::Expect(_)
1061 | ExprKind::Val { .. }
1062 | ExprKind::Wire(_)
1063 | ExprKind::ListLit(_)
1064 | ExprKind::Observation(_)
1065 | ExprKind::Faults(_)
1066 | ExprKind::Trace { .. } => expr_children(e)
1067 .into_iter()
1068 .find_map(|c| find_stmt_run_in_expr(c, target)),
1069 }
1070}
1071
1072fn align_stmt_run(block: &Block, target: Span) -> Option<StmtRun<'_>> {
1078 if block
1082 .statements
1083 .iter()
1084 .any(|s| matches!(s, Statement::Let(l) | Statement::EffectLet(l) if l.let_else.is_some()))
1085 && block
1086 .statements
1087 .last()
1088 .is_some_and(|s| s.span().end <= target.end)
1089 {
1090 return None;
1091 }
1092 let start_idx = block
1093 .statements
1094 .iter()
1095 .position(|s| s.span().start == target.start)?;
1096 if target.end == block.tail.span.end {
1097 let full = Span::new(
1098 block.statements[start_idx].span().start,
1099 block.tail.span.end,
1100 );
1101 if full == target {
1102 return Some((
1103 &block.statements[start_idx..],
1104 Some(block.tail.as_ref()),
1105 Vec::new(),
1106 ));
1107 }
1108 }
1109 let end_idx = block
1110 .statements
1111 .iter()
1112 .position(|s| s.span().end == target.end)?;
1113 if end_idx < start_idx {
1114 return None;
1115 }
1116 let full = Span::new(
1117 block.statements[start_idx].span().start,
1118 block.statements[end_idx].span().end,
1119 );
1120 if full != target {
1121 return None;
1122 }
1123 let mut remainder = Vec::new();
1124 for s in &block.statements[end_idx + 1..] {
1125 statement_exprs(s, &mut remainder);
1126 }
1127 remainder.push(&block.tail);
1128 Some((&block.statements[start_idx..=end_idx], None, remainder))
1129}
1130
1131fn leaks_a_binding(
1137 stmts: &[Statement],
1138 remainder: &[&Expr],
1139 locals: &[LocalBinding],
1140 run_span: Span,
1141) -> bool {
1142 let bound_names: Vec<&str> = stmts
1143 .iter()
1144 .filter_map(|s| match s {
1145 Statement::Let(l) | Statement::EffectLet(l) if l.name.name != "_" => {
1146 Some(l.name.name.as_str())
1147 }
1148 _ => None,
1149 })
1150 .collect();
1151 if bound_names.is_empty() {
1152 return false;
1153 }
1154 let mut used = Vec::new();
1155 for e in remainder {
1156 collect_idents(e, &mut used);
1157 }
1158 used.iter().any(|id| {
1159 bound_names.contains(&id.name.as_str())
1160 && locals_at(locals, id.span.start)
1161 .into_iter()
1162 .find(|b| b.name == id.name)
1163 .is_some_and(|b| contains(run_span, b.def_span))
1164 })
1165}
1166
1167fn stmts_match(stmts: &[Statement], pred: &impl Fn(&Statement) -> bool) -> bool {
1176 stmts.iter().any(|s| pred(s) || stmt_value_matches(s, pred))
1177}
1178
1179fn stmt_value_matches(s: &Statement, pred: &impl Fn(&Statement) -> bool) -> bool {
1180 let mut exprs = Vec::new();
1183 statement_exprs(s, &mut exprs);
1184 exprs.into_iter().any(|e| expr_matches(e, pred))
1185}
1186
1187fn block_matches(b: &Block, pred: &impl Fn(&Statement) -> bool) -> bool {
1188 stmts_match(&b.statements, pred) || expr_matches(&b.tail, pred)
1189}
1190
1191fn expr_matches(e: &Expr, pred: &impl Fn(&Statement) -> bool) -> bool {
1192 match &e.kind {
1193 ExprKind::Block(b) => block_matches(b, pred),
1194 ExprKind::If {
1195 cond,
1196 then_block,
1197 else_block,
1198 } => {
1199 expr_matches(cond, pred)
1200 || block_matches(then_block, pred)
1201 || block_matches(else_block, pred)
1202 }
1203 ExprKind::Match { discriminant, arms } => {
1207 expr_matches(discriminant, pred)
1208 || arms.iter().any(|arm| {
1209 arm.guard.as_ref().is_some_and(|g| expr_matches(g, pred))
1210 || match &arm.body {
1211 MatchBody::Expr(e) => expr_matches(e, pred),
1212 MatchBody::Block(b) => block_matches(b, pred),
1213 }
1214 })
1215 }
1216 ExprKind::IntLit { .. }
1225 | ExprKind::FloatLit { .. }
1226 | ExprKind::DurationLit { .. }
1227 | ExprKind::StrLit(_)
1228 | ExprKind::InterpStr(_)
1229 | ExprKind::BoolLit(_)
1230 | ExprKind::Ident(_)
1231 | ExprKind::Call { .. }
1232 | ExprKind::Lambda(_)
1233 | ExprKind::BinOp(..)
1234 | ExprKind::UnaryOp(..)
1235 | ExprKind::Paren(_)
1236 | ExprKind::Ok(_)
1237 | ExprKind::Err(_)
1238 | ExprKind::Question(_)
1239 | ExprKind::RecordConstruction { .. }
1240 | ExprKind::FieldAccess { .. }
1241 | ExprKind::MethodCall { .. }
1242 | ExprKind::Is { .. }
1243 | ExprKind::Some(_)
1244 | ExprKind::None
1245 | ExprKind::UnitLit
1246 | ExprKind::RecordSpread { .. }
1247 | ExprKind::EffectPure(_)
1248 | ExprKind::Expect(_)
1249 | ExprKind::Val { .. }
1250 | ExprKind::Wire(_)
1251 | ExprKind::ListLit(_)
1252 | ExprKind::Observation(_)
1253 | ExprKind::Faults(_)
1254 | ExprKind::Trace { .. } => expr_children(e).into_iter().any(|c| expr_matches(c, pred)),
1255 }
1256}
1257
1258fn stmts_contain_effect_stmt(stmts: &[Statement]) -> bool {
1265 stmts_match(stmts, &|s| {
1266 matches!(
1267 s,
1268 Statement::EffectLet(_) | Statement::Send(_) | Statement::Do(_)
1269 )
1270 })
1271}
1272
1273fn stmts_contain_assign_stmt(stmts: &[Statement]) -> bool {
1279 stmts_match(stmts, &|s| matches!(s, Statement::Assign(_)))
1280}
1281
1282fn collect_idents<'a>(expr: &'a Expr, out: &mut Vec<&'a Ident>) {
1292 match &expr.kind {
1293 ExprKind::Ident(id) => out.push(id),
1294 ExprKind::Call { name, .. } => out.push(name),
1295 _ => {}
1296 }
1297 for child in expr_children(expr) {
1298 collect_idents(child, out);
1299 }
1300}
1301
1302fn line_indent(text: &str, offset: usize) -> String {
1307 let line_start = text[..offset].rfind('\n').map_or(0, |i| i + 1);
1308 let prefix = &text[line_start..offset];
1309 if prefix.chars().all(|c| c == ' ' || c == '\t') {
1310 prefix.to_string()
1311 } else {
1312 String::new()
1313 }
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318 use super::*;
1319
1320 #[test]
1323 fn early_return_detection_stops_at_a_lambda() {
1324 let src = "commons c\n\nfn g(r: Result[Int, String]) -> Result[Int, String] {\n let a = r? + 1\n let f = (x: Result[Int, String]) => Ok(x? + 1)\n f(r)\n}\n";
1325 let tokens = tokenize(src).unwrap();
1326 let (Some(SourceUnit::Commons(c)), _) = parse_unit_with_recovery(&tokens, src) else {
1327 panic!("parses");
1328 };
1329 let CommonsItem::Fn(f) = &c.items[0] else {
1330 panic!("a fn");
1331 };
1332 let value = |i: usize| match &f.body.statements[i] {
1333 Statement::Let(l) => &l.value,
1334 _ => panic!("a let"),
1335 };
1336 assert!(has_early_return(value(0)));
1337 assert!(!has_early_return(value(1)));
1338 }
1339
1340 fn actions_for(text: &str, needle: &str) -> Vec<CodeActionOrCommand> {
1341 let start = text.find(needle).expect("needle present");
1342 let requested = Span::new(start, start + needle.len());
1343 let uri = Url::parse("file:///a.bynk").unwrap();
1344 extract_variable(text, requested, &uri, Some(3))
1345 }
1346
1347 fn sole_edit(action: &CodeActionOrCommand) -> Vec<TextEdit> {
1348 let CodeActionOrCommand::CodeAction(a) = action else {
1349 panic!("expected a CodeAction");
1350 };
1351 assert_eq!(a.kind, Some(CodeActionKind::REFACTOR_EXTRACT));
1352 let Some(DocumentChanges::Edits(doc_edits)) = &a.edit.as_ref().unwrap().document_changes
1353 else {
1354 panic!("expected versioned document edits");
1355 };
1356 assert_eq!(doc_edits[0].text_document.version, Some(3));
1357 doc_edits[0]
1358 .edits
1359 .iter()
1360 .map(|e| {
1361 let OneOf::Left(edit) = e else {
1362 panic!("expected a plain TextEdit")
1363 };
1364 edit.clone()
1365 })
1366 .collect()
1367 }
1368
1369 #[test]
1370 fn extracts_a_sub_expression_of_a_let_value() {
1371 let src = "context c\n\nfn f() -> Int {\n let y = 1 + 2\n y\n}\n";
1372 let actions = actions_for(src, "1 + 2");
1373 assert_eq!(actions.len(), 1);
1374 let edits = sole_edit(&actions[0]);
1375 assert_eq!(edits.len(), 2);
1376 assert!(edits[0].new_text.starts_with("let extracted = 1 + 2\n"));
1377 assert_eq!(edits[1].new_text, "extracted");
1378 assert_eq!(edits[0].range.start, edits[0].range.end);
1382 assert_eq!(edits[0].range.start.line, edits[1].range.start.line);
1383 assert!(edits[0].range.start.character < edits[1].range.start.character);
1384 }
1385
1386 #[test]
1387 fn extracts_the_whole_tail_expression() {
1388 let src = "context c\n\nfn f() -> Int {\n let x = 1\n x + 1\n}\n";
1389 let actions = actions_for(src, "x + 1");
1390 let edits = sole_edit(&actions[0]);
1391 assert!(edits[0].new_text.starts_with("let extracted = x + 1\n"));
1392 assert_eq!(edits[1].new_text, "extracted");
1393 }
1394
1395 #[test]
1396 fn resets_the_insertion_point_inside_a_nested_if_block() {
1397 let src = concat!(
1398 "context c\n\n",
1399 "fn f(n: Int) -> Int {\n",
1400 " if n > 0 {\n",
1401 " let z = n * 2\n",
1402 " z\n",
1403 " } else {\n",
1404 " 0\n",
1405 " }\n",
1406 "}\n",
1407 );
1408 let actions = actions_for(src, "n * 2");
1409 let edits = sole_edit(&actions[0]);
1410 let insert_line = edits[0].range.start.line;
1413 let let_z_line = crate::position::offset_to_position(src, src.find("let z").unwrap()).line;
1414 assert_eq!(insert_line, let_z_line);
1415 }
1416
1417 #[test]
1418 fn resets_the_insertion_point_inside_a_nested_match_arm_block() {
1419 let src = concat!(
1420 "context c\n\n",
1421 "fn f(n: Int) -> Int {\n",
1422 " match n {\n",
1423 " 0 => {\n",
1424 " let z = n * 2\n",
1425 " z\n",
1426 " }\n",
1427 " _ => 0\n",
1428 " }\n",
1429 "}\n",
1430 );
1431 let actions = actions_for(src, "n * 2");
1432 let edits = sole_edit(&actions[0]);
1433 let insert_line = edits[0].range.start.line;
1436 let let_z_line = crate::position::offset_to_position(src, src.find("let z").unwrap()).line;
1437 assert_eq!(insert_line, let_z_line);
1438 }
1439
1440 #[test]
1444 fn declines_a_selection_reading_an_arm_or_lambda_binding() {
1445 let arm = concat!(
1446 "context c\n\n",
1447 "fn f(o: Option[Int]) -> Int {\n",
1448 " let a = match o {\n",
1449 " Some(x) => x + 1\n",
1450 " _ => 0\n",
1451 " }\n",
1452 " a\n",
1453 "}\n",
1454 );
1455 assert!(actions_for(arm, "x + 1").is_empty(), "arm binding hoisted");
1456 let lambda = concat!(
1457 "context c\n\n",
1458 "fn f(xs: List[Int]) -> List[Int] {\n",
1459 " let ys = xs.map((v) => v * 2)\n",
1460 " ys\n",
1461 "}\n",
1462 );
1463 assert!(
1464 actions_for(lambda, "v * 2").is_empty(),
1465 "lambda parameter hoisted"
1466 );
1467 let guard = concat!(
1470 "context c\n\n",
1471 "fn f(o: Option[Int]) -> Int {\n",
1472 " let a = match o {\n",
1473 " Some(x) if x > 1 => 1\n",
1474 " _ => 0\n",
1475 " }\n",
1476 " a\n",
1477 "}\n",
1478 );
1479 let guard_actions = actions_for(guard, "x > 1");
1480 assert_eq!(
1481 guard_actions.len(),
1482 1,
1483 "a guard selection extracts the match"
1484 );
1485 {
1486 let edits = sole_edit(&guard_actions[0]);
1487 assert!(
1488 edits[0].new_text.starts_with("let extracted = match o {"),
1489 "a guard selection must extract the whole match: {:?}",
1490 edits[0].new_text
1491 );
1492 }
1493 }
1494
1495 #[test]
1498 fn still_extracts_inside_an_arm_or_lambda_when_no_binding_is_read() {
1499 let arm = concat!(
1500 "context c\n\n",
1501 "fn f(o: Option[Int], n: Int) -> Int {\n",
1502 " let a = match o {\n",
1503 " Some(x) => n + 1\n",
1504 " _ => 0\n",
1505 " }\n",
1506 " a\n",
1507 "}\n",
1508 );
1509 let edits = sole_edit(&actions_for(arm, "n + 1")[0]);
1510 assert!(edits[0].new_text.starts_with("let extracted = n + 1\n"));
1511 let lambda = concat!(
1512 "context c\n\n",
1513 "fn f(xs: List[Int], k: Int) -> List[Int] {\n",
1514 " let ys = xs.map((v) => k * 2)\n",
1515 " ys\n",
1516 "}\n",
1517 );
1518 let edits = sole_edit(&actions_for(lambda, "k * 2")[0]);
1519 assert!(edits[0].new_text.starts_with("let extracted = k * 2\n"));
1520 }
1521
1522 #[test]
1527 fn declines_a_selection_reading_an_is_binding_or_an_observed_parameter() {
1528 let and = concat!(
1529 "context c\n\n",
1530 "fn f(r: Result[Int, String]) -> Bool {\n",
1531 " let ok = r is Ok(n) && n > 0\n",
1532 " ok\n",
1533 "}\n",
1534 );
1535 assert!(actions_for(and, "n > 0").is_empty(), "`&&` binding hoisted");
1536 let implies = concat!(
1537 "context c\n\n",
1538 "fn f(r: Result[Int, String]) -> Bool {\n",
1539 " let ok = r is Ok(n) implies n > 0\n",
1540 " ok\n",
1541 "}\n",
1542 );
1543 assert!(
1544 actions_for(implies, "n > 0").is_empty(),
1545 "`implies` binding hoisted"
1546 );
1547 let condition = concat!(
1548 "context c\n\n",
1549 "fn f(r: Result[Int, String]) -> Int {\n",
1550 " if r is Ok(n) && n > 0 {\n",
1551 " 1\n",
1552 " } else {\n",
1553 " 0\n",
1554 " }\n",
1555 "}\n",
1556 );
1557 assert!(
1558 actions_for(condition, "n > 0").is_empty(),
1559 "`if` condition binding hoisted"
1560 );
1561 let observed = concat!(
1562 "suite c {\n",
1563 " case \"logs\" {\n",
1564 " expect Logger.log called with msg == \"start\"\n",
1565 " }\n",
1566 "}\n",
1567 );
1568 assert!(
1569 actions_for(observed, "msg == \"start\"").is_empty(),
1570 "observed parameter hoisted"
1571 );
1572 let lhs = concat!(
1574 "context c\n\n",
1575 "fn f(r: Result[Int, String], k: Int) -> Bool {\n",
1576 " let ok = k > 1 && r is Ok(n)\n",
1577 " ok\n",
1578 "}\n",
1579 );
1580 let edits = sole_edit(&actions_for(lhs, "k > 1")[0]);
1581 assert!(edits[0].new_text.starts_with("let extracted = k > 1\n"));
1582 }
1583
1584 #[test]
1587 fn still_extracts_a_binding_read_inside_a_block_body() {
1588 let arm = concat!(
1589 "context c\n\n",
1590 "fn f(o: Option[Int]) -> Int {\n",
1591 " let a = match o {\n",
1592 " Some(x) => {\n",
1593 " let b = x + 1\n",
1594 " b\n",
1595 " }\n",
1596 " _ => 0\n",
1597 " }\n",
1598 " a\n",
1599 "}\n",
1600 );
1601 let edits = sole_edit(&actions_for(arm, "x + 1")[0]);
1602 assert!(edits[0].new_text.starts_with("let extracted = x + 1\n"));
1603 let lambda = concat!(
1604 "context c\n\n",
1605 "fn f(xs: List[Int]) -> List[Int] {\n",
1606 " let ys = xs.map((v) => {\n",
1607 " let w = v * 2\n",
1608 " w\n",
1609 " })\n",
1610 " ys\n",
1611 "}\n",
1612 );
1613 let edits = sole_edit(&actions_for(lambda, "v * 2")[0]);
1614 assert!(edits[0].new_text.starts_with("let extracted = v * 2\n"));
1615 }
1616
1617 #[test]
1618 fn selection_crossing_a_statement_boundary_offers_nothing() {
1619 let src = "context c\n\nfn f() -> Int {\n let a = 1\n let b = 2\n a\n}\n";
1620 let actions = actions_for(src, "1\n let b");
1623 assert!(actions.is_empty());
1624 }
1625
1626 #[test]
1627 fn selection_of_the_whole_block_braces_included_offers_nothing() {
1628 let src = "context c\n\nfn f() -> Int {\n let a = 1\n a\n}\n";
1629 let actions = actions_for(src, "{\n let a = 1\n a\n}");
1632 assert!(actions.is_empty());
1633 }
1634
1635 #[test]
1636 fn extracts_a_call_argument() {
1637 let src = "context c\n\nfn f() -> Int {\n g(1 + 2, 3)\n}\n";
1638 let actions = actions_for(src, "1 + 2");
1639 let edits = sole_edit(&actions[0]);
1640 assert_eq!(edits[1].new_text, "extracted");
1641 assert!(edits[0].new_text.contains("let extracted = 1 + 2"));
1642 }
1643
1644 #[test]
1645 fn bumps_the_suffix_on_a_name_collision() {
1646 let src = "context c\n\nfn f() -> Int {\n let extracted = 0\n 1 + 2\n}\n";
1647 let actions = actions_for(src, "1 + 2");
1648 let edits = sole_edit(&actions[0]);
1649 assert!(edits[0].new_text.starts_with("let extracted2 = 1 + 2\n"));
1650 assert_eq!(edits[1].new_text, "extracted2");
1651 }
1652
1653 #[test]
1654 fn empty_selection_offers_nothing() {
1655 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1656 let off = src.find("1 + 2").unwrap();
1657 let uri = Url::parse("file:///a.bynk").unwrap();
1658 let actions = extract_variable(src, Span::new(off, off), &uri, Some(1));
1659 assert!(actions.is_empty());
1660 }
1661
1662 #[test]
1663 fn selection_outside_any_body_offers_nothing() {
1664 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1665 let actions = actions_for(src, "context c");
1666 assert!(actions.is_empty());
1667 }
1668
1669 #[test]
1670 fn preserves_indentation_on_a_multi_statement_block() {
1671 let src = "context c\n\nfn f() -> Int {\n let a = 1\n let b = a + 2\n b\n}\n";
1672 let actions = actions_for(src, "a + 2");
1673 let edits = sole_edit(&actions[0]);
1674 assert_eq!(edits[0].new_text, "let extracted = a + 2\n ");
1675 }
1676
1677 mod extract_function_tests {
1678 use super::*;
1679 use bynk_check::locals::LocalKind;
1680 use bynk_check::requirements::RequirementSource;
1681
1682 fn function_actions_for(
1683 text: &str,
1684 needle: &str,
1685 requirements: &[Requirement],
1686 locals: &[LocalBinding],
1687 expr_types: &[(Span, TyId)],
1688 ) -> Vec<CodeActionOrCommand> {
1689 let start = text.find(needle).expect("needle present");
1690 let requested = Span::new(start, start + needle.len());
1691 let uri = Url::parse("file:///a.bynk").unwrap();
1692 extract_function(
1693 text,
1694 requested,
1695 &uri,
1696 Some(3),
1697 requirements,
1698 locals,
1699 expr_types,
1700 &TYS,
1701 )
1702 }
1703
1704 fn nth_offset(text: &str, needle: &str, n: usize) -> usize {
1705 text.match_indices(needle)
1706 .nth(n)
1707 .expect("occurrence present")
1708 .0
1709 }
1710
1711 fn param(text: &str, name: &str, ty: &str) -> LocalBinding {
1715 let def_start = nth_offset(text, name, 0);
1716 LocalBinding {
1717 name: name.to_string(),
1718 def_span: Span::new(def_start, def_start + name.len()),
1719 kind: LocalKind::Param,
1720 ty: ty.to_string(),
1721 scope: Span::new(0, text.len()),
1722 }
1723 }
1724
1725 use bynk_check::checker::Ty;
1726
1727 static TYS: std::sync::LazyLock<Types> = std::sync::LazyLock::new(Types::new);
1732
1733 fn int_type(text: &str, needle: &str) -> (Span, TyId) {
1734 let start = text.find(needle).expect("needle present");
1735 (
1736 Span::new(start, start + needle.len()),
1737 TYS.intern(Ty::Base(BaseType::Int)),
1738 )
1739 }
1740
1741 fn let_binding(text: &str, name: &str, ty: &str) -> LocalBinding {
1744 let def_start = nth_offset(text, name, 0);
1745 LocalBinding {
1746 name: name.to_string(),
1747 def_span: Span::new(def_start, def_start + name.len()),
1748 kind: LocalKind::Let,
1749 ty: ty.to_string(),
1750 scope: Span::new(0, text.len()),
1751 }
1752 }
1753
1754 fn capability_use(site_needle_offset: usize, len: usize) -> Requirement {
1755 Requirement {
1756 capability: "Clock".to_string(),
1757 site: Span::new(site_needle_offset, site_needle_offset + len),
1758 source: RequirementSource::DirectCall {
1759 op: "now".to_string(),
1760 },
1761 covered: false,
1762 materialize: None,
1763 }
1764 }
1765
1766 #[test]
1767 fn extracts_a_free_variable_as_a_parameter() {
1768 let src = "context c\n\nfn f(num: Int) -> Int {\n num * 2\n}\n";
1769 let locals = vec![param(src, "num", "Int")];
1770 let types = vec![int_type(src, "num * 2")];
1771 let actions = function_actions_for(src, "num * 2", &[], &locals, &types);
1772 assert_eq!(actions.len(), 1);
1773 let edits = sole_edit(&actions[0]);
1774 assert_eq!(
1775 edits[0].new_text,
1776 "fn extractedFn(num: Int) -> Int {\n num * 2\n}\n\n"
1777 );
1778 assert_eq!(edits[0].range.start, Position::new(2, 0));
1780 assert_eq!(edits[1].new_text, "extractedFn(num)");
1781 }
1782
1783 #[test]
1788 fn a_name_read_only_in_a_guard_is_a_parameter() {
1789 let src = "context c\n\nfn f(num: Int, lim: Int) -> Int {\n match num {\n n if n > lim => 1\n _ => 0\n }\n}\n";
1790 let selection = "match num {\n n if n > lim => 1\n _ => 0\n }";
1791 let locals = vec![param(src, "num", "Int"), param(src, "lim", "Int")];
1792 let types = vec![int_type(src, selection)];
1793 let actions = function_actions_for(src, selection, &[], &locals, &types);
1794 assert_eq!(actions.len(), 1);
1795 let edits = sole_edit(&actions[0]);
1796 assert!(
1797 edits[0]
1798 .new_text
1799 .starts_with("fn extractedFn(num: Int, lim: Int) -> Int {"),
1800 "{}",
1801 edits[0].new_text
1802 );
1803 assert_eq!(edits[1].new_text, "extractedFn(num, lim)");
1804 }
1805
1806 #[test]
1807 fn no_free_variables_yields_a_nullary_call() {
1808 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1809 let types = vec![int_type(src, "1 + 2")];
1810 let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
1811 let edits = sole_edit(&actions[0]);
1812 assert_eq!(
1813 edits[0].new_text,
1814 "fn extractedFn() -> Int {\n 1 + 2\n}\n\n"
1815 );
1816 assert_eq!(edits[1].new_text, "extractedFn()");
1817 }
1818
1819 #[test]
1820 fn capability_using_selection_offers_nothing() {
1821 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1822 let site = src.find("1 + 2").unwrap();
1823 let reqs = vec![capability_use(site, "1 + 2".len())];
1824 let types = vec![int_type(src, "1 + 2")];
1825 let actions = function_actions_for(src, "1 + 2", &reqs, &[], &types);
1826 assert!(actions.is_empty());
1827 }
1828
1829 #[test]
1830 fn bumps_the_suffix_on_a_name_collision() {
1831 let src =
1832 "context c\n\nfn extractedFn() -> Int {\n 0\n}\n\nfn f() -> Int {\n 1 + 2\n}\n";
1833 let types = vec![int_type(src, "1 + 2")];
1834 let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
1835 let edits = sole_edit(&actions[0]);
1836 assert!(edits[0].new_text.starts_with("fn extractedFn2("));
1837 assert_eq!(edits[1].new_text, "extractedFn2()");
1838 }
1839
1840 #[test]
1841 fn selection_outside_any_body_offers_nothing() {
1842 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1843 let types = vec![int_type(src, "1 + 2")];
1844 let actions = function_actions_for(src, "context c", &[], &[], &types);
1845 assert!(actions.is_empty());
1846 }
1847
1848 #[test]
1849 fn adapter_files_offer_nothing() {
1850 let src = "adapter tokens {\n binding \"./b.ts\"\n}\n";
1853 let actions = function_actions_for(src, "binding", &[], &[], &[]);
1854 assert!(actions.is_empty());
1855 }
1856
1857 #[test]
1858 fn suite_test_cases_offer_nothing() {
1859 let src = "suite thing\n\ncase \"it works\" {\n 1 + 2\n}\n";
1864 let types = vec![int_type(src, "1 + 2")];
1865 let actions = function_actions_for(src, "1 + 2", &[], &[], &types);
1866 assert!(actions.is_empty());
1867 }
1868
1869 #[test]
1870 fn a_dirty_file_with_no_recorded_type_offers_nothing() {
1871 let src = "context c\n\nfn f() -> Int {\n 1 + 2\n}\n";
1872 let actions = function_actions_for(src, "1 + 2", &[], &[], &[]);
1875 assert!(actions.is_empty());
1876 }
1877
1878 #[test]
1879 fn ambiguous_same_name_shadow_from_two_outer_scopes_declines() {
1880 let src = "context c\n\nfn f() -> Int {\n x + x\n}\n";
1881 let types = vec![int_type(src, "x + x")];
1882 let first_x = nth_offset(src, "x + x", 0); let second_x = first_x + "x + ".len(); let locals = vec![
1889 LocalBinding {
1890 name: "x".to_string(),
1891 def_span: Span::new(0, 1),
1892 kind: LocalKind::Let,
1893 ty: "Int".to_string(),
1894 scope: Span::new(first_x, first_x),
1895 },
1896 LocalBinding {
1897 name: "x".to_string(),
1898 def_span: Span::new(1, 2),
1899 kind: LocalKind::Let,
1900 ty: "Int".to_string(),
1901 scope: Span::new(second_x, second_x),
1902 },
1903 ];
1904 let actions = function_actions_for(src, "x + x", &[], &locals, &types);
1905 assert!(actions.is_empty());
1906 }
1907
1908 mod multi_statement_tests {
1909 use super::*;
1910
1911 #[test]
1912 fn a_run_including_the_tail_becomes_a_call_expression() {
1913 let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let b = a * 2\n b + num\n}\n";
1914 let locals = vec![
1915 param(src, "num", "Int"),
1916 let_binding(src, "a", "Int"),
1917 let_binding(src, "b", "Int"),
1918 ];
1919 let types = vec![int_type(src, "b + num")];
1920 let needle = "let a = num + 1\n let b = a * 2\n b + num";
1921 let actions = function_actions_for(src, needle, &[], &locals, &types);
1922 assert_eq!(actions.len(), 1);
1923 let edits = sole_edit(&actions[0]);
1924 assert_eq!(
1925 edits[0].new_text,
1926 format!("fn extractedFn(num: Int) -> Int {{\n {needle}\n}}\n\n")
1927 );
1928 assert_eq!(edits[1].new_text, "extractedFn(num)");
1929 }
1930
1931 #[test]
1932 fn a_run_excluding_the_tail_yields_a_discard_call() {
1933 let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
1934 let locals = vec![
1935 param(src, "num", "Int"),
1936 let_binding(src, "a", "Int"),
1937 let_binding(src, "valC", "Int"),
1938 ];
1939 let needle = "let a = num + 1\n let valC = a * 2";
1940 let actions = function_actions_for(src, needle, &[], &locals, &[]);
1941 assert_eq!(actions.len(), 1);
1942 let edits = sole_edit(&actions[0]);
1943 assert_eq!(
1944 edits[0].new_text,
1945 format!("fn extractedFn(num: Int) -> () {{\n {needle}\n}}\n\n")
1946 );
1947 assert_eq!(edits[1].new_text, "let _ = extractedFn(num)");
1948 }
1949
1950 #[test]
1951 fn an_effectful_run_excluding_the_tail_yields_an_effect_return_and_a_do_call() {
1952 let src = "context c\n\nfn f() -> Effect[()] {\n do g()\n do h()\n ()\n}\n";
1958 let needle = "do g()\n do h()";
1959 let actions = function_actions_for(src, needle, &[], &[], &[]);
1960 assert_eq!(actions.len(), 1);
1961 let edits = sole_edit(&actions[0]);
1962 assert_eq!(
1963 edits[0].new_text,
1964 format!("fn extractedFn() -> Effect[()] {{\n {needle}\n}}\n\n")
1965 );
1966 assert_eq!(edits[1].new_text, "do extractedFn()");
1967 }
1968
1969 #[test]
1970 fn a_run_whose_match_guard_binds_an_effect_yields_an_effect_return() {
1971 let src = concat!(
1975 "context c\n\n",
1976 "fn f(o: Option[Int]) -> Effect[()] {\n",
1977 " let a = match o {\n",
1978 " Some(n) if if n > 0 {\n",
1979 " let _ <- g()\n",
1980 " true\n",
1981 " } else {\n",
1982 " false\n",
1983 " } => n\n",
1984 " _ => 0\n",
1985 " }\n",
1986 " ()\n",
1987 "}\n",
1988 );
1989 let needle = "let a = match o {\n Some(n) if if n > 0 {\n let _ <- g()\n true\n } else {\n false\n } => n\n _ => 0\n }";
1990 let actions = function_actions_for(src, needle, &[], &[], &[]);
1991 assert_eq!(actions.len(), 1);
1992 let edits = sole_edit(&actions[0]);
1993 assert!(
1994 edits[0]
1995 .new_text
1996 .starts_with("fn extractedFn() -> Effect[()] {"),
1997 "{}",
1998 edits[0].new_text
1999 );
2000 assert_eq!(edits[1].new_text, "do extractedFn()");
2001 }
2002
2003 #[test]
2004 fn a_binding_the_run_introduces_still_used_in_the_tail_declines() {
2005 let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let b = a * 2\n b\n}\n";
2006 let locals = vec![
2007 param(src, "num", "Int"),
2008 let_binding(src, "a", "Int"),
2009 let_binding(src, "b", "Int"),
2010 ];
2011 let needle = "let a = num + 1\n let b = a * 2";
2014 let actions = function_actions_for(src, needle, &[], &locals, &[]);
2015 assert!(actions.is_empty());
2016 }
2017
2018 #[test]
2019 fn a_run_containing_a_cell_write_declines() {
2020 let src = "context c\n\nfn f() -> Int {\n cell := 1\n x\n}\n";
2023 let actions = function_actions_for(src, "cell := 1", &[], &[], &[]);
2024 assert!(actions.is_empty());
2025 }
2026
2027 #[test]
2028 fn a_run_containing_a_nested_cell_write_declines() {
2029 let src = concat!(
2035 "context c\n\n",
2036 "fn f(cond: Bool) -> Int {\n",
2037 " let a = if cond {\n",
2038 " cell := 1\n",
2039 " 0\n",
2040 " } else {\n",
2041 " 1\n",
2042 " }\n",
2043 " a\n",
2044 "}\n",
2045 );
2046 let needle = "let a = if cond {\n cell := 1\n 0\n } else {\n 1\n }";
2047 let actions = function_actions_for(src, needle, &[], &[], &[]);
2048 assert!(actions.is_empty());
2049 }
2050
2051 #[test]
2052 fn a_run_containing_a_cell_write_in_a_match_guard_declines() {
2053 let src = concat!(
2056 "context c\n\n",
2057 "fn f(o: Option[Int]) -> Int {\n",
2058 " let a = match o {\n",
2059 " Some(x) if if x > 0 {\n",
2060 " cell := x\n",
2061 " true\n",
2062 " } else {\n",
2063 " false\n",
2064 " } => x\n",
2065 " _ => 0\n",
2066 " }\n",
2067 " a\n",
2068 "}\n",
2069 );
2070 let needle = "let a = match o {\n Some(x) if if x > 0 {\n cell := x\n true\n } else {\n false\n } => x\n _ => 0\n }";
2071 let actions = function_actions_for(src, needle, &[], &[], &[]);
2072 assert!(actions.is_empty());
2073 }
2074
2075 #[test]
2076 fn a_run_found_inside_a_match_guard() {
2077 let src = concat!(
2081 "context c\n\n",
2082 "fn f(o: Option[Int]) -> Int {\n",
2083 " match o {\n",
2084 " Some(num) if if num > 0 {\n",
2085 " let valA = num * 2\n",
2086 " let valB = valA + 1\n",
2087 " num > 3\n",
2088 " } else {\n",
2089 " false\n",
2090 " } => num\n",
2091 " _ => 0\n",
2092 " }\n",
2093 "}\n",
2094 );
2095 let locals = vec![
2096 param(src, "num", "Int"),
2097 let_binding(src, "valA", "Int"),
2098 let_binding(src, "valB", "Int"),
2099 ];
2100 let needle = "let valA = num * 2\n let valB = valA + 1";
2101 let actions = function_actions_for(src, needle, &[], &locals, &[]);
2102 assert_eq!(actions.len(), 1, "a run in a guard is extractable");
2103 }
2104
2105 #[test]
2106 fn a_run_found_inside_a_nested_if_branch() {
2107 let src = concat!(
2108 "context c\n\n",
2109 "fn f(num: Int) -> Int {\n",
2110 " if num > 0 {\n",
2111 " let valA = num * 2\n",
2112 " let valB = valA + 1\n",
2113 " valB\n",
2114 " } else {\n",
2115 " 0\n",
2116 " }\n",
2117 "}\n",
2118 );
2119 let locals = vec![
2120 param(src, "num", "Int"),
2121 let_binding(src, "valA", "Int"),
2122 let_binding(src, "valB", "Int"),
2123 ];
2124 let tail_val_b = nth_offset(src, "valB", 1); let types = vec![(
2126 Span::new(tail_val_b, tail_val_b + "valB".len()),
2127 TYS.intern(Ty::Base(BaseType::Int)),
2128 )];
2129 let needle = "let valA = num * 2\n let valB = valA + 1\n valB";
2130 let actions = function_actions_for(src, needle, &[], &locals, &types);
2131 assert_eq!(actions.len(), 1);
2132 let edits = sole_edit(&actions[0]);
2133 assert_eq!(
2134 edits[0].new_text,
2135 format!("fn extractedFn(num: Int) -> Int {{\n {needle}\n}}\n\n")
2136 );
2137 assert_eq!(edits[0].range.start, Position::new(2, 0));
2139 assert_eq!(edits[1].new_text, "extractedFn(num)");
2140 }
2141
2142 #[test]
2146 fn a_let_else_statement_is_not_extracted() {
2147 let src = "context c\n\nfn f(r: Result[Int, String]) -> Int {\n let Ok(n) = load(r) else { 0 }\n let m = n + 1\n m\n}\n";
2148 let uri = Url::parse("file:///a.bynk").unwrap();
2149 let locals = vec![param(src, "r", "Result[Int, String]")];
2150 for sel in [
2151 "let Ok(n) = load(r) else { 0 }",
2152 "let Ok(n) = load(r) else { 0 }\n let m = n + 1",
2153 "let Ok(n) = load(r) else { 0 }\n let m = n + 1\n m",
2154 ] {
2155 let start = src.find(sel).unwrap();
2156 let requested = Span::new(start, start + sel.len());
2157 assert!(
2158 extract_function(src, requested, &uri, Some(3), &[], &locals, &[], &TYS)
2159 .is_empty(),
2160 "extract function must decline {sel:?}"
2161 );
2162 assert!(
2163 extract_variable(src, requested, &uri, Some(3)).is_empty(),
2164 "extract variable must decline {sel:?}"
2165 );
2166 }
2167 let start = src.find("load(r)").unwrap();
2168 let requested = Span::new(start, start + "load(r)".len());
2169 let actions = extract_variable(src, requested, &uri, Some(3));
2170 assert_eq!(actions.len(), 1, "the value is the author's expression");
2171 let edits = sole_edit(&actions[0]);
2172 assert!(
2173 edits
2174 .iter()
2175 .any(|e| e.new_text.starts_with("let extracted = load(r)")),
2176 "{edits:?}"
2177 );
2178 }
2179
2180 #[test]
2183 fn statements_after_a_let_else_still_extract() {
2184 let src = "context c\n\nfn f(r: Result[Int, String]) -> Int {\n let Ok(n) = load(r) else { 0 }\n let m = n + 1\n let k = m * 2\n k\n}\n";
2185 let uri = Url::parse("file:///a.bynk").unwrap();
2186 let sel = "let m = n + 1\n let k = m * 2\n k";
2187 let start = src.find(sel).unwrap();
2188 let requested = Span::new(start, start + sel.len());
2189 let locals = vec![
2190 param(src, "r", "Result[Int, String]"),
2191 let_binding(src, "m", "Int"),
2192 let_binding(src, "k", "Int"),
2193 LocalBinding {
2194 name: "n".to_string(),
2195 def_span: Span::new(nth_offset(src, "n)", 0), nth_offset(src, "n)", 0) + 1),
2196 kind: LocalKind::Let,
2197 ty: "Int".to_string(),
2198 scope: Span::new(0, src.len()),
2199 },
2200 ];
2201 let tail = src.find(" k\n}").unwrap() + 2;
2204 let types = vec![(
2205 Span::new(tail, tail + 1),
2206 TYS.intern(Ty::Base(BaseType::Int)),
2207 )];
2208 let actions =
2209 extract_function(src, requested, &uri, Some(3), &[], &locals, &types, &TYS);
2210 assert_eq!(actions.len(), 1, "{actions:#?}");
2211 }
2212
2213 #[test]
2214 fn a_selection_extending_past_a_statement_boundary_declines() {
2215 let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
2216 let start = src.find("let a = num + 1").unwrap();
2217 let end = start + "let a = num + 1\n let valC = a * 2".len() - 1;
2222 let requested = Span::new(start, end);
2223 let uri = Url::parse("file:///a.bynk").unwrap();
2224 let actions = extract_function(src, requested, &uri, Some(3), &[], &[], &[], &TYS);
2225 assert!(actions.is_empty());
2226 }
2227
2228 #[test]
2229 fn a_selection_padded_with_surrounding_whitespace_still_aligns() {
2230 let src = "context c\n\nfn f(num: Int) -> Int {\n let a = num + 1\n let valC = a * 2\n num\n}\n";
2231 let inner = "let a = num + 1\n let valC = a * 2";
2232 let start = src.find(inner).unwrap();
2233 let requested = Span::new(start - 2, start + inner.len() + 1);
2236 let locals = vec![
2237 param(src, "num", "Int"),
2238 let_binding(src, "a", "Int"),
2239 let_binding(src, "valC", "Int"),
2240 ];
2241 let uri = Url::parse("file:///a.bynk").unwrap();
2242 let actions =
2243 extract_function(src, requested, &uri, Some(3), &[], &locals, &[], &TYS);
2244 assert_eq!(actions.len(), 1);
2245 let edits = sole_edit(&actions[0]);
2246 assert_eq!(edits[1].new_text, "let _ = extractedFn(num)");
2247 }
2248 }
2249
2250 mod analysed_fixtures {
2256 use super::*;
2257 use std::path::Path;
2258 use std::sync::Arc;
2259
2260 fn analysed(
2264 test_name: &str,
2265 src: &str,
2266 expected: &[&str],
2267 ) -> (Vec<(Span, TyId)>, Vec<LocalBinding>, Arc<Types>) {
2268 let root = std::env::temp_dir().join(format!(
2269 "bynk-lsp-extract-{test_name}-{}",
2270 std::process::id()
2271 ));
2272 let _ = std::fs::remove_dir_all(&root);
2273 std::fs::create_dir_all(&root).expect("create test root");
2274 std::fs::write(root.join("c.bynk"), src).expect("write file");
2275 let root = root.canonicalize().expect("canonical root");
2276 let overlay = HashMap::from([(root.join("c.bynk"), src.to_string())]);
2277 let result = bynk_ide::diagnose_project(&root, &overlay);
2278 let _ = std::fs::remove_dir_all(&root);
2279 let rel = Path::new("c.bynk");
2280 let categories: Vec<&str> = result
2281 .files
2282 .iter()
2283 .filter(|f| f.source_path == rel)
2284 .flat_map(|f| f.diagnostics.iter().map(|d| d.error.category))
2285 .collect();
2286 assert_eq!(categories, expected);
2287 (
2288 result.expr_types.get(rel).cloned().unwrap_or_default(),
2289 result.locals.get(rel).cloned().unwrap_or_default(),
2290 Arc::clone(&result.ty_intern),
2291 )
2292 }
2293
2294 fn offered(test_name: &str, src: &str, needle: &str, expected: &[&str]) -> Vec<String> {
2296 let (types, locals, tys) = analysed(test_name, src, expected);
2297 let start = src.find(needle).expect("needle present");
2298 let uri = Url::parse("file:///c.bynk").unwrap();
2299 extract_function(
2300 src,
2301 Span::new(start, start + needle.len()),
2302 &uri,
2303 Some(3),
2304 &[],
2305 &locals,
2306 &types,
2307 &tys,
2308 )
2309 .iter()
2310 .map(|a| sole_edit(a)[0].new_text.clone())
2311 .collect()
2312 }
2313
2314 #[test]
2320 fn a_clean_file_offers_the_action() {
2321 let src = "context c\n\nfn f(num: Int) -> Int {\n num * 2\n}\n";
2322 assert_eq!(
2323 offered("clean", src, "num * 2", &[]),
2324 ["fn extractedFn(num: Int) -> Int {\n num * 2\n}\n\n"]
2325 );
2326 }
2327
2328 #[test]
2331 fn an_error_typed_selection_declines() {
2332 let src = "context c\n\nfn f(num: Int) -> Int {\n num + \"s\"\n}\n";
2333 let offered = offered(
2334 "error-typed-selection",
2335 src,
2336 "num + \"s\"",
2337 &["bynk.types.type_mismatch"],
2338 );
2339 assert!(offered.is_empty(), "expected no action; got {offered:?}");
2340 }
2341
2342 #[test]
2347 fn a_selection_reading_an_error_typed_binding_declines() {
2348 let src = "context c\n\nfn f(num: Int) -> Int {\n let z = num + \"s\"\n let w = if num > 0 {\n let q = z\n 1\n } else {\n 2\n }\n w\n}\n";
2349 let offered = offered(
2350 "error-typed-binding",
2351 src,
2352 "if num > 0 {\n let q = z\n 1\n } else {\n 2\n }",
2353 &["bynk.types.type_mismatch"],
2354 );
2355 assert!(offered.is_empty(), "expected no action; got {offered:?}");
2356 }
2357
2358 #[test]
2363 fn a_statement_run_reading_an_error_typed_binding_declines() {
2364 let src = "context c\n\nfn f(num: Int) -> Int {\n let z = num + \"s\"\n let q = z\n num\n}\n";
2365 let offered = offered(
2366 "error-typed-stmts",
2367 src,
2368 "let q = z",
2369 &["bynk.types.type_mismatch"],
2370 );
2371 assert!(offered.is_empty(), "expected no action; got {offered:?}");
2372 }
2373
2374 #[test]
2377 fn an_error_elsewhere_still_offers_the_action() {
2378 let src = "context c\n\nfn f(num: Int) -> Int {\n num * 2\n}\n\nfn g(num: Int) -> Int {\n num + \"s\"\n}\n";
2379 assert_eq!(
2380 offered(
2381 "error-elsewhere",
2382 src,
2383 "num * 2",
2384 &["bynk.types.type_mismatch"]
2385 ),
2386 ["fn extractedFn(num: Int) -> Int {\n num * 2\n}\n\n"]
2387 );
2388 }
2389 }
2390 }
2391
2392 #[test]
2393 fn extracts_inside_the_value_of_a_let_with_a_call_site_principal() {
2394 let src = "suite demo.s {\n case \"c\" {\n let who = \"alice\"\n let item <- api.get(1 + 2) by User(who)\n expect item\n }\n}\n";
2397 assert_eq!(actions_for(src, "1 + 2").len(), 1);
2398 }
2399}