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::rules::check_script;
14use crate::typed_ast::TypedAst;
15use crate::{
16    Asi, Ast, DerivedCapability, Diagnostic, FileId, ModulePath, PackageDeclaration, Severity,
17    Sources, StmtKind, Token, TokenKind, capture, desugar, lower_patterns, runtime,
18};
19
20/// Wall-clock spent in each compile phase, filled in as the pipeline runs. A
21/// phase that never runs (e.g. `codegen` when the front-end errors) stays zero.
22/// Surfaced on [`CompiledScript`] for embedders that report compile-time
23/// performance.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub struct PhaseTimings {
26    pub lex: Duration,
27    /// Parse plus pattern-lowering — both run together in the front-end.
28    pub parse: Duration,
29    /// Type inference plus the post-inference check pass.
30    pub typecheck: Duration,
31    pub capture: Duration,
32    pub desugar: Duration,
33    pub codegen: Duration,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct ScriptImports {
38    pub stdlib: BTreeSet<String>,
39    pub registry_packages: BTreeSet<String>,
40    pub mcp_servers: BTreeSet<String>,
41}
42
43#[derive(Clone, Debug)]
44pub struct ParsedScript {
45    file: FileId,
46    source: String,
47    failure: Option<CompileError>,
48    ast: Ast,
49    diagnostics: Vec<Diagnostic>,
50    timings: PhaseTimings,
51}
52
53impl ParsedScript {
54    pub fn external_imports(&self) -> Result<ScriptImports, CompileError> {
55        let stdlib_names: BTreeSet<String> = runtime::stdlib_package_declarations()
56            .into_iter()
57            .map(|defs| defs.package_name)
58            .collect();
59        let mut imports = ScriptImports::default();
60        for stmt_id in &self.ast.top_level {
61            let StmtKind::Import { module, .. } = &self
62                .ast
63                .try_stmt(*stmt_id)
64                .map_err(|error| {
65                    CompileError::from(
66                        error.into_compiler_failure(crate::compiler_error::CompilerStage::Parse),
67                    )
68                    .with_prior_diagnostics(&self.diagnostics)
69                })?
70                .kind
71            else {
72                continue;
73            };
74            if crate::source::is_relative_specifier(module) {
75                continue;
76            }
77            if let Some(server) = module.strip_prefix("@mcp/") {
78                imports.mcp_servers.insert(server.to_string());
79            } else if stdlib_names.contains(module)
80                || crate::stdlib::OptionalPackage::from_module_name(module).is_some()
81            {
82                // An optional package counts as stdlib whether or not the embedder
83                // enables it: a disabled one is reported as unavailable, never
84                // looked up as a registry package.
85                imports.stdlib.insert(module.clone());
86            } else {
87                imports.registry_packages.insert(module.clone());
88            }
89        }
90        Ok(imports)
91    }
92
93    pub fn has_errors(&self) -> bool {
94        self.failure.is_some() || has_errors(&self.diagnostics)
95    }
96
97    pub fn diagnostics(&self) -> &[Diagnostic] {
98        &self.diagnostics
99    }
100}
101
102pub fn parse_script(source: &str, file: FileId) -> ParsedScript {
103    let mut timings = PhaseTimings::default();
104
105    let lex_start = Instant::now();
106    let mut asi = Asi::new(source, file);
107    let mut tokens: Vec<Token> = Vec::new();
108    loop {
109        let tok = asi.next_token();
110        let is_eof = matches!(tok.kind, TokenKind::Eof);
111        tokens.push(tok);
112        if is_eof {
113            break;
114        }
115    }
116    let (mut diagnostics, mut failure) = match asi.finish() {
117        Ok(diagnostics) => (diagnostics, None),
118        Err(error) => (error.clone().into_diagnostics(file), Some(error)),
119    };
120    timings.lex = lex_start.elapsed();
121
122    let parse_start = Instant::now();
123    let (mut ast, parse_diags) = if failure.is_some() {
124        (Ast::new(), Vec::new())
125    } else {
126        match crate::parser::parse_checked(source, tokens, file) {
127            Ok(result) => result,
128            Err(mut error) => {
129                error.diagnostics.splice(0..0, diagnostics.clone());
130                let rendered = error.clone().into_diagnostics(file);
131                diagnostics.clear();
132                failure = Some(error);
133                (Ast::new(), rendered)
134            }
135        }
136    };
137    diagnostics.extend(parse_diags);
138    // must run before type-checking; the typechecker assumes patterns are already lowered
139    if failure.is_none() {
140        match lower_patterns(ast) {
141            Ok(lowered) => ast = lowered,
142            Err(fatal) => {
143                let error = CompileError {
144                    diagnostics: diagnostics.clone(),
145                    fatal: Some(fatal),
146                };
147                diagnostics = error.clone().into_diagnostics(file);
148                failure = Some(error);
149                ast = Ast::new();
150            }
151        }
152    }
153    timings.parse = parse_start.elapsed();
154
155    ParsedScript {
156        file,
157        source: source.to_owned(),
158        failure,
159        ast,
160        diagnostics,
161        timings,
162    }
163}
164
165/// Run the front-end (infer → check) and return the typed
166/// AST alongside the accumulated diagnostics and per-phase timings. Shared by
167/// [`compile_script`] and [`typecheck`]; the typed AST is only meaningful when
168/// `diags` is error-free. `file` is the [`FileId`] the caller assigned this
169/// source in its [`Sources`](crate::Sources) registry; every span is stamped
170/// with it so diagnostics resolve against that entry. `packages` join the stdlib
171/// in the set the typechecker resolves imports against (e.g. the server's
172/// `@mcp/<server>` virtual packages).
173fn front_end(
174    source: &str,
175    parsed: &ParsedScript,
176    stdlib_defs: &[PackageDeclaration],
177    packages: &[&PackageDeclaration],
178) -> Result<(TypedAst, Vec<Diagnostic>, PhaseTimings), CompileError> {
179    front_end_with_transitive(source, parsed, stdlib_defs, packages, &[])
180}
181
182fn front_end_with_transitive(
183    source: &str,
184    parsed: &ParsedScript,
185    stdlib_defs: &[PackageDeclaration],
186    packages: &[&PackageDeclaration],
187    transitive: &[&PackageDeclaration],
188) -> Result<(TypedAst, Vec<Diagnostic>, PhaseTimings), CompileError> {
189    if source != parsed.source {
190        return Err(CompilerFailure::Internal {
191            stage: CompilerStage::Infer,
192            span: None,
193            message: "parsed script does not belong to the supplied source".into(),
194        }
195        .into());
196    }
197    if let Some(error) = &parsed.failure {
198        return Err(error.clone());
199    }
200    let mut timings = parsed.timings;
201    let mut diags = parsed.diagnostics.clone();
202    let typecheck_start = Instant::now();
203    let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
204    let mut infer_refs: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
205    infer_refs.extend(host_defs.iter());
206    infer_refs.extend(stdlib_defs.iter());
207    infer_refs.extend_from_slice(packages);
208    let (ta, infer_diags) = crate::typechecker::infer::infer_with_transitive_checked(
209        source,
210        crate::mangle::USER_PACKAGE,
211        &parsed.ast,
212        &infer_refs,
213        transitive,
214    )
215    .map_err(|mut error| {
216        error.diagnostics.splice(0..0, diags.clone());
217        error
218    })?;
219    diags.extend(infer_diags);
220    infer_refs.extend_from_slice(transitive);
221    diags.extend(
222        check_script(&ta, &infer_refs).map_err(|error| error.with_prior_diagnostics(&diags))?,
223    );
224    timings.typecheck = typecheck_start.elapsed();
225
226    Ok((ta, diags, timings))
227}
228
229#[derive(Clone, Debug)]
230pub struct PackageSourceModule<'a> {
231    pub path: ModulePath,
232    pub source: &'a str,
233}
234
235#[derive(Clone, Debug, PartialEq)]
236pub struct CompiledPackage {
237    pub wasm: Vec<u8>,
238    pub type_info: crate::TypeInfoTable,
239    pub declaration: PackageDeclaration,
240    pub required_capabilities: Vec<DerivedCapability>,
241    pub authority_map: crate::AuthorityMap,
242    pub warnings: Vec<Diagnostic>,
243}
244
245#[derive(Clone, Debug)]
246pub struct CompiledScript {
247    pub wasm: Vec<u8>,
248    pub type_info: crate::TypeInfoTable,
249    pub warnings: Vec<Diagnostic>,
250    pub timings: PhaseTimings,
251}
252
253/// Typecheck only — runs the front-end and stops before codegen. Returns
254/// `Ok(warnings)` when the program is error-free (warnings are non-fatal),
255/// `Err(diagnostics)` if any phase errors. Backs `submilli check`.
256pub fn typecheck_checked(source: &str, file: FileId) -> Result<Vec<Diagnostic>, CompileError> {
257    typecheck_script(source, file).map_err(|error| {
258        error.with_limit_span_cut(|span_file| (span_file == file).then_some(source))
259    })
260}
261
262fn typecheck_script(source: &str, file: FileId) -> Result<Vec<Diagnostic>, CompileError> {
263    let parsed = parse_script(source, file);
264    let stdlib_defs = runtime::stdlib_package_declarations();
265    typecheck_parsed_script(source, &parsed, &stdlib_defs)
266}
267
268/// [`typecheck_checked`] for an already parsed script, against `stdlib_defs`
269/// (see [`Stdlib::package_declarations`](crate::stdlib::Stdlib::package_declarations)).
270pub fn typecheck_parsed_checked(
271    source: &str,
272    parsed: &ParsedScript,
273    stdlib_defs: &[PackageDeclaration],
274) -> Result<Vec<Diagnostic>, CompileError> {
275    let file = parsed.file;
276    typecheck_parsed_script(source, parsed, stdlib_defs).map_err(|error| {
277        error.with_limit_span_cut(|span_file| (span_file == file).then_some(source))
278    })
279}
280
281fn typecheck_parsed_script(
282    source: &str,
283    parsed: &ParsedScript,
284    stdlib_defs: &[PackageDeclaration],
285) -> Result<Vec<Diagnostic>, CompileError> {
286    let file = parsed.file;
287    let (ta, diags, _timings) = front_end(source, parsed, stdlib_defs, &[])?;
288    if has_errors(&diags) {
289        return Err(diags.into());
290    }
291    // Lowering deepens the typed tree, so check enforces compilation's limit on
292    // the lowered tree too.
293    let with_front_end_diagnostics = |failure| CompileError {
294        diagnostics: diags.clone(),
295        fatal: Some(failure),
296    };
297    let ta = capture(ta).map_err(with_front_end_diagnostics)?;
298    desugar(ta, file).map_err(with_front_end_diagnostics)?;
299    Ok(warnings_only(diags))
300}
301
302/// Typecheck and return the typed AST together with every diagnostic, errors
303/// included. For tools that inspect what the typechecker inferred — the
304/// TypeScript baseline comparison in `crates/conformance` — rather than compile.
305/// The typed AST is only meaningful where the diagnostics are error-free.
306pub fn typecheck_to_typed_ast(source: &str, file: FileId) -> (TypedAst, Vec<Diagnostic>) {
307    let parsed = parse_script(source, file);
308    let stdlib_defs = runtime::stdlib_package_declarations();
309    match front_end(source, &parsed, &stdlib_defs, &[]) {
310        Ok((ta, diags, _)) => (ta, diags),
311        Err(error) => (
312            TypedAst::new(),
313            error
314                .with_limit_span_cut(|span_file| (span_file == file).then_some(source))
315                .into_diagnostics(file),
316        ),
317    }
318}
319
320/// Compile a script module. `filename` is the DWARF compile-unit name for
321/// backtrace source mapping; `file` is this source's [`FileId`] in the caller's
322/// [`Sources`](crate::Sources) registry. `packages` are real package
323/// declarations that emit Wasm imports; `mcps` are declaration-only virtual MCP
324/// packages.
325///
326/// Returns [`CompiledScript`] on success. Warnings are non-fatal and should be
327/// surfaced without aborting; callers that only need Wasm bytes can read
328/// [`CompiledScript::wasm`]. Returns `Err(diagnostics)` if any phase errors.
329pub fn compile_script_checked(
330    source: &str,
331    filename: &str,
332    file: FileId,
333    packages: &[&PackageDeclaration],
334    mcps: &[&PackageDeclaration],
335) -> Result<CompiledScript, CompileError> {
336    let parsed = parse_script(source, file);
337    let stdlib_defs = runtime::stdlib_package_declarations();
338    compile_parsed_script_timed_checked(source, filename, &parsed, &stdlib_defs, packages, mcps)
339}
340
341/// Compiles a script whose code belongs to `owning_package` rather than to `main`.
342///
343/// The one caller is a package's test file: it compiles as a script, so its entry point stays
344/// mangled as `main`, but its gated calls must be attributed to the package that ships it.
345pub fn compile_script_owned_by_checked(
346    owning_package: &str,
347    source: &str,
348    filename: &str,
349    file: FileId,
350    packages: &[&PackageDeclaration],
351    mcps: &[&PackageDeclaration],
352) -> Result<CompiledScript, CompileError> {
353    let parsed = parse_script(source, file);
354    let stdlib_defs = runtime::stdlib_package_declarations();
355    let declarations = [packages, mcps].concat();
356    compile_parsed_script_owned_by(
357        Some(owning_package),
358        source,
359        filename,
360        &parsed,
361        &stdlib_defs,
362        &declarations,
363        &[],
364    )
365}
366
367pub fn compile_parsed_script_timed_checked(
368    source: &str,
369    filename: &str,
370    parsed: &ParsedScript,
371    stdlib_defs: &[PackageDeclaration],
372    packages: &[&PackageDeclaration],
373    mcps: &[&PackageDeclaration],
374) -> Result<CompiledScript, CompileError> {
375    compile_parsed_script_with_transitive_checked(
376        source,
377        filename,
378        parsed,
379        stdlib_defs,
380        packages,
381        mcps,
382        &[],
383    )
384}
385
386/// Compile with declarations needed to reconstruct imported types without
387/// exposing those transitive packages to source-level imports.
388pub fn compile_parsed_script_with_transitive_checked(
389    source: &str,
390    filename: &str,
391    parsed: &ParsedScript,
392    stdlib_defs: &[PackageDeclaration],
393    packages: &[&PackageDeclaration],
394    mcps: &[&PackageDeclaration],
395    transitive: &[&PackageDeclaration],
396) -> Result<CompiledScript, CompileError> {
397    let declarations = [packages, mcps].concat();
398    compile_parsed_script_owned_by(
399        None,
400        source,
401        filename,
402        parsed,
403        stdlib_defs,
404        &declarations,
405        transitive,
406    )
407}
408
409fn compile_parsed_script_owned_by(
410    owning_package: Option<&str>,
411    source: &str,
412    filename: &str,
413    parsed: &ParsedScript,
414    stdlib_defs: &[PackageDeclaration],
415    external_declarations: &[&PackageDeclaration],
416    transitive: &[&PackageDeclaration],
417) -> Result<CompiledScript, CompileError> {
418    compile_parsed_script(
419        owning_package,
420        source,
421        filename,
422        parsed,
423        stdlib_defs,
424        external_declarations,
425        transitive,
426    )
427    .map_err(|error| error.with_limit_span_cut(|file| (file == parsed.file).then_some(source)))
428}
429
430fn compile_parsed_script(
431    owning_package: Option<&str>,
432    source: &str,
433    filename: &str,
434    parsed: &ParsedScript,
435    stdlib_defs: &[PackageDeclaration],
436    external_declarations: &[&PackageDeclaration],
437    transitive: &[&PackageDeclaration],
438) -> Result<CompiledScript, CompileError> {
439    let (mut ta, diags, mut timings) = front_end_with_transitive(
440        source,
441        parsed,
442        stdlib_defs,
443        external_declarations,
444        transitive,
445    )?;
446    if has_errors(&diags) {
447        return Err(diags.into());
448    }
449    let capture_start = Instant::now();
450    ta = capture(ta).map_err(|fatal| CompileError {
451        diagnostics: diags.clone(),
452        fatal: Some(fatal),
453    })?;
454    timings.capture = capture_start.elapsed();
455
456    let desugar_start = Instant::now();
457    ta = desugar(ta, parsed.file).map_err(|fatal| CompileError {
458        diagnostics: diags.clone(),
459        fatal: Some(fatal),
460    })?;
461    timings.desugar = desugar_start.elapsed();
462
463    let (prelude_defs, host_defs, internal_defs) = prelude::cached_runtime_package_declarations();
464    let mut dependencies: Vec<&crate::PackageDeclaration> = prelude_defs.iter().collect();
465    for defs in host_defs {
466        dependencies.push(defs);
467    }
468    for defs in internal_defs {
469        dependencies.push(defs);
470    }
471    for defs in stdlib_defs {
472        dependencies.push(defs);
473    }
474    dependencies.extend_from_slice(external_declarations);
475    dependencies.extend_from_slice(transitive);
476
477    let codegen_start = Instant::now();
478    let generated = match owning_package {
479        Some(pkg) => {
480            crate::codegen::codegen_owned_by(pkg, source, filename, parsed.file, &ta, &dependencies)
481        }
482        None => crate::codegen::codegen_with_type_info(
483            source,
484            filename,
485            parsed.file,
486            &ta,
487            &dependencies,
488        ),
489    }
490    .map_err(|fatal| CompileError {
491        diagnostics: diags.clone(),
492        fatal: Some(fatal),
493    })?;
494    timings.codegen = codegen_start.elapsed();
495
496    Ok(CompiledScript {
497        wasm: generated.wasm,
498        type_info: generated.type_info,
499        warnings: warnings_only(diags),
500        timings,
501    })
502}
503
504pub fn compile_package_checked(
505    package_name: &str,
506    root_module: ModulePath,
507    modules: &[PackageSourceModule<'_>],
508    dependencies: &[&PackageDeclaration],
509) -> Result<CompiledPackage, CompileError> {
510    compile_package_with_transitive_checked(package_name, root_module, modules, dependencies, &[])
511}
512
513/// [`compile_package`] with the rest of the dependency closure supplied
514/// separately.
515///
516/// `dependencies` is what the package declares, and only those are importable.
517/// `transitive` is everything they in turn depend on: a direct dependency's
518/// public surface can name a class from one of them — as a parent, a return
519/// type, a static's type — and resolving that needs the declaration even though
520/// this package must not `import` from it. Without it the ancestor walk
521/// truncates silently and codegen reconstructs a layout that doesn't
522/// canonicalize against the producer's, which surfaces at link time as
523/// `imported global type mismatch`.
524pub fn compile_package_with_transitive_checked(
525    package_name: &str,
526    root_module: ModulePath,
527    modules: &[PackageSourceModule<'_>],
528    dependencies: &[&PackageDeclaration],
529    transitive: &[&PackageDeclaration],
530) -> Result<CompiledPackage, CompileError> {
531    compile_package_with_transitive_checked_for(
532        Stdlib::core(),
533        package_name,
534        root_module,
535        modules,
536        dependencies,
537        transitive,
538    )
539}
540
541/// [`compile_package_with_transitive_checked`] for an embedder that offers
542/// `stdlib`, so the package may import its optional packages.
543pub fn compile_package_with_transitive_checked_for(
544    stdlib: Stdlib,
545    package_name: &str,
546    root_module: ModulePath,
547    modules: &[PackageSourceModule<'_>],
548    dependencies: &[&PackageDeclaration],
549    transitive: &[&PackageDeclaration],
550) -> Result<CompiledPackage, CompileError> {
551    let mut sources = Sources::new();
552    let stdlib_defs = stdlib.package_declarations();
553    compile_package_sources(
554        &mut sources,
555        &stdlib_defs,
556        package_name,
557        root_module,
558        modules,
559        dependencies,
560        transitive,
561    )
562    .map_err(|error| {
563        error.with_limit_span_cut(|file| sources.get(file).map(crate::SourceFile::text))
564    })
565}
566
567fn compile_package_sources(
568    sources: &mut Sources,
569    stdlib_defs: &[PackageDeclaration],
570    package_name: &str,
571    root_module: ModulePath,
572    modules: &[PackageSourceModule<'_>],
573    dependencies: &[&PackageDeclaration],
574    transitive: &[&PackageDeclaration],
575) -> Result<CompiledPackage, CompileError> {
576    // Public declarations may be constructed directly. Validate recursive
577    // metadata before cloning them into the inference registries.
578    crate::typechecker::infer::check_declaration_types(
579        dependencies.iter().chain(transitive).copied(),
580    )?;
581    let mut parsed_modules = Vec::with_capacity(modules.len());
582    let mut diagnostics = Vec::new();
583    for module in modules {
584        let file = sources
585            .add(module.path.clone(), module.source)
586            .map_err(|error| {
587                CompileError::from(error.into_compiler_failure(CompilerStage::Parse))
588                    .with_prior_diagnostics(&diagnostics)
589            })?;
590        let (ast, mut module_diags) =
591            parse_package_module(module.source, file).map_err(|mut error| {
592                error.diagnostics.splice(0..0, diagnostics.clone());
593                error
594            })?;
595        diagnostics.append(&mut module_diags);
596        parsed_modules.push((module.path.clone(), file, ast));
597    }
598    if has_errors(&diagnostics) {
599        return Err(diagnostics.into());
600    }
601
602    let module_refs: Vec<_> = parsed_modules
603        .iter()
604        .map(|(path, file, ast)| (path.clone(), *file, ast))
605        .collect();
606    let mut external_packages: BTreeMap<String, PackageDeclaration> = stdlib_defs
607        .iter()
608        .map(|defs| (defs.package_name.clone(), defs.clone()))
609        .collect();
610    let (prelude_defs, host_defs, _) = prelude::cached_runtime_package_declarations();
611    for defs in prelude_defs {
612        external_packages.insert(defs.package_name.clone(), defs.clone());
613    }
614    for defs in host_defs {
615        external_packages.insert(defs.package_name.clone(), defs.clone());
616    }
617    for defs in dependencies {
618        external_packages.insert(defs.package_name.clone(), (*defs).clone());
619    }
620    let transitive_packages: BTreeMap<String, PackageDeclaration> = transitive
621        .iter()
622        .filter(|defs| !external_packages.contains_key(&defs.package_name))
623        .map(|defs| (defs.package_name.clone(), (*defs).clone()))
624        .collect();
625    let (mut ta, mut declaration, mut package_diags) =
626        crate::typechecker::infer::infer_package_checked(
627            package_name,
628            root_module.clone(),
629            module_refs,
630            sources,
631            external_packages,
632            transitive_packages,
633        )
634        .map_err(|mut error| {
635            error.diagnostics.splice(0..0, diagnostics.clone());
636            error
637        })?;
638    diagnostics.append(&mut package_diags);
639    if has_errors(&diagnostics) {
640        return Err(diagnostics.into());
641    }
642    let (required_capabilities, capability_warnings) =
643        compile_capabilities::derive_package_requirements(
644            &declaration,
645            &ta,
646            stdlib_defs,
647            dependencies,
648            transitive,
649        )
650        .map_err(|error| error.with_prior_diagnostics(&diagnostics))?;
651    diagnostics.extend(capability_warnings);
652    let (prelude_defs, host_defs, internal_defs) = prelude::cached_runtime_package_declarations();
653    let authority_map = crate::authority::analyse(
654        &declaration,
655        &ta,
656        sources,
657        stdlib_defs
658            .iter()
659            .chain(prelude_defs)
660            .chain(host_defs)
661            .chain(dependencies.iter().copied())
662            .chain(transitive.iter().copied()),
663    )
664    .map_err(|fatal| CompileError {
665        diagnostics: diagnostics.clone(),
666        fatal: Some(fatal),
667    })?;
668    ta = capture(ta).map_err(|fatal| CompileError {
669        diagnostics: diagnostics.clone(),
670        fatal: Some(fatal),
671    })?;
672    let root_file = parsed_modules
673        .iter()
674        .find(|(path, _, _)| *path == root_module)
675        .map(|(_, file, _)| *file)
676        .ok_or_else(|| CompilerFailure::Internal {
677            stage: CompilerStage::Infer,
678            span: None,
679            message: "compiled package root module is missing".into(),
680        })?;
681    ta = desugar(ta, root_file).map_err(|fatal| CompileError {
682        diagnostics: diagnostics.clone(),
683        fatal: Some(fatal),
684    })?;
685
686    let mut codegen_deps: Vec<&PackageDeclaration> = prelude_defs.iter().collect();
687    codegen_deps.extend(host_defs.iter());
688    codegen_deps.extend(internal_defs.iter());
689    codegen_deps.extend(stdlib_defs.iter());
690    codegen_deps.extend_from_slice(dependencies);
691    codegen_deps.extend_from_slice(transitive);
692    let generated =
693        crate::codegen::codegen_package_with_type_info(sources, root_file, &ta, &codegen_deps)
694            .map_err(|fatal| CompileError {
695                diagnostics: diagnostics.clone(),
696                fatal: Some(fatal),
697            })?;
698    declaration.runtime_functions = generated.runtime_functions;
699    declaration.runtime_globals = generated.runtime_globals;
700    declaration.closure_caches = generated.closure_caches;
701    Ok(CompiledPackage {
702        wasm: generated.wasm,
703        type_info: generated.type_info,
704        declaration,
705        required_capabilities,
706        authority_map,
707        warnings: warnings_only(diagnostics),
708    })
709}
710
711fn parse_package_module(
712    source: &str,
713    file: FileId,
714) -> Result<(crate::Ast, Vec<Diagnostic>), CompileError> {
715    let mut asi = Asi::new(source, file);
716    let mut tokens: Vec<Token> = Vec::new();
717    loop {
718        let tok = asi.next_token();
719        let is_eof = matches!(tok.kind, TokenKind::Eof);
720        tokens.push(tok);
721        if is_eof {
722            break;
723        }
724    }
725    let mut diagnostics = asi.finish()?;
726    let (mut ast, parse_diags) =
727        crate::parser::parse_checked(source, tokens, file).map_err(|mut error| {
728            error.diagnostics.splice(0..0, diagnostics.clone());
729            error
730        })?;
731    diagnostics.extend(parse_diags);
732    if !has_errors(&diagnostics) {
733        ast = lower_patterns(ast).map_err(|fatal| CompileError {
734            diagnostics: diagnostics.clone(),
735            fatal: Some(fatal),
736        })?;
737    }
738    Ok((ast, diagnostics))
739}
740
741fn has_errors(diags: &[Diagnostic]) -> bool {
742    diags.iter().any(|d| d.severity == Severity::Error)
743}
744
745fn warnings_only(diags: Vec<Diagnostic>) -> Vec<Diagnostic> {
746    diags
747        .into_iter()
748        .filter(|d| d.severity == Severity::Warning)
749        .collect()
750}
751
752/// Compatibility adapter returning source and fatal failures as diagnostics.
753pub fn typecheck(source: &str, file: FileId) -> Result<Vec<Diagnostic>, Vec<Diagnostic>> {
754    typecheck_checked(source, file).map_err(|error| error.into_diagnostics(file))
755}
756
757/// Compatibility adapter returning source and fatal failures as diagnostics.
758pub fn compile_script(
759    source: &str,
760    filename: &str,
761    file: FileId,
762    packages: &[&PackageDeclaration],
763    mcps: &[&PackageDeclaration],
764) -> Result<CompiledScript, Vec<Diagnostic>> {
765    compile_script_checked(source, filename, file, packages, mcps)
766        .map_err(|error| error.into_diagnostics(file))
767}
768
769/// Compatibility adapter returning source and fatal failures as diagnostics.
770pub fn compile_script_owned_by(
771    owning_package: &str,
772    source: &str,
773    filename: &str,
774    file: FileId,
775    packages: &[&PackageDeclaration],
776    mcps: &[&PackageDeclaration],
777) -> Result<CompiledScript, Vec<Diagnostic>> {
778    compile_script_owned_by_checked(owning_package, source, filename, file, packages, mcps)
779        .map_err(|error| error.into_diagnostics(file))
780}
781
782/// Compatibility adapter returning source and fatal failures as diagnostics.
783pub fn compile_parsed_script_timed(
784    source: &str,
785    filename: &str,
786    parsed: &ParsedScript,
787    stdlib_defs: &[PackageDeclaration],
788    packages: &[&PackageDeclaration],
789    mcps: &[&PackageDeclaration],
790) -> Result<CompiledScript, Vec<Diagnostic>> {
791    compile_parsed_script_timed_checked(source, filename, parsed, stdlib_defs, packages, mcps)
792        .map_err(|error| error.into_diagnostics(parsed.file))
793}
794
795/// Compatibility adapter returning source and fatal failures as diagnostics.
796pub fn compile_parsed_script_with_transitive(
797    source: &str,
798    filename: &str,
799    parsed: &ParsedScript,
800    stdlib_defs: &[PackageDeclaration],
801    packages: &[&PackageDeclaration],
802    mcps: &[&PackageDeclaration],
803    transitive: &[&PackageDeclaration],
804) -> Result<CompiledScript, Vec<Diagnostic>> {
805    compile_parsed_script_with_transitive_checked(
806        source,
807        filename,
808        parsed,
809        stdlib_defs,
810        packages,
811        mcps,
812        transitive,
813    )
814    .map_err(|error| error.into_diagnostics(parsed.file))
815}
816
817/// Compatibility adapter returning source and fatal failures as diagnostics.
818pub fn compile_package(
819    package_name: &str,
820    root_module: ModulePath,
821    modules: &[PackageSourceModule<'_>],
822    dependencies: &[&PackageDeclaration],
823) -> Result<CompiledPackage, Vec<Diagnostic>> {
824    compile_package_checked(package_name, root_module, modules, dependencies)
825        .map_err(|error| error.into_diagnostics(FileId(0)))
826}
827
828/// Compatibility adapter returning source and fatal failures as diagnostics.
829pub fn compile_package_with_transitive(
830    package_name: &str,
831    root_module: ModulePath,
832    modules: &[PackageSourceModule<'_>],
833    dependencies: &[&PackageDeclaration],
834    transitive: &[&PackageDeclaration],
835) -> Result<CompiledPackage, Vec<Diagnostic>> {
836    compile_package_with_transitive_checked(
837        package_name,
838        root_module,
839        modules,
840        dependencies,
841        transitive,
842    )
843    .map_err(|error| error.into_diagnostics(FileId(0)))
844}
845
846#[cfg(test)]
847mod parsed_script_tests {
848    use super::*;
849
850    #[test]
851    fn typed_compile_errors_distinguish_source_limits_and_internal_failures() {
852        let source_error = compile_script_checked(
853            "function main(): number { return false; }",
854            "test.ts",
855            FileId(0),
856            &[],
857            &[],
858        )
859        .expect_err("source type mismatch");
860        assert!(source_error.fatal.is_none());
861        assert!(!source_error.diagnostics.is_empty());
862
863        let nested = format!(
864            "function main(): number {{ return {}1{}; }}",
865            "(".repeat(2048),
866            ")".repeat(2048)
867        );
868        let limit = compile_script_checked(&nested, "test.ts", FileId(0), &[], &[])
869            .expect_err("parser limit");
870        assert!(matches!(
871            limit.fatal,
872            Some(CompilerFailure::Limit {
873                stage: CompilerStage::Parse,
874                ..
875            })
876        ));
877        let rendered = limit.into_diagnostics(FileId(0));
878        assert_eq!(
879            rendered
880                .iter()
881                .filter(|d| d.message.contains("parser recursion limit exceeded"))
882                .count(),
883            1
884        );
885
886        let parsed = parse_script("function main(): number { return 42; }", FileId(0));
887        let internal = compile_parsed_script_timed_checked(
888            "different source",
889            "test.ts",
890            &parsed,
891            &[],
892            &[],
893            &[],
894        )
895        .expect_err("source metadata mismatch");
896        assert!(matches!(
897            internal.fatal,
898            Some(CompilerFailure::Internal {
899                stage: CompilerStage::Infer,
900                ..
901            })
902        ));
903    }
904
905    #[test]
906    fn external_imports_come_from_parsed_imports_only() {
907        let parsed = parse_script(
908            r#"
909                // import fake from "@mcp/commented";
910                const s: string = "import x from \"@acme/string\"";
911                import { v4 } from "submilli:uuid";
912                import linear from "@mcp/linear";
913                import { answer } from "@acme/util";
914                function main(): string { return v4(); }
915            "#,
916            FileId(0),
917        );
918
919        let imports = parsed.external_imports().unwrap();
920
921        assert_eq!(
922            imports.stdlib,
923            BTreeSet::from(["submilli:uuid".to_string()])
924        );
925        assert_eq!(imports.mcp_servers, BTreeSet::from(["linear".to_string()]));
926        assert_eq!(
927            imports.registry_packages,
928            BTreeSet::from(["@acme/util".to_string()])
929        );
930    }
931
932    #[test]
933    fn relative_imports_are_not_external_imports() {
934        let parsed = parse_script(
935            r#"
936                import { local } from "./util";
937                function main(): number { return 1; }
938            "#,
939            FileId(0),
940        );
941
942        assert_eq!(parsed.external_imports().unwrap(), ScriptImports::default());
943    }
944}
945
946#[cfg(test)]
947mod importless_library_tests {
948    //! SUB-386: a value whose type comes from a library must support member
949    //! access and use without the consumer separately importing the type name.
950    //! The http tests cover this against the real stdlib; these exercise the
951    //! mechanism for an arbitrary package declaration — proving the FQN registry is
952    //! populated from `packages_by_name`, not just the prelude — and for the
953    //! other by-name return shapes (an interface *and* an enum).
954
955    use std::collections::BTreeMap;
956
957    use crate::mangle::package_symbol;
958    use crate::package_declaration::{Dispatch, MethodSig, PropertySig, TypeKind, ValueKind};
959    use crate::{ObjectField, Package, PackageDeclaration, Span, Type, TypeSymbol, ValueSymbol};
960
961    const SYNTH: &str = "submilli:synth";
962    const ACME: &str = "@acme/util";
963
964    fn synth_package() -> PackageDeclaration {
965        let mut defs = PackageDeclaration::with_package(SYNTH);
966
967        let mut properties = BTreeMap::new();
968        properties.insert(
969            "label".to_string(),
970            PropertySig {
971                ty: Type::String,
972                readonly: true,
973                intrinsic: false,
974                optional: false,
975                doc: None,
976            },
977        );
978        let mut methods = BTreeMap::new();
979        methods.insert(
980            "size".to_string(),
981            MethodSig {
982                optional: false,
983                generics: Vec::new(),
984                params: Vec::new(),
985                ret: Type::Number,
986                predicate: None,
987                doc: None,
988            },
989        );
990        defs.types.insert(
991            "Widget".to_string(),
992            TypeSymbol {
993                name: "Widget".to_string(),
994                mangled_name: package_symbol(SYNTH, "Widget"),
995                declaration_span: Span::at(crate::FileId(0)),
996                kind: TypeKind::Interface {
997                    index: None,
998                    generics: Vec::new(),
999                    methods,
1000                    properties,
1001                    dispatch: Dispatch::Direct,
1002                    doc: None,
1003                },
1004            },
1005        );
1006
1007        defs.types.insert(
1008            "Color".to_string(),
1009            TypeSymbol {
1010                name: "Color".to_string(),
1011                mangled_name: package_symbol(SYNTH, "Color"),
1012                declaration_span: Span::at(crate::FileId(0)),
1013                kind: TypeKind::NumberEnum {
1014                    variants: vec![("Red".to_string(), 0.0), ("Green".to_string(), 1.0)],
1015                    doc: None,
1016                },
1017            },
1018        );
1019
1020        // A recursive alias `type Tree = { value: number; children: Tree[] }`,
1021        // built the way a library shim would: an inline body whose recursive
1022        // position is a [`Type::AliasRef`] back-edge carrying its own package.
1023        let tree_ref = || Type::AliasRef {
1024            mangled: crate::mangle::package_symbol(SYNTH, "Tree"),
1025            package: Package(SYNTH.to_string()),
1026            name: "Tree".to_string(),
1027            args: Vec::new(),
1028        };
1029        let tree_body = || {
1030            let mut fields = BTreeMap::new();
1031            fields.insert("value".to_string(), ObjectField::required(Type::Number));
1032            fields.insert(
1033                "children".to_string(),
1034                ObjectField::required(Type::Array(Box::new(tree_ref()))),
1035            );
1036            Type::Object {
1037                index: None,
1038                fields,
1039            }
1040        };
1041        defs.types.insert(
1042            "Tree".to_string(),
1043            TypeSymbol {
1044                name: "Tree".to_string(),
1045                mangled_name: package_symbol(SYNTH, "Tree"),
1046                declaration_span: Span::at(crate::FileId(0)),
1047                kind: TypeKind::Alias {
1048                    generics: Vec::new(),
1049                    ty: tree_body(),
1050                    doc: None,
1051                },
1052            },
1053        );
1054
1055        let synth_ref = |name: &str, ty: Type| ValueSymbol {
1056            name: name.to_string(),
1057            mangled_name: package_symbol(SYNTH, name),
1058            declaration_span: Span::at(crate::FileId(0)),
1059            kind: ValueKind::Function {
1060                generics: Vec::new(),
1061                params: Vec::new(),
1062                ret: ty,
1063                type_predicate: None,
1064                doc: None,
1065            },
1066        };
1067        defs.values.insert(
1068            "makeWidget".to_string(),
1069            synth_ref(
1070                "makeWidget",
1071                Type::InterfaceRef {
1072                    mangled: crate::mangle::package_symbol(SYNTH, "Widget"),
1073                    package: Package(SYNTH.to_string()),
1074                    name: "Widget".to_string(),
1075                    args: Vec::new(),
1076                },
1077            ),
1078        );
1079        defs.values.insert(
1080            "pickColor".to_string(),
1081            synth_ref(
1082                "pickColor",
1083                Type::NumberEnum {
1084                    mangled: crate::mangle::package_symbol(SYNTH, "Color"),
1085                    package: Package(SYNTH.to_string()),
1086                    name: "Color".to_string(),
1087                    member: None,
1088                },
1089            ),
1090        );
1091        defs.values.insert(
1092            "makeTree".to_string(),
1093            synth_ref(
1094                "makeTree",
1095                Type::Alias {
1096                    mangled: crate::mangle::package_symbol(SYNTH, "Tree"),
1097                    package: Package(SYNTH.to_string()),
1098                    name: "Tree".to_string(),
1099                    args: Vec::new(),
1100                    ty: Box::new(tree_body()),
1101                },
1102            ),
1103        );
1104        defs
1105    }
1106
1107    fn acme_package() -> PackageDeclaration {
1108        let mut defs = PackageDeclaration::with_package(ACME);
1109        defs.values.insert(
1110            "greet".to_string(),
1111            ValueSymbol {
1112                name: "greet".to_string(),
1113                mangled_name: package_symbol(ACME, "greet"),
1114                declaration_span: Span::at(crate::FileId(0)),
1115                kind: ValueKind::Function {
1116                    generics: Vec::new(),
1117                    params: Vec::new(),
1118                    ret: Type::String,
1119                    type_predicate: None,
1120                    doc: None,
1121                },
1122            },
1123        );
1124        defs
1125    }
1126
1127    fn errors(source: &str, packages: &[&PackageDeclaration]) -> Vec<String> {
1128        let parsed = super::parse_script(source, crate::FileId(0));
1129        let stdlib_defs = crate::runtime::stdlib_package_declarations();
1130        let (_ta, diags, _timings) =
1131            super::front_end(source, &parsed, &stdlib_defs, packages).expect("front end");
1132        diags
1133            .into_iter()
1134            .filter(|d| d.severity == crate::Severity::Error)
1135            .map(|d| d.message)
1136            .collect()
1137    }
1138
1139    fn compile_errors(source: &str, packages: &[&PackageDeclaration]) -> Vec<String> {
1140        super::compile_script(source, "main.subm", crate::FileId(0), packages, &[])
1141            .expect_err("expected compile error")
1142            .into_iter()
1143            .filter(|d| d.severity == crate::Severity::Error)
1144            .map(|d| d.message)
1145            .collect()
1146    }
1147
1148    #[test]
1149    fn package_declarations_passed_to_compile_are_importable() {
1150        let acme = acme_package();
1151        let source = r#"
1152            import { greet } from "@acme/util";
1153            function main(): void {}
1154        "#;
1155        super::compile_script(source, "main.subm", crate::FileId(0), &[&acme], &[])
1156            .expect("package compiles");
1157    }
1158
1159    #[test]
1160    fn package_not_passed_to_compile_uses_not_found_diagnostic() {
1161        let acme = acme_package();
1162        let source = r#"
1163            import { greet } from "@acme/util";
1164            function main(): void {}
1165        "#;
1166        let _package_not_supplied = acme;
1167        let errs = compile_errors(source, &[]);
1168        assert!(
1169            errs.iter()
1170                .any(|m| m.contains("package `@acme/util` not found")),
1171            "expected not-found diagnostic, got: {errs:?}"
1172        );
1173    }
1174
1175    #[test]
1176    fn unknown_package_still_uses_not_found_diagnostic() {
1177        let source = r#"
1178            import { greet } from "@acme/missing";
1179            function main(): void {}
1180        "#;
1181        let errs = compile_errors(source, &[]);
1182        assert!(
1183            errs.iter()
1184                .any(|m| m.contains("package `@acme/missing` not found")),
1185            "expected not-found diagnostic, got: {errs:?}"
1186        );
1187    }
1188
1189    #[test]
1190    fn stdlib_imports_do_not_require_blueprint_packages() {
1191        let source = r#"
1192            import { v4 } from "submilli:uuid";
1193            function main(): void {}
1194        "#;
1195        super::compile_script(source, "main.subm", crate::FileId(0), &[], &[])
1196            .expect("stdlib package compiles without package declarations");
1197    }
1198
1199    #[test]
1200    fn importless_interface_member_access() {
1201        let synth = synth_package();
1202        // Imports only the function — never names `Widget`.
1203        let source = r#"
1204            import { makeWidget } from "submilli:synth";
1205            function main(): void {
1206                const w = makeWidget();
1207                assert(w.label === "x", "property read");
1208                const n: number = w.size();
1209                assert(n >= 0, "method call");
1210            }
1211        "#;
1212        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1213    }
1214
1215    #[test]
1216    fn importless_enum_value_use() {
1217        let synth = synth_package();
1218        // Imports only the function — never names `Color`.
1219        let source = r#"
1220            import { pickColor } from "submilli:synth";
1221            function main(): void {
1222                const a = pickColor();
1223                const b = pickColor();
1224                assert(a === b, "enum values compare");
1225            }
1226        "#;
1227        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1228    }
1229
1230    #[test]
1231    fn importless_recursive_alias_deep_member_access() {
1232        // `makeTree(): Tree` where `Tree = { value: number; children: Tree[] }`.
1233        // Reaching `children[0]` resolves a `Type::AliasRef` recursion back-edge
1234        // by its package — the alias name `Tree` is never imported. This guards
1235        // the registry routing in `rehydrate_alias_refs` / `expand_alias_ref`.
1236        let synth = synth_package();
1237        let source = r#"
1238            import { makeTree } from "submilli:synth";
1239            function main(): void {
1240                const t = makeTree();
1241                const root: number = t.value;
1242                assert(root >= 0, "root value");
1243                const child: number = t.children[0].value;
1244                assert(child >= 0, "deep recursive-alias value");
1245                const grand: number = t.children[0].children[0].value;
1246                assert(grand >= 0, "two levels deep");
1247            }
1248        "#;
1249        assert_eq!(errors(source, &[&synth]), Vec::<String>::new());
1250    }
1251
1252    #[test]
1253    fn importless_recursive_alias_resolves_body_shape() {
1254        // Reading a *nonexistent* field on a deep recursion back-edge must still
1255        // produce a precise field-not-found diagnostic — proving the body shape
1256        // resolved through the registry rather than collapsing to `Error` (which
1257        // would silently swallow the access).
1258        let synth = synth_package();
1259        let source = r#"
1260            import { makeTree } from "submilli:synth";
1261            function main(): void {
1262                const t = makeTree();
1263                const _x = t.children[0].nope;
1264            }
1265        "#;
1266        let errs = errors(source, &[&synth]);
1267        assert!(
1268            errs.iter().any(|m| m.contains("nope")),
1269            "expected a field-not-found error naming `nope`; got: {errs:?}"
1270        );
1271    }
1272
1273    #[test]
1274    fn unimported_type_name_still_unresolved_in_source() {
1275        // The fix must NOT make the library type name resolvable as a *source*
1276        // annotation without an import — only structural access is import-free.
1277        let synth = synth_package();
1278        let source = r#"
1279            import { makeWidget } from "submilli:synth";
1280            function main(): void {
1281                const w: Widget = makeWidget();
1282            }
1283        "#;
1284        let errs = errors(source, &[&synth]);
1285        assert!(
1286            errs.iter().any(|m| m.contains("unknown type `Widget`")),
1287            "expected unknown-type error for unimported source annotation; got: {errs:?}"
1288        );
1289    }
1290}
1291
1292#[cfg(test)]
1293mod untyped_arena_failures {
1294    use super::*;
1295    use crate::arena::{ArenaKind, with_node_limit};
1296    use crate::compiler_error::{CompilerFailure, CompilerStage};
1297
1298    #[test]
1299    fn parser_arena_limit_is_fatal_and_keeps_prior_diagnostics() {
1300        let source = "let = 0; function main(): number { return 1; }";
1301        for arena in [ArenaKind::Expressions, ArenaKind::Statements] {
1302            let error = with_node_limit(arena, 0, || {
1303                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1304            })
1305            .unwrap_err();
1306            assert!(matches!(
1307                error.fatal,
1308                Some(CompilerFailure::Limit {
1309                    stage: CompilerStage::Parse,
1310                    span: None,
1311                    ..
1312                })
1313            ));
1314            assert!(
1315                !error.diagnostics.is_empty(),
1316                "earlier syntax diagnostics must survive"
1317            );
1318        }
1319        assert!(
1320            !compile_script_checked(
1321                "function main(): number { return 1; }",
1322                "healthy.ts",
1323                FileId(0),
1324                &[],
1325                &[]
1326            )
1327            .unwrap()
1328            .wasm
1329            .is_empty()
1330        );
1331    }
1332
1333    #[test]
1334    fn lowering_arena_limit_stops_script_before_inference() {
1335        let source = "function main(): number { const [x] = [1]; return x; }";
1336        let mut lexer = crate::Asi::new(source, FileId(0));
1337        let mut tokens = Vec::new();
1338        loop {
1339            let token = lexer.next_token();
1340            let done = matches!(token.kind, crate::TokenKind::Eof);
1341            tokens.push(token);
1342            if done {
1343                break;
1344            }
1345        }
1346        let (ast, _) = crate::parser::parse_checked(source, tokens, FileId(0)).unwrap();
1347        for (arena, count) in [
1348            (ArenaKind::Expressions, ast.exprs_len()),
1349            (ArenaKind::Statements, ast.stmts_len()),
1350        ] {
1351            let error = with_node_limit(arena, u32::try_from(count).unwrap(), || {
1352                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1353            })
1354            .unwrap_err();
1355            assert!(matches!(
1356                error.fatal,
1357                Some(CompilerFailure::Limit {
1358                    stage: CompilerStage::Infer,
1359                    span: None,
1360                    ..
1361                })
1362            ));
1363        }
1364        assert!(
1365            !compile_script_checked(source, "healthy.ts", FileId(0), &[], &[])
1366                .unwrap()
1367                .wasm
1368                .is_empty()
1369        );
1370    }
1371
1372    #[test]
1373    fn package_arena_limit_returns_no_artifact() {
1374        let modules = [PackageSourceModule {
1375            path: ModulePath::from("lib"),
1376            source: "export function value(): number { return 1; }",
1377        }];
1378        let error = with_node_limit(ArenaKind::Expressions, 0, || {
1379            compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1380        })
1381        .unwrap_err();
1382        assert!(matches!(
1383            error.fatal,
1384            Some(CompilerFailure::Limit {
1385                stage: CompilerStage::Parse,
1386                ..
1387            })
1388        ));
1389        compile_package_checked("test", ModulePath::from("lib"), &modules, &[]).unwrap();
1390    }
1391
1392    #[test]
1393    fn corrupt_parsed_imports_return_an_internal_failure() {
1394        let mut parsed = parse_script("function main(): number { return 1; }", FileId(0));
1395        parsed.ast.top_level.push(crate::StmtId(u32::MAX));
1396        let error = parsed.external_imports().unwrap_err();
1397        assert!(matches!(
1398            error.fatal,
1399            Some(CompilerFailure::Internal { span: None, .. })
1400        ));
1401    }
1402}
1403
1404#[cfg(test)]
1405mod typed_arena_failures {
1406    use super::*;
1407    use crate::arena::{ArenaKind, with_node_limit};
1408
1409    fn assert_limit(error: CompileError) {
1410        assert!(
1411            matches!(error.fatal, Some(CompilerFailure::Limit { span: None, .. })),
1412            "{error:?}"
1413        );
1414    }
1415
1416    #[test]
1417    fn typed_allocation_failure_stops_script_and_package_compilation() {
1418        let source = "function main(): number { return 1; }";
1419        let modules = [PackageSourceModule {
1420            path: ModulePath::from("lib"),
1421            source: "export function value(): number { return 1; }",
1422        }];
1423        for arena in [ArenaKind::TypedExpressions, ArenaKind::TypedStatements] {
1424            assert_limit(
1425                with_node_limit(arena, 0, || {
1426                    compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1427                })
1428                .unwrap_err(),
1429            );
1430            assert_limit(
1431                with_node_limit(arena, 0, || {
1432                    compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1433                })
1434                .unwrap_err(),
1435            );
1436        }
1437        assert!(
1438            !compile_script_checked(source, "healthy.ts", FileId(0), &[], &[])
1439                .unwrap()
1440                .wasm
1441                .is_empty()
1442        );
1443        assert!(
1444            !compile_package_checked("test", ModulePath::from("lib"), &modules, &[])
1445                .unwrap()
1446                .wasm
1447                .is_empty()
1448        );
1449    }
1450
1451    #[test]
1452    fn typed_allocation_failure_keeps_prior_inference_diagnostics() {
1453        let source = "function main(): number { let value: number = \"wrong\"; return 2; }";
1454        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1455        assert!(!diagnostics.is_empty());
1456        for (arena, count) in [
1457            (ArenaKind::TypedExpressions, ast.exprs_len()),
1458            (ArenaKind::TypedStatements, ast.stmts_len()),
1459        ] {
1460            let error = with_node_limit(arena, u32::try_from(count - 1).unwrap(), || {
1461                compile_script_checked(source, "limit.ts", FileId(0), &[], &[])
1462            })
1463            .unwrap_err();
1464            assert!(!error.diagnostics.is_empty(), "{error:?}");
1465            assert_limit(error);
1466        }
1467    }
1468
1469    #[test]
1470    fn codegen_allocation_failure_returns_no_wasm() {
1471        let source = "function main(): number { let value: number | null = 1; if (value !== null) { return value; } return 0; }";
1472        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1473        assert!(diagnostics.is_empty());
1474        let ast = desugar(capture(ast).unwrap(), FileId(0)).unwrap();
1475        let (prelude, host, internal) = prelude::cached_runtime_package_declarations();
1476        let dependencies: Vec<_> = prelude.iter().chain(host).chain(internal).collect();
1477        let failure = with_node_limit(
1478            ArenaKind::TypedExpressions,
1479            u32::try_from(ast.exprs_len()).unwrap(),
1480            || crate::codegen::codegen(source, "limit.ts", FileId(0), &ast, &dependencies),
1481        )
1482        .unwrap_err();
1483        assert!(
1484            matches!(
1485                failure,
1486                CompilerFailure::Limit {
1487                    stage: CompilerStage::Codegen,
1488                    span: None,
1489                    ..
1490                }
1491            ),
1492            "{failure}"
1493        );
1494        assert!(
1495            !crate::codegen::codegen(source, "healthy.ts", FileId(0), &ast, &dependencies)
1496                .unwrap()
1497                .is_empty()
1498        );
1499    }
1500
1501    #[test]
1502    fn desugaring_capacity_failure_does_not_return_a_transformed_tree() {
1503        let source = "function main(): void { do {} while (false); }";
1504        let (ast, diagnostics) = typecheck_to_typed_ast(source, FileId(0));
1505        assert!(diagnostics.is_empty());
1506        for (arena, count) in [
1507            (ArenaKind::TypedExpressions, ast.exprs_len()),
1508            (ArenaKind::TypedStatements, ast.stmts_len()),
1509        ] {
1510            let failure = with_node_limit(arena, u32::try_from(count).unwrap(), || {
1511                desugar(ast.clone(), FileId(0))
1512            })
1513            .unwrap_err();
1514            assert_limit(failure.into());
1515        }
1516        desugar(ast, FileId(0)).unwrap();
1517    }
1518}