Skip to main content

submilli_engine/
compile.rs

1//! End-to-end source-to-Wasm pipeline.
2//!
3//! Callers run these entry points on a thread with at least
4//! [`crate::compiler_limits::COMPILER_STACK_BYTES`] of stack.
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::time::{Duration, Instant};
8
9use crate::compile_capabilities;
10use crate::compiler_error::{CompileError, CompilerFailure, CompilerStage};
11use crate::runtime::prelude;
12use crate::stdlib::Stdlib;
13use crate::typechecker::infer::{TestModule, TestModules};
14use crate::typechecker::rules::check_script;
15use crate::typed_ast::TypedAst;
16use crate::{
17    Asi, Ast, DerivedCapability, Diagnostic, FileId, ModulePath, PackageDeclaration, Severity,
18    Sources, StmtKind, Token, TokenKind, capture, desugar, lower_patterns, runtime,
19};
20
21/// Wall-clock spent in each compile phase, filled in as the pipeline runs. A
22/// phase that never runs (e.g. `codegen` when the front-end errors) stays zero.
23/// Surfaced on [`CompiledScript`] for embedders that report compile-time
24/// performance.
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
26pub struct PhaseTimings {
27    pub lex: Duration,
28    /// Parse plus pattern-lowering — both run together in the front-end.
29    pub parse: Duration,
30    /// Type inference plus the post-inference check pass.
31    pub typecheck: Duration,
32    pub capture: Duration,
33    pub desugar: Duration,
34    pub codegen: Duration,
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub struct ScriptImports {
39    pub stdlib: BTreeSet<String>,
40    pub registry_packages: BTreeSet<String>,
41    pub mcp_servers: BTreeSet<String>,
42    /// The script imports `@tools`, the session's application tools.
43    pub tools: bool,
44}
45
46#[derive(Clone, Debug)]
47pub struct ParsedScript {
48    file: FileId,
49    source: String,
50    failure: Option<CompileError>,
51    ast: Ast,
52    diagnostics: Vec<Diagnostic>,
53    timings: PhaseTimings,
54}
55
56impl ParsedScript {
57    pub fn external_imports(&self) -> Result<ScriptImports, CompileError> {
58        self.external_imports_with_stdlib(&runtime::stdlib_package_declarations())
59    }
60
61    /// Classify imports using declarations the caller already prepared, without
62    /// constructing the standard library a second time.
63    pub fn external_imports_with_stdlib(
64        &self,
65        stdlib: &[PackageDeclaration],
66    ) -> Result<ScriptImports, CompileError> {
67        let stdlib_names: BTreeSet<&str> = stdlib
68            .iter()
69            .map(|defs| defs.package_name.as_str())
70            .collect();
71        let mut imports = ScriptImports::default();
72        for stmt_id in &self.ast.top_level {
73            let StmtKind::Import { module, .. } = &self
74                .ast
75                .try_stmt(*stmt_id)
76                .map_err(|error| {
77                    CompileError::from(
78                        error.into_compiler_failure(crate::compiler_error::CompilerStage::Parse),
79                    )
80                    .with_prior_diagnostics(&self.diagnostics)
81                })?
82                .kind
83            else {
84                continue;
85            };
86            if crate::source::is_relative_specifier(module) {
87                continue;
88            }
89            if let Some(server) = module.strip_prefix("@mcp/") {
90                imports.mcp_servers.insert(server.to_string());
91            } else if module == runtime::TOOLS_PACKAGE {
92                imports.tools = true;
93            } else if stdlib_names.contains(module.as_str())
94                || crate::stdlib::OptionalPackage::from_module_name(module).is_some()
95            {
96                // An optional package counts as stdlib whether or not the embedder
97                // enables it: a disabled one is reported as unavailable, never
98                // looked up as a registry package.
99                imports.stdlib.insert(module.clone());
100            } else {
101                imports.registry_packages.insert(module.clone());
102            }
103        }
104        Ok(imports)
105    }
106
107    pub fn has_errors(&self) -> bool {
108        self.failure.is_some() || has_errors(&self.diagnostics)
109    }
110
111    pub fn diagnostics(&self) -> &[Diagnostic] {
112        &self.diagnostics
113    }
114}
115
116pub fn parse_script(source: &str, file: FileId) -> ParsedScript {
117    let mut timings = PhaseTimings::default();
118
119    let lex_start = Instant::now();
120    let mut asi = Asi::new(source, file);
121    let mut tokens: Vec<Token> = Vec::new();
122    loop {
123        let tok = asi.next_token();
124        let is_eof = matches!(tok.kind, TokenKind::Eof);
125        tokens.push(tok);
126        if is_eof {
127            break;
128        }
129    }
130    let (mut diagnostics, mut failure) = match asi.finish() {
131        Ok(diagnostics) => (diagnostics, None),
132        Err(error) => (error.clone().into_diagnostics(file), Some(error)),
133    };
134    timings.lex = lex_start.elapsed();
135
136    let parse_start = Instant::now();
137    let (mut ast, parse_diags) = if failure.is_some() {
138        (Ast::new(), Vec::new())
139    } else {
140        match crate::parser::parse_checked(source, tokens, file) {
141            Ok(result) => result,
142            Err(mut error) => {
143                error.diagnostics.splice(0..0, diagnostics.clone());
144                let rendered = error.clone().into_diagnostics(file);
145                diagnostics.clear();
146                failure = Some(error);
147                (Ast::new(), rendered)
148            }
149        }
150    };
151    diagnostics.extend(parse_diags);
152    // must run before type-checking; the typechecker assumes patterns are already lowered
153    if failure.is_none() {
154        match lower_patterns(ast) {
155            Ok(lowered) => ast = lowered,
156            Err(fatal) => {
157                let error = CompileError {
158                    diagnostics: diagnostics.clone(),
159                    fatal: Some(fatal),
160                };
161                diagnostics = error.clone().into_diagnostics(file);
162                failure = Some(error);
163                ast = Ast::new();
164            }
165        }
166    }
167    timings.parse = parse_start.elapsed();
168
169    ParsedScript {
170        file,
171        source: source.to_owned(),
172        failure,
173        ast,
174        diagnostics,
175        timings,
176    }
177}
178
179/// Run the front-end (infer → check) and return the typed
180/// AST alongside the accumulated diagnostics and per-phase timings. Shared by
181/// [`compile_script`] and [`typecheck`]; the typed AST is only meaningful when
182/// `diags` is error-free. `file` is the [`FileId`] the caller assigned this
183/// source in its [`Sources`](crate::Sources) registry; every span is stamped
184/// with it so diagnostics resolve against that entry. `packages` join the stdlib
185/// in the set the typechecker resolves imports against (e.g. the server's
186/// `@mcp/<server>` virtual packages).
187fn front_end(
188    source: &str,
189    parsed: &ParsedScript,
190    stdlib_defs: &[PackageDeclaration],
191    packages: &[&PackageDeclaration],
192) -> Result<(TypedAst, Vec<Diagnostic>, PhaseTimings), CompileError> {
193    front_end_with_transitive(None, source, parsed, stdlib_defs, packages, &[], None)
194}
195
196fn front_end_with_transitive(
197    owning_package: Option<&str>,
198    source: &str,
199    parsed: &ParsedScript,
200    stdlib_defs: &[PackageDeclaration],
201    packages: &[&PackageDeclaration],
202    transitive: &[&PackageDeclaration],
203    test_modules: Option<TestModules<'_>>,
204) -> Result<(TypedAst, Vec<Diagnostic>, PhaseTimings), CompileError> {
205    if source != parsed.source {
206        return Err(CompilerFailure::Internal {
207            stage: CompilerStage::Infer,
208            span: None,
209            message: "parsed script does not belong to the supplied source".into(),
210        }
211        .into());
212    }
213    if let Some(error) = &parsed.failure {
214        return Err(error.clone());
215    }
216    let mut timings = parsed.timings;
217    let mut diags = parsed.diagnostics.clone();
218    let typecheck_start = Instant::now();
219    let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
220    let mut infer_refs: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
221    infer_refs.extend(host_defs.iter());
222    infer_refs.extend(stdlib_defs.iter());
223    infer_refs.extend_from_slice(packages);
224    let (ta, infer_diags) = crate::typechecker::infer::infer_with_transitive_for_owner_checked(
225        owning_package,
226        source,
227        crate::mangle::USER_PACKAGE,
228        &parsed.ast,
229        &infer_refs,
230        transitive,
231        test_modules,
232    )
233    .map_err(|mut error| {
234        error.diagnostics.splice(0..0, diags.clone());
235        error
236    })?;
237    diags.extend(infer_diags);
238    infer_refs.extend_from_slice(transitive);
239    diags.extend(
240        check_script(&ta, &infer_refs).map_err(|error| error.with_prior_diagnostics(&diags))?,
241    );
242    timings.typecheck = typecheck_start.elapsed();
243
244    Ok((ta, diags, timings))
245}
246
247#[derive(Clone, Debug)]
248pub struct PackageSourceModule<'a> {
249    pub path: ModulePath,
250    pub source: &'a str,
251}
252
253#[derive(Clone, Debug, PartialEq)]
254pub struct CompiledPackage {
255    pub wasm: Vec<u8>,
256    pub type_info: crate::TypeInfoTable,
257    pub declaration: PackageDeclaration,
258    pub required_capabilities: Vec<DerivedCapability>,
259    pub authority_map: crate::AuthorityMap,
260    pub warnings: Vec<Diagnostic>,
261    /// The non-entry modules the package's tests may import. `Some` only for
262    /// [`PackageSurface::WithTestModules`].
263    pub test_surface: Option<PackageTestSurface>,
264}
265
266/// What a compiled package's Wasm exports.
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub enum PackageSurface {
269    /// The entry module's exports: what programs and other packages link against.
270    Public,
271    /// Also every function, global and class static that a non-entry module
272    /// exports, for the package's own tests under `submilli build test`. Never
273    /// publish such a build: Wasm compiled elsewhere could import an internal
274    /// helper and skip the checks the public functions run. A public build
275    /// still exports every class's constructor and methods, internal classes'
276    /// included, so keep secrets and checks out of internal classes. Internal
277    /// functions escape in this build, so their Wasm signatures can be wider
278    /// than the public build's.
279    WithTestModules,
280}
281
282/// The exports of a package's non-entry modules, by path under `src/`, which
283/// [`compile_package_test_checked`] binds a test's relative imports to. The
284/// declaration, capabilities, docs and authority map never include them.
285#[derive(Clone, Debug, Default, PartialEq)]
286pub struct PackageTestSurface {
287    pub modules: BTreeMap<ModulePath, crate::InternalModuleExports>,
288}
289
290#[derive(Clone, Debug)]
291pub struct CompiledScript {
292    pub wasm: Vec<u8>,
293    pub type_info: crate::TypeInfoTable,
294    pub warnings: Vec<Diagnostic>,
295    pub timings: PhaseTimings,
296}
297
298/// Typecheck only — runs the front-end and stops before codegen. Returns
299/// `Ok(warnings)` when the program is error-free (warnings are non-fatal),
300/// `Err(diagnostics)` if any phase errors. Backs `submilli check`.
301pub fn typecheck_checked(source: &str, file: FileId) -> Result<Vec<Diagnostic>, CompileError> {
302    typecheck_script(source, file).map_err(|error| {
303        error.with_limit_span_cut(|span_file| (span_file == file).then_some(source))
304    })
305}
306
307fn typecheck_script(source: &str, file: FileId) -> Result<Vec<Diagnostic>, CompileError> {
308    let parsed = parse_script(source, file);
309    let stdlib_defs = runtime::stdlib_package_declarations();
310    typecheck_parsed_script(source, &parsed, &stdlib_defs)
311}
312
313/// [`typecheck_checked`] for an already parsed script, against `stdlib_defs`
314/// (see [`Stdlib::package_declarations`](crate::stdlib::Stdlib::package_declarations)).
315pub fn typecheck_parsed_checked(
316    source: &str,
317    parsed: &ParsedScript,
318    stdlib_defs: &[PackageDeclaration],
319) -> Result<Vec<Diagnostic>, CompileError> {
320    let file = parsed.file;
321    typecheck_parsed_script(source, parsed, stdlib_defs).map_err(|error| {
322        error.with_limit_span_cut(|span_file| (span_file == file).then_some(source))
323    })
324}
325
326fn typecheck_parsed_script(
327    source: &str,
328    parsed: &ParsedScript,
329    stdlib_defs: &[PackageDeclaration],
330) -> Result<Vec<Diagnostic>, CompileError> {
331    let file = parsed.file;
332    let (ta, diags, _timings) = front_end(source, parsed, stdlib_defs, &[])?;
333    if has_errors(&diags) {
334        return Err(diags.into());
335    }
336    // Lowering deepens the typed tree, so check enforces compilation's limit on
337    // the lowered tree too.
338    let with_front_end_diagnostics = |failure| CompileError {
339        diagnostics: diags.clone(),
340        fatal: Some(failure),
341    };
342    let ta = capture(ta).map_err(with_front_end_diagnostics)?;
343    desugar(ta, file).map_err(with_front_end_diagnostics)?;
344    Ok(warnings_only(diags))
345}
346
347/// Typecheck and return the typed AST together with every diagnostic, errors
348/// included. For tools that inspect what the typechecker inferred — the
349/// TypeScript baseline comparison in `crates/conformance` — rather than compile.
350/// The typed AST is only meaningful where the diagnostics are error-free.
351pub fn typecheck_to_typed_ast(source: &str, file: FileId) -> (TypedAst, Vec<Diagnostic>) {
352    let parsed = parse_script(source, file);
353    let stdlib_defs = runtime::stdlib_package_declarations();
354    match front_end(source, &parsed, &stdlib_defs, &[]) {
355        Ok((ta, diags, _)) => (ta, diags),
356        Err(error) => (
357            TypedAst::new(),
358            error
359                .with_limit_span_cut(|span_file| (span_file == file).then_some(source))
360                .into_diagnostics(file),
361        ),
362    }
363}
364
365/// [`typecheck_to_typed_ast`] for the modules of one package, which import each
366/// other by relative path. Every module is checked, imported or not, and inference
367/// runs past syntax errors, as for a script. The typed AST holds every module's
368/// declarations; each span carries the [`FileId`] of its module, which is its index
369/// in `modules`.
370pub fn typecheck_package_to_typed_ast(
371    package_name: &str,
372    root_module: ModulePath,
373    modules: &[PackageSourceModule<'_>],
374) -> (TypedAst, Vec<Diagnostic>) {
375    let mut sources = Sources::new();
376    let mut diagnostics = Vec::new();
377    let Some(parsed_modules) = parse_modules_past_errors(&mut sources, modules, &mut diagnostics)
378    else {
379        return (TypedAst::new(), diagnostics);
380    };
381    let module_refs = parsed_modules
382        .iter()
383        .map(|(path, file, ast)| (path.clone(), *file, ast))
384        .collect();
385    let stdlib_defs = runtime::stdlib_package_declarations();
386    match crate::typechecker::infer::infer_package_checked(
387        package_name,
388        root_module,
389        module_refs,
390        &sources,
391        runtime_external_packages(&stdlib_defs),
392        BTreeMap::new(),
393    ) {
394        Ok((ta, _, package_diagnostics)) => {
395            diagnostics.extend(package_diagnostics);
396            (ta, diagnostics)
397        }
398        Err(error) => {
399            let error =
400                error.with_limit_span_cut(|file| sources.get(file).map(crate::SourceFile::text));
401            diagnostics.extend(error.into_diagnostics(FileId(0)));
402            (TypedAst::new(), diagnostics)
403        }
404    }
405}
406
407/// Lower a parsed module that has syntax errors: `parse_package_module` leaves
408/// such a module unlowered, but inference past syntax errors assumes lowering.
409fn lower_recovered_module(
410    (ast, diagnostics): (Ast, Vec<Diagnostic>),
411) -> Result<(Ast, Vec<Diagnostic>), CompileError> {
412    if ast.lowered_local_types.is_some() {
413        return Ok((ast, diagnostics));
414    }
415    match lower_patterns(ast) {
416        Ok(ast) => Ok((ast, diagnostics)),
417        Err(fatal) => Err(CompileError {
418            diagnostics,
419            fatal: Some(fatal),
420        }),
421    }
422}
423
424/// Registers, parses and lowers each module, adding its diagnostics, errors
425/// included, to `diagnostics`. `None` when one fails fatally, so nothing can be
426/// checked.
427fn parse_modules_past_errors(
428    sources: &mut Sources,
429    modules: &[PackageSourceModule<'_>],
430    diagnostics: &mut Vec<Diagnostic>,
431) -> Option<Vec<(ModulePath, FileId, Ast)>> {
432    let mut parsed_modules = Vec::with_capacity(modules.len());
433    for module in modules {
434        let file = match sources.add(module.path.clone(), module.source) {
435            Ok(file) => file,
436            Err(error) => {
437                let error = CompileError::from(error.into_compiler_failure(CompilerStage::Parse));
438                diagnostics.extend(error.into_diagnostics(FileId(0)));
439                return None;
440            }
441        };
442        // A config module has no blocks to lower.
443        let (ast, mut module_diagnostics) = match if module.path.as_str() == "config" {
444            parse_config_module(module.source, file, &BTreeMap::new())
445        } else {
446            parse_package_module(module.source, file).and_then(lower_recovered_module)
447        } {
448            Ok(parsed) => parsed,
449            Err(error) => {
450                diagnostics.extend(error.into_diagnostics(file));
451                return None;
452            }
453        };
454        diagnostics.append(&mut module_diagnostics);
455        parsed_modules.push((module.path.clone(), file, ast));
456    }
457    Some(parsed_modules)
458}
459
460/// The packages every package may import without declaring them: the stdlib's,
461/// the prelude's and the host's, by name.
462fn runtime_external_packages(
463    stdlib_defs: &[PackageDeclaration],
464) -> BTreeMap<String, PackageDeclaration> {
465    let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
466    stdlib_defs
467        .iter()
468        .chain(prelude_defs)
469        .chain(host_defs)
470        .map(|defs| (defs.package_name.clone(), defs.clone()))
471        .collect()
472}
473
474/// Compile a script module. `filename` is the DWARF compile-unit name for
475/// backtrace source mapping; `file` is this source's [`FileId`] in the caller's
476/// [`Sources`](crate::Sources) registry. `packages` are real package
477/// declarations that emit Wasm imports; `mcps` are declaration-only virtual MCP
478/// packages.
479///
480/// Returns [`CompiledScript`] on success. Warnings are non-fatal and should be
481/// surfaced without aborting; callers that only need Wasm bytes can read
482/// [`CompiledScript::wasm`]. Returns `Err(diagnostics)` if any phase errors.
483pub fn compile_script_checked(
484    source: &str,
485    filename: &str,
486    file: FileId,
487    packages: &[&PackageDeclaration],
488    mcps: &[&PackageDeclaration],
489) -> Result<CompiledScript, CompileError> {
490    let parsed = parse_script(source, file);
491    let stdlib_defs = runtime::stdlib_package_declarations();
492    compile_parsed_script_timed_checked(source, filename, &parsed, &stdlib_defs, packages, mcps)
493}
494
495/// Compiles a script whose code belongs to `owning_package` rather than to `main`.
496///
497/// The one caller is a package's test file: it compiles as a script, so its entry point stays
498/// mangled as `main`, but its gated calls must be attributed to the package that ships it.
499pub fn compile_script_owned_by_checked(
500    owning_package: &str,
501    source: &str,
502    filename: &str,
503    file: FileId,
504    packages: &[&PackageDeclaration],
505    mcps: &[&PackageDeclaration],
506) -> Result<CompiledScript, CompileError> {
507    let parsed = parse_script(source, file);
508    let stdlib_defs = runtime::stdlib_package_declarations();
509    let declarations = [packages, mcps].concat();
510    compile_parsed_script_owned_by(
511        Some(owning_package),
512        source,
513        filename,
514        &parsed,
515        &stdlib_defs,
516        &declarations,
517        &[],
518        None,
519    )
520}
521
522/// Compiles a test file of `owner`'s package, which runs under `submilli build test`.
523///
524/// It compiles as [`compile_script_owned_by_checked`] does, and may also import
525/// the package's non-entry modules by relative path (`../src/paths`). Those
526/// imports bind to the exports in `surface`, so the test must link against the
527/// [`PackageSurface::WithTestModules`] build that produced it. `importer` is the
528/// test file's path in the package directory without its extension
529/// (`tests/paths.test`).
530pub fn compile_package_test_checked(
531    owner: &PackageDeclaration,
532    surface: &PackageTestSurface,
533    importer: &ModulePath,
534    source: &str,
535    filename: &str,
536    file: FileId,
537    packages: &[&PackageDeclaration],
538) -> Result<CompiledScript, CompileError> {
539    let modules: BTreeMap<ModulePath, TestModule> = surface
540        .modules
541        .iter()
542        .map(|(path, exports)| (path.clone(), test_module(owner, exports)))
543        .collect();
544    crate::typechecker::infer::check_declaration_types(
545        modules.values().map(|module| &module.declaration),
546    )?;
547    let codegen_owner = test_codegen_owner(owner, surface);
548    let mut declarations = packages.to_vec();
549    if !declarations
550        .iter()
551        .any(|declaration| declaration.package_name == owner.package_name)
552    {
553        declarations.push(owner);
554    }
555    let parsed = parse_script(source, file);
556    let stdlib_defs = runtime::stdlib_package_declarations();
557    compile_parsed_script_owned_by(
558        Some(&owner.package_name),
559        source,
560        filename,
561        &parsed,
562        &stdlib_defs,
563        &declarations,
564        &[],
565        Some(TestBuild {
566            modules: TestModules {
567                importer,
568                owner: &owner.package_name,
569                modules: &modules,
570            },
571            codegen_owner: &codegen_owner,
572        }),
573    )
574}
575
576/// What a test file compiles against besides its declared packages.
577#[derive(Clone, Copy)]
578struct TestBuild<'a> {
579    modules: TestModules<'a>,
580    /// The owner's declaration with its internal exports added. It replaces the
581    /// owner in codegen only, which must import what the typechecker bound to
582    /// the package's modules; the typechecker keeps the public declaration, so
583    /// `from "<owner>"` still reaches only the entry module.
584    codegen_owner: &'a PackageDeclaration,
585}
586
587/// A module's exports as a declaration named after its package, so a test
588/// binds them as it binds a package's.
589fn test_module(owner: &PackageDeclaration, exports: &crate::InternalModuleExports) -> TestModule {
590    let mut declaration = PackageDeclaration::with_package(owner.package_name.clone());
591    declaration.values = exports.values.clone();
592    declaration.types = exports.types.clone();
593    declaration.refresh_shapes();
594    TestModule {
595        declaration,
596        unexported: exports.unexported.clone(),
597    }
598}
599
600/// `owner` plus the internal values and classes in `surface`, keyed by their
601/// mangled names, which no source identifier can spell. A class joins `types`
602/// so its statics are imported as a public class's are; its layout stays in
603/// `runtime_types`.
604fn test_codegen_owner(
605    owner: &PackageDeclaration,
606    surface: &PackageTestSurface,
607) -> PackageDeclaration {
608    let mut declaration = owner.clone();
609    let public_values: BTreeSet<&crate::MangledName> = owner
610        .values
611        .values()
612        .map(|value| &value.mangled_name)
613        .collect();
614    let public_types: BTreeSet<&crate::MangledName> = owner
615        .types
616        .values()
617        .map(|symbol| &symbol.mangled_name)
618        .collect();
619    for exports in surface.modules.values() {
620        for value in exports.values.values() {
621            if !public_values.contains(&value.mangled_name) {
622                declaration
623                    .values
624                    .entry(value.mangled_name.to_string())
625                    .or_insert_with(|| value.clone());
626            }
627        }
628        for symbol in exports.types.values() {
629            if matches!(symbol.kind, crate::TypeKind::Class { .. })
630                && !public_types.contains(&symbol.mangled_name)
631            {
632                declaration
633                    .types
634                    .entry(symbol.mangled_name.to_string())
635                    .or_insert_with(|| symbol.clone());
636            }
637        }
638    }
639    declaration.refresh_shapes();
640    declaration
641}
642
643pub fn compile_parsed_script_timed_checked(
644    source: &str,
645    filename: &str,
646    parsed: &ParsedScript,
647    stdlib_defs: &[PackageDeclaration],
648    packages: &[&PackageDeclaration],
649    mcps: &[&PackageDeclaration],
650) -> Result<CompiledScript, CompileError> {
651    compile_parsed_script_with_transitive_checked(
652        source,
653        filename,
654        parsed,
655        stdlib_defs,
656        packages,
657        mcps,
658        &[],
659    )
660}
661
662/// Compile with declarations needed to reconstruct imported types without
663/// exposing those transitive packages to source-level imports.
664pub fn compile_parsed_script_with_transitive_checked(
665    source: &str,
666    filename: &str,
667    parsed: &ParsedScript,
668    stdlib_defs: &[PackageDeclaration],
669    packages: &[&PackageDeclaration],
670    mcps: &[&PackageDeclaration],
671    transitive: &[&PackageDeclaration],
672) -> Result<CompiledScript, CompileError> {
673    let declarations = [packages, mcps].concat();
674    compile_parsed_script_owned_by(
675        None,
676        source,
677        filename,
678        parsed,
679        stdlib_defs,
680        &declarations,
681        transitive,
682        None,
683    )
684}
685
686#[allow(clippy::too_many_arguments)]
687fn compile_parsed_script_owned_by(
688    owning_package: Option<&str>,
689    source: &str,
690    filename: &str,
691    parsed: &ParsedScript,
692    stdlib_defs: &[PackageDeclaration],
693    external_declarations: &[&PackageDeclaration],
694    transitive: &[&PackageDeclaration],
695    test: Option<TestBuild<'_>>,
696) -> Result<CompiledScript, CompileError> {
697    compile_parsed_script(
698        owning_package,
699        source,
700        filename,
701        parsed,
702        stdlib_defs,
703        external_declarations,
704        transitive,
705        test,
706    )
707    .map_err(|error| error.with_limit_span_cut(|file| (file == parsed.file).then_some(source)))
708}
709
710#[allow(clippy::too_many_arguments)]
711fn compile_parsed_script(
712    owning_package: Option<&str>,
713    source: &str,
714    filename: &str,
715    parsed: &ParsedScript,
716    stdlib_defs: &[PackageDeclaration],
717    external_declarations: &[&PackageDeclaration],
718    transitive: &[&PackageDeclaration],
719    test: Option<TestBuild<'_>>,
720) -> Result<CompiledScript, CompileError> {
721    let (mut ta, diags, mut timings) = front_end_with_transitive(
722        owning_package,
723        source,
724        parsed,
725        stdlib_defs,
726        external_declarations,
727        transitive,
728        test.map(|test| test.modules),
729    )?;
730    if has_errors(&diags) {
731        return Err(diags.into());
732    }
733    let capture_start = Instant::now();
734    ta = capture(ta).map_err(|fatal| CompileError {
735        diagnostics: diags.clone(),
736        fatal: Some(fatal),
737    })?;
738    timings.capture = capture_start.elapsed();
739
740    let desugar_start = Instant::now();
741    ta = desugar(ta, parsed.file).map_err(|fatal| CompileError {
742        diagnostics: diags.clone(),
743        fatal: Some(fatal),
744    })?;
745    timings.desugar = desugar_start.elapsed();
746
747    let (prelude_defs, host_defs, internal_defs) = prelude::cached_runtime_package_declarations();
748    let mut dependencies: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
749    for defs in host_defs {
750        dependencies.push(defs);
751    }
752    for defs in internal_defs {
753        dependencies.push(defs);
754    }
755    for defs in stdlib_defs {
756        dependencies.push(defs);
757    }
758    dependencies.extend(external_declarations.iter().map(|declaration| match test {
759        Some(test) if test.codegen_owner.package_name == declaration.package_name => {
760            test.codegen_owner
761        }
762        _ => *declaration,
763    }));
764    dependencies.extend_from_slice(transitive);
765
766    let codegen_start = Instant::now();
767    let generated = match owning_package {
768        Some(pkg) => {
769            crate::codegen::codegen_owned_by(pkg, source, filename, parsed.file, &ta, &dependencies)
770        }
771        None => crate::codegen::codegen_with_type_info(
772            source,
773            filename,
774            parsed.file,
775            &ta,
776            &dependencies,
777        ),
778    }
779    .map_err(|fatal| CompileError {
780        diagnostics: diags.clone(),
781        fatal: Some(fatal),
782    })?;
783    timings.codegen = codegen_start.elapsed();
784
785    Ok(CompiledScript {
786        wasm: generated.wasm,
787        type_info: generated.type_info,
788        warnings: warnings_only(diags),
789        timings,
790    })
791}
792
793pub fn compile_package_checked(
794    package_name: &str,
795    root_module: ModulePath,
796    modules: &[PackageSourceModule<'_>],
797    dependencies: &[&PackageDeclaration],
798) -> Result<CompiledPackage, CompileError> {
799    compile_package_with_transitive_checked(package_name, root_module, modules, dependencies, &[])
800}
801
802/// [`compile_package`] with the rest of the dependency closure supplied
803/// separately.
804///
805/// `dependencies` is what the package declares, and only those are importable.
806/// `transitive` is everything they in turn depend on: a direct dependency's
807/// public surface can name a class from one of them — as a parent, a return
808/// type, a static's type — and resolving that needs the declaration even though
809/// this package must not `import` from it. Without it the ancestor walk
810/// truncates silently and codegen reconstructs a layout that doesn't
811/// canonicalize against the producer's, which surfaces at link time as
812/// `imported global type mismatch`.
813pub fn compile_package_with_transitive_checked(
814    package_name: &str,
815    root_module: ModulePath,
816    modules: &[PackageSourceModule<'_>],
817    dependencies: &[&PackageDeclaration],
818    transitive: &[&PackageDeclaration],
819) -> Result<CompiledPackage, CompileError> {
820    compile_package_with_transitive_checked_for(
821        Stdlib::core(),
822        package_name,
823        root_module,
824        modules,
825        dependencies,
826        transitive,
827    )
828}
829
830/// [`compile_package_with_transitive_checked`] for an embedder that offers
831/// `stdlib`, so the package may import its optional packages.
832pub fn compile_package_with_transitive_checked_for(
833    stdlib: Stdlib,
834    package_name: &str,
835    root_module: ModulePath,
836    modules: &[PackageSourceModule<'_>],
837    dependencies: &[&PackageDeclaration],
838    transitive: &[&PackageDeclaration],
839) -> Result<CompiledPackage, CompileError> {
840    compile_package_configured(
841        stdlib,
842        package_name,
843        root_module,
844        modules,
845        dependencies,
846        transitive,
847        &BTreeMap::new(),
848        PackageSurface::Public,
849    )
850}
851
852/// Compile a package with immutable overrides for its root config module.
853#[allow(clippy::too_many_arguments)]
854pub fn compile_package_configured(
855    stdlib: Stdlib,
856    package_name: &str,
857    root_module: ModulePath,
858    modules: &[PackageSourceModule<'_>],
859    dependencies: &[&PackageDeclaration],
860    transitive: &[&PackageDeclaration],
861    config: &BTreeMap<String, String>,
862    surface: PackageSurface,
863) -> Result<CompiledPackage, CompileError> {
864    if !modules
865        .iter()
866        .any(|module| module.path.as_str() == "config")
867    {
868        crate::package_config::resolve(&[], config, FileId(0))?;
869    }
870    let mut sources = Sources::new();
871    let stdlib_defs = stdlib.package_declarations();
872    compile_package_sources(
873        &mut sources,
874        &stdlib_defs,
875        package_name,
876        root_module,
877        modules,
878        dependencies,
879        transitive,
880        config,
881        surface,
882    )
883    .map_err(|error| {
884        error.with_limit_span_cut(|file| sources.get(file).map(crate::SourceFile::text))
885    })
886}
887
888#[allow(clippy::too_many_arguments)]
889fn compile_package_sources(
890    sources: &mut Sources,
891    stdlib_defs: &[PackageDeclaration],
892    package_name: &str,
893    root_module: ModulePath,
894    modules: &[PackageSourceModule<'_>],
895    dependencies: &[&PackageDeclaration],
896    transitive: &[&PackageDeclaration],
897    config: &BTreeMap<String, String>,
898    surface: PackageSurface,
899) -> Result<CompiledPackage, CompileError> {
900    // Public declarations may be constructed directly. Validate recursive
901    // metadata before cloning them into the inference registries.
902    crate::typechecker::infer::check_declaration_types(
903        dependencies.iter().chain(transitive).copied(),
904    )?;
905    let mut parsed_modules = Vec::with_capacity(modules.len());
906    let mut diagnostics = Vec::new();
907    for module in modules {
908        let file = sources
909            .add(module.path.clone(), module.source)
910            .map_err(|error| {
911                CompileError::from(error.into_compiler_failure(CompilerStage::Parse))
912                    .with_prior_diagnostics(&diagnostics)
913            })?;
914        let (ast, mut module_diags) = if module.path.as_str() == "config" {
915            parse_config_module(module.source, file, config)
916        } else {
917            parse_package_module(module.source, file)
918        }
919        .map_err(|mut error| {
920            error.diagnostics.splice(0..0, diagnostics.clone());
921            error
922        })?;
923        diagnostics.append(&mut module_diags);
924        parsed_modules.push((module.path.clone(), file, ast));
925    }
926    if has_errors(&diagnostics) {
927        return Err(diagnostics.into());
928    }
929
930    let module_refs: Vec<_> = parsed_modules
931        .iter()
932        .map(|(path, file, ast)| (path.clone(), *file, ast))
933        .collect();
934    let mut external_packages = runtime_external_packages(stdlib_defs);
935    for defs in dependencies {
936        external_packages.insert(defs.package_name.clone(), (*defs).clone());
937    }
938    let transitive_packages: BTreeMap<String, PackageDeclaration> = transitive
939        .iter()
940        .filter(|defs| !external_packages.contains_key(&defs.package_name))
941        .map(|defs| (defs.package_name.clone(), (*defs).clone()))
942        .collect();
943    let (mut ta, mut declaration, mut package_diags) =
944        crate::typechecker::infer::infer_package_checked(
945            package_name,
946            root_module.clone(),
947            module_refs,
948            sources,
949            external_packages,
950            transitive_packages,
951        )
952        .map_err(|mut error| {
953            error.diagnostics.splice(0..0, diagnostics.clone());
954            error
955        })?;
956    diagnostics.append(&mut package_diags);
957    if has_errors(&diagnostics) {
958        return Err(diagnostics.into());
959    }
960    let internal_modules = std::mem::take(&mut ta.internal_modules);
961    let test_surface = match surface {
962        PackageSurface::Public => None,
963        PackageSurface::WithTestModules => Some(own_test_surface(&ta, internal_modules)),
964    };
965    let (required_capabilities, capability_warnings) =
966        compile_capabilities::derive_package_requirements(
967            &declaration,
968            &ta,
969            stdlib_defs,
970            dependencies,
971            transitive,
972        )
973        .map_err(|error| error.with_prior_diagnostics(&diagnostics))?;
974    diagnostics.extend(capability_warnings);
975    let (prelude_defs, host_defs, internal_defs) = prelude::cached_runtime_package_declarations();
976    let authority_map = crate::authority::analyse(
977        &declaration,
978        &ta,
979        sources,
980        stdlib_defs
981            .iter()
982            .chain(prelude_defs)
983            .chain(host_defs)
984            .chain(dependencies.iter().copied())
985            .chain(transitive.iter().copied()),
986    )
987    .map_err(|fatal| CompileError {
988        diagnostics: diagnostics.clone(),
989        fatal: Some(fatal),
990    })?;
991    ta = capture(ta).map_err(|fatal| CompileError {
992        diagnostics: diagnostics.clone(),
993        fatal: Some(fatal),
994    })?;
995    let root_file = parsed_modules
996        .iter()
997        .find(|(path, _, _)| *path == root_module)
998        .map(|(_, file, _)| *file)
999        .ok_or_else(|| CompilerFailure::Internal {
1000            stage: CompilerStage::Infer,
1001            span: None,
1002            message: "compiled package root module is missing".into(),
1003        })?;
1004    ta = desugar(ta, root_file).map_err(|fatal| CompileError {
1005        diagnostics: diagnostics.clone(),
1006        fatal: Some(fatal),
1007    })?;
1008    if let Some(surface) = &test_surface {
1009        ta.test_exports = test_exports(surface, &ta.exports);
1010    }
1011
1012    let mut codegen_deps: Vec<&PackageDeclaration> = prelude_defs.iter().collect();
1013    codegen_deps.extend(host_defs.iter());
1014    codegen_deps.extend(internal_defs.iter());
1015    codegen_deps.extend(stdlib_defs.iter());
1016    codegen_deps.extend_from_slice(dependencies);
1017    codegen_deps.extend_from_slice(transitive);
1018    let generated =
1019        crate::codegen::codegen_package_with_type_info(sources, root_file, &ta, &codegen_deps)
1020            .map_err(|fatal| CompileError {
1021                diagnostics: diagnostics.clone(),
1022                fatal: Some(fatal),
1023            })?;
1024    declaration.runtime_functions = generated.runtime_functions;
1025    declaration.runtime_globals = generated.runtime_globals;
1026    declaration.closure_caches = generated.closure_caches;
1027    Ok(CompiledPackage {
1028        wasm: generated.wasm,
1029        type_info: generated.type_info,
1030        declaration,
1031        required_capabilities,
1032        authority_map,
1033        warnings: warnings_only(diagnostics),
1034        test_surface,
1035    })
1036}
1037
1038/// The modules' exports minus values and classes another package declares,
1039/// which a module can re-export but this package's Wasm cannot export.
1040fn own_test_surface(
1041    ta: &TypedAst,
1042    modules: BTreeMap<ModulePath, crate::InternalModuleExports>,
1043) -> PackageTestSurface {
1044    let own_values: BTreeSet<&crate::MangledName> = ta
1045        .functions
1046        .iter()
1047        .map(|function| &function.mangled_name)
1048        .chain(ta.globals.iter().map(|global| &global.mangled_name))
1049        .collect();
1050    let own_classes: BTreeSet<&crate::MangledName> = ta
1051        .types
1052        .iter()
1053        .filter_map(|declaration| match declaration {
1054            crate::TypedTypeDecl::Class(class) => Some(&class.mangled_name),
1055            _ => None,
1056        })
1057        .collect();
1058    let modules = modules
1059        .into_iter()
1060        .map(|(path, mut exports)| {
1061            exports
1062                .values
1063                .retain(|_, value| own_values.contains(&value.mangled_name));
1064            exports.types.retain(|_, symbol| {
1065                !matches!(symbol.kind, crate::TypeKind::Class { .. })
1066                    || own_classes.contains(&symbol.mangled_name)
1067            });
1068            (path, exports)
1069        })
1070        .collect();
1071    PackageTestSurface { modules }
1072}
1073
1074/// The Wasm exports that let a test call into `surface`: each value under its
1075/// mangled name, and the public statics of each class, each once and none
1076/// already among the package's `public` exports. Two modules can export one
1077/// declaration, and a re-exported one is public under the same mangled name.
1078fn test_exports(
1079    surface: &PackageTestSurface,
1080    public: &[crate::ExportEntry],
1081) -> Vec<crate::ExportEntry> {
1082    let mut seen: BTreeSet<crate::MangledName> = public
1083        .iter()
1084        .map(|entry| entry.public_name.clone())
1085        .collect();
1086    let mut exports = Vec::new();
1087    for module in surface.modules.values() {
1088        for value in module.values.values() {
1089            exports.push(crate::ExportEntry {
1090                public_name: value.mangled_name.clone(),
1091                target: value.mangled_name.clone(),
1092                kind: match value.kind {
1093                    crate::ValueKind::Function { .. } => crate::ExportKind::Function,
1094                    crate::ValueKind::Let { .. } | crate::ValueKind::Const { .. } => {
1095                        crate::ExportKind::Global
1096                    }
1097                },
1098                span: value.declaration_span,
1099            });
1100        }
1101        for symbol in module.types.values() {
1102            exports.extend(crate::typechecker::infer::static_export_entries(
1103                &symbol.kind,
1104                &symbol.mangled_name,
1105                symbol.declaration_span,
1106            ));
1107        }
1108    }
1109    exports.retain(|entry| seen.insert(entry.public_name.clone()));
1110    exports
1111}
1112
1113fn parse_config_module(
1114    source: &str,
1115    file: FileId,
1116    config: &BTreeMap<String, String>,
1117) -> Result<(Ast, Vec<Diagnostic>), CompileError> {
1118    let tokens = crate::package_config::resolved_tokens(source, file, config)?;
1119    let (ast, diagnostics) = crate::parser::parse_checked(source, tokens, file)?;
1120    Ok((ast, diagnostics))
1121}
1122
1123fn parse_package_module(
1124    source: &str,
1125    file: FileId,
1126) -> Result<(crate::Ast, Vec<Diagnostic>), CompileError> {
1127    let mut asi = Asi::new(source, file);
1128    let mut tokens: Vec<Token> = Vec::new();
1129    loop {
1130        let tok = asi.next_token();
1131        let is_eof = matches!(tok.kind, TokenKind::Eof);
1132        tokens.push(tok);
1133        if is_eof {
1134            break;
1135        }
1136    }
1137    let mut diagnostics = asi.finish()?;
1138    let (mut ast, parse_diags) =
1139        crate::parser::parse_checked(source, tokens, file).map_err(|mut error| {
1140            error.diagnostics.splice(0..0, diagnostics.clone());
1141            error
1142        })?;
1143    diagnostics.extend(parse_diags);
1144    if !has_errors(&diagnostics) {
1145        ast = lower_patterns(ast).map_err(|fatal| CompileError {
1146            diagnostics: diagnostics.clone(),
1147            fatal: Some(fatal),
1148        })?;
1149    }
1150    Ok((ast, diagnostics))
1151}
1152
1153fn has_errors(diags: &[Diagnostic]) -> bool {
1154    diags.iter().any(|d| d.severity == Severity::Error)
1155}
1156
1157fn warnings_only(diags: Vec<Diagnostic>) -> Vec<Diagnostic> {
1158    diags
1159        .into_iter()
1160        .filter(|d| d.severity == Severity::Warning)
1161        .collect()
1162}
1163
1164/// Compatibility adapter returning source and fatal failures as diagnostics.
1165pub fn typecheck(source: &str, file: FileId) -> Result<Vec<Diagnostic>, Vec<Diagnostic>> {
1166    typecheck_checked(source, file).map_err(|error| error.into_diagnostics(file))
1167}
1168
1169/// Compatibility adapter returning source and fatal failures as diagnostics.
1170pub fn compile_script(
1171    source: &str,
1172    filename: &str,
1173    file: FileId,
1174    packages: &[&PackageDeclaration],
1175    mcps: &[&PackageDeclaration],
1176) -> Result<CompiledScript, Vec<Diagnostic>> {
1177    compile_script_checked(source, filename, file, packages, mcps)
1178        .map_err(|error| error.into_diagnostics(file))
1179}
1180
1181/// Compatibility adapter returning source and fatal failures as diagnostics.
1182pub fn compile_script_owned_by(
1183    owning_package: &str,
1184    source: &str,
1185    filename: &str,
1186    file: FileId,
1187    packages: &[&PackageDeclaration],
1188    mcps: &[&PackageDeclaration],
1189) -> Result<CompiledScript, Vec<Diagnostic>> {
1190    compile_script_owned_by_checked(owning_package, source, filename, file, packages, mcps)
1191        .map_err(|error| error.into_diagnostics(file))
1192}
1193
1194/// Compatibility adapter returning source and fatal failures as diagnostics.
1195pub fn compile_package_test(
1196    owner: &PackageDeclaration,
1197    surface: &PackageTestSurface,
1198    importer: &ModulePath,
1199    source: &str,
1200    filename: &str,
1201    file: FileId,
1202    packages: &[&PackageDeclaration],
1203) -> Result<CompiledScript, Vec<Diagnostic>> {
1204    compile_package_test_checked(owner, surface, importer, source, filename, file, packages)
1205        .map_err(|error| error.into_diagnostics(file))
1206}
1207
1208/// Compatibility adapter returning source and fatal failures as diagnostics.
1209pub fn compile_parsed_script_timed(
1210    source: &str,
1211    filename: &str,
1212    parsed: &ParsedScript,
1213    stdlib_defs: &[PackageDeclaration],
1214    packages: &[&PackageDeclaration],
1215    mcps: &[&PackageDeclaration],
1216) -> Result<CompiledScript, Vec<Diagnostic>> {
1217    compile_parsed_script_timed_checked(source, filename, parsed, stdlib_defs, packages, mcps)
1218        .map_err(|error| error.into_diagnostics(parsed.file))
1219}
1220
1221/// Compatibility adapter returning source and fatal failures as diagnostics.
1222pub fn compile_parsed_script_with_transitive(
1223    source: &str,
1224    filename: &str,
1225    parsed: &ParsedScript,
1226    stdlib_defs: &[PackageDeclaration],
1227    packages: &[&PackageDeclaration],
1228    mcps: &[&PackageDeclaration],
1229    transitive: &[&PackageDeclaration],
1230) -> Result<CompiledScript, Vec<Diagnostic>> {
1231    compile_parsed_script_with_transitive_checked(
1232        source,
1233        filename,
1234        parsed,
1235        stdlib_defs,
1236        packages,
1237        mcps,
1238        transitive,
1239    )
1240    .map_err(|error| error.into_diagnostics(parsed.file))
1241}
1242
1243/// Compatibility adapter returning source and fatal failures as diagnostics.
1244pub fn compile_package(
1245    package_name: &str,
1246    root_module: ModulePath,
1247    modules: &[PackageSourceModule<'_>],
1248    dependencies: &[&PackageDeclaration],
1249) -> Result<CompiledPackage, Vec<Diagnostic>> {
1250    compile_package_checked(package_name, root_module, modules, dependencies)
1251        .map_err(|error| error.into_diagnostics(FileId(0)))
1252}
1253
1254/// Compatibility adapter returning source and fatal failures as diagnostics.
1255pub fn compile_package_with_transitive(
1256    package_name: &str,
1257    root_module: ModulePath,
1258    modules: &[PackageSourceModule<'_>],
1259    dependencies: &[&PackageDeclaration],
1260    transitive: &[&PackageDeclaration],
1261) -> Result<CompiledPackage, Vec<Diagnostic>> {
1262    compile_package_with_transitive_checked(
1263        package_name,
1264        root_module,
1265        modules,
1266        dependencies,
1267        transitive,
1268    )
1269    .map_err(|error| error.into_diagnostics(FileId(0)))
1270}
1271
1272#[cfg(test)]
1273mod parsed_script_tests {
1274    use super::*;
1275
1276    #[test]
1277    fn a_package_build_reports_every_module_syntax_error() {
1278        crate::type_size::tests::on_compiler_stack(|| {
1279            // A module with syntax errors isn't lowered for a build: a recovered
1280            // parse can leave nodes lowering would trip on, and the build
1281            // stops before inference anyway.
1282            let modules = [
1283                PackageSourceModule {
1284                    path: ModulePath::from("lib"),
1285                    source: "export function f(): void {\n  \
1286                             const x = g<number>(({ a }: { a: number }) => a;\n}\n",
1287                },
1288                PackageSourceModule {
1289                    path: ModulePath::from("other"),
1290                    source: "export const y = ;\n",
1291                },
1292            ];
1293            let error = compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1294                .expect_err("syntax errors");
1295            assert!(error.fatal.is_none(), "{error:?}");
1296            let files: std::collections::BTreeSet<u32> =
1297                error.diagnostics.iter().map(|d| d.span.file.0).collect();
1298            assert_eq!(files.len(), 2, "{:?}", error.diagnostics);
1299        });
1300    }
1301
1302    #[test]
1303    fn package_inference_past_syntax_errors_sees_lowered_modules() {
1304        crate::type_size::tests::on_compiler_stack(|| {
1305            // Inference runs past `lib`'s syntax error, so `lib` is lowered
1306            // anyway: its local interface is lifted and its destructured
1307            // parameter lowered, and the other module's type error is still
1308            // reported.
1309            let modules = [
1310                PackageSourceModule {
1311                    path: ModulePath::from("lib"),
1312                    source: "export function f([n]: number[]): number {\n  \
1313                             interface Row { n: number }\n  const r: Row = { n };\n  \
1314                             return r.n;\n}\nconst broken = ;\n",
1315                },
1316                PackageSourceModule {
1317                    path: ModulePath::from("other"),
1318                    source: "const text: string = 1;\n",
1319                },
1320            ];
1321            let (_, diagnostics) =
1322                typecheck_package_to_typed_ast("test", ModulePath::from("lib"), &modules);
1323            let messages: Vec<&str> = diagnostics.iter().map(|d| d.message.as_str()).collect();
1324            assert!(
1325                messages.iter().any(|m| m.contains("expected expression")),
1326                "{messages:?}"
1327            );
1328            assert!(
1329                messages.iter().any(|m| m.contains("expected `string`")),
1330                "{messages:?}"
1331            );
1332            // An unlowered module fails inference as an internal compiler
1333            // failure.
1334            assert!(
1335                !messages
1336                    .iter()
1337                    .any(|m| m.contains("internal compiler failure")),
1338                "{messages:?}"
1339            );
1340        });
1341    }
1342
1343    #[test]
1344    fn typed_compile_errors_distinguish_source_limits_and_internal_failures() {
1345        let source_error = compile_script_checked(
1346            "function main(): number { return false; }",
1347            "test.ts",
1348            FileId(0),
1349            &[],
1350            &[],
1351        )
1352        .expect_err("source type mismatch");
1353        assert!(source_error.fatal.is_none());
1354        assert!(!source_error.diagnostics.is_empty());
1355
1356        let nested = format!(
1357            "function main(): number {{ return {}1{}; }}",
1358            "(".repeat(2048),
1359            ")".repeat(2048)
1360        );
1361        let limit = compile_script_checked(&nested, "test.ts", FileId(0), &[], &[])
1362            .expect_err("parser limit");
1363        assert!(matches!(
1364            limit.fatal,
1365            Some(CompilerFailure::Limit {
1366                stage: CompilerStage::Parse,
1367                ..
1368            })
1369        ));
1370        let rendered = limit.into_diagnostics(FileId(0));
1371        assert_eq!(
1372            rendered
1373                .iter()
1374                .filter(|d| d.message.contains("parser recursion limit exceeded"))
1375                .count(),
1376            1
1377        );
1378
1379        let parsed = parse_script("function main(): number { return 42; }", FileId(0));
1380        let internal = compile_parsed_script_timed_checked(
1381            "different source",
1382            "test.ts",
1383            &parsed,
1384            &[],
1385            &[],
1386            &[],
1387        )
1388        .expect_err("source metadata mismatch");
1389        assert!(matches!(
1390            internal.fatal,
1391            Some(CompilerFailure::Internal {
1392                stage: CompilerStage::Infer,
1393                ..
1394            })
1395        ));
1396    }
1397
1398    #[test]
1399    fn external_imports_come_from_parsed_imports_only() {
1400        let parsed = parse_script(
1401            r#"
1402                // import fake from "@mcp/commented";
1403                const s: string = "import x from \"@pied-piper/string\"";
1404                import { v4 } from "submilli:uuid";
1405                import linear from "@mcp/linear";
1406                import { answer } from "@pied-piper/util";
1407                function main(): string { return v4(); }
1408            "#,
1409            FileId(0),
1410        );
1411
1412        let imports = parsed.external_imports().unwrap();
1413
1414        assert_eq!(
1415            imports.stdlib,
1416            BTreeSet::from(["submilli:uuid".to_string()])
1417        );
1418        assert_eq!(imports.mcp_servers, BTreeSet::from(["linear".to_string()]));
1419        assert_eq!(
1420            imports.registry_packages,
1421            BTreeSet::from(["@pied-piper/util".to_string()])
1422        );
1423    }
1424
1425    #[test]
1426    fn relative_imports_are_not_external_imports() {
1427        let parsed = parse_script(
1428            r#"
1429                import { local } from "./util";
1430                function main(): number { return 1; }
1431            "#,
1432            FileId(0),
1433        );
1434
1435        assert_eq!(parsed.external_imports().unwrap(), ScriptImports::default());
1436    }
1437}
1438
1439#[cfg(test)]
1440mod importless_library_tests {
1441    //! SUB-386: a value whose type comes from a library must support member
1442    //! access and use without the consumer separately importing the type name.
1443    //! The http tests cover this against the real stdlib; these exercise the
1444    //! mechanism for an arbitrary package declaration — proving the FQN registry is
1445    //! populated from `packages_by_name`, not just the prelude — and for the
1446    //! other by-name return shapes (an interface *and* an enum).
1447
1448    use std::collections::BTreeMap;
1449
1450    use crate::mangle::package_symbol;
1451    use crate::package_declaration::{Dispatch, MethodSig, PropertySig, TypeKind, ValueKind};
1452    use crate::{ObjectField, Package, PackageDeclaration, Span, Type, TypeSymbol, ValueSymbol};
1453
1454    const SYNTH: &str = "submilli:synth";
1455    const PIED_PIPER: &str = "@pied-piper/util";
1456
1457    fn synth_package() -> PackageDeclaration {
1458        let mut defs = PackageDeclaration::with_package(SYNTH);
1459
1460        let mut properties = BTreeMap::new();
1461        properties.insert(
1462            "label".to_string(),
1463            PropertySig {
1464                ty: Type::String,
1465                readonly: true,
1466                intrinsic: false,
1467                optional: false,
1468                doc: None,
1469            },
1470        );
1471        let mut methods = BTreeMap::new();
1472        methods.insert(
1473            "size".to_string(),
1474            MethodSig {
1475                optional: false,
1476                generics: Default::default(),
1477                params: Vec::new(),
1478                ret: Type::Number,
1479                predicate: None,
1480                doc: None,
1481            },
1482        );
1483        defs.types.insert(
1484            "Widget".to_string(),
1485            TypeSymbol {
1486                name: "Widget".to_string(),
1487                mangled_name: package_symbol(SYNTH, "Widget"),
1488                declaration_span: Span::at(crate::FileId(0)),
1489                kind: TypeKind::Interface {
1490                    index: None,
1491                    generics: Default::default(),
1492                    methods,
1493                    properties,
1494                    dispatch: Dispatch::Direct,
1495                    doc: None,
1496                },
1497            },
1498        );
1499
1500        defs.types.insert(
1501            "Color".to_string(),
1502            TypeSymbol {
1503                name: "Color".to_string(),
1504                mangled_name: package_symbol(SYNTH, "Color"),
1505                declaration_span: Span::at(crate::FileId(0)),
1506                kind: TypeKind::NumberEnum {
1507                    variants: vec![("Red".to_string(), 0.0), ("Green".to_string(), 1.0)],
1508                    doc: None,
1509                },
1510            },
1511        );
1512
1513        // A recursive alias `type Tree = { value: number; children: Tree[] }`,
1514        // built the way a library shim would: an inline body whose recursive
1515        // position is a [`Type::AliasRef`] back-edge carrying its own package.
1516        let tree_ref = || Type::AliasRef {
1517            mangled: crate::mangle::package_symbol(SYNTH, "Tree"),
1518            package: Package(SYNTH.to_string()),
1519            name: "Tree".to_string(),
1520            args: Vec::new(),
1521        };
1522        let tree_body = || {
1523            let mut fields = BTreeMap::new();
1524            fields.insert("value".to_string(), ObjectField::required(Type::Number));
1525            fields.insert(
1526                "children".to_string(),
1527                ObjectField::required(Type::Array(Box::new(tree_ref()))),
1528            );
1529            Type::Object {
1530                index: None,
1531                fields,
1532            }
1533        };
1534        defs.types.insert(
1535            "Tree".to_string(),
1536            TypeSymbol {
1537                name: "Tree".to_string(),
1538                mangled_name: package_symbol(SYNTH, "Tree"),
1539                declaration_span: Span::at(crate::FileId(0)),
1540                kind: TypeKind::Alias {
1541                    generics: Default::default(),
1542                    ty: tree_body(),
1543                    doc: None,
1544                },
1545            },
1546        );
1547
1548        let synth_ref = |name: &str, ty: Type| ValueSymbol {
1549            name: name.to_string(),
1550            mangled_name: package_symbol(SYNTH, name),
1551            declaration_span: Span::at(crate::FileId(0)),
1552            kind: ValueKind::Function {
1553                generics: Default::default(),
1554                params: Vec::new(),
1555                ret: ty,
1556                type_predicate: None,
1557                doc: None,
1558            },
1559        };
1560        defs.values.insert(
1561            "makeWidget".to_string(),
1562            synth_ref(
1563                "makeWidget",
1564                Type::InterfaceRef {
1565                    mangled: crate::mangle::package_symbol(SYNTH, "Widget"),
1566                    package: Package(SYNTH.to_string()),
1567                    name: "Widget".to_string(),
1568                    args: Vec::new(),
1569                },
1570            ),
1571        );
1572        defs.values.insert(
1573            "pickColor".to_string(),
1574            synth_ref(
1575                "pickColor",
1576                Type::NumberEnum {
1577                    mangled: crate::mangle::package_symbol(SYNTH, "Color"),
1578                    package: Package(SYNTH.to_string()),
1579                    name: "Color".to_string(),
1580                    member: None,
1581                },
1582            ),
1583        );
1584        defs.values.insert(
1585            "makeTree".to_string(),
1586            synth_ref(
1587                "makeTree",
1588                Type::Alias {
1589                    mangled: crate::mangle::package_symbol(SYNTH, "Tree"),
1590                    package: Package(SYNTH.to_string()),
1591                    name: "Tree".to_string(),
1592                    args: Vec::new(),
1593                    ty: Box::new(tree_body()),
1594                },
1595            ),
1596        );
1597        defs
1598    }
1599
1600    fn pied_piper_package() -> PackageDeclaration {
1601        let mut defs = PackageDeclaration::with_package(PIED_PIPER);
1602        defs.values.insert(
1603            "greet".to_string(),
1604            ValueSymbol {
1605                name: "greet".to_string(),
1606                mangled_name: package_symbol(PIED_PIPER, "greet"),
1607                declaration_span: Span::at(crate::FileId(0)),
1608                kind: ValueKind::Function {
1609                    generics: Default::default(),
1610                    params: Vec::new(),
1611                    ret: Type::String,
1612                    type_predicate: None,
1613                    doc: None,
1614                },
1615            },
1616        );
1617        defs
1618    }
1619
1620    fn errors(source: &str, packages: &[&PackageDeclaration]) -> Vec<String> {
1621        let parsed = super::parse_script(source, crate::FileId(0));
1622        let stdlib_defs = crate::runtime::stdlib_package_declarations();
1623        let (_ta, diags, _timings) =
1624            super::front_end(source, &parsed, &stdlib_defs, packages).expect("front end");
1625        diags
1626            .into_iter()
1627            .filter(|d| d.severity == crate::Severity::Error)
1628            .map(|d| d.message)
1629            .collect()
1630    }
1631
1632    fn compile_errors(source: &str, packages: &[&PackageDeclaration]) -> Vec<String> {
1633        super::compile_script(source, "main.subm", crate::FileId(0), packages, &[])
1634            .expect_err("expected compile error")
1635            .into_iter()
1636            .filter(|d| d.severity == crate::Severity::Error)
1637            .map(|d| d.message)
1638            .collect()
1639    }
1640
1641    #[test]
1642    fn package_declarations_passed_to_compile_are_importable() {
1643        let pied_piper = pied_piper_package();
1644        let source = r#"
1645            import { greet } from "@pied-piper/util";
1646            function main(): void {}
1647        "#;
1648        super::compile_script(source, "main.subm", crate::FileId(0), &[&pied_piper], &[])
1649            .expect("package compiles");
1650    }
1651
1652    #[test]
1653    fn package_not_passed_to_compile_uses_not_found_diagnostic() {
1654        let pied_piper = pied_piper_package();
1655        let source = r#"
1656            import { greet } from "@pied-piper/util";
1657            function main(): void {}
1658        "#;
1659        let _package_not_supplied = pied_piper;
1660        let errs = compile_errors(source, &[]);
1661        assert!(
1662            errs.iter()
1663                .any(|m| m.contains("package `@pied-piper/util` not found")),
1664            "expected not-found diagnostic, got: {errs:?}"
1665        );
1666    }
1667
1668    #[test]
1669    fn unknown_package_still_uses_not_found_diagnostic() {
1670        let source = r#"
1671            import { greet } from "@pied-piper/missing";
1672            function main(): void {}
1673        "#;
1674        let errs = compile_errors(source, &[]);
1675        assert!(
1676            errs.iter()
1677                .any(|m| m.contains("package `@pied-piper/missing` not found")),
1678            "expected not-found diagnostic, got: {errs:?}"
1679        );
1680    }
1681
1682    #[test]
1683    fn stdlib_imports_do_not_require_blueprint_packages() {
1684        let source = r#"
1685            import { v4 } from "submilli:uuid";
1686            function main(): void {}
1687        "#;
1688        super::compile_script(source, "main.subm", crate::FileId(0), &[], &[])
1689            .expect("stdlib package compiles without package declarations");
1690    }
1691
1692    #[test]
1693    fn importless_interface_member_access() {
1694        let synth = synth_package();
1695        // Imports only the function — never names `Widget`.
1696        let source = r#"
1697            import { makeWidget } from "submilli:synth";
1698            function main(): void {
1699                const w = makeWidget();
1700                assert(w.label === "x", "property read");
1701                const n: number = w.size();
1702                assert(n >= 0, "method call");
1703            }
1704        "#;
1705        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1706    }
1707
1708    #[test]
1709    fn importless_enum_value_use() {
1710        let synth = synth_package();
1711        // Imports only the function — never names `Color`.
1712        let source = r#"
1713            import { pickColor } from "submilli:synth";
1714            function main(): void {
1715                const a = pickColor();
1716                const b = pickColor();
1717                assert(a === b, "enum values compare");
1718            }
1719        "#;
1720        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1721    }
1722
1723    #[test]
1724    fn importless_recursive_alias_deep_member_access() {
1725        // `makeTree(): Tree` where `Tree = { value: number; children: Tree[] }`.
1726        // Reaching `children[0]` resolves a `Type::AliasRef` recursion back-edge
1727        // by its package — the alias name `Tree` is never imported. This guards
1728        // the registry routing in `rehydrate_alias_refs` / `expand_alias_ref`.
1729        let synth = synth_package();
1730        let source = r#"
1731            import { makeTree } from "submilli:synth";
1732            function main(): void {
1733                const t = makeTree();
1734                const root: number = t.value;
1735                assert(root >= 0, "root value");
1736                const child: number = t.children[0].value;
1737                assert(child >= 0, "deep recursive-alias value");
1738                const grand: number = t.children[0].children[0].value;
1739                assert(grand >= 0, "two levels deep");
1740            }
1741        "#;
1742        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1743    }
1744
1745    #[test]
1746    fn importless_recursive_alias_resolves_body_shape() {
1747        // Reading a *nonexistent* field on a deep recursion back-edge must still
1748        // produce a precise field-not-found diagnostic — proving the body shape
1749        // resolved through the registry rather than collapsing to `Error` (which
1750        // would silently swallow the access).
1751        let synth = synth_package();
1752        let source = r#"
1753            import { makeTree } from "submilli:synth";
1754            function main(): void {
1755                const t = makeTree();
1756                const _x = t.children[0].nope;
1757            }
1758        "#;
1759        let errs = errors(source, &[&synth]);
1760        assert!(
1761            errs.iter().any(|m| m.contains("nope")),
1762            "expected a field-not-found error naming `nope`; got: {errs:?}"
1763        );
1764    }
1765
1766    #[test]
1767    fn unimported_type_name_still_unresolved_in_source() {
1768        // The fix must NOT make the library type name resolvable as a *source*
1769        // annotation without an import — only structural access is import-free.
1770        let synth = synth_package();
1771        let source = r#"
1772            import { makeWidget } from "submilli:synth";
1773            function main(): void {
1774                const w: Widget = makeWidget();
1775            }
1776        "#;
1777        let errs = errors(source, &[&synth]);
1778        assert!(
1779            errs.iter().any(|m| m.contains("unknown type `Widget`")),
1780            "expected unknown-type error for unimported source annotation; got: {errs:?}"
1781        );
1782    }
1783}
1784
1785#[cfg(test)]
1786mod untyped_arena_failures {
1787    use super::*;
1788    use crate::arena::{ArenaKind, with_node_limit};
1789    use crate::compiler_error::{CompilerFailure, CompilerStage};
1790
1791    #[test]
1792    fn parser_arena_limit_is_fatal_and_keeps_prior_diagnostics() {
1793        let source = "let = 0; function main(): number { return 1; }";
1794        for arena in [ArenaKind::Expressions, ArenaKind::Statements] {
1795            let error = with_node_limit(arena, 0, || {
1796                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1797            })
1798            .unwrap_err();
1799            assert!(matches!(
1800                error.fatal,
1801                Some(CompilerFailure::Limit {
1802                    stage: CompilerStage::Parse,
1803                    span: None,
1804                    ..
1805                })
1806            ));
1807            assert!(
1808                !error.diagnostics.is_empty(),
1809                "earlier syntax diagnostics must survive"
1810            );
1811        }
1812        assert!(
1813            !compile_script_checked(
1814                "function main(): number { return 1; }",
1815                "healthy.ts",
1816                FileId(0),
1817                &[],
1818                &[]
1819            )
1820            .unwrap()
1821            .wasm
1822            .is_empty()
1823        );
1824    }
1825
1826    #[test]
1827    fn lowering_arena_limit_stops_script_before_inference() {
1828        let source = "function main(): number { const [x] = [1]; return x; }";
1829        let mut lexer = crate::Asi::new(source, FileId(0));
1830        let mut tokens = Vec::new();
1831        loop {
1832            let token = lexer.next_token();
1833            let done = matches!(token.kind, crate::TokenKind::Eof);
1834            tokens.push(token);
1835            if done {
1836                break;
1837            }
1838        }
1839        let (ast, _) = crate::parser::parse_checked(source, tokens, FileId(0)).unwrap();
1840        for (arena, count) in [
1841            (ArenaKind::Expressions, ast.exprs_len()),
1842            (ArenaKind::Statements, ast.stmts_len()),
1843        ] {
1844            let error = with_node_limit(arena, u32::try_from(count).unwrap(), || {
1845                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1846            })
1847            .unwrap_err();
1848            assert!(matches!(
1849                error.fatal,
1850                Some(CompilerFailure::Limit {
1851                    stage: CompilerStage::Infer,
1852                    span: None,
1853                    ..
1854                })
1855            ));
1856        }
1857        assert!(
1858            !compile_script_checked(source, "healthy.ts", FileId(0), &[], &[])
1859                .unwrap()
1860                .wasm
1861                .is_empty()
1862        );
1863    }
1864
1865    #[test]
1866    fn package_arena_limit_returns_no_artifact() {
1867        let modules = [PackageSourceModule {
1868            path: ModulePath::from("lib"),
1869            source: "export function value(): number { return 1; }",
1870        }];
1871        let error = with_node_limit(ArenaKind::Expressions, 0, || {
1872            compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1873        })
1874        .unwrap_err();
1875        assert!(matches!(
1876            error.fatal,
1877            Some(CompilerFailure::Limit {
1878                stage: CompilerStage::Parse,
1879                ..
1880            })
1881        ));
1882        compile_package_checked("test", ModulePath::from("lib"), &modules, &[]).unwrap();
1883    }
1884
1885    #[test]
1886    fn corrupt_parsed_imports_return_an_internal_failure() {
1887        let mut parsed = parse_script("function main(): number { return 1; }", FileId(0));
1888        parsed.ast.top_level.push(crate::StmtId(u32::MAX));
1889        let error = parsed.external_imports().unwrap_err();
1890        assert!(matches!(
1891            error.fatal,
1892            Some(CompilerFailure::Internal { span: None, .. })
1893        ));
1894    }
1895}
1896
1897#[cfg(test)]
1898mod typed_arena_failures {
1899    use super::*;
1900    use crate::arena::{ArenaKind, with_node_limit};
1901
1902    fn assert_limit(error: CompileError) {
1903        assert!(
1904            matches!(error.fatal, Some(CompilerFailure::Limit { span: None, .. })),
1905            "{error:?}"
1906        );
1907    }
1908
1909    #[test]
1910    fn typed_allocation_failure_stops_script_and_package_compilation() {
1911        let source = "function main(): number { return 1; }";
1912        let modules = [PackageSourceModule {
1913            path: ModulePath::from("lib"),
1914            source: "export function value(): number { return 1; }",
1915        }];
1916        for arena in [ArenaKind::TypedExpressions, ArenaKind::TypedStatements] {
1917            assert_limit(
1918                with_node_limit(arena, 0, || {
1919                    compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1920                })
1921                .unwrap_err(),
1922            );
1923            assert_limit(
1924                with_node_limit(arena, 0, || {
1925                    compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1926                })
1927                .unwrap_err(),
1928            );
1929        }
1930        assert!(
1931            !compile_script_checked(source, "healthy.ts", FileId(0), &[], &[])
1932                .unwrap()
1933                .wasm
1934                .is_empty()
1935        );
1936        assert!(
1937            !compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1938                .unwrap()
1939                .wasm
1940                .is_empty()
1941        );
1942    }
1943
1944    #[test]
1945    fn typed_allocation_failure_keeps_prior_inference_diagnostics() {
1946        let source = "function main(): number { let value: number = \"wrong\"; return 2; }";
1947        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1948        assert!(!diagnostics.is_empty());
1949        for (arena, count) in [
1950            (ArenaKind::TypedExpressions, ast.exprs_len()),
1951            (ArenaKind::TypedStatements, ast.stmts_len()),
1952        ] {
1953            let error = with_node_limit(arena, u32::try_from(count - 1).unwrap(), || {
1954                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1955            })
1956            .unwrap_err();
1957            assert!(!error.diagnostics.is_empty(), "{error:?}");
1958            assert_limit(error);
1959        }
1960    }
1961
1962    #[test]
1963    fn codegen_allocation_failure_returns_no_wasm() {
1964        let source = "function main(): number { let value: number | null = 1; if (value !== null) { return value; } return 0; }";
1965        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1966        assert!(diagnostics.is_empty());
1967        let ast = desugar(capture(ast).unwrap(), FileId(0)).unwrap();
1968        let (prelude, host, internal) = prelude::cached_runtime_package_declarations();
1969        let dependencies: Vec<_> = prelude.iter().chain(host).chain(internal).collect();
1970        let failure = with_node_limit(
1971            ArenaKind::TypedExpressions,
1972            u32::try_from(ast.exprs_len()).unwrap(),
1973            || crate::codegen::codegen(source, "limit.ts", FileId(0), &ast, &dependencies),
1974        )
1975        .unwrap_err();
1976        assert!(
1977            matches!(
1978                failure,
1979                CompilerFailure::Limit {
1980                    stage: CompilerStage::Codegen,
1981                    span: None,
1982                    ..
1983                }
1984            ),
1985            "{failure}"
1986        );
1987        assert!(
1988            !crate::codegen::codegen(source, "healthy.ts", FileId(0), &ast, &dependencies)
1989                .unwrap()
1990                .is_empty()
1991        );
1992    }
1993
1994    #[test]
1995    fn desugaring_capacity_failure_does_not_return_a_transformed_tree() {
1996        let source = "function main(): void { do {} while (false); }";
1997        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1998        assert!(diagnostics.is_empty());
1999        for (arena, count) in [
2000            (ArenaKind::TypedExpressions, ast.exprs_len()),
2001            (ArenaKind::TypedStatements, ast.stmts_len()),
2002        ] {
2003            let failure = with_node_limit(arena, u32::try_from(count).unwrap(), || {
2004                desugar(ast.clone(), FileId(0))
2005            })
2006            .unwrap_err();
2007            assert_limit(failure.into());
2008        }
2009        desugar(ast, FileId(0)).unwrap();
2010    }
2011}