Skip to main content

perry_wit/compiler/
mod.rs

1//! TypeScript → MIR → core Wasm → WASI 0.3 component.
2
3mod bind;
4mod http;
5mod imports;
6mod resources;
7
8use crate::{audit, component, link, strip};
9use anyhow::{Context as _, Result, anyhow};
10use perry_codegen::Exports;
11use pliron::{context::Context, op::Op, pass::AnalysisManager, printable::Printable};
12use std::{
13    collections::BTreeMap,
14    fs,
15    path::{Path, PathBuf},
16};
17
18#[derive(Debug, Clone)]
19pub struct CompileOptions {
20    pub wit_dir: PathBuf,
21    pub world: Option<String>,
22    pub core_only: bool,
23}
24
25impl Default for CompileOptions {
26    fn default() -> Self {
27        Self {
28            wit_dir: "wit".into(),
29            world: None,
30            core_only: false,
31        }
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct Compiled {
37    pub core: Vec<u8>,
38    pub component: Option<ComponentOutput>,
39    pub allocation_report: String,
40}
41
42/// Both forms of the same component, present together when componentization is requested.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ComponentOutput {
45    pub bytes: Vec<u8>,
46    pub stripped: Vec<u8>,
47}
48
49impl ComponentOutput {
50    fn encode(
51        core: &[u8],
52        resolve: &wit_parser::Resolve,
53        world: wit_parser::WorldId,
54    ) -> Result<Self> {
55        let bytes = component::encode_resolved(core, resolve, world)?;
56        let stripped = strip::component(&bytes)?;
57        Ok(Self { bytes, stripped })
58    }
59}
60
61fn compile_file_impl(input_file: &Path, options: &CompileOptions) -> Result<Compiled> {
62    let source = fs::read_to_string(input_file)
63        .with_context(|| format!("Reading TypeScript source {}", input_file.display()))?;
64    compile_typescript_impl(&source, &input_file.to_string_lossy(), options)
65}
66
67/// Compile against the world selected from `options.wit_dir`.
68fn compile_typescript_impl(
69    source: &str,
70    file_name: &str,
71    options: &CompileOptions,
72) -> Result<Compiled> {
73    let (resolve, package) = component::wit::resolve_wit(&options.wit_dir)?;
74    let world = component::wit::select_world(&resolve, package, options.world.as_deref())?;
75    compile_for_world_impl(source, file_name, &resolve, world, !options.core_only)
76}
77
78/// Compile against a resolved world, wrapping the output in a component when `componentize`
79/// is true.
80fn compile_for_world_impl(
81    source: &str,
82    file_name: &str,
83    resolve: &wit_parser::Resolve,
84    world: wit_parser::WorldId,
85    componentize: bool,
86) -> Result<Compiled> {
87    compile_world(source, file_name, resolve, world, componentize, false)
88}
89
90/// Compile a component test with forced safe-point collection and reclaimed-memory poisoning.
91/// Production compilation omits the instrumentation.
92fn compile_for_world_with_memory_checks_impl(
93    source: &str,
94    file_name: &str,
95    resolve: &wit_parser::Resolve,
96    world: wit_parser::WorldId,
97) -> Result<Compiled> {
98    compile_world(source, file_name, resolve, world, true, true)
99}
100
101fn load_source_files(source: &str, file_name: &str) -> Result<BTreeMap<String, String>> {
102    perry_frontend::source_files::load(source, file_name)
103}
104
105fn compile_world(
106    source: &str,
107    file_name: &str,
108    resolve: &wit_parser::Resolve,
109    world: wit_parser::WorldId,
110    componentize: bool,
111    memory_checks: bool,
112) -> Result<Compiled> {
113    let modules = load_source_files(source, file_name)?;
114    compile_sources(
115        source,
116        file_name,
117        &modules,
118        resolve,
119        world,
120        componentize,
121        memory_checks,
122    )
123}
124
125pub(crate) fn compile_sources(
126    source: &str,
127    identity: &str,
128    modules: &BTreeMap<String, String>,
129    resolve: &wit_parser::Resolve,
130    world: wit_parser::WorldId,
131    componentize: bool,
132    memory_checks: bool,
133) -> Result<Compiled> {
134    audit::audit_no_llvm(include_str!("../../Cargo.lock"))
135        .context("LLVM audit verification failed")?;
136    if http::is_handler_world(resolve, world) {
137        return http::compile(
138            source,
139            identity,
140            modules,
141            resolve,
142            world,
143            componentize,
144            memory_checks,
145        );
146    }
147    let ctx = &mut Context::new();
148    let mut foreign = perry_frontend::wit::WorldTypes::new(resolve, Some(world));
149    let mut bindings = imports::bindings(resolve, world, &mut foreign);
150    let resources = resources::definitions(resolve, world, &mut foreign, &mut bindings)?;
151    let program = perry_frontend::load_sources(ctx, source, identity, modules, foreign.foreign())
152        .map_err(map_lower_error)?;
153    let mir = perry_lower::lower_program(ctx, &program).map_err(map_lower_error)?;
154    let mut exports = bind::exports(ctx, &mir, resolve, world)?;
155    bind::constrain_numbers(ctx, &mir, &exports)?;
156    let mut analyses = optimize(ctx, &mir)?;
157    bind::specialized_implementations(ctx, &mir, &mut exports);
158    bind::check_main(ctx, &mir, &exports)?;
159    Ok(emit_component(
160        ctx,
161        &mir,
162        &mut analyses,
163        resolve,
164        world,
165        memory_checks,
166        componentize,
167        exports,
168        bindings,
169        resources,
170        true,
171    )
172    .map_err(|error| {
173        crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::CodegenFailure)
174    })?)
175}
176
177#[allow(clippy::too_many_arguments)]
178pub(super) fn emit_component(
179    ctx: &mut Context,
180    mir: &pliron::builtin::ops::ModuleOp,
181    analyses: &mut AnalysisManager,
182    resolve: &wit_parser::Resolve,
183    world: wit_parser::WorldId,
184    memory_checks: bool,
185    componentize: bool,
186    exports: Vec<perry_codegen::boundary::Export>,
187    bindings: Vec<perry_codegen::boundary::Import>,
188    resources: Vec<perry_codegen::boundary::ResourceDefinition>,
189    strip_unused_stdio: bool,
190) -> Result<Compiled> {
191    let imports = bind::imports(resolve, world);
192    let streams = bind::stream_anchor(resolve, world);
193    let (mut has_stdout, mut stderr) = (false, false);
194    for name in &imports {
195        has_stdout |= name.starts_with("wasi:cli/stdout@");
196        stderr |= name.starts_with("wasi:cli/stderr@");
197    }
198    let stdout = has_stdout && uses_stdout(ctx, mir);
199    let exports = Exports::Canonical {
200        memory_checks,
201        exports,
202        imports,
203        bindings,
204        resources,
205    };
206    let mut allocation_report = perry_codegen::explain_tiers(ctx, mir, &exports, analyses)?;
207    let core = perry_codegen::compile(ctx, mir, &exports, analyses).map_err(map_codegen_error)?;
208    let (mut core, helpers) = finish(
209        core,
210        streams.as_ref(),
211        &link::canonical::Requirements::for_world(resolve, world),
212    )?;
213    if strip_unused_stdio {
214        core = link::raw::available_stdio(core, stdout, stderr)?;
215    }
216    use std::fmt::Write as _;
217    let _ = writeln!(allocation_report, "Linked helper functions: {helpers}");
218    let component = componentize
219        .then(|| ComponentOutput::encode(&core, resolve, world))
220        .transpose()?;
221    Ok(Compiled {
222        core,
223        component,
224        allocation_report,
225    })
226}
227
228/// Build a linked core module exporting every exported TypeScript function with its raw core
229/// signature (numbers as `f64`, references as addresses, plus an `i32` exception status for
230/// functions that may throw). Components use [compile_for_world].
231fn compile_core_impl(source: &str, file_name: &str) -> Result<Vec<u8>> {
232    let modules = load_source_files(source, file_name)?;
233    compile_core_sources(source, file_name, &modules, false)
234}
235
236/// Compile a raw test module with `__perry_gc_debug(flags)` and
237/// `__perry_gc_counter(index)`. Flags enable forced safe-point collection (1) and poisoning
238/// (2), plus buffer-growth counters (4). Counter indices are allocations (0), collections (1),
239/// live bytes (2), reserved bytes (3), transfer-copy bytes (4), active pins (5), active roots (6),
240/// compiled root publications (7), in-place growths (8), copy fallbacks (9), and growth-copy
241/// bytes (10). Canonical production components omit these exports.
242fn compile_core_with_memory_checks_impl(source: &str, file_name: &str) -> Result<Vec<u8>> {
243    let modules = load_source_files(source, file_name)?;
244    compile_core_sources(source, file_name, &modules, true)
245}
246
247pub(crate) fn compile_core_sources(
248    source: &str,
249    identity: &str,
250    modules: &BTreeMap<String, String>,
251    memory_checks: bool,
252) -> Result<Vec<u8>> {
253    let ctx = &mut Context::new();
254    let (mir, mut analyses) = lower(ctx, source, identity, modules, &Default::default())?;
255    let exports = if memory_checks {
256        Exports::RawChecked
257    } else {
258        Exports::Raw
259    };
260    let core =
261        perry_codegen::compile(ctx, &mir, &exports, &mut analyses).map_err(map_codegen_error)?;
262    let (core, _) =
263        finish(core, None, &link::canonical::Requirements::default()).map_err(|error| {
264            crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::CodegenFailure)
265        })?;
266    Ok(link::raw::without_p3(core).map_err(|error| {
267        crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::CodegenFailure)
268    })?)
269}
270
271fn lower(
272    ctx: &mut Context,
273    source: &str,
274    file_name: &str,
275    modules: &BTreeMap<String, String>,
276    foreign: &perry_frontend::wit::WorldTypes,
277) -> Result<(pliron::builtin::ops::ModuleOp, AnalysisManager)> {
278    let program = perry_frontend::load_sources(ctx, source, file_name, modules, foreign.foreign())
279        .map_err(map_lower_error)?;
280    let mir = perry_lower::lower_program(ctx, &program).map_err(map_lower_error)?;
281    let analyses = optimize(ctx, &mir)?;
282    Ok((mir, analyses))
283}
284
285fn map_lower_error(error: anyhow::Error) -> crate::PerryDiagnostic {
286    match perry_lower::LowerError::try_from_anyhow(error) {
287        Ok(lower) => lower.into(),
288        Err(error) => {
289            crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::UnsupportedExpression)
290        }
291    }
292}
293
294fn optimize(ctx: &mut Context, mir: &pliron::builtin::ops::ModuleOp) -> Result<AnalysisManager> {
295    let analyses = perry_mir::canonicalize(mir.get_operation(), ctx)
296        .map_err(|error| anyhow!("{}", error.disp(ctx)))
297        .context("optimizing MIR")
298        .map_err(|error| {
299            crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::CodegenFailure)
300        })?;
301    Ok(analyses)
302}
303
304fn map_codegen_error(error: anyhow::Error) -> crate::PerryDiagnostic {
305    match error.downcast::<perry_codegen::CodegenError>() {
306        Ok(codegen_err) => {
307            let message = codegen_err.to_string();
308            match codegen_err {
309                perry_codegen::CodegenError::PendingInitialization { span } => {
310                    crate::PerryDiagnostic::error(crate::DiagnosticCode::CodegenFailure, message)
311                        .with_optional_span(span)
312                        .with_help(
313                            "Await the asynchronous work in the top-level statements, so every entry point waits for it.",
314                        )
315                }
316                perry_codegen::CodegenError::AsyncInitializerRequiresAsyncExport {
317                    span, ..
318                } => crate::PerryDiagnostic::error(
319                    crate::DiagnosticCode::MissingErrorChannel,
320                    message,
321                )
322                .with_optional_span(span)
323                .with_help(
324                    "Declare this WIT export async so it can await module initialization and its error channel.",
325                ),
326                perry_codegen::CodegenError::MissingErrorChannel { span, .. } => {
327                    crate::PerryDiagnostic::error(
328                        crate::DiagnosticCode::MissingErrorChannel,
329                        message,
330                    )
331                    .with_optional_span(span)
332                    .with_help(
333                        "Declare a WIT result error, or use an async export with wasi:cli/stderr support.",
334                    )
335                }
336                perry_codegen::CodegenError::ExportBinding { span, source, .. } => {
337                    crate::PerryDiagnostic::error(crate::DiagnosticCode::ExportMismatch, message)
338                        .with_optional_span(span)
339                        .with_source_notes(source.as_ref())
340                }
341            }
342        }
343        Err(error) => {
344            crate::PerryDiagnostic::from_error(error, crate::DiagnosticCode::CodegenFailure)
345        }
346    }
347}
348
349fn finish(
350    core: Vec<u8>,
351    streams: Option<&link::StreamAnchor>,
352    canonical: &link::canonical::Requirements,
353) -> Result<(Vec<u8>, u32)> {
354    let (core, helpers) = link::link_helpers_with(core, streams, canonical)
355        .context("linking perry-runtime and helpers")?;
356    let core = link::compact(&core).context("interning core function signatures")?;
357    audit::validate_guest(&core).context("validating stackless core Wasm")?;
358    Ok((core, helpers))
359}
360
361fn uses_stdout(ctx: &Context, mir: &pliron::builtin::ops::ModuleOp) -> bool {
362    use perry_mir::{Intrinsic, IntrinsicOp};
363    perry_mir::functions(mir.get_operation(), ctx)
364        .iter()
365        .flat_map(|f| perry_mir::operations(ctx, f))
366        .filter_map(|op| pliron::operation::Operation::get_op::<IntrinsicOp>(op, ctx))
367        .any(|op| {
368            matches!(
369                op.intrinsic(ctx),
370                Intrinsic::PrintOut | Intrinsic::WritableStdout
371            )
372        })
373}
374
375pub fn compile_file(
376    input_file: &Path,
377    options: &CompileOptions,
378) -> std::result::Result<Compiled, crate::CompileError> {
379    compile_file_impl(input_file, options)
380        .map_err(|error| crate::CompileError::from_error(error, crate::DiagnosticCode::Io))
381}
382
383pub fn compile_typescript(
384    source: &str,
385    file_name: &str,
386    options: &CompileOptions,
387) -> std::result::Result<Compiled, crate::CompileError> {
388    compile_typescript_impl(source, file_name, options).map_err(|error| {
389        crate::CompileError::from_error(error, crate::DiagnosticCode::WitResolution)
390    })
391}
392
393pub fn compile_for_world(
394    source: &str,
395    file_name: &str,
396    resolve: &wit_parser::Resolve,
397    world: wit_parser::WorldId,
398    componentize: bool,
399) -> std::result::Result<Compiled, crate::CompileError> {
400    compile_for_world_impl(source, file_name, resolve, world, componentize).map_err(|error| {
401        crate::CompileError::from_error(error, crate::DiagnosticCode::CodegenFailure)
402    })
403}
404
405pub fn compile_for_world_with_memory_checks(
406    source: &str,
407    file_name: &str,
408    resolve: &wit_parser::Resolve,
409    world: wit_parser::WorldId,
410) -> std::result::Result<Compiled, crate::CompileError> {
411    compile_for_world_with_memory_checks_impl(source, file_name, resolve, world).map_err(|error| {
412        crate::CompileError::from_error(error, crate::DiagnosticCode::CodegenFailure)
413    })
414}
415
416pub fn compile_core(
417    source: &str,
418    file_name: &str,
419) -> std::result::Result<Vec<u8>, crate::CompileError> {
420    compile_core_impl(source, file_name).map_err(|error| {
421        crate::CompileError::from_error(error, crate::DiagnosticCode::CodegenFailure)
422    })
423}
424
425pub fn compile_core_with_memory_checks(
426    source: &str,
427    file_name: &str,
428) -> std::result::Result<Vec<u8>, crate::CompileError> {
429    compile_core_with_memory_checks_impl(source, file_name).map_err(|error| {
430        crate::CompileError::from_error(error, crate::DiagnosticCode::CodegenFailure)
431    })
432}