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