diff --git a/crates/core/src/ir/lowerer.rs b/crates/core/src/ir/lowerer.rs index cb8d644..7dedba2 100644 --- a/crates/core/src/ir/lowerer.rs +++ b/crates/core/src/ir/lowerer.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use super::*; use crate::ast::{self, Declaration as AstDeclaration, Expression as AstExpression, Pattern, Statement}; @@ -795,6 +795,7 @@ impl Lowerer { .map(|argument| self.lower_constructor_pattern_argument(context, argument, subject_type)) .collect(); }; + let substitutions = constructor_type_substitutions(&info, subject_type); let mut ordered = Vec::new(); for (index, field) in info.fields.iter().enumerate() { let argument = arguments @@ -802,7 +803,8 @@ impl Lowerer { .find(|argument| argument.label.as_ref().is_some_and(|label| label.text == field.name)) .or_else(|| arguments.get(index)); let Some(argument) = argument else { continue }; - ordered.push(self.lower_constructor_pattern_argument(context, argument, &field.type_)?); + let field_type = substitute_type_generics(&field.type_, &substitutions); + ordered.push(self.lower_constructor_pattern_argument(context, argument, &field_type)?); } Some(ordered) } @@ -1133,13 +1135,16 @@ impl Lowerer { } fn lower_stdlib_host_imports(&self, ast: &ast::Module) -> Vec { + let used_host_calls = used_stdlib_host_calls(ast); let mut imports = Vec::new(); for import in &ast.imports { let Some(module) = StdlibRegistry::new().module(&import.module.text).cloned() else { continue; }; for member in module.members { - if member.strategy != MemberStrategy::HostImport { + if member.strategy != MemberStrategy::HostImport + || !used_host_calls.contains(&(module.name.into(), member.name.into())) + { continue; } let Some(type_) = module.interface.functions.get(member.name).cloned() else { @@ -1244,6 +1249,7 @@ fn stdlib_lowered_name(module: &str, member: &str) -> String { format!("__stdlib_{}_{}", module.replace('/', "_"), member) } +// TODO: make this a "static" method/constructor fn stdlib_boundary(strategy: MemberStrategy, member: &str) -> CallBoundary { match strategy { MemberStrategy::HostImport => { @@ -1253,6 +1259,211 @@ fn stdlib_boundary(strategy: MemberStrategy, member: &str) -> CallBoundary { } } +/// TODO: This could be an instance method on [ast::Module] +fn used_stdlib_host_calls(module: &ast::Module) -> HashSet<(String, String)> { + let mut calls = HashSet::new(); + for declaration in &module.declarations { + collect_stdlib_host_calls_in_declaration(module, declaration, &mut calls); + } + calls +} + +fn collect_stdlib_host_calls_in_declaration( + module: &ast::Module, declaration: &AstDeclaration, calls: &mut HashSet<(String, String)>, +) { + match declaration { + AstDeclaration::Function(function) => collect_stdlib_host_calls_in_block(module, &function.body, calls), + AstDeclaration::Constant(constant) => collect_stdlib_host_calls_in_expression(module, &constant.value, calls), + AstDeclaration::TargetGroup(group) => { + for declaration in &group.declarations { + collect_stdlib_host_calls_in_declaration(module, declaration, calls); + } + } + AstDeclaration::Import(_) + | AstDeclaration::ExternalFunction(_) + | AstDeclaration::ExternalType(_) + | AstDeclaration::TypeAlias(_) + | AstDeclaration::TypeDefinition(_) + | AstDeclaration::Attribute(_) + | AstDeclaration::Comment(_) + | AstDeclaration::Statement(_) => {} + } +} + +fn collect_stdlib_host_calls_in_block(module: &ast::Module, block: &ast::Block, calls: &mut HashSet<(String, String)>) { + for statement in &block.statements { + match statement { + Statement::Let(let_) => collect_stdlib_host_calls_in_expression(module, &let_.value, calls), + Statement::LetAssert(let_assert) => { + collect_stdlib_host_calls_in_expression(module, &let_assert.value, calls); + if let Some(message) = &let_assert.message { + collect_stdlib_host_calls_in_expression(module, message, calls); + } + } + Statement::Expression(expression) => collect_stdlib_host_calls_in_expression(module, expression, calls), + } + } +} + +fn collect_stdlib_host_calls_in_expression( + module: &ast::Module, expression: &AstExpression, calls: &mut HashSet<(String, String)>, +) { + if let AstExpression::Call(call) = expression + && let Some(stdlib_call) = stdlib_call(module, &call.function) + && stdlib_call.strategy == MemberStrategy::HostImport + { + calls.insert((stdlib_call.module, stdlib_call.member)); + } + + match expression { + AstExpression::Call(call) => { + collect_stdlib_host_calls_in_expression(module, &call.function, calls); + for argument in &call.arguments { + collect_stdlib_host_calls_in_expression(module, &argument.value, calls); + } + } + AstExpression::FieldAccess(access) => collect_stdlib_host_calls_in_expression(module, &access.record, calls), + AstExpression::Block(block) => collect_stdlib_host_calls_in_block(module, block, calls), + AstExpression::Case(case) => { + for subject in &case.subjects { + collect_stdlib_host_calls_in_expression(module, subject, calls); + } + for clause in &case.clauses { + if let Some(guard) = &clause.guard { + collect_stdlib_host_calls_in_expression(module, guard, calls); + } + collect_stdlib_host_calls_in_expression(module, &clause.value, calls); + } + } + AstExpression::BinaryOperation(operation) => { + collect_stdlib_host_calls_in_expression(module, &operation.left, calls); + collect_stdlib_host_calls_in_expression(module, &operation.right, calls); + } + AstExpression::Pipeline(pipeline) => { + collect_stdlib_host_calls_in_expression(module, &pipeline.value, calls); + collect_stdlib_host_calls_in_expression(module, &pipeline.into, calls); + } + AstExpression::UnaryOperation(operation) => { + collect_stdlib_host_calls_in_expression(module, &operation.value, calls); + } + AstExpression::Use(use_) => collect_stdlib_host_calls_in_expression(module, &use_.value, calls), + AstExpression::AnonymousFunction(function) => collect_stdlib_host_calls_in_block(module, &function.body, calls), + AstExpression::Capture(capture) => { + collect_stdlib_host_calls_in_expression(module, &capture.function, calls); + for argument in capture.arguments.iter().flatten() { + collect_stdlib_host_calls_in_expression(module, &argument.value, calls); + } + } + AstExpression::Record(record) => { + for argument in &record.arguments { + collect_stdlib_host_calls_in_expression(module, &argument.value, calls); + } + } + AstExpression::RecordUpdate(update) => { + collect_stdlib_host_calls_in_expression(module, &update.spread, calls); + for argument in &update.updates { + collect_stdlib_host_calls_in_expression(module, &argument.value, calls); + } + } + AstExpression::Tuple(tuple) => { + for element in &tuple.elements { + collect_stdlib_host_calls_in_expression(module, element, calls); + } + } + AstExpression::TupleAccess(access) => collect_stdlib_host_calls_in_expression(module, &access.tuple, calls), + AstExpression::List(list) => { + for element in &list.elements { + collect_stdlib_host_calls_in_expression(module, element, calls); + } + if let Some(spread) = &list.spread { + collect_stdlib_host_calls_in_expression(module, spread, calls); + } + } + AstExpression::Panic(failure) | AstExpression::Todo(failure) => { + if let Some(message) = &failure.message { + collect_stdlib_host_calls_in_expression(module, message, calls); + } + } + AstExpression::Assert(assert) => collect_stdlib_host_calls_in_expression(module, &assert.value, calls), + AstExpression::Echo(echo) => collect_stdlib_host_calls_in_expression(module, &echo.value, calls), + AstExpression::Literal(_) | AstExpression::Variable(_) | AstExpression::BitArray(_) | AstExpression::Raw(_) => { + } + } +} + +fn constructor_type_substitutions(info: &ConstructorInfo, subject_type: &Type) -> HashMap { + let (Type::Custom { name: return_name, args: return_args } | Type::Opaque { name: return_name, args: return_args }) = + &info.return_type + else { + return HashMap::new(); + }; + let (Type::Custom { name: subject_name, args: subject_args } + | Type::Opaque { name: subject_name, args: subject_args }) = subject_type + else { + return HashMap::new(); + }; + if return_name != subject_name || return_args.len() != subject_args.len() { + return HashMap::new(); + } + return_args + .iter() + .zip(subject_args.iter()) + .filter_map(|(parameter, argument)| match parameter { + Type::Generic(name) => Some((name.clone(), argument.clone())), + _ => None, + }) + .collect() +} + +fn substitute_type_generics(type_: &Type, substitutions: &HashMap) -> Type { + match type_ { + Type::Generic(name) => substitutions + .get(name) + .cloned() + .unwrap_or_else(|| Type::Generic(name.clone())), + Type::Tuple(items) => Type::Tuple( + items + .iter() + .map(|item| substitute_type_generics(item, substitutions)) + .collect(), + ), + Type::List(item) => Type::List(Box::new(substitute_type_generics(item, substitutions))), + Type::Record { name, fields } => Type::Record { + name: name.clone(), + fields: fields + .iter() + .map(|field| crate::types::FieldInfo { + name: field.name.clone(), + type_: substitute_type_generics(&field.type_, substitutions), + }) + .collect(), + }, + Type::Custom { name, args } => Type::Custom { + name: name.clone(), + args: args + .iter() + .map(|arg| substitute_type_generics(arg, substitutions)) + .collect(), + }, + Type::Opaque { name, args } => Type::Opaque { + name: name.clone(), + args: args + .iter() + .map(|arg| substitute_type_generics(arg, substitutions)) + .collect(), + }, + Type::Function { params, return_type } => Type::Function { + params: params + .iter() + .map(|param| substitute_type_generics(param, substitutions)) + .collect(), + return_type: Box::new(substitute_type_generics(return_type, substitutions)), + }, + Type::Int | Type::Float | Type::String | Type::BitArray | Type::Bool | Type::Nil => type_.clone(), + } +} + +// TODO: instance method fn type_has_generic(type_: &Type) -> bool { match type_ { Type::Generic(_) => true, diff --git a/crates/core/src/stdlib.rs b/crates/core/src/stdlib.rs index 3a2fd70..7ea1ed3 100644 --- a/crates/core/src/stdlib.rs +++ b/crates/core/src/stdlib.rs @@ -114,7 +114,7 @@ impl StdlibModule { "debug", vec![Type::generic("a")], Type::generic("a"), - MemberStrategy::InterfaceOnly, + MemberStrategy::Intrinsic, ), ], &[], diff --git a/crates/core/src/wasm.rs b/crates/core/src/wasm.rs index 23341e5..40363e4 100644 --- a/crates/core/src/wasm.rs +++ b/crates/core/src/wasm.rs @@ -1,6 +1,9 @@ mod helpers; -use std::{collections::HashMap, fmt::Write}; +use std::{ + collections::{HashMap, HashSet}, + fmt::Write, +}; use crate::diagnostic::{Diagnostic, DiagnosticCode, Diagnostics, Label}; use crate::ir::{self, ExpressionKind, Instruction}; @@ -123,6 +126,7 @@ pub fn emit_wat_with_options(module: &ir::Module, options: EmitOptions) -> Resul .map(|function| (function.name.clone(), function.closure_captures.clone())) .collect(), current: CurrentEmission::default(), + debug_imports: HashSet::new(), options, }; @@ -170,6 +174,9 @@ struct CurrentEmission { capture_slots: Option, record_update_source: Option, record_update_slots: Option, + debug_i32: Option, + debug_i64: Option, + debug_f64: Option, } struct Emitter { @@ -185,6 +192,7 @@ struct Emitter { function_signatures: HashMap, Type)>, closure_captures: HashMap>, current: CurrentEmission, + debug_imports: HashSet, options: EmitOptions, } @@ -194,6 +202,33 @@ struct ConcreteHostImport { name: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum DebugImport { + Bool, + Value, + I64, + F64, +} + +impl DebugImport { + fn name(self) -> &'static str { + match self { + Self::Bool => "debug_bool", + Self::Value => "debug_value", + Self::I64 => "debug_i64", + Self::F64 => "debug_f64", + } + } + + fn wasm_type(self) -> &'static str { + match self { + Self::Bool | Self::Value => "i32", + Self::I64 => "i64", + Self::F64 => "f64", + } + } +} + impl Emitter { fn constant(&mut self, constant: &ir::Constant) { if let ir::ConstantValue::Literal(ir::Literal { kind: LiteralKind::String, source }) = &constant.value { @@ -313,6 +348,19 @@ impl Emitter { writeln!(self.functions, " (local $__record_update_source i32)").expect("write WAT"); self.current.record_update_source = Some("__record_update_source".into()); } + let debug_locals = block_debug_local_types(&function.body); + if debug_locals.i32 { + writeln!(self.functions, " (local $__debug_i32 i32)").expect("write WAT"); + self.current.debug_i32 = Some("__debug_i32".into()); + } + if debug_locals.i64 { + writeln!(self.functions, " (local $__debug_i64 i64)").expect("write WAT"); + self.current.debug_i64 = Some("__debug_i64".into()); + } + if debug_locals.f64 { + writeln!(self.functions, " (local $__debug_f64 f64)").expect("write WAT"); + self.current.debug_f64 = Some("__debug_f64".into()); + } self.block(&function.body); self.current = previous_current; @@ -505,6 +553,7 @@ impl Emitter { writeln!(self.functions, " call $__list_reverse").expect("write WAT"); self.uses_runtime = true; } + "__stdlib_gleam_io_debug" => self.stdlib_io_debug(call), _ => { for argument in &call.arguments { self.expression(&argument.value); @@ -514,6 +563,71 @@ impl Emitter { } } + fn stdlib_io_debug(&mut self, call: &ir::DirectCall) { + let value = &call.arguments[0].value; + match value.type_ { + Type::Int => self.debug_scalar(value, DebugImport::I64), + Type::Float => self.debug_scalar(value, DebugImport::F64), + Type::Bool => self.debug_scalar(value, DebugImport::Bool), + Type::String + | Type::BitArray + | Type::Tuple(_) + | Type::List(_) + | Type::Record { .. } + | Type::Custom { .. } + | Type::Opaque { .. } + | Type::Function { .. } => self.debug_scalar(value, DebugImport::Value), + Type::Nil => {} + Type::Generic(_) => self.unsupported_type(&value.type_, value.span), + } + } + + fn debug_scalar(&mut self, value: &ir::Expression, import: DebugImport) { + let Some(local) = self.debug_local(import, value.span) else { + return; + }; + self.ensure_debug_import(import, value.span); + self.expression(value); + writeln!(self.functions, " local.tee ${local}").expect("write WAT"); + writeln!(self.functions, " call ${}", import.name()).expect("write WAT"); + writeln!(self.functions, " local.get ${local}").expect("write WAT"); + } + + fn debug_local(&mut self, import: DebugImport, span: crate::source::Span) -> Option { + let local = match import { + DebugImport::Bool | DebugImport::Value => &self.current.debug_i32, + DebugImport::I64 => &self.current.debug_i64, + DebugImport::F64 => &self.current.debug_f64, + }; + if let Some(local) = local { + return Some(local.clone()); + } + self.diagnostics.push( + Diagnostic::new(DiagnosticCode::WasmError, "debug intrinsic needs a temporary local") + .with_label(Label::primary(span, "debug value here")), + ); + None + } + + fn ensure_debug_import(&mut self, import: DebugImport, span: crate::source::Span) { + if self.options.target == WasmTarget::Wasi { + self.unsupported_stdlib_host_call("gleam/io", "debug", span); + return; + } + if !self.debug_imports.insert(import) { + return; + } + let module = self.options.target.host_module(); + writeln!( + self.imports, + " (import \"{module}\" \"{}\" (func ${} (param {})))", + import.name(), + import.name(), + import.wasm_type(), + ) + .expect("write WAT"); + } + fn binary_scalar_op(&mut self, call: &ir::DirectCall, instruction: &'static str) { self.expression(&call.arguments[0].value); self.expression(&call.arguments[1].value); @@ -1511,6 +1625,127 @@ fn closure_constant_usize(value: ClosureConstants) -> usize { u32::from(value) as usize } +#[derive(Default)] +struct DebugLocalTypes { + i32: bool, + i64: bool, + f64: bool, +} + +fn block_debug_local_types(block: &ir::Block) -> DebugLocalTypes { + let mut locals = DebugLocalTypes::default(); + for instruction in &block.instructions { + match instruction { + Instruction::Evaluate { expression, .. } + | Instruction::LocalSet { value: expression, .. } + | Instruction::AssertMatch { value: expression, .. } => { + expression_debug_local_types(expression, &mut locals) + } + } + } + expression_debug_local_types(&block.result, &mut locals); + locals +} + +fn expression_debug_local_types(expression: &ir::Expression, locals: &mut DebugLocalTypes) { + if let ExpressionKind::DirectCall(call) = &expression.kind + && call.function == "__stdlib_gleam_io_debug" + && let Some(argument) = call.arguments.first() + { + match argument.value.type_ { + Type::Int => locals.i64 = true, + Type::Float => locals.f64 = true, + Type::Bool + | Type::String + | Type::BitArray + | Type::Tuple(_) + | Type::List(_) + | Type::Record { .. } + | Type::Custom { .. } + | Type::Opaque { .. } + | Type::Function { .. } => locals.i32 = true, + Type::Nil | Type::Generic(_) => {} + } + } + + match &expression.kind { + ExpressionKind::DirectCall(call) => { + for argument in &call.arguments { + expression_debug_local_types(&argument.value, locals); + } + } + ExpressionKind::IndirectCall(call) => { + expression_debug_local_types(&call.callee, locals); + for argument in &call.arguments { + expression_debug_local_types(&argument.value, locals); + } + } + ExpressionKind::Branch(branch) => { + for subject in &branch.subjects { + expression_debug_local_types(subject, locals); + } + for clause in &branch.clauses { + if let Some(guard) = &clause.guard { + expression_debug_local_types(guard, locals); + } + expression_debug_local_types(&clause.body, locals); + } + } + ExpressionKind::Tuple(items) | ExpressionKind::List(items) => { + for item in items { + expression_debug_local_types(item, locals); + } + } + ExpressionKind::BitArrayConcat { left, right } + | ExpressionKind::Compare { left, right, .. } + | ExpressionKind::RuntimeEquality { left, right } + | ExpressionKind::ListCons { head: left, tail: right } => { + expression_debug_local_types(left, locals); + expression_debug_local_types(right, locals); + } + ExpressionKind::BitStringDeconstruct { bit_array, .. } + | ExpressionKind::FieldAccess { record: bit_array, .. } + | ExpressionKind::TupleElement { tuple: bit_array, .. } + | ExpressionKind::ListDeconstruct { list: bit_array, .. } => expression_debug_local_types(bit_array, locals), + ExpressionKind::Record(record) => { + for field in &record.fields { + expression_debug_local_types(&field.value, locals); + } + } + ExpressionKind::RecordUpdate { record, fields, .. } => { + expression_debug_local_types(record, locals); + for field in fields { + if let Some(value) = &field.value { + expression_debug_local_types(value, locals); + } + } + } + ExpressionKind::Memory(operation) => match operation { + ir::MemoryOperation::Allocate { bytes } => expression_debug_local_types(bytes, locals), + ir::MemoryOperation::Load { address, .. } => expression_debug_local_types(address, locals), + ir::MemoryOperation::Store { address, value } => { + expression_debug_local_types(address, locals); + expression_debug_local_types(value, locals); + } + }, + ExpressionKind::Pipeline(pipeline) => { + expression_debug_local_types(&pipeline.input, locals); + expression_debug_local_types(&pipeline.call, locals); + } + ExpressionKind::Use(use_) => { + expression_debug_local_types(&use_.callback, locals); + expression_debug_local_types(&use_.call, locals); + } + ExpressionKind::Literal(_) + | ExpressionKind::LocalGet(_) + | ExpressionKind::BitArray(_) + | ExpressionKind::Constructor(_) + | ExpressionKind::FunctionValue(_) + | ExpressionKind::AnonymousFunction(_) + | ExpressionKind::Failure(_) => {} + } +} + fn block_contains_indirect_call(block: &ir::Block) -> bool { block.instructions.iter().any(|instruction| match instruction { Instruction::Evaluate { expression, .. } @@ -1901,7 +2136,7 @@ mod tests { use crate::runtime::ObjectTag; use crate::source::{SourceFile, SourceFileId, Span}; use crate::{ast, ir, parse, resolve, types}; - use wasmtime::{Engine, Instance, Linker, Module, Store}; + use wasmtime::{Caller, Engine, Instance, Linker, Module, Store}; fn compile_wasm(source: &str) -> WasmModule { let source = SourceFile::new(SourceFileId(0), source); @@ -2722,16 +2957,43 @@ pub fn main() { let engine = Engine::default(); let module = Module::new(&engine, &wasm.bytes).expect("compile wasm module"); let mut linker = Linker::new(&engine); - linker.func_wrap("env", "print", |_ptr: i32| {}).expect("define print"); linker - .func_wrap("env", "println", |_ptr: i32| {}) + .func_wrap("env", "print", |mut caller: Caller<'_, String>, ptr: i32| { + let text = read_host_string(&mut caller, ptr); + caller.data_mut().push_str(&text); + }) + .expect("define print"); + linker + .func_wrap("env", "println", |mut caller: Caller<'_, String>, ptr: i32| { + let text = read_host_string(&mut caller, ptr); + caller.data_mut().push_str(&text); + caller.data_mut().push('\n'); + }) .expect("define println"); - let mut store = Store::new(&engine, ()); + let mut store = Store::new(&engine, String::new()); let instance = linker.instantiate(&mut store, &module).expect("instantiate module"); let main = instance .get_typed_func::<(), ()>(&mut store, "main") .expect("get main export"); main.call(&mut store, ()).expect("call main"); + assert_eq!(store.data(), "hi!\n"); + } + + fn read_host_string(caller: &mut Caller<'_, String>, ptr: i32) -> String { + let memory = caller + .get_export("memory") + .and_then(|export| export.into_memory()) + .expect("memory export"); + let ptr = ptr as usize; + let mut header = [0; 8]; + memory.read(&mut *caller, ptr, &mut header).expect("read string header"); + assert_eq!(u32::from_le_bytes(header[0..4].try_into().unwrap()), 1); + let len = u32::from_le_bytes(header[4..8].try_into().unwrap()) as usize; + let mut bytes = vec![0; len]; + memory + .read(&mut *caller, ptr + 8, &mut bytes) + .expect("read string data"); + String::from_utf8(bytes).expect("utf-8 string") } #[test] @@ -2751,6 +3013,7 @@ pub fn main() { io.println("hi") } "{}", wasm.wat ); + assert!(!wasm.wat.contains("__stdlib_gleam_io_print "), "{}", wasm.wat); } #[test] @@ -2775,6 +3038,7 @@ pub fn main() { io.println("hi") } fn runs_initial_stdlib_intrinsics() { let wasm = compile_wasm( r#"import gleam/int +import gleam/io import gleam/string import gleam/list @@ -2789,12 +3053,28 @@ pub fn reversed_head() -> Int { _ -> 0 } } +pub fn debugged() -> Int { io.debug(42) } +pub fn debugged_text() -> String { io.debug("ok") } "#, ); let engine = Engine::default(); let module = Module::new(&engine, &wasm.bytes).expect("compile wasm module"); - let mut store = Store::new(&engine, ()); - let instance = Instance::new(&mut store, &module, &[]).expect("instantiate module"); + let mut linker = Linker::new(&engine); + linker + .func_wrap("env", "debug_i64", |mut caller: Caller<'_, String>, value: i64| { + caller.data_mut().push_str(&value.to_string()); + caller.data_mut().push('\n'); + }) + .expect("define debug_i64"); + linker + .func_wrap("env", "debug_value", |mut caller: Caller<'_, String>, ptr: i32| { + let text = read_host_string(&mut caller, ptr); + caller.data_mut().push_str(&text); + caller.data_mut().push('\n'); + }) + .expect("define debug_value"); + let mut store = Store::new(&engine, String::new()); + let instance = linker.instantiate(&mut store, &module).expect("instantiate module"); let memory = instance.get_memory(&mut store, "memory").expect("memory export"); let number = instance @@ -2829,6 +3109,26 @@ pub fn reversed_head() -> Int { .get_typed_func::<(), i64>(&mut store, "reversed_head") .expect("get reversed_head export"); assert_eq!(reversed_head.call(&mut store, ()).expect("call reversed_head"), 3); + let debugged = instance + .get_typed_func::<(), i64>(&mut store, "debugged") + .expect("get debugged export"); + assert_eq!(debugged.call(&mut store, ()).expect("call debugged"), 42); + let debugged_text = instance + .get_typed_func::<(), i32>(&mut store, "debugged_text") + .expect("get debugged_text export"); + let pointer = debugged_text.call(&mut store, ()).expect("call debugged_text") as usize; + memory + .read(&store, pointer, &mut bytes) + .expect("read debugged text string"); + assert_eq!(&bytes[8..10], b"ok"); + assert_eq!(store.data(), "42\nok\n"); + } + + #[test] + fn compiles_common_stdlib_fixture() { + let wasm = compile_wasm(include_str!("../../../fixtures/wasm/common_stdlib.gleam")); + + assert!(!wasm.wat.contains("(import \"env\" \"print\""), "{}", wasm.wat); } #[test] diff --git a/docs/src/chapter_7/running_wasmtime_browser.md b/docs/src/chapter_7/running_wasmtime_browser.md index 719f5a9..3861111 100644 --- a/docs/src/chapter_7/running_wasmtime_browser.md +++ b/docs/src/chapter_7/running_wasmtime_browser.md @@ -62,18 +62,12 @@ A browser host usually fetches bytes and instantiates them with an import object: ```js -const imports = { - gleam: { - print_i64(value) { - console.log(value); - }, - }, -}; - -const { instance } = await WebAssembly.instantiateStreaming( - fetch("module.wasm"), - imports, -); +import { createRegulusBrowserImports } from "./host.js"; + +let instance; +const imports = createRegulusBrowserImports(() => instance); + +({ instance } = await WebAssembly.instantiateStreaming(fetch("module.wasm"), imports)); console.log(instance.exports.id(42n)); ``` @@ -82,6 +76,12 @@ console.log(instance.exports.id(42n)); instantiating from a `Response`. Hosts that cannot stream can fetch an `ArrayBuffer` and call `WebAssembly.instantiate` instead. +The browser import module is named `browser`. The example glue in +`examples/browser/host.js` provides `print`, `println`, `debug_i64`, +`debug_f64`, `debug_bool`, and `debug_value`. The debug imports print and +return control to compiled code; the compiler preserves the debugged value on +its own stack. + ## Browser memory access When a module exports memory, JavaScript sees it as a `WebAssembly.Memory` diff --git a/docs/src/internal/tasks/14_stdlib_and_host_interop.md b/docs/src/internal/tasks/14_stdlib_and_host_interop.md index 6bd9731..494e22c 100644 --- a/docs/src/internal/tasks/14_stdlib_and_host_interop.md +++ b/docs/src/internal/tasks/14_stdlib_and_host_interop.md @@ -52,27 +52,27 @@ Support useful standard library modules and host calls. ### Intrinsics and host calls -- [ ] Implement or import IO functions for Wasmtime tests and browser examples. -- [ ] Implement equality, string, bit-array, list, result, option, and debug +- [x] Implement or import IO functions for Wasmtime tests and browser examples. +- [x] Implement equality, string, bit-array, list, result, option, and debug helpers where needed by compiled programs. -- [ ] Ensure host imports and adapters are target-specific and produce +- [x] Ensure host imports and adapters are target-specific and produce diagnostics on targets where they are unavailable. -- [ ] Add Wasmtime tests for host imported functions. -- [ ] Add fixtures using common Gleam stdlib modules. +- [x] Add Wasmtime tests for host imported functions. +- [x] Add fixtures using common Gleam stdlib modules. ### Group 2: remaining stdlib -- [ ] Support `gleam/bit_array`. - [ ] Support `gleam/bool`. -- [ ] Support `gleam/bytes_tree`. - [ ] Support `gleam/dict`. -- [ ] Support `gleam/dynamic`. -- [ ] Support `gleam/dynamic/decode`. - [ ] Support `gleam/float`. - [ ] Support `gleam/function`. +- [ ] Support `gleam/bit_array`. +- [ ] Support `gleam/bytes_tree`. +- [ ] Support `gleam/string_tree`. +- [ ] Support `gleam/dynamic`. +- [ ] Support `gleam/dynamic/decode`. - [ ] Support `gleam/pair`. - [ ] Support `gleam/set`. -- [ ] Support `gleam/string_tree`. - [ ] Support `gleam/uri`. - [ ] Prefer compiling stdlib Gleam source for Group 2 where possible. - [ ] Add target-specific intrinsics or host adapters only when source diff --git a/examples/browser/host.js b/examples/browser/host.js new file mode 100644 index 0000000..fc01bd6 --- /dev/null +++ b/examples/browser/host.js @@ -0,0 +1,124 @@ +const textDecoder = new TextDecoder(); + +const TAG = { + STRING: 1, + LIST_CONS: 2, + TUPLE: 3, + RECORD: 4, + CUSTOM: 5, + CLOSURE: 6, + BIT_ARRAY: 7, + OPAQUE: 8, + ERROR: 9, + PANIC: 10, +}; + +export async function instantiateRegulus(source, options = {}) { + let instance; + const imports = createRegulusBrowserImports(() => instance, options); + const result = await instantiate(source, imports); + instance = result.instance; + return result; +} + +export function createRegulusBrowserImports(getInstance, options = {}) { + const write = options.write ?? ((text) => console.log(text)); + const debug = options.debug ?? ((value) => console.debug(value)); + + const memory = () => { + const instance = typeof getInstance === "function" ? getInstance() : getInstance; + if (!instance?.exports?.memory) { + throw new Error("Regulus instance memory is not available"); + } + return instance.exports.memory; + }; + + const readString = (ptr) => readRegulusString(memory(), ptr); + const readDebugValue = (ptr) => inspectRegulusValue(memory(), ptr); + + return { + browser: { + print(ptr) { + write(readString(ptr)); + }, + println(ptr) { + write(`${readString(ptr)}\n`); + }, + debug_i64(value) { + debug(value.toString()); + }, + debug_f64(value) { + debug(value); + }, + debug_bool(value) { + debug(value !== 0); + }, + debug_value(ptr) { + debug(readDebugValue(ptr)); + }, + }, + }; +} + +export function readRegulusString(memory, ptr) { + const view = new DataView(memory.buffer); + const tag = view.getUint32(ptr, true); + if (tag !== TAG.STRING) { + throw new Error(`expected string at ${ptr}, found tag ${tag}`); + } + const len = view.getUint32(ptr + 4, true); + const bytes = new Uint8Array(memory.buffer, ptr + 8, len); + return textDecoder.decode(bytes); +} + +export function inspectRegulusValue(memory, ptr) { + if (ptr === 0) { + return "Nil"; + } + + const view = new DataView(memory.buffer); + const tag = view.getUint32(ptr, true); + const size = view.getUint32(ptr + 4, true); + + switch (tag) { + case TAG.STRING: + return JSON.stringify(readRegulusString(memory, ptr)); + case TAG.LIST_CONS: + return `ListCons(size: ${size}, ptr: ${ptr})`; + case TAG.TUPLE: + return `Tuple(size: ${size}, ptr: ${ptr})`; + case TAG.RECORD: + return `Record(size: ${size}, ptr: ${ptr})`; + case TAG.CUSTOM: { + const constructor = view.getUint32(ptr + 8, true); + return `Custom#${constructor}(size: ${size}, ptr: ${ptr})`; + } + case TAG.CLOSURE: { + const functionId = view.getUint32(ptr + 8, true); + return `Closure#${functionId}(captures: ${size}, ptr: ${ptr})`; + } + case TAG.BIT_ARRAY: + return `BitArray(bits: ${size}, ptr: ${ptr})`; + case TAG.OPAQUE: + return `Opaque(ptr: ${ptr})`; + case TAG.ERROR: + return `Error(size: ${size}, ptr: ${ptr})`; + case TAG.PANIC: + return `Panic(size: ${size}, ptr: ${ptr})`; + default: + return `Unknown(tag: ${tag}, ptr: ${ptr})`; + } +} + +async function instantiate(source, imports) { + if (source instanceof Response) { + return WebAssembly.instantiateStreaming(source, imports); + } + if (typeof source === "string" || source instanceof URL) { + return WebAssembly.instantiateStreaming(fetch(source), imports); + } + if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + return WebAssembly.instantiate(source, imports); + } + throw new TypeError("source must be a URL, Response, ArrayBuffer, or view"); +} diff --git a/fixtures/wasm/common_stdlib.gleam b/fixtures/wasm/common_stdlib.gleam new file mode 100644 index 0000000..009ffe2 --- /dev/null +++ b/fixtures/wasm/common_stdlib.gleam @@ -0,0 +1,48 @@ +import gleam/int +import gleam/io +import gleam/list +import gleam/option.{None, Some} +import gleam/order.{Eq, Gt, Lt} +import gleam/result.{Error, Ok} +import gleam/string + +pub fn message() -> String { + string.append("answer: ", int.to_string(42)) +} + +pub fn string_size() -> Int { + string.length(string.concat(["a", "bc"])) +} + +pub fn reversed_head() -> Int { + case list.reverse([1, 2, 3]) { + [head, ..] -> head + _ -> 0 + } +} + +pub fn ok_value(result: Result(Int, Int)) -> Int { + case result { + Ok(value) -> value + Error(_) -> 0 + } +} + +pub fn option_value(option: Option(Int)) -> Int { + case option { + Some(value) -> value + None -> 0 + } +} + +pub fn order_rank(order: Order) -> Int { + case order { + Lt -> -1 + Eq -> 0 + Gt -> 1 + } +} + +pub fn debug_identity() -> Int { + io.debug(42) +}