Skip to main content

bock_codegen/
gaps.rs

1//! Capability gap detection — identifies mismatches between AIR constructs and target support.
2
3use bock_air::node::{AIRNode, NodeKind};
4use bock_types::AIRModule;
5
6use crate::profile::{Support, TargetProfile};
7
8/// A mismatch between an AIR construct and the target's support level.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct CapabilityGap {
11    /// The construct that has a gap (e.g., `"algebraic_types"`).
12    pub construct: String,
13    /// The target's support level for this construct.
14    pub target_support: Support,
15    /// The suggested synthesis strategy (e.g., `"Tagged objects + switch"`).
16    pub synthesis_strategy: String,
17}
18
19/// Detects capability gaps between AIR constructs used in a module and the
20/// target's capabilities.
21///
22/// Walks the AIR module to find which language constructs are actually used,
23/// then checks each against the target profile's capability matrix. Only
24/// constructs that are *used* and *not natively supported* produce gaps.
25#[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// ─── Construct usage collector ───────────────────────────────────────────────
103
104/// Tracks which AIR constructs are present in a module.
105#[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
115/// Walks the AIR tree and records which construct categories are used.
116fn 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        // Declarations
125        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        // Control flow
151        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        // Ownership
160        NodeKind::Move { expr } | NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } => {
161            usage.has_ownership = true;
162            visit_node(expr, usage);
163        }
164
165        // Effects
166        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        // String interpolation
175        NodeKind::Interpolation { .. } => {
176            usage.has_interpolation = true;
177        }
178
179        // Recurse into children for compound nodes
180        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        // Leaf nodes — no children to visit
329        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        // Collection literals
344        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        // Remaining composite nodes
359        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        // Catch-all for future NodeKind variants (non_exhaustive)
442        _ => {}
443    }
444}
445
446// ─── Tests ───────────────────────────────────────────────────────────────────
447
448#[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        // Effects produce a gap for all targets (even Rust)
659        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}