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