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