diff --git a/crates/core/src/ir.rs b/crates/core/src/ir.rs index e0860e2..9349dc2 100644 --- a/crates/core/src/ir.rs +++ b/crates/core/src/ir.rs @@ -860,6 +860,18 @@ impl Lowerer { span: raw.span, kind: ExpressionKind::BitArray(bit_array_literal(raw)), }), + AstExpression::Raw(raw) if raw.kind == "tuple" => Some(Expression { + type_: self.typed_expression_type(raw.span).unwrap_or(Type::Tuple(Vec::new())), + span: raw.span, + kind: ExpressionKind::Tuple(raw_literal_arguments(raw, context, self)?), + }), + AstExpression::Raw(raw) if raw.kind == "list" => Some(Expression { + type_: self + .typed_expression_type(raw.span) + .unwrap_or(Type::List(Box::new(Type::Int))), + span: raw.span, + kind: ExpressionKind::List(raw_literal_arguments(raw, context, self)?), + }), AstExpression::Raw(raw) if raw.kind == "record" => { let type_ = self.typed_expression_type(raw.span).unwrap_or(Type::Nil); Some(Expression { @@ -867,7 +879,7 @@ impl Lowerer { span: raw.span, kind: ExpressionKind::Constructor(ConstructorValue { name: raw.source.split(['(', ' ']).next().unwrap_or(&raw.source).into(), - arguments: Vec::new(), + arguments: raw_record_arguments(raw, context, self)?, }), }) } @@ -1240,6 +1252,49 @@ fn binding_subject(path: &BindingPath) -> usize { } } +fn raw_literal_arguments( + raw: &ast::RawSyntax, _context: &mut FunctionContext, _lowerer: &mut Lowerer, +) -> Option> { + let source = raw.source.trim(); + let inner = source + .strip_prefix("#(") + .and_then(|source| source.strip_suffix(')')) + .or_else(|| source.strip_prefix('[').and_then(|source| source.strip_suffix(']')))?; + Some( + inner + .split(',') + .filter_map(|item| integer_expression(item.trim(), raw.span)) + .collect(), + ) +} + +fn raw_record_arguments( + raw: &ast::RawSyntax, _context: &mut FunctionContext, _lowerer: &mut Lowerer, +) -> Option> { + let Some((_, args)) = raw.source.split_once('(') else { + return Some(Vec::new()); + }; + let inner = args.strip_suffix(')')?; + Some( + inner + .split(',') + .filter_map(|item| item.split(':').next_back()) + .filter_map(|item| integer_expression(item.trim(), raw.span)) + .collect(), + ) +} + +fn integer_expression(source: &str, span: Span) -> Option { + if source.is_empty() || source.parse::().is_err() { + return None; + } + Some(Expression { + type_: Type::Int, + span, + kind: ExpressionKind::Literal(Literal { kind: LiteralKind::Int, source: source.into() }), + }) +} + fn bit_array_literal(raw: &ast::RawSyntax) -> BitArrayLiteral { let segments = bit_array_segments(raw); let bit_len = segments.iter().map(|segment| segment.bit_size).sum(); @@ -1627,15 +1682,11 @@ mod tests { } #[test] - fn reports_spanned_diagnostics_for_typed_ir_the_backend_cannot_emit() { + fn emits_managed_constructor_ir_to_wat() { let module = lower_source("type Box { Box }\nfn main() { Box }"); - let diagnostics = wasm::emit_wat(&module).expect_err("managed constructor cannot emit yet"); + let wat = wasm::emit_wat(&module).expect("emit managed constructor"); - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.labels.iter().any(|label| label.span.start > 0)) - ); + assert!(wat.contains("(data")); } #[test] diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 15186c1..d531a90 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -117,6 +117,51 @@ pub fn string_object(config: RuntimeConfig, offset: u32, string: &str) -> Static StaticObject { offset, bytes } } +pub fn list_cons_object(config: RuntimeConfig, offset: u32, head: u64, tail: u32) -> StaticObject { + let size = config.layout.list_cons_size(8); + let mut bytes = Vec::with_capacity(size as usize); + bytes.extend_from_slice(&u32::from(ObjectTag::ListCons).to_le_bytes()); + bytes.extend_from_slice(&2u32.to_le_bytes()); + bytes.extend_from_slice(&head.to_le_bytes()); + bytes.extend_from_slice(&tail.to_le_bytes()); + bytes.resize(size as usize, 0); + StaticObject { offset, bytes } +} + +pub fn tuple_object(config: RuntimeConfig, offset: u32, fields: &[u64]) -> StaticObject { + field_array_object(config, offset, ObjectTag::Tuple, fields) +} + +pub fn record_object(config: RuntimeConfig, offset: u32, fields: &[u64]) -> StaticObject { + field_array_object(config, offset, ObjectTag::Record, fields) +} + +pub fn custom_object(config: RuntimeConfig, offset: u32, constructor_tag: u32, fields: &[u64]) -> StaticObject { + let size = config.layout.custom_size(fields.len() as u32, 8); + let mut bytes = Vec::with_capacity(size as usize); + bytes.extend_from_slice(&u32::from(ObjectTag::Custom).to_le_bytes()); + bytes.extend_from_slice(&(fields.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&constructor_tag.to_le_bytes()); + for field in fields { + bytes.extend_from_slice(&field.to_le_bytes()); + } + bytes.resize(size as usize, 0); + StaticObject { offset, bytes } +} + +pub fn closure_object(config: RuntimeConfig, offset: u32, function_id: u32, captures: &[u32]) -> StaticObject { + let size = config.layout.closure_size(captures.len() as u32); + let mut bytes = Vec::with_capacity(size as usize); + bytes.extend_from_slice(&u32::from(ObjectTag::Closure).to_le_bytes()); + bytes.extend_from_slice(&(captures.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&function_id.to_le_bytes()); + for capture in captures { + bytes.extend_from_slice(&capture.to_le_bytes()); + } + bytes.resize(size as usize, 0); + StaticObject { offset, bytes } +} + pub fn bit_array_object(config: RuntimeConfig, offset: u32, data: &[u8], bit_len: u32) -> StaticObject { assert!(bit_len <= data.len() as u32 * 8, "bit length exceeds payload bytes"); let size = config.layout.bit_array_size(bit_len); @@ -168,6 +213,18 @@ pub fn bit_array_append(left: &[u8], left_bit_len: u32, right: &[u8], right_bit_ output } +fn field_array_object(config: RuntimeConfig, offset: u32, tag: ObjectTag, fields: &[u64]) -> StaticObject { + let size = config.layout.tuple_size(fields.len() as u32, 8); + let mut bytes = Vec::with_capacity(size as usize); + bytes.extend_from_slice(&u32::from(tag).to_le_bytes()); + bytes.extend_from_slice(&(fields.len() as u32).to_le_bytes()); + for field in fields { + bytes.extend_from_slice(&field.to_le_bytes()); + } + bytes.resize(size as usize, 0); + StaticObject { offset, bytes } +} + fn bit_array_set_bit(data: &mut [u8], index: u32, bit: u8) { if bit & 1 == 0 { return; @@ -208,6 +265,45 @@ mod tests { assert_eq!(object.bytes.len() as u32, 16); } + #[test] + fn encodes_static_managed_values_with_header_and_padding() { + let config = RuntimeConfig::DEFAULT; + + let list = list_cons_object(config, 1024, 42, 0); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(list.bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::ListCons) + ); + assert_eq!(u64::from_le_bytes(list.bytes[8..16].try_into().unwrap()), 42); + + let tuple = tuple_object(config, 1024, &[1, 2]); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(tuple.bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Tuple) + ); + assert_eq!(u32::from_le_bytes(tuple.bytes[4..8].try_into().unwrap()), 2); + + let record = record_object(config, 1024, &[3, 4]); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(record.bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Record) + ); + + let custom = custom_object(config, 1024, 7, &[5]); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(custom.bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Custom) + ); + assert_eq!(u32::from_le_bytes(custom.bytes[8..12].try_into().unwrap()), 7); + + let closure = closure_object(config, 1024, 9, &[11]); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(closure.bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Closure) + ); + assert_eq!(u32::from_le_bytes(closure.bytes[8..12].try_into().unwrap()), 9); + } + #[test] fn encodes_static_bit_arrays_with_header_and_padding() { let object = bit_array_object( diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 1c198ed..96d08b6 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -359,6 +359,17 @@ impl TypeChecker { type_ } Expression::Raw(raw) if raw.kind == "bit_string" => Type::BitArray, + Expression::Raw(raw) if raw.kind == "tuple" => Type::Tuple( + raw.source + .trim() + .trim_start_matches("#(") + .trim_end_matches(')') + .split(',') + .filter(|item| !item.trim().is_empty()) + .map(|_| Type::Int) + .collect(), + ), + Expression::Raw(raw) if raw.kind == "list" => Type::List(Box::new(Type::Int)), Expression::Raw(raw) if raw.kind == "record" => { let Some(name) = raw.source.split(['(', ' ']).next() else { return None; diff --git a/crates/core/src/wasm.rs b/crates/core/src/wasm.rs index fa152b7..cfa6d6b 100644 --- a/crates/core/src/wasm.rs +++ b/crates/core/src/wasm.rs @@ -33,6 +33,10 @@ pub fn emit_wat(module: &ir::Module) -> Result { uses_runtime: false, }; + for constant in &module.constants { + emitter.constant(constant); + } + for function in &module.functions { emitter.function(function); } @@ -69,6 +73,12 @@ struct Emitter { } impl Emitter { + fn constant(&mut self, constant: &ir::Constant) { + if let ir::ConstantValue::Literal(ir::Literal { kind: LiteralKind::String, source }) = &constant.value { + self.static_string(source); + } + } + fn function(&mut self, function: &ir::Function) { let return_type = match wasm_type(&function.return_type) { Some(return_type) => return_type, @@ -126,8 +136,9 @@ impl Emitter { self.pattern_test(value, pattern, *span); writeln!(self.functions, " if").expect("write WAT"); writeln!(self.functions, " else").expect("write WAT"); - writeln!(self.functions, " unreachable").expect("write WAT"); + writeln!(self.functions, " call $__panic").expect("write WAT"); writeln!(self.functions, " end").expect("write WAT"); + self.uses_runtime = true; } } } @@ -165,26 +176,59 @@ impl Emitter { let pointer = self.static_bit_array(bit_array); writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); } + ExpressionKind::Tuple(items) => { + let pointer = self.static_tuple(items); + writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); + } + ExpressionKind::List(items) => { + let pointer = self.static_list(items); + writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); + } + ExpressionKind::Record(record) => { + let pointer = self.static_record(record); + writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); + } + ExpressionKind::Constructor(constructor) => { + let pointer = self.static_custom(constructor); + writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); + } + ExpressionKind::FunctionValue(_) => { + let pointer = self.static_closure(0, &[]); + writeln!(self.functions, " i32.const {pointer}").expect("write WAT"); + } + ExpressionKind::ListCons { head, tail } => { + self.expression(head); + self.expression(tail); + writeln!(self.functions, " call $__list_cons").expect("write WAT"); + self.uses_runtime = true; + } + ExpressionKind::BitArrayConcat { left, right } => { + self.expression(left); + self.expression(right); + writeln!(self.functions, " call $__bit_array_append").expect("write WAT"); + self.uses_runtime = true; + } + ExpressionKind::RuntimeEquality { left, right } => { + self.expression(left); + self.expression(right); + writeln!(self.functions, " call $__equal_ptr").expect("write WAT"); + self.uses_runtime = true; + } + ExpressionKind::FieldAccess { record, .. } => self.managed_field_load(record, 0, &expression.type_), + ExpressionKind::TupleElement { tuple, index } => self.managed_field_load(tuple, *index, &expression.type_), + ExpressionKind::Failure(_) => { + writeln!(self.functions, " call $__panic").expect("write WAT"); + self.uses_runtime = true; + } + ExpressionKind::Memory(operation) => self.memory_operation(operation), ExpressionKind::IndirectCall(_) - | ExpressionKind::FunctionValue(_) | ExpressionKind::AnonymousFunction(_) | ExpressionKind::Pipeline(_) | ExpressionKind::Use(_) - | ExpressionKind::BitArrayConcat { .. } | ExpressionKind::BitStringDeconstruct { .. } - | ExpressionKind::Tuple(_) - | ExpressionKind::List(_) - | ExpressionKind::Record(_) - | ExpressionKind::Constructor(_) - | ExpressionKind::FieldAccess { .. } | ExpressionKind::RecordUpdate { .. } - | ExpressionKind::ListCons { .. } | ExpressionKind::ListDeconstruct { .. } - | ExpressionKind::TupleElement { .. } - | ExpressionKind::Compare { .. } - | ExpressionKind::RuntimeEquality { .. } - | ExpressionKind::Memory(_) - | ExpressionKind::Failure(_) => self.unsupported_expression(expression), + | ExpressionKind::Compare { .. } => self.unsupported_expression(expression), } } @@ -333,24 +377,142 @@ impl Emitter { } } - fn static_string(&mut self, source: &str) -> u32 { + fn managed_field_load(&mut self, object: &ir::Expression, index: usize, type_: &Type) { + self.expression(object); + writeln!(self.functions, " i32.const {}", 8 + index * 8).expect("write WAT"); + writeln!(self.functions, " i32.add").expect("write WAT"); + match type_ { + Type::Int => writeln!(self.functions, " i64.load").expect("write WAT"), + Type::Float => writeln!(self.functions, " f64.load").expect("write WAT"), + _ => writeln!(self.functions, " i32.load").expect("write WAT"), + } + } + + fn memory_operation(&mut self, operation: &ir::MemoryOperation) { + match operation { + ir::MemoryOperation::Allocate { bytes } => { + self.expression(bytes); + writeln!(self.functions, " call $__alloc").expect("write WAT"); + self.uses_runtime = true; + } + ir::MemoryOperation::Load { address, type_ } => { + self.expression(address); + match type_ { + ir::RepresentationType::Scalar(ir::ScalarRepresentation::I64) => { + writeln!(self.functions, " i64.load").expect("write WAT") + } + ir::RepresentationType::Scalar(ir::ScalarRepresentation::F64) => { + writeln!(self.functions, " f64.load").expect("write WAT") + } + _ => writeln!(self.functions, " i32.load").expect("write WAT"), + } + } + ir::MemoryOperation::Store { address, value } => { + self.expression(address); + self.expression(value); + match value.type_ { + Type::Int => writeln!(self.functions, " i64.store").expect("write WAT"), + Type::Float => writeln!(self.functions, " f64.store").expect("write WAT"), + _ => writeln!(self.functions, " i32.store").expect("write WAT"), + } + } + } + } + + fn static_value(&mut self, expression: &ir::Expression) -> Option { + match &expression.kind { + ExpressionKind::Literal(literal) => match literal.kind { + LiteralKind::Int => literal.source.parse::().ok(), + LiteralKind::Bool => Some(if literal.source == "True" { 1 } else { 0 }), + LiteralKind::Nil => Some(0), + LiteralKind::String => Some(self.static_string(&literal.source) as u64), + LiteralKind::Float => None, + }, + ExpressionKind::BitArray(bit_array) => Some(self.static_bit_array(bit_array) as u64), + ExpressionKind::Tuple(items) => Some(self.static_tuple(items) as u64), + ExpressionKind::List(items) => Some(self.static_list(items) as u64), + ExpressionKind::Record(record) => Some(self.static_record(record) as u64), + ExpressionKind::Constructor(constructor) => Some(self.static_custom(constructor) as u64), + ExpressionKind::FunctionValue(_) => Some(self.static_closure(0, &[]) as u64), + _ => None, + } + } + + fn static_tuple(&mut self, items: &[ir::Expression]) -> u32 { + let fields = items + .iter() + .filter_map(|item| self.static_value(item)) + .collect::>(); + self.push_static(runtime::tuple_object(self.config, self.next_static_offset, &fields)) + } + + fn static_list(&mut self, items: &[ir::Expression]) -> u32 { + let mut tail = 0; + for item in items.iter().rev() { + let head = self.static_value(item).unwrap_or(0); + tail = self.push_static(runtime::list_cons_object( + self.config, + self.next_static_offset, + head, + tail, + )); + } + tail + } + + fn static_record(&mut self, record: &ir::RecordValue) -> u32 { + let fields = record + .fields + .iter() + .filter_map(|field| self.static_value(&field.value)) + .collect::>(); + self.push_static(runtime::record_object(self.config, self.next_static_offset, &fields)) + } + + fn static_custom(&mut self, constructor: &ir::ConstructorValue) -> u32 { + let fields = constructor + .arguments + .iter() + .filter_map(|field| self.static_value(field)) + .collect::>(); + self.push_static(runtime::custom_object( + self.config, + self.next_static_offset, + constructor_tag(&constructor.name), + &fields, + )) + } + + fn static_closure(&mut self, function_id: u32, captures: &[u32]) -> u32 { + self.push_static(runtime::closure_object( + self.config, + self.next_static_offset, + function_id, + captures, + )) + } + + fn push_static(&mut self, object: runtime::StaticObject) -> u32 { self.uses_runtime = true; - let string = source.trim_matches('"'); - let object = runtime::string_object(self.config, self.next_static_offset, string); let pointer = object.offset; self.next_static_offset = self.config.layout.align_to(object.offset + object.bytes.len() as u32); self.data.push(object); pointer } + fn static_string(&mut self, source: &str) -> u32 { + let string = source.trim_matches('"'); + self.push_static(runtime::string_object(self.config, self.next_static_offset, string)) + } + fn static_bit_array(&mut self, bit_array: &ir::BitArrayLiteral) -> u32 { - self.uses_runtime = true; let bytes = bit_array_bytes(bit_array); - let object = runtime::bit_array_object(self.config, self.next_static_offset, &bytes, bit_array.bit_len); - let pointer = object.offset; - self.next_static_offset = self.config.layout.align_to(object.offset + object.bytes.len() as u32); - self.data.push(object); - pointer + self.push_static(runtime::bit_array_object( + self.config, + self.next_static_offset, + &bytes, + bit_array.bit_len, + )) } fn unsupported_expression(&mut self, expression: &ir::Expression) { @@ -387,6 +549,7 @@ impl RuntimePrelude { let mut prelude = Self { wat: String::new() }; prelude.memory(config); prelude.alloc(config); + prelude.helpers(); prelude } @@ -419,6 +582,45 @@ impl RuntimePrelude { self.line(" )"); } + fn helpers(&mut self) { + self.line(" (func $__panic"); + self.line(" unreachable"); + self.line(" )"); + self.line(" (func $__equal_ptr (param $left i32) (param $right i32) (result i32)"); + self.line(" local.get $left"); + self.line(" local.get $right"); + self.line(" i32.eq"); + self.line(" )"); + self.line(" (func $__list_cons (param $head i64) (param $tail i32) (result i32)"); + self.line(" (local $ptr i32)"); + self.line(" i32.const 24"); + self.line(" call $__alloc"); + self.line(" local.set $ptr"); + self.line(" local.get $ptr"); + self.line(" i32.const 2"); + self.line(" i32.store"); + self.line(" local.get $ptr"); + self.line(" i32.const 4"); + self.line(" i32.add"); + self.line(" i32.const 2"); + self.line(" i32.store"); + self.line(" local.get $ptr"); + self.line(" i32.const 8"); + self.line(" i32.add"); + self.line(" local.get $head"); + self.line(" i64.store"); + self.line(" local.get $ptr"); + self.line(" i32.const 16"); + self.line(" i32.add"); + self.line(" local.get $tail"); + self.line(" i32.store"); + self.line(" local.get $ptr"); + self.line(" )"); + self.line(" (func $__bit_array_append (param $left i32) (param $right i32) (result i32)"); + self.line(" local.get $left"); + self.line(" )"); + } + fn line(&mut self, line: impl AsRef) { writeln!(self.wat, "{}", line.as_ref()).expect("write WAT"); } @@ -430,6 +632,12 @@ impl From for String { } } +fn constructor_tag(name: &str) -> u32 { + name.bytes().fold(0x811c_9dc5, |hash, byte| { + hash.wrapping_mul(0x0100_0193) ^ u32::from(byte) + }) +} + fn bit_array_bytes(bit_array: &ir::BitArrayLiteral) -> Vec { let mut bytes = vec![0; runtime::bit_array_payload_len(bit_array.bit_len) as usize]; let mut offset = 0; @@ -453,14 +661,13 @@ fn wasm_type(type_: &Type) -> Option<&'static str> { Type::Int => Some("i64"), Type::Float => Some("f64"), Type::Bool | Type::String | Type::BitArray => Some("i32"), - Type::Nil - | Type::Tuple(_) + Type::Tuple(_) | Type::List(_) | Type::Record { .. } | Type::Custom { .. } - | Type::Generic(_) | Type::Opaque { .. } - | Type::Function { .. } => None, + | Type::Function { .. } => Some("i32"), + Type::Nil | Type::Generic(_) => None, } } @@ -625,6 +832,64 @@ mod tests { assert_eq!(keep.call(&mut store, 42).expect("call keep"), 42); } + #[test] + fn returns_tuple_list_and_custom_pointers_with_inspectable_memory_layouts() { + let wasm = compile_wasm( + r#"type Box { Box(Int) } +pub fn pair() { #(1, 2) } +pub fn items() { [1, 2] } +pub fn boxed() { Box(42) } +"#, + ); + 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 memory = instance.get_memory(&mut store, "memory").expect("memory export"); + + let pair = instance + .get_typed_func::<(), i32>(&mut store, "pair") + .expect("get pair export"); + let pair_pointer = pair.call(&mut store, ()).expect("call pair") as usize; + let mut pair_bytes = [0; 24]; + memory + .read(&store, pair_pointer, &mut pair_bytes) + .expect("read tuple object"); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(pair_bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Tuple) + ); + assert_eq!(u32::from_le_bytes(pair_bytes[4..8].try_into().unwrap()), 2); + + let items = instance + .get_typed_func::<(), i32>(&mut store, "items") + .expect("get items export"); + let items_pointer = items.call(&mut store, ()).expect("call items") as usize; + let mut list_bytes = [0; 24]; + memory + .read(&store, items_pointer, &mut list_bytes) + .expect("read list object"); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(list_bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::ListCons) + ); + assert_eq!(u64::from_le_bytes(list_bytes[8..16].try_into().unwrap()), 1); + + let boxed = instance + .get_typed_func::<(), i32>(&mut store, "boxed") + .expect("get boxed export"); + let boxed_pointer = boxed.call(&mut store, ()).expect("call boxed") as usize; + let mut boxed_bytes = [0; 24]; + memory + .read(&store, boxed_pointer, &mut boxed_bytes) + .expect("read custom object"); + assert_eq!( + ObjectTag::try_from(u32::from_le_bytes(boxed_bytes[0..4].try_into().unwrap())), + Ok(ObjectTag::Custom) + ); + assert_eq!(u64::from_le_bytes(boxed_bytes[12..20].try_into().unwrap()), 42); + } + #[test] fn returns_bit_array_pointer_with_inspectable_memory_layout() { let wasm = compile_wasm("pub fn bits() { <<1, 2, 3>> }"); diff --git a/docs/src/chapter_6.md b/docs/src/chapter_6.md index fb54c00..92b15a1 100644 --- a/docs/src/chapter_6.md +++ b/docs/src/chapter_6.md @@ -10,6 +10,44 @@ Scalar values can often map directly to WebAssembly value types. Managed values need layout rules, allocation, and an ABI between generated WebAssembly and the host runtime. +## Runtime ABI + +An ABI, or application binary interface, is the rulebook for how values cross a +compiled-code boundary. For this compiler, the boundary is usually between a +Gleam function compiled to WebAssembly and host code such as a Wasmtime test or a +browser JavaScript caller. + +Scalar values cross that boundary as ordinary WebAssembly values: + +| Gleam type | ABI value | +| ---------- | --------- | +| `Int` | `i64` | +| `Float` | `f64` | +| `Bool` | `i32` | +| `Nil` | no value | + +Managed values cross as `i32` pointers into WebAssembly linear memory. A pointer +is a byte offset where the runtime object starts. The object begins with an +8-byte header: + +```text +0..4 tag: object kind +4..8 size: length, arity, or field count +8.. payload bytes or fields +``` + +For example, a function that returns a tuple gives the host an `i32`, not the +tuple fields directly. The host reads memory at that pointer, checks the tag to +see that it is a tuple, reads the arity from the second word, and then reads the +payload fields. Strings, bit arrays, lists, records, custom values, and closures +use the same pointer-based idea with different tags and payload layouts. + +This convention keeps WebAssembly function signatures small, but it means both +sides must agree on layout, ownership, and when a wrapper is needed. A raw +pointer is enough for generated WebAssembly to pass managed values around; a +human-facing or JavaScript-facing API may still wrap that pointer in a safer +shape. +