From 4544b0a981e384ee76cbc17cfb364494150bab96 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 5 Jun 2026 03:09:41 -0500 Subject: [PATCH] feat: target aware emission & validation --- crates/core/src/wasm.rs | 120 +++++++++++- docs/src/CHANGELOG.md | 61 ++++-- docs/src/SUMMARY.md | 17 +- .../development/wasm_backend_and_runtime.md | 20 +- .../wasm_backend_completeness.md} | 2 +- .../specs/11_type_and_generic_inference.md | 60 ++++++ .../specs/12_runtime_memory_and_semantics.md | 45 +++++ .../specs/13_remaining_language_semantics.md | 42 +++++ ...terop.md => 14_stdlib_and_host_interop.md} | 11 +- ...outputs.md => 15_cli_and_build_outputs.md} | 7 +- docs/src/internal/supported_subset.md | 175 ++++++++++++++---- .../tasks/10_wasm_backend_completeness.md | 59 ------ .../tasks/11_type_and_generic_inference.md | 47 +++++ .../tasks/12_runtime_memory_and_semantics.md | 45 +++++ .../tasks/13_remaining_language_semantics.md | 51 +++++ ...terop.md => 14_stdlib_and_host_interop.md} | 12 +- ...outputs.md => 15_cli_and_build_outputs.md} | 4 +- 17 files changed, 640 insertions(+), 138 deletions(-) rename docs/src/internal/{specs/10_wasm_backend_completeness.md => development/wasm_backend_completeness.md} (97%) create mode 100644 docs/src/internal/specs/11_type_and_generic_inference.md create mode 100644 docs/src/internal/specs/12_runtime_memory_and_semantics.md create mode 100644 docs/src/internal/specs/13_remaining_language_semantics.md rename docs/src/internal/specs/{11_stdlib_and_host_interop.md => 14_stdlib_and_host_interop.md} (61%) rename docs/src/internal/specs/{12_cli_and_build_outputs.md => 15_cli_and_build_outputs.md} (76%) delete mode 100644 docs/src/internal/tasks/10_wasm_backend_completeness.md create mode 100644 docs/src/internal/tasks/11_type_and_generic_inference.md create mode 100644 docs/src/internal/tasks/12_runtime_memory_and_semantics.md create mode 100644 docs/src/internal/tasks/13_remaining_language_semantics.md rename docs/src/internal/tasks/{11_stdlib_and_host_interop.md => 14_stdlib_and_host_interop.md} (81%) rename docs/src/internal/tasks/{12_cli_and_build_outputs.md => 15_cli_and_build_outputs.md} (91%) diff --git a/crates/core/src/wasm.rs b/crates/core/src/wasm.rs index 6a16da9..1675574 100644 --- a/crates/core/src/wasm.rs +++ b/crates/core/src/wasm.rs @@ -13,8 +13,40 @@ pub struct WasmModule { pub bytes: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EmitOptions { + pub target: WasmTarget, +} + +impl Default for EmitOptions { + fn default() -> Self { + Self { target: WasmTarget::Wasmtime } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WasmTarget { + Wasmtime, + Browser, + Wasi, +} + +impl WasmTarget { + fn host_module(self) -> &'static str { + match self { + Self::Wasmtime => "env", + Self::Browser => "browser", + Self::Wasi => "wasi_snapshot_preview1", + } + } +} + pub fn emit(module: &ir::Module) -> Result { - let wat = emit_wat(module)?; + emit_with_options(module, EmitOptions::default()) +} + +pub fn emit_with_options(module: &ir::Module, options: EmitOptions) -> Result { + let wat = emit_wat_with_options(module, options)?; let bytes = wat::parse_str(&wat).map_err(|error| { vec![Diagnostic::new( DiagnosticCode::WasmError, @@ -26,6 +58,10 @@ pub fn emit(module: &ir::Module) -> Result { } pub fn emit_wat(module: &ir::Module) -> Result { + emit_wat_with_options(module, EmitOptions::default()) +} + +pub fn emit_wat_with_options(module: &ir::Module, options: EmitOptions) -> Result { let mut emitter = Emitter { imports: String::new(), functions: String::new(), @@ -55,6 +91,7 @@ pub fn emit_wat(module: &ir::Module) -> Result { }) .collect(), current_scratch: None, + options, }; for constant in &module.constants { @@ -105,6 +142,7 @@ struct Emitter { function_order: Vec, function_signatures: HashMap, Type)>, current_scratch: Option, + options: EmitOptions, } impl Emitter { @@ -190,6 +228,34 @@ impl Emitter { self.block(&function.body); self.current_scratch = previous_scratch; self.functions.push_str(" )\n"); + self.export_adapters(function); + } + + fn export_adapters(&mut self, function: &ir::Function) { + if !matches!(function.abi.boundary, ir::CallBoundary::ModuleExport) { + return; + } + if function.params.is_empty() && function.return_type == Type::String { + self.uses_runtime = true; + writeln!( + self.functions, + " (func ${}__data (export \"{}__data\") (result i32)", + function.name, function.name + ) + .expect("write WAT"); + writeln!(self.functions, " call ${}", function.name).expect("write WAT"); + writeln!(self.functions, " call $__string_data").expect("write WAT"); + self.functions.push_str(" )\n"); + writeln!( + self.functions, + " (func ${}__len (export \"{}__len\") (result i32)", + function.name, function.name + ) + .expect("write WAT"); + writeln!(self.functions, " call ${}", function.name).expect("write WAT"); + writeln!(self.functions, " call $__string_len").expect("write WAT"); + self.functions.push_str(" )\n"); + } } fn block(&mut self, block: &ir::Block) { @@ -872,8 +938,12 @@ impl Emitter { } fn validate_host_abi(&mut self, function: &ir::Function) -> bool { - match function.abi.boundary { + match &function.abi.boundary { ir::CallBoundary::Internal => return true, + ir::CallBoundary::HostImport { module, .. } if module != self.options.target.host_module() => { + self.unsupported_target_import(module, function.span, &function.name); + return false; + } ir::CallBoundary::ModuleExport | ir::CallBoundary::ModuleImport { .. } | ir::CallBoundary::HostImport { .. } => {} @@ -893,6 +963,20 @@ impl Emitter { supported } + fn unsupported_target_import(&mut self, module: &str, span: crate::source::Span, function: &str) { + self.diagnostics.push( + Diagnostic::new( + DiagnosticCode::WasmError, + format!( + "function `{function}` imports host module `{module}`, but target {:?} expects `{}`", + self.options.target, + self.options.target.host_module() + ), + ) + .with_label(Label::primary(span, "unsupported target import here")), + ); + } + fn unsupported_abi_type(&mut self, type_: &Type, span: crate::source::Span, function: &str) { self.diagnostics.push( Diagnostic::new( @@ -1291,6 +1375,38 @@ mod tests { assert_eq!(main.call(&mut store, ()).expect("call main"), 42); } + #[test] + fn emits_string_export_adapters_for_host_boundaries() { + let wasm = compile_wasm("pub fn greeting() { \"hello\" }"); + + assert!(wasm.wat.contains("(func $greeting__data (export \"greeting__data\")")); + assert!(wasm.wat.contains("(func $greeting__len (export \"greeting__len\")")); + } + + #[test] + fn keeps_generated_wat_and_wasm_deterministic() { + let source = "pub fn add(x: Int) -> Int { x + 1 }"; + let first = compile_wasm(source); + let second = compile_wasm(source); + + assert_eq!(first.wat, second.wat); + assert_eq!(first.bytes, second.bytes); + } + + #[test] + fn rejects_host_imports_for_the_wrong_target_before_assembly() { + let span = Span::new(SourceFileId(0), 0, 0); + let module = host_import_module(span); + let diagnostics = emit_wat_with_options(&module, EmitOptions { target: WasmTarget::Browser }) + .expect_err("unsupported target import"); + + assert!( + diagnostics + .iter() + .any(|diagnostic| { diagnostic.message.contains("target Browser expects `browser`") }) + ); + } + #[test] fn rejects_unsupported_export_abi_before_wat_assembly() { let span = Span::new(SourceFileId(0), 0, 0); diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md index efc74b9..dbcf0c4 100644 --- a/docs/src/CHANGELOG.md +++ b/docs/src/CHANGELOG.md @@ -4,25 +4,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -### [0.1.0] - 2026-06-04 - -#### Added - -- Project model and module loading now reads Gleam project metadata, discovers modules, assigns - stable source IDs, and reports project graph diagnostics. -- Full Gleam syntax is represented in the compiler AST or rejected with targeted source-spanned - diagnostics for known limitations. -- Name resolution now uses Gleam-like namespaces across values, types, constructors, fields, - modules, imports, and project visibility checks. -- Type checking now records module interfaces, constructors, fields, generics, typed expressions, - and real-language pattern metadata for lowering. -- Runtime representation now documents and tests object headers, tags, alignment, strings, lists, - tuples, records, custom values, closures, managed pointers, and allocation helpers. -- Pattern matching now parses, resolves, type-checks, lowers, diagnoses, and emits the supported - scalar and structured pattern forms with explicit branch behavior. +## [1.0.0] - 2026-06-05 + +### Added + +- The WASM backend now documents and emits scalar and managed value + representations for strings, bit arrays, lists, tuples, records, custom + values, closures, opaque values, results, options, errors, and panics. +- The WASM runtime prelude now includes allocation, string, bit-array, list, + tuple, record, custom-type, closure, equality, ordering, panic, assertion, + and debug helpers. +- WASM code generation now emits current IR expression and instruction forms, + branches, guards, lowered patterns, failure paths, operators, + short-circuiting booleans, direct/imported/exported/indirect calls, and + module constants/static data in deterministic order. +- The WASM backend now validates target-aware ABI rules for Wasmtime, browser, + and WASI host modules, emits raw-Wasm string export adapters, diagnoses + unsupported target and ABI combinations before assembly, and keeps WAT/Wasm + output deterministic. +- Backend validation now covers WAT snapshots, Wasmtime execution, static and + dynamic memory inspection, runtime helpers, import/export ABI checks, and + unsupported target/ABI diagnostics. + +## [0.1.0] - 2026-06-04 + +### Added + +- Project model and module loading now reads Gleam project metadata, discovers + modules, assigns stable source IDs, and reports project graph diagnostics. +- Full Gleam syntax is represented in the compiler AST or rejected with targeted + source-spanned diagnostics for known limitations. +- Name resolution now uses Gleam-like namespaces across values, types, + constructors, fields, modules, imports, and project visibility checks. +- Type checking now records module interfaces, constructors, fields, generics, + typed expressions, and real-language pattern metadata for lowering. +- Runtime representation now documents and tests object headers, tags, + alignment, strings, lists, tuples, records, custom values, closures, managed + pointers, and allocation helpers. +- Pattern matching now parses, resolves, type-checks, lowers, diagnoses, and + emits the supported scalar and structured pattern forms with explicit branch + behavior. - Structured language support now covers declarations, constants, externals, target groups, operators, pipelines, `use`, anonymous functions, captures, records, updates, tuples, lists, bit arrays, imported members, opaque values, and module interfaces. -- Core IR now represents module declarations, constants, managed value forms, function values, call - ABI metadata, structured control flow, failure paths, and stable debug output. +- Core IR now represents module declarations, constants, managed value forms, + function values, call ABI metadata, structured control flow, failure paths, + and stable debug output. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fa4645c..cfb8b86 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -73,6 +73,7 @@ TODO: - [Pattern matching](internal/development/pattern_matching.md) - [Core IR](internal/development/core_ir_for_real_programs.md) - [WASM backend and runtime](internal/development/wasm_backend_and_runtime.md) + - [Backend completeness](internal/development/wasm_backend_completeness.md)