Embedding
The compiler ships as the perry_wit Rust crate and as a standalone WASI 0.2 component.
The runtime and standard libraries are precompiled and embedded in the compiler binary, so compilation runs entirely in memory.
Rust
perry_wit::api::compile compiles an entry module, auxiliary ESM modules and WIT packages from byte buffers:
use perry_wit::api::{self, Options, Program, Wit};
let program = Program {
source: br#"
export function authorize(token: string): boolean {
return token.startsWith("Bearer ") && token.length > 16;
}
"#
.as_slice()
.into(),
modules: vec![],
};
let wit = Wit {
application: br#"
package example:auth@0.1.0;
world filter {
export authorize: func(token: string) -> bool;
}
"#
.as_slice()
.into(),
dependencies: vec![],
};
let compiled = api::compile(&program, &wit, &Options::default())?;
assert!(!compiled.core.is_empty());
assert!(compiled.component.is_some_and(|component| !component.bytes.is_empty()));
Errors are CompileError values holding PerryDiagnostic records with a source span and a DiagnosticCode.
WebAssembly Component
perry-wit.wasm is a WASI 0.2 component that exports this interface (compiler.wit) next to a wasi:cli/run command:
package perry:compiler@0.1.2;
interface api {
/// Optional diagnostic name; for an entrypoint, also its logical module identity.
/// Anonymous single-source compilation needs no name. Names never access host files.
record source {
name: option<string>,
contents: list<u8>,
}
/// Dependency modules require package-relative logical identifiers for import resolution.
record module {
name: string,
contents: list<u8>,
}
record program {
source: source,
modules: list<module>,
}
variant wit-package {
sources(list<source>),
encoded(list<u8>),
}
record wit-input {
application: wit-package,
dependencies: list<wit-package>,
}
record compile-options {
%world: option<string>,
/// Omit component wrapping, retaining the selected WIT world's canonical ABI.
core-only: bool,
}
record component-output {
bytes: list<u8>,
stripped: list<u8>,
}
record compiled {
core: list<u8>,
component: option<component-output>,
allocation-report: string,
}
record artifact {
name: string,
contents: list<u8>,
}
enum diagnostic-code {
parse-error,
unsupported-operator,
unsupported-spread,
missing-type-annotation,
unsupported-type,
unsupported-expression,
incompatible-record-layout,
missing-required-field,
absence-mismatch,
interface-mismatch,
export-mismatch,
import-signature-mismatch,
module-resolution,
invalid-input,
wit-resolution,
codegen-failure,
missing-error-channel,
io,
internal-compiler-error,
}
enum diagnostic-severity { error, warning, info }
record source-span {
file: string,
start-line: u32,
start-column: u32,
end-line: u32,
end-column: u32,
start-byte: u64,
end-byte: u64,
}
record diagnostic {
code: diagnostic-code,
severity: diagnostic-severity,
message: string,
span: option<source-span>,
notes: list<string>,
help: option<string>,
}
compile: func(program: program, wit: wit-input, options: compile-options) -> result<compiled, list<diagnostic>>;
/// Raw TypeScript exports without WIT. This differs from compile with core-only,
/// which still checks a WIT world and emits its canonical ABI.
compile-core: func(program: program) -> result<list<u8>, list<diagnostic>>;
generate-sdk: func(wit: wit-input, %world: option<string>) -> result<list<artifact>, list<diagnostic>>;
}
world compiler {
export api;
}
The playground runs it in a Web Worker.
Because the api export operates on in-memory buffers, a server can instantiate it with empty filesystem preopens, fuel limits and memory caps to compile untrusted TypeScript.