1use bock_air::node::{AIRNode, NodeKind};
4use bock_types::AIRModule;
5
6use crate::profile::{Support, TargetProfile};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct CapabilityGap {
11 pub construct: String,
13 pub target_support: Support,
15 pub synthesis_strategy: String,
17}
18
19#[must_use]
26pub fn detect_gaps(module: &AIRModule, target: &TargetProfile) -> Vec<CapabilityGap> {
27 let usage = collect_construct_usage(module);
28 let mut gaps = Vec::new();
29 let caps = &target.capabilities;
30
31 if usage.has_enum_decls && caps.algebraic_types != Support::Native {
32 gaps.push(CapabilityGap {
33 construct: "algebraic_types".into(),
34 target_support: caps.algebraic_types,
35 synthesis_strategy: match caps.algebraic_types {
36 Support::Emulated | Support::SwitchBased | Support::InterfaceBased => {
37 "Tagged objects + switch".into()
38 }
39 Support::None => "Cannot represent algebraic types".into(),
40 Support::Native => unreachable!(),
41 },
42 });
43 }
44
45 if usage.has_match && caps.pattern_matching != Support::Native {
46 gaps.push(CapabilityGap {
47 construct: "pattern_matching".into(),
48 target_support: caps.pattern_matching,
49 synthesis_strategy: match caps.pattern_matching {
50 Support::SwitchBased | Support::Emulated => "Switch-based dispatch".into(),
51 Support::InterfaceBased | Support::None => "if/else chains".into(),
52 Support::Native => unreachable!(),
53 },
54 });
55 }
56
57 if usage.has_traits && !matches!(caps.traits, Support::Native | Support::InterfaceBased) {
58 gaps.push(CapabilityGap {
59 construct: "traits".into(),
60 target_support: caps.traits,
61 synthesis_strategy: match caps.traits {
62 Support::Emulated | Support::SwitchBased => "Duck typing / protocol classes".into(),
63 Support::None => "Cannot represent traits".into(),
64 Support::Native | Support::InterfaceBased => unreachable!(),
65 },
66 });
67 }
68
69 if usage.has_ownership && caps.memory_model != crate::profile::MemoryModel::Manual {
70 gaps.push(CapabilityGap {
71 construct: "ownership".into(),
72 target_support: Support::Emulated,
73 synthesis_strategy: "Erase ownership annotations".into(),
74 });
75 }
76
77 if usage.has_effects {
78 gaps.push(CapabilityGap {
79 construct: "effects".into(),
80 target_support: Support::Emulated,
81 synthesis_strategy: "Parameter passing".into(),
82 });
83 }
84
85 if usage.has_interpolation && caps.string_interpolation != Support::Native {
86 gaps.push(CapabilityGap {
87 construct: "string_interpolation".into(),
88 target_support: caps.string_interpolation,
89 synthesis_strategy: match caps.string_interpolation {
90 Support::Emulated => "String concatenation / format macro".into(),
91 Support::SwitchBased | Support::InterfaceBased | Support::None => {
92 "String concatenation".into()
93 }
94 Support::Native => unreachable!(),
95 },
96 });
97 }
98
99 gaps
100}
101
102#[derive(Debug, Default)]
106struct ConstructUsage {
107 has_enum_decls: bool,
108 has_match: bool,
109 has_traits: bool,
110 has_ownership: bool,
111 has_effects: bool,
112 has_interpolation: bool,
113}
114
115fn collect_construct_usage(module: &AIRModule) -> ConstructUsage {
117 let mut usage = ConstructUsage::default();
118 visit_node(module, &mut usage);
119 usage
120}
121
122fn visit_node(node: &AIRNode, usage: &mut ConstructUsage) {
123 match &node.kind {
124 NodeKind::EnumDecl { variants, .. } => {
126 usage.has_enum_decls = true;
127 for v in variants {
128 visit_node(v, usage);
129 }
130 }
131 NodeKind::TraitDecl { methods, .. } => {
132 usage.has_traits = true;
133 for m in methods {
134 visit_node(m, usage);
135 }
136 }
137 NodeKind::ImplBlock { methods, .. } => {
138 usage.has_traits = true;
139 for m in methods {
140 visit_node(m, usage);
141 }
142 }
143 NodeKind::EffectDecl { operations, .. } => {
144 usage.has_effects = true;
145 for op in operations {
146 visit_node(op, usage);
147 }
148 }
149
150 NodeKind::Match { scrutinee, arms } => {
152 usage.has_match = true;
153 visit_node(scrutinee, usage);
154 for arm in arms {
155 visit_node(arm, usage);
156 }
157 }
158
159 NodeKind::Move { expr } | NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } => {
161 usage.has_ownership = true;
162 visit_node(expr, usage);
163 }
164
165 NodeKind::EffectOp { .. } => {
167 usage.has_effects = true;
168 }
169 NodeKind::HandlingBlock { body, .. } => {
170 usage.has_effects = true;
171 visit_node(body, usage);
172 }
173
174 NodeKind::Interpolation { .. } => {
176 usage.has_interpolation = true;
177 }
178
179 NodeKind::Module { imports, items, .. } => {
181 for i in imports {
182 visit_node(i, usage);
183 }
184 for i in items {
185 visit_node(i, usage);
186 }
187 }
188 NodeKind::FnDecl {
189 params,
190 return_type,
191 body,
192 ..
193 } => {
194 for p in params {
195 visit_node(p, usage);
196 }
197 if let Some(rt) = return_type {
198 visit_node(rt, usage);
199 }
200 visit_node(body, usage);
201 }
202 NodeKind::ClassDecl { methods, .. } => {
203 for m in methods {
204 visit_node(m, usage);
205 }
206 }
207 NodeKind::Block { stmts, tail } => {
208 for s in stmts {
209 visit_node(s, usage);
210 }
211 if let Some(t) = tail {
212 visit_node(t, usage);
213 }
214 }
215 NodeKind::If {
216 condition,
217 then_block,
218 else_block,
219 ..
220 } => {
221 visit_node(condition, usage);
222 visit_node(then_block, usage);
223 if let Some(e) = else_block {
224 visit_node(e, usage);
225 }
226 }
227 NodeKind::For {
228 pattern,
229 iterable,
230 body,
231 } => {
232 visit_node(pattern, usage);
233 visit_node(iterable, usage);
234 visit_node(body, usage);
235 }
236 NodeKind::While { condition, body } => {
237 visit_node(condition, usage);
238 visit_node(body, usage);
239 }
240 NodeKind::Loop { body } => visit_node(body, usage),
241 NodeKind::LetBinding {
242 pattern, value, ty, ..
243 } => {
244 visit_node(pattern, usage);
245 visit_node(value, usage);
246 if let Some(t) = ty {
247 visit_node(t, usage);
248 }
249 }
250 NodeKind::BinaryOp { left, right, .. } => {
251 visit_node(left, usage);
252 visit_node(right, usage);
253 }
254 NodeKind::UnaryOp { operand, .. } => visit_node(operand, usage),
255 NodeKind::Call { callee, args, .. } => {
256 visit_node(callee, usage);
257 for a in args {
258 visit_node(&a.value, usage);
259 }
260 }
261 NodeKind::MethodCall { receiver, args, .. } => {
262 visit_node(receiver, usage);
263 for a in args {
264 visit_node(&a.value, usage);
265 }
266 }
267 NodeKind::Lambda { params, body } => {
268 for p in params {
269 visit_node(p, usage);
270 }
271 visit_node(body, usage);
272 }
273 NodeKind::Return { value } | NodeKind::Break { value } => {
274 if let Some(v) = value {
275 visit_node(v, usage);
276 }
277 }
278 NodeKind::MatchArm {
279 pattern,
280 guard,
281 body,
282 } => {
283 visit_node(pattern, usage);
284 if let Some(g) = guard {
285 visit_node(g, usage);
286 }
287 visit_node(body, usage);
288 }
289 NodeKind::Assign { target, value, .. } => {
290 visit_node(target, usage);
291 visit_node(value, usage);
292 }
293 NodeKind::FieldAccess { object, .. } => visit_node(object, usage),
294 NodeKind::Index { object, index } => {
295 visit_node(object, usage);
296 visit_node(index, usage);
297 }
298 NodeKind::Pipe { left, right } | NodeKind::Compose { left, right } => {
299 visit_node(left, usage);
300 visit_node(right, usage);
301 }
302 NodeKind::Await { expr } | NodeKind::Propagate { expr } => visit_node(expr, usage),
303 NodeKind::Guard {
304 let_pattern,
305 condition,
306 else_block,
307 } => {
308 if let Some(pat) = let_pattern {
309 visit_node(pat, usage);
310 }
311 visit_node(condition, usage);
312 visit_node(else_block, usage);
313 }
314 NodeKind::Param {
315 pattern,
316 ty,
317 default,
318 } => {
319 visit_node(pattern, usage);
320 if let Some(t) = ty {
321 visit_node(t, usage);
322 }
323 if let Some(d) = default {
324 visit_node(d, usage);
325 }
326 }
327
328 NodeKind::Literal { .. }
330 | NodeKind::Identifier { .. }
331 | NodeKind::Continue
332 | NodeKind::Placeholder
333 | NodeKind::Unreachable
334 | NodeKind::WildcardPat
335 | NodeKind::BindPat { .. }
336 | NodeKind::LiteralPat { .. }
337 | NodeKind::RestPat
338 | NodeKind::TypeSelf
339 | NodeKind::Error
340 | NodeKind::ImportDecl { .. }
341 | NodeKind::EffectRef { .. } => {}
342
343 NodeKind::ListLiteral { elems }
345 | NodeKind::SetLiteral { elems }
346 | NodeKind::TupleLiteral { elems } => {
347 for e in elems {
348 visit_node(e, usage);
349 }
350 }
351 NodeKind::MapLiteral { entries } => {
352 for e in entries {
353 visit_node(&e.key, usage);
354 visit_node(&e.value, usage);
355 }
356 }
357
358 NodeKind::RecordDecl { .. } => {}
360 NodeKind::EnumVariant { .. } => {}
361 NodeKind::RecordConstruct { fields, spread, .. } => {
362 for f in fields {
363 if let Some(v) = &f.value {
364 visit_node(v, usage);
365 }
366 }
367 if let Some(s) = spread {
368 visit_node(s, usage);
369 }
370 }
371 NodeKind::Range { lo, hi, .. } => {
372 visit_node(lo, usage);
373 visit_node(hi, usage);
374 }
375 NodeKind::ResultConstruct { value: Some(v), .. } => {
376 visit_node(v, usage);
377 }
378 NodeKind::TypeNamed { args, .. } => {
379 for a in args {
380 visit_node(a, usage);
381 }
382 }
383 NodeKind::TypeTuple { elems } => {
384 for e in elems {
385 visit_node(e, usage);
386 }
387 }
388 NodeKind::TypeFunction { params, ret, .. } => {
389 for p in params {
390 visit_node(p, usage);
391 }
392 visit_node(ret, usage);
393 }
394 NodeKind::TypeOptional { inner } => visit_node(inner, usage),
395 NodeKind::TypeAlias { ty, .. } => visit_node(ty, usage),
396 NodeKind::ConstDecl { ty, value, .. } => {
397 visit_node(ty, usage);
398 visit_node(value, usage);
399 }
400 NodeKind::ModuleHandle { handler, .. } => visit_node(handler, usage),
401 NodeKind::PropertyTest { body, .. } => visit_node(body, usage),
402 NodeKind::ConstructorPat { fields, .. } => {
403 for f in fields {
404 visit_node(f, usage);
405 }
406 }
407 NodeKind::RecordPat { fields, .. } => {
408 for f in fields {
409 if let Some(p) = &f.pattern {
410 visit_node(p, usage);
411 }
412 }
413 }
414 NodeKind::TuplePat { elems } => {
415 for e in elems {
416 visit_node(e, usage);
417 }
418 }
419 NodeKind::ListPat { elems, rest } => {
420 for e in elems {
421 visit_node(e, usage);
422 }
423 if let Some(r) = rest {
424 visit_node(r, usage);
425 }
426 }
427 NodeKind::OrPat { alternatives } => {
428 for a in alternatives {
429 visit_node(a, usage);
430 }
431 }
432 NodeKind::GuardPat { pattern, guard } => {
433 visit_node(pattern, usage);
434 visit_node(guard, usage);
435 }
436 NodeKind::RangePat { lo, hi, .. } => {
437 visit_node(lo, usage);
438 visit_node(hi, usage);
439 }
440
441 _ => {}
443 }
444}
445
446#[cfg(test)]
449mod tests {
450 use super::*;
451 use bock_air::node::{AIRNode, AirHandlerPair, NodeKind};
452 use bock_ast::{Ident, TypePath, Visibility};
453 use bock_errors::{FileId, Span};
454
455 fn span() -> Span {
456 Span {
457 file: FileId(0),
458 start: 0,
459 end: 0,
460 }
461 }
462
463 fn ident(name: &str) -> Ident {
464 Ident {
465 name: name.into(),
466 span: span(),
467 }
468 }
469
470 fn node(id: u32, kind: NodeKind) -> AIRNode {
471 AIRNode::new(id, span(), kind)
472 }
473
474 fn empty_module() -> AIRModule {
475 node(
476 0,
477 NodeKind::Module {
478 path: None,
479 annotations: vec![],
480 imports: vec![],
481 items: vec![],
482 },
483 )
484 }
485
486 #[test]
487 fn empty_module_has_no_gaps() {
488 let module = empty_module();
489 let gaps = detect_gaps(&module, &TargetProfile::javascript());
490 assert!(gaps.is_empty());
491 }
492
493 #[test]
494 fn enum_decl_detected_as_gap_for_js() {
495 let module = node(
496 0,
497 NodeKind::Module {
498 path: None,
499 annotations: vec![],
500 imports: vec![],
501 items: vec![node(
502 1,
503 NodeKind::EnumDecl {
504 annotations: vec![],
505 visibility: Visibility::Public,
506 name: ident("Color"),
507 generic_params: vec![],
508 variants: vec![],
509 },
510 )],
511 },
512 );
513 let gaps = detect_gaps(&module, &TargetProfile::javascript());
514 assert!(gaps.iter().any(|g| g.construct == "algebraic_types"));
515 }
516
517 #[test]
518 fn enum_decl_no_gap_for_rust() {
519 let module = node(
520 0,
521 NodeKind::Module {
522 path: None,
523 annotations: vec![],
524 imports: vec![],
525 items: vec![node(
526 1,
527 NodeKind::EnumDecl {
528 annotations: vec![],
529 visibility: Visibility::Public,
530 name: ident("Color"),
531 generic_params: vec![],
532 variants: vec![],
533 },
534 )],
535 },
536 );
537 let gaps = detect_gaps(&module, &TargetProfile::rust());
538 assert!(!gaps.iter().any(|g| g.construct == "algebraic_types"));
539 }
540
541 #[test]
542 fn match_expr_gap_for_go() {
543 let module = node(
544 0,
545 NodeKind::Module {
546 path: None,
547 annotations: vec![],
548 imports: vec![],
549 items: vec![node(
550 1,
551 NodeKind::Match {
552 scrutinee: Box::new(node(2, NodeKind::Identifier { name: ident("x") })),
553 arms: vec![],
554 },
555 )],
556 },
557 );
558 let gaps = detect_gaps(&module, &TargetProfile::go());
559 let pm_gap = gaps
560 .iter()
561 .find(|g| g.construct == "pattern_matching")
562 .unwrap();
563 assert_eq!(pm_gap.target_support, Support::None);
564 assert_eq!(pm_gap.synthesis_strategy, "if/else chains");
565 }
566
567 #[test]
568 fn match_expr_no_gap_for_rust() {
569 let module = node(
570 0,
571 NodeKind::Module {
572 path: None,
573 annotations: vec![],
574 imports: vec![],
575 items: vec![node(
576 1,
577 NodeKind::Match {
578 scrutinee: Box::new(node(2, NodeKind::Identifier { name: ident("x") })),
579 arms: vec![],
580 },
581 )],
582 },
583 );
584 let gaps = detect_gaps(&module, &TargetProfile::rust());
585 assert!(!gaps.iter().any(|g| g.construct == "pattern_matching"));
586 }
587
588 #[test]
589 fn ownership_gap_for_gc_targets() {
590 let module = node(
591 0,
592 NodeKind::Module {
593 path: None,
594 annotations: vec![],
595 imports: vec![],
596 items: vec![node(
597 1,
598 NodeKind::Move {
599 expr: Box::new(node(2, NodeKind::Identifier { name: ident("x") })),
600 },
601 )],
602 },
603 );
604 let gaps = detect_gaps(&module, &TargetProfile::javascript());
605 assert!(gaps.iter().any(|g| g.construct == "ownership"));
606 }
607
608 #[test]
609 fn ownership_no_gap_for_rust() {
610 let module = node(
611 0,
612 NodeKind::Module {
613 path: None,
614 annotations: vec![],
615 imports: vec![],
616 items: vec![node(
617 1,
618 NodeKind::Move {
619 expr: Box::new(node(2, NodeKind::Identifier { name: ident("x") })),
620 },
621 )],
622 },
623 );
624 let gaps = detect_gaps(&module, &TargetProfile::rust());
625 assert!(!gaps.iter().any(|g| g.construct == "ownership"));
626 }
627
628 #[test]
629 fn effects_always_produce_gap() {
630 let tp = TypePath {
631 segments: vec![ident("Log")],
632 span: span(),
633 };
634 let module = node(
635 0,
636 NodeKind::Module {
637 path: None,
638 annotations: vec![],
639 imports: vec![],
640 items: vec![node(
641 1,
642 NodeKind::HandlingBlock {
643 handlers: vec![AirHandlerPair {
644 effect: tp,
645 handler: Box::new(node(2, NodeKind::Identifier { name: ident("h") })),
646 }],
647 body: Box::new(node(
648 3,
649 NodeKind::Block {
650 stmts: vec![],
651 tail: None,
652 },
653 )),
654 },
655 )],
656 },
657 );
658 let gaps = detect_gaps(&module, &TargetProfile::rust());
660 assert!(gaps.iter().any(|g| g.construct == "effects"));
661 }
662
663 #[test]
664 fn interpolation_gap_for_rust() {
665 let module = node(
666 0,
667 NodeKind::Module {
668 path: None,
669 annotations: vec![],
670 imports: vec![],
671 items: vec![node(
672 1,
673 NodeKind::Interpolation {
674 parts: vec![bock_air::node::AirInterpolationPart::Literal(
675 "hello".into(),
676 )],
677 },
678 )],
679 },
680 );
681 let gaps = detect_gaps(&module, &TargetProfile::rust());
682 assert!(gaps.iter().any(|g| g.construct == "string_interpolation"));
683 }
684
685 #[test]
686 fn interpolation_no_gap_for_js() {
687 let module = node(
688 0,
689 NodeKind::Module {
690 path: None,
691 annotations: vec![],
692 imports: vec![],
693 items: vec![node(
694 1,
695 NodeKind::Interpolation {
696 parts: vec![bock_air::node::AirInterpolationPart::Literal(
697 "hello".into(),
698 )],
699 },
700 )],
701 },
702 );
703 let gaps = detect_gaps(&module, &TargetProfile::javascript());
704 assert!(!gaps.iter().any(|g| g.construct == "string_interpolation"));
705 }
706
707 #[test]
708 fn multiple_gaps_detected() {
709 let tp = TypePath {
710 segments: vec![ident("Log")],
711 span: span(),
712 };
713 let module = node(
714 0,
715 NodeKind::Module {
716 path: None,
717 annotations: vec![],
718 imports: vec![],
719 items: vec![
720 node(
721 1,
722 NodeKind::EnumDecl {
723 annotations: vec![],
724 visibility: Visibility::Public,
725 name: ident("Color"),
726 generic_params: vec![],
727 variants: vec![],
728 },
729 ),
730 node(
731 2,
732 NodeKind::Match {
733 scrutinee: Box::new(node(3, NodeKind::Identifier { name: ident("x") })),
734 arms: vec![],
735 },
736 ),
737 node(
738 4,
739 NodeKind::EffectOp {
740 effect: tp,
741 operation: ident("log"),
742 args: vec![],
743 },
744 ),
745 ],
746 },
747 );
748 let gaps = detect_gaps(&module, &TargetProfile::go());
749 assert!(gaps.iter().any(|g| g.construct == "algebraic_types"));
750 assert!(gaps.iter().any(|g| g.construct == "pattern_matching"));
751 assert!(gaps.iter().any(|g| g.construct == "effects"));
752 }
753}