1mod 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#[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
67fn 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
78fn 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
90fn 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
228fn 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
236fn 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}