Skip to main content

presolve_compiler/
binding_table.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use crate::compilation_unit::CompilationUnit;
5use crate::module_graph::{ModuleEdgeKind, ModuleGraph, ModuleTarget};
6use crate::semantic_package::{
7    SemanticPackageKind, SemanticPackageOpaqueTerminal, SemanticPackagePureOperation,
8    SemanticPackageResolutionTable, SemanticPackageResourceEndpoint, SemanticPackageRouteLoader,
9    SemanticPackageServerAction,
10};
11use crate::symbol_table::{ModuleSymbol, SymbolKind, SymbolTable};
12
13/// Resolved local exports and relative-module imports.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BindingTable {
16    pub modules: Vec<ModuleBindingTable>,
17    pub diagnostics: Vec<BindingDiagnostic>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ModuleBindingTable {
22    pub path: PathBuf,
23    pub exports: BTreeMap<String, ExportBinding>,
24    pub imports: BTreeMap<String, ImportBinding>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ExportBinding {
29    pub exported_name: String,
30    pub local_name: String,
31    pub symbol: ModuleSymbol,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ImportBinding {
36    pub local_name: String,
37    pub imported_name: String,
38    pub source_module: PathBuf,
39    pub target: ImportBindingTarget,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ImportBindingTarget {
44    Symbol(ModuleSymbol),
45    Namespace {
46        module: PathBuf,
47        exports: BTreeMap<String, ModuleSymbol>,
48    },
49    SemanticPackage {
50        package: String,
51        version: String,
52        integrity: String,
53        export: String,
54        kind: SemanticPackageKind,
55        type_signature: String,
56        runtime_module: String,
57        resume_policy: String,
58        pure_operation: Option<SemanticPackagePureOperation>,
59        resource_endpoint: Option<SemanticPackageResourceEndpoint>,
60        route_loader: Option<SemanticPackageRouteLoader>,
61        server_action: Option<SemanticPackageServerAction>,
62        opaque_terminal: Option<SemanticPackageOpaqueTerminal>,
63    },
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct BindingDiagnostic {
68    pub code: String,
69    pub module: PathBuf,
70    pub name: String,
71    pub message: String,
72}
73
74impl BindingTable {
75    #[must_use]
76    pub fn module(&self, path: impl AsRef<Path>) -> Option<&ModuleBindingTable> {
77        self.modules
78            .iter()
79            .find(|module| module.path == path.as_ref())
80    }
81
82    #[must_use]
83    pub fn resolve_import(
84        &self,
85        module_path: impl AsRef<Path>,
86        local_name: &str,
87    ) -> Option<&ImportBinding> {
88        self.module(module_path)
89            .and_then(|module| module.imports.get(local_name))
90    }
91}
92
93#[must_use]
94pub fn build_binding_table(
95    unit: &CompilationUnit,
96    symbols: &SymbolTable,
97    modules: &ModuleGraph,
98) -> BindingTable {
99    build_binding_table_with_packages(
100        unit,
101        symbols,
102        modules,
103        &SemanticPackageResolutionTable::default(),
104    )
105}
106
107#[must_use]
108pub fn build_binding_table_with_packages(
109    unit: &CompilationUnit,
110    symbols: &SymbolTable,
111    modules: &ModuleGraph,
112    packages: &SemanticPackageResolutionTable,
113) -> BindingTable {
114    let mut tables = unit
115        .files()
116        .iter()
117        .map(|file| ModuleBindingTable {
118            path: file.path.clone(),
119            exports: BTreeMap::new(),
120            imports: BTreeMap::new(),
121        })
122        .collect::<Vec<_>>();
123    let mut diagnostics = Vec::new();
124
125    collect_local_exports(unit, symbols, &mut tables, &mut diagnostics);
126    resolve_relative_reexports(unit, modules, &mut tables, &mut diagnostics);
127    collect_identity_collisions(symbols, &mut diagnostics);
128
129    let exports_by_path = tables
130        .iter()
131        .map(|table| (table.path.clone(), table.exports.clone()))
132        .collect::<BTreeMap<_, _>>();
133
134    resolve_relative_imports(
135        unit,
136        modules,
137        &exports_by_path,
138        &mut tables,
139        &mut diagnostics,
140        packages,
141    );
142
143    BindingTable {
144        modules: tables,
145        diagnostics,
146    }
147}
148
149fn collect_local_exports(
150    unit: &CompilationUnit,
151    symbols: &SymbolTable,
152    tables: &mut [ModuleBindingTable],
153    diagnostics: &mut Vec<BindingDiagnostic>,
154) {
155    for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
156        let Some(module_symbols) = symbols.module(&file.path) else {
157            continue;
158        };
159
160        for export in file.exports.iter().filter(|export| export.source.is_none()) {
161            for specifier in &export.specifiers {
162                let Some(local_name) = &specifier.local else {
163                    diagnostics.push(BindingDiagnostic {
164                        code: "PSBIND1001".to_string(),
165                        module: file.path.clone(),
166                        name: specifier.exported.clone(),
167                        message: format!(
168                            "export `{}` has no local declaration to bind",
169                            specifier.exported
170                        ),
171                    });
172                    continue;
173                };
174                let Some(symbol) = module_symbols.symbols.get(local_name) else {
175                    diagnostics.push(BindingDiagnostic {
176                        code: "PSBIND1001".to_string(),
177                        module: file.path.clone(),
178                        name: specifier.exported.clone(),
179                        message: format!(
180                            "export `{}` refers to unsupported or missing local symbol `{local_name}`",
181                            specifier.exported
182                        ),
183                    });
184                    continue;
185                };
186
187                insert_export(
188                    table,
189                    diagnostics,
190                    ExportBinding {
191                        exported_name: specifier.exported.clone(),
192                        local_name: local_name.clone(),
193                        symbol: symbol.clone(),
194                    },
195                );
196            }
197        }
198    }
199}
200
201fn resolve_relative_imports(
202    unit: &CompilationUnit,
203    modules: &ModuleGraph,
204    exports_by_path: &BTreeMap<PathBuf, BTreeMap<String, ExportBinding>>,
205    tables: &mut [ModuleBindingTable],
206    diagnostics: &mut Vec<BindingDiagnostic>,
207    packages: &SemanticPackageResolutionTable,
208) {
209    for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
210        for import in &file.imports {
211            let target = module_target(modules, &file.path, import.span, ModuleEdgeKind::Import);
212            if matches!(target, ModuleTarget::External) {
213                resolve_semantic_package_import(import, packages, table, diagnostics, &file.path);
214                continue;
215            }
216            let ModuleTarget::Resolved(target_path) = target else {
217                if matches!(target, ModuleTarget::Unresolved) {
218                    diagnostics.push(BindingDiagnostic {
219                        code: "PSBIND1002".to_string(),
220                        module: file.path.clone(),
221                        name: import.source.clone(),
222                        message: format!(
223                            "relative import `{}` does not resolve to a module",
224                            import.source
225                        ),
226                    });
227                }
228                continue;
229            };
230            let target_exports = exports_by_path
231                .get(&target_path)
232                .cloned()
233                .unwrap_or_default();
234
235            for specifier in &import.specifiers {
236                let binding = resolve_import_binding(specifier, &target_path, &target_exports);
237
238                let Some(binding) = binding else {
239                    diagnostics.push(BindingDiagnostic {
240                        code: "PSBIND1003".to_string(),
241                        module: file.path.clone(),
242                        name: specifier.local.clone(),
243                        message: format!(
244                            "import `{}` is not exported by `{}`",
245                            specifier.imported,
246                            target_path.display()
247                        ),
248                    });
249                    continue;
250                };
251
252                insert_import(table, diagnostics, binding);
253            }
254        }
255    }
256}
257
258fn resolve_semantic_package_import(
259    import: &presolve_parser::ParsedImport,
260    packages: &SemanticPackageResolutionTable,
261    table: &mut ModuleBindingTable,
262    diagnostics: &mut Vec<BindingDiagnostic>,
263    module: &Path,
264) {
265    // `@presolve/core` is the public declaration-only authoring vocabulary. Its
266    // imports select compiler intrinsics that are already recognized by the
267    // parser/component model; unlike third-party packages, they have no
268    // runtime module, integrity coordinate, or semantic capability to bind.
269    if import.source == "@presolve/core" {
270        return;
271    }
272    let Some(contract) = packages.contract(&import.source) else {
273        diagnostics.push(BindingDiagnostic {
274            code: "PSBIND1009".into(),
275            module: module.to_path_buf(),
276            name: import.source.clone(),
277            message: format!(
278                "external import `{}` has no semantic package contract",
279                import.source
280            ),
281        });
282        return;
283    };
284    for specifier in &import.specifiers {
285        let Some(export) = contract.exports.get(&specifier.imported) else {
286            diagnostics.push(BindingDiagnostic {
287                code: "PSBIND1010".into(),
288                module: module.to_path_buf(),
289                name: specifier.imported.clone(),
290                message: format!(
291                    "semantic package `{}` does not declare export `{}`",
292                    import.source, specifier.imported
293                ),
294            });
295            continue;
296        };
297        insert_import(
298            table,
299            diagnostics,
300            ImportBinding {
301                local_name: specifier.local.clone(),
302                imported_name: specifier.imported.clone(),
303                source_module: PathBuf::from(&import.source),
304                target: ImportBindingTarget::SemanticPackage {
305                    package: contract.package.clone(),
306                    version: contract.version.clone(),
307                    integrity: contract.integrity.clone(),
308                    export: specifier.imported.clone(),
309                    kind: export.kind.clone(),
310                    type_signature: export.type_signature.clone(),
311                    runtime_module: export.runtime_module.clone(),
312                    resume_policy: export.resume_policy.clone(),
313                    pure_operation: export.pure_operation,
314                    resource_endpoint: export.resource_endpoint.clone(),
315                    route_loader: export.route_loader.clone(),
316                    server_action: export.server_action.clone(),
317                    opaque_terminal: export.opaque_terminal.clone(),
318                },
319            },
320        );
321    }
322}
323
324fn resolve_relative_reexports(
325    unit: &CompilationUnit,
326    modules: &ModuleGraph,
327    tables: &mut [ModuleBindingTable],
328    diagnostics: &mut Vec<BindingDiagnostic>,
329) {
330    for _ in 0..unit.files().len() {
331        let exports_by_path = tables
332            .iter()
333            .map(|table| (table.path.clone(), table.exports.clone()))
334            .collect::<BTreeMap<_, _>>();
335        let mut changed = false;
336
337        for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
338            for export in file.exports.iter().filter(|export| export.source.is_some()) {
339                let edge_kind = match export.kind {
340                    presolve_parser::ParsedExportKind::Named => ModuleEdgeKind::NamedReExport,
341                    presolve_parser::ParsedExportKind::All => ModuleEdgeKind::ExportAll,
342                    presolve_parser::ParsedExportKind::Default => continue,
343                };
344                let target = module_target(modules, &file.path, export.span, edge_kind);
345                let ModuleTarget::Resolved(target_path) = target else {
346                    if matches!(target, ModuleTarget::Unresolved) {
347                        push_diagnostic_once(
348                            diagnostics,
349                            BindingDiagnostic {
350                                code: "PSBIND1006".to_string(),
351                                module: file.path.clone(),
352                                name: export.source.clone().unwrap_or_default(),
353                                message: "relative re-export does not resolve to a module"
354                                    .to_string(),
355                            },
356                        );
357                    }
358                    continue;
359                };
360                let target_exports = exports_by_path
361                    .get(&target_path)
362                    .cloned()
363                    .unwrap_or_default();
364
365                match export.kind {
366                    presolve_parser::ParsedExportKind::Named => {
367                        for specifier in &export.specifiers {
368                            let Some(local_name) = &specifier.local else {
369                                continue;
370                            };
371                            let Some(target) = target_exports.get(local_name) else {
372                                continue;
373                            };
374                            changed |= insert_reexport(
375                                table,
376                                ExportBinding {
377                                    exported_name: specifier.exported.clone(),
378                                    local_name: local_name.clone(),
379                                    symbol: target.symbol.clone(),
380                                },
381                            );
382                        }
383                    }
384                    presolve_parser::ParsedExportKind::All if export.specifiers.is_empty() => {
385                        for (name, target) in target_exports {
386                            if name != "default" {
387                                changed |= insert_reexport(
388                                    table,
389                                    ExportBinding {
390                                        exported_name: name.clone(),
391                                        local_name: target.local_name,
392                                        symbol: target.symbol,
393                                    },
394                                );
395                            }
396                        }
397                    }
398                    presolve_parser::ParsedExportKind::All => {
399                        push_diagnostic_once(
400                            diagnostics,
401                            BindingDiagnostic {
402                                code: "PSBIND1007".to_string(),
403                                module: file.path.clone(),
404                                name: export.source.clone().unwrap_or_default(),
405                                message: "namespace re-export bindings are not supported yet"
406                                    .to_string(),
407                            },
408                        );
409                    }
410                    presolve_parser::ParsedExportKind::Default => {}
411                }
412            }
413        }
414
415        if !changed {
416            break;
417        }
418    }
419}
420
421fn insert_reexport(table: &mut ModuleBindingTable, binding: ExportBinding) -> bool {
422    if table.exports.contains_key(&binding.exported_name) {
423        return false;
424    }
425
426    table.exports.insert(binding.exported_name.clone(), binding);
427    true
428}
429
430fn collect_identity_collisions(symbols: &SymbolTable, diagnostics: &mut Vec<BindingDiagnostic>) {
431    let mut paths_by_id = BTreeMap::<String, Vec<PathBuf>>::new();
432
433    for module in &symbols.modules {
434        for symbol in module
435            .symbols
436            .values()
437            .filter(|symbol| symbol.kind == SymbolKind::Component)
438        {
439            paths_by_id
440                .entry(symbol.id.as_str().to_string())
441                .or_default()
442                .push(module.path.clone());
443        }
444    }
445
446    for paths in paths_by_id.values_mut() {
447        paths.sort();
448        paths.dedup();
449    }
450
451    for (id, paths) in paths_by_id.into_iter().filter(|(_, paths)| paths.len() > 1) {
452        diagnostics.push(BindingDiagnostic {
453            code: "PSBIND1008".to_string(),
454            module: paths[0].clone(),
455            name: id.clone(),
456            message: format!(
457                "semantic identity `{id}` collides across modules: {}",
458                paths
459                    .iter()
460                    .map(|path| path.display().to_string())
461                    .collect::<Vec<_>>()
462                    .join(", ")
463            ),
464        });
465    }
466}
467
468fn push_diagnostic_once(diagnostics: &mut Vec<BindingDiagnostic>, diagnostic: BindingDiagnostic) {
469    if diagnostics.iter().any(|existing| existing == &diagnostic) {
470        return;
471    }
472
473    diagnostics.push(diagnostic);
474}
475
476fn resolve_import_binding(
477    specifier: &presolve_parser::ParsedImportSpecifier,
478    target_path: &Path,
479    target_exports: &BTreeMap<String, ExportBinding>,
480) -> Option<ImportBinding> {
481    if specifier.imported == "*" {
482        return Some(ImportBinding {
483            local_name: specifier.local.clone(),
484            imported_name: specifier.imported.clone(),
485            source_module: target_path.to_path_buf(),
486            target: ImportBindingTarget::Namespace {
487                module: target_path.to_path_buf(),
488                exports: target_exports
489                    .iter()
490                    .map(|(name, export)| (name.clone(), export.symbol.clone()))
491                    .collect(),
492            },
493        });
494    }
495
496    target_exports
497        .get(&specifier.imported)
498        .map(|export| ImportBinding {
499            local_name: specifier.local.clone(),
500            imported_name: specifier.imported.clone(),
501            source_module: target_path.to_path_buf(),
502            target: ImportBindingTarget::Symbol(export.symbol.clone()),
503        })
504}
505
506fn module_target(
507    modules: &ModuleGraph,
508    source: &Path,
509    span: presolve_parser::SourceSpan,
510    kind: ModuleEdgeKind,
511) -> ModuleTarget {
512    modules
513        .edges
514        .iter()
515        .find(|edge| edge.kind == kind && edge.source == source && edge.span == span)
516        .map_or(ModuleTarget::Unresolved, |edge| edge.target.clone())
517}
518
519fn insert_export(
520    table: &mut ModuleBindingTable,
521    diagnostics: &mut Vec<BindingDiagnostic>,
522    binding: ExportBinding,
523) {
524    if table.exports.contains_key(&binding.exported_name) {
525        diagnostics.push(BindingDiagnostic {
526            code: "PSBIND1004".to_string(),
527            module: table.path.clone(),
528            name: binding.exported_name.clone(),
529            message: format!("duplicate export binding `{}`", binding.exported_name),
530        });
531        return;
532    }
533
534    table.exports.insert(binding.exported_name.clone(), binding);
535}
536
537fn insert_import(
538    table: &mut ModuleBindingTable,
539    diagnostics: &mut Vec<BindingDiagnostic>,
540    binding: ImportBinding,
541) {
542    if table.imports.contains_key(&binding.local_name) {
543        diagnostics.push(BindingDiagnostic {
544            code: "PSBIND1005".to_string(),
545            module: table.path.clone(),
546            name: binding.local_name.clone(),
547            message: format!("duplicate import binding `{}`", binding.local_name),
548        });
549        return;
550    }
551
552    table.imports.insert(binding.local_name.clone(), binding);
553}
554
555#[cfg(test)]
556mod tests {
557    use super::{build_binding_table, build_binding_table_with_packages, ImportBindingTarget};
558    use crate::semantic_package::{
559        parse_semantic_package_contract, SemanticPackageResolutionTable,
560    };
561    use crate::{build_module_graph, build_symbol_table, CompilationUnit, SemanticId};
562
563    #[test]
564    fn resolves_relative_named_default_and_namespace_imports() {
565        let unit = CompilationUnit::parse_sources([
566            (
567                "src/app/App.tsx",
568                r#"
569import { Widget as CounterWidget } from "../ui/Counter";
570import StatusCard from "../ui/Status";
571import * as counter from "../ui/Counter";
572import { packageValue } from "presolve-runtime";
573"#,
574            ),
575            (
576                "src/ui/Counter.tsx",
577                r#"
578@component("x-counter")
579export class Counter extends Component {
580  render() {
581    return <div>Counter</div>;
582  }
583}
584
585export { Counter as Widget };
586"#,
587            ),
588            (
589                "src/ui/Status.tsx",
590                r#"
591@component("x-status")
592export default class Status extends Component {
593  render() {
594    return <div>Status</div>;
595  }
596}
597"#,
598            ),
599        ]);
600        let symbols = build_symbol_table(&unit);
601        let modules = build_module_graph(&unit);
602        let bindings = build_binding_table(&unit, &symbols, &modules);
603        let app = bindings.module("src/app/App.tsx").expect("app module");
604
605        assert_eq!(
606            bindings
607                .diagnostics
608                .iter()
609                .map(|diagnostic| diagnostic.code.as_str())
610                .collect::<Vec<_>>(),
611            vec!["PSBIND1009"]
612        );
613        assert_eq!(app.imports.len(), 3);
614        assert_eq!(app.exports.len(), 0);
615        assert_eq!(
616            app.imports["CounterWidget"].target,
617            ImportBindingTarget::Symbol(
618                bindings
619                    .module("src/ui/Counter.tsx")
620                    .expect("counter module")
621                    .exports["Widget"]
622                    .symbol
623                    .clone()
624            )
625        );
626        assert_eq!(
627            app.imports["StatusCard"].target,
628            ImportBindingTarget::Symbol(
629                bindings
630                    .module("src/ui/Status.tsx")
631                    .expect("status module")
632                    .exports["default"]
633                    .symbol
634                    .clone()
635            )
636        );
637
638        let ImportBindingTarget::Namespace { module, exports } = &app.imports["counter"].target
639        else {
640            panic!("expected namespace binding");
641        };
642        assert_eq!(module, &std::path::PathBuf::from("src/ui/Counter.tsx"));
643        assert_eq!(
644            exports["Widget"].id,
645            SemanticId::component_in_module("src/ui/Counter.tsx", Some("x-counter"), "Counter")
646        );
647        assert!(bindings
648            .resolve_import("src/app/App.tsx", "packageValue")
649            .is_none());
650    }
651
652    #[test]
653    fn reports_unresolved_relative_and_missing_export_bindings() {
654        let unit = CompilationUnit::parse_sources([
655            (
656                "src/App.tsx",
657                r#"
658import { Missing } from "./Counter";
659import { MissingModule } from "./missing";
660"#,
661            ),
662            (
663                "src/Counter.tsx",
664                r#"
665@component("x-counter")
666export class Counter extends Component {
667  render() {
668    return <div>Counter</div>;
669  }
670}
671"#,
672            ),
673        ]);
674        let symbols = build_symbol_table(&unit);
675        let modules = build_module_graph(&unit);
676        let bindings = build_binding_table(&unit, &symbols, &modules);
677
678        let codes = bindings
679            .diagnostics
680            .iter()
681            .map(|diagnostic| diagnostic.code.as_str())
682            .collect::<Vec<_>>();
683
684        assert_eq!(codes, vec!["PSBIND1003", "PSBIND1002"]);
685    }
686
687    #[test]
688    fn resolves_named_and_export_all_reexport_chains() {
689        let unit = CompilationUnit::parse_sources([
690            (
691                "src/App.tsx",
692                r#"
693import { PrimaryButton } from "./index";
694"#,
695            ),
696            (
697                "src/base.tsx",
698                r#"
699@component("x-button")
700export class Button extends Component {
701  render() {
702    return <button>Button</button>;
703  }
704}
705"#,
706            ),
707            (
708                "src/bridge.ts",
709                r#"
710export { Button as PrimaryButton } from "./base";
711"#,
712            ),
713            (
714                "src/index.ts",
715                r#"
716export * from "./bridge";
717"#,
718            ),
719        ]);
720        let symbols = build_symbol_table(&unit);
721        let modules = build_module_graph(&unit);
722        let bindings = build_binding_table(&unit, &symbols, &modules);
723
724        assert!(bindings.diagnostics.is_empty());
725        assert_eq!(
726            bindings
727                .resolve_import("src/App.tsx", "PrimaryButton")
728                .expect("resolved re-export")
729                .target,
730            ImportBindingTarget::Symbol(
731                bindings
732                    .module("src/index.ts")
733                    .expect("index module")
734                    .exports["PrimaryButton"]
735                    .symbol
736                    .clone()
737            )
738        );
739    }
740
741    #[test]
742    fn module_qualified_component_ids_do_not_collide_across_modules() {
743        let unit = CompilationUnit::parse_sources([
744            (
745                "src/First.tsx",
746                r#"
747@component("x-duplicate")
748class First extends Component {
749  render() {
750    return <div>First</div>;
751  }
752}
753"#,
754            ),
755            (
756                "src/Second.tsx",
757                r#"
758@component("x-duplicate")
759class Second extends Component {
760  render() {
761    return <div>Second</div>;
762  }
763}
764"#,
765            ),
766        ]);
767        let symbols = build_symbol_table(&unit);
768        let modules = build_module_graph(&unit);
769        let bindings = build_binding_table(&unit, &symbols, &modules);
770
771        assert!(bindings.diagnostics.is_empty());
772        assert_ne!(
773            symbols
774                .module("src/First.tsx")
775                .expect("first module")
776                .symbols["First"]
777                .id,
778            symbols
779                .module("src/Second.tsx")
780                .expect("second module")
781                .symbols["Second"]
782                .id
783        );
784    }
785
786    #[test]
787    fn resolves_external_imports_only_through_semantic_package_contracts() {
788        let unit = CompilationUnit::parse_sources([(
789            "src/App.tsx",
790            "import { format } from \"date-kit\"; export class App {}",
791        )]);
792        let symbols = build_symbol_table(&unit);
793        let modules = build_module_graph(&unit);
794        let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#).unwrap();
795        let mut packages = SemanticPackageResolutionTable::default();
796        packages.insert("date-kit".into(), contract).unwrap();
797        let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
798        assert!(bindings.diagnostics.is_empty());
799        let ImportBindingTarget::SemanticPackage {
800            package,
801            version,
802            integrity,
803            export,
804            kind,
805            type_signature,
806            runtime_module,
807            resume_policy,
808            pure_operation: _,
809            resource_endpoint: _,
810            route_loader: _,
811            server_action: _,
812            opaque_terminal: _,
813        } = &bindings
814            .resolve_import("src/App.tsx", "format")
815            .unwrap()
816            .target
817        else {
818            panic!("expected semantic package binding");
819        };
820        assert_eq!(package, "date-kit");
821        assert_eq!(version, "1.0.0");
822        assert_eq!(
823            integrity,
824            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
825        );
826        assert_eq!(export, "format");
827        assert_eq!(kind, &crate::semantic_package::SemanticPackageKind::Pure);
828        assert_eq!(type_signature, "(Date)->string");
829        assert_eq!(runtime_module, "dist/format.js");
830        assert_eq!(resume_policy, "input_only");
831    }
832
833    #[test]
834    fn accepts_the_public_core_authoring_import_without_a_package_capability_contract() {
835        let unit = CompilationUnit::parse_sources([(
836            "src/App.tsx",
837            "import { component, state } from \"@presolve/core\"; export class App {}",
838        )]);
839        let symbols = build_symbol_table(&unit);
840        let modules = build_module_graph(&unit);
841        let bindings = build_binding_table(&unit, &symbols, &modules);
842        assert!(bindings.diagnostics.is_empty());
843        assert!(bindings.module("src/App.tsx").unwrap().imports.is_empty());
844    }
845
846    #[test]
847    fn rejects_external_imports_without_a_matching_contract_or_export() {
848        let unit = CompilationUnit::parse_sources([(
849            "src/App.tsx",
850            "import present from \"date-kit\"; import { absent } from \"date-kit\"; import { other } from \"other-kit\"; export class App {}",
851        )]);
852        let symbols = build_symbol_table(&unit);
853        let modules = build_module_graph(&unit);
854        let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"default":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/default.js","resume_policy":"input_only"}}}"#).unwrap();
855        let mut packages = SemanticPackageResolutionTable::default();
856        packages.insert("date-kit".into(), contract).unwrap();
857
858        let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
859        assert_eq!(
860            bindings
861                .diagnostics
862                .iter()
863                .map(|diagnostic| diagnostic.code.as_str())
864                .collect::<Vec<_>>(),
865            vec!["PSBIND1010", "PSBIND1009"]
866        );
867        assert!(bindings.resolve_import("src/App.tsx", "present").is_some());
868        assert!(bindings.resolve_import("src/App.tsx", "absent").is_none());
869        assert!(bindings.resolve_import("src/App.tsx", "other").is_none());
870    }
871}