diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a86dcb49..de1337521 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,7 +79,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # moonbit removed from language matrix for now - causing CI failures - lang: [c, rust, csharp, cpp, go] + lang: [c, rust, csharp, cpp, go, d] exclude: # For now csharp doesn't work on macos, so exclude it from testing. - os: macos-latest @@ -121,6 +121,12 @@ jobs: go-version: 1.25.4 if: matrix.lang == 'go' && matrix.os != 'ubuntu-latest' + - name: Setup D + uses: dlang-community/setup-dlang@v2 + with: + compiler: ldc-1.42 + if: matrix.lang == 'd' + # Hacky work-around for https://github.com/dotnet/runtime/issues/80619 - run: dotnet new console -o /tmp/foo if: matrix.os != 'windows-latest' && matrix.lang == 'csharp' diff --git a/Cargo.lock b/Cargo.lock index ec2bd3b10..de39607ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,6 +1396,7 @@ dependencies = [ "wit-bindgen-core", "wit-bindgen-cpp", "wit-bindgen-csharp", + "wit-bindgen-d", "wit-bindgen-go", "wit-bindgen-markdown", "wit-bindgen-moonbit", @@ -1447,6 +1448,20 @@ dependencies = [ "wit-parser", ] +[[package]] +name = "wit-bindgen-d" +version = "0.60.0" +dependencies = [ + "anyhow", + "clap", + "heck", + "indexmap", + "wasm-encoder 0.254.0", + "wasm-metadata 0.254.0", + "wit-bindgen-core", + "wit-component", +] + [[package]] name = "wit-bindgen-go" version = "0.60.0" diff --git a/Cargo.toml b/Cargo.toml index c1344cd1f..e8e1ccb7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ wit-bindgen-csharp = { path = 'crates/csharp', version = '0.60.0' } wit-bindgen-markdown = { path = 'crates/markdown', version = '0.60.0' } wit-bindgen-moonbit = { path = 'crates/moonbit', version = '0.60.0' } wit-bindgen-go = { path = 'crates/go', version = '0.60.0' } +wit-bindgen-d = { path = 'crates/d', version = '0.60.0' } wit-bindgen = { path = 'crates/guest-rust', version = '0.60.0', default-features = false } wit-bindgen-test = { path = 'crates/test', version = '0.60.0' } @@ -95,6 +96,7 @@ wit-bindgen-markdown = { workspace = true, features = ['clap'], optional = true wit-bindgen-moonbit = { workspace = true, features = ['clap'], optional = true } wit-bindgen-csharp = { workspace = true, features = ['clap'], optional = true } wit-bindgen-go = { workspace = true, features = ['clap'], optional = true } +wit-bindgen-d = { workspace = true, features = ['clap'], optional = true } wit-bindgen-test = { workspace = true } wit-component = { workspace = true } wasm-encoder = { workspace = true } @@ -109,7 +111,8 @@ default = [ 'csharp', 'cpp', 'moonbit', - 'async', + 'd', + 'async' ] c = ['dep:wit-bindgen-c'] cpp = ['dep:wit-bindgen-cpp'] @@ -119,4 +122,5 @@ go = ['dep:wit-bindgen-go'] csharp = ['dep:wit-bindgen-csharp'] csharp-mono = ['csharp'] moonbit = ['dep:wit-bindgen-moonbit'] +d = ['dep:wit-bindgen-d'] async = [] diff --git a/ci/publish.rs b/ci/publish.rs index b5fec6431..4f2dc76e3 100644 --- a/ci/publish.rs +++ b/ci/publish.rs @@ -25,6 +25,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[ "wit-bindgen-markdown", "wit-bindgen-moonbit", "wit-bindgen-go", + "wit-bindgen-d", "wit-bindgen-rust-macro", "wit-bindgen-rt", "wit-bindgen", diff --git a/crates/d/Cargo.toml b/crates/d/Cargo.toml new file mode 100644 index 000000000..e90a308c9 --- /dev/null +++ b/crates/d/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "wit-bindgen-d" +authors = ["Demetrius Kanios "] +version = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +homepage = 'https://github.com/bytecodealliance/wit-bindgen' +description = """ +D bindings generator for WIT and the component model, typically used through the +`wit-bindgen-cli` crate. +""" + +[lints] +workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +wit-bindgen-core = { workspace = true } +wit-component = { workspace = true } +wasm-encoder = { workspace = true } +wasm-metadata = { workspace = true } +anyhow = { workspace = true } +heck = { workspace = true } +clap = { workspace = true, optional = true } +indexmap = { workspace = true } + +[features] +clap = ['dep:clap', 'wit-bindgen-core/clap'] diff --git a/crates/d/LICENSE-APACHE b/crates/d/LICENSE-APACHE new file mode 120000 index 000000000..1cd601d0a --- /dev/null +++ b/crates/d/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/d/LICENSE-Apache-2.0_WITH_LLVM-exception b/crates/d/LICENSE-Apache-2.0_WITH_LLVM-exception new file mode 120000 index 000000000..3a28a354e --- /dev/null +++ b/crates/d/LICENSE-Apache-2.0_WITH_LLVM-exception @@ -0,0 +1 @@ +../../LICENSE-Apache-2.0_WITH_LLVM-exception \ No newline at end of file diff --git a/crates/d/LICENSE-MIT b/crates/d/LICENSE-MIT new file mode 120000 index 000000000..b2cfbdc7b --- /dev/null +++ b/crates/d/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/d/README.md b/crates/d/README.md new file mode 100644 index 000000000..646c52eb6 --- /dev/null +++ b/crates/d/README.md @@ -0,0 +1,47 @@ +# `wit-bindgen` D Bindings Generator + +This tool generates [D](https://dlang.org) bindings for a chosen WIT world. + +## Usage + +To generate bindings with this crate, issue the `d` subcommand to `wit-bindgen`: + +```bash +$ wit-bindgen d [OPTIONS] +``` + +See the output of `wit-bindgen help d` for available options. + +## Output Structure + +Running `wit-bindgen d` on a WIT world will produce a full package structure mirroring the organization of the WIT world and the interfaces it uses. The default output directory and root package are both `wit`. + +`wit.common` holds some definitions used across the generated bindings (for e.g. `result`, `option`, `list`, etc.). + +The generated file for the world will by default, `public import` all relavent definitions. More selective imports can be made by importing the appropriate modules corresponding to particular interfaces. + +The module for the world `foo:bar/world` will be placed in `wit/foo/bar/world/package.d`. Anonymous interface import and exports for the world will emitted in `wit/foo/bar/world/{imports|exports}`. + +Top-level interfaces will be output similarly to worlds. `foo:bar/interface` gets split across multiple modules in `wit/foo/bar/interface/*.d`. Type definitions that are agnostic to import or export (any records, tuples, lists, etc. that do not contain resource handles) will go in `/common.d`. Function imports and import-specific types go in `/imports.d`, and exports go in `/exports.d` + +## Memory/Resource Management + +Currently, all imports take in parameters as "borrowing" and return memory "owning", with some caveats due to resource handles. + +Memory is managed on the C `malloc` heap. This assumption can be used in some cases to reduce copies when unecessary. + +When giving parameters to imports, any `list` or `string` memory is NOT freed, and the caller retains ownership/responibility for it. However, the ownership rules of WIT mean that any owning resource handles are invalidated. The bindings will not automatically "consume" these for you, and you will have to zero them out (default init) yourself after the call to prevent double-free. + +When receiving parameters to exports, `list` and `string` memory is automatically freed. You must copy this memory elsewhere to retain it past the end of the call. Resource handles are not dropped however, and BOTH owning and borrowing handles (e.g. `Res` and `Res.Borrow`) must be dropped. To drop a handle, use `witDrop`. + +When receiving values from imports, you are in charge of freeing the memory and dropping resource handles after you are finished with it/them. `witFree` deeply frees all such memory, and `witDrop` is also deep. + +When return values from exports, all memory must be on the WIT/C heap, so it can be reliably `free`d by the bindings. `witClone` deeply copies all `list`s and `string`s. + +`scope(exit)` is a valuable tool in helping keep track of this. + +In the future, changes may be made to help make this more automatic. + +## Examples + +It is recommended to peruse `tests/runtime` to find concrete examples of how to use the bindings. diff --git a/crates/d/src/lib.rs b/crates/d/src/lib.rs new file mode 100644 index 000000000..536ed32b6 --- /dev/null +++ b/crates/d/src/lib.rs @@ -0,0 +1,3507 @@ +use anyhow::Result; +use heck::*; +use std::borrow::Cow; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::mem::{replace, take}; +use std::path::PathBuf; +use wit_bindgen_core::{ + Direction, Files, InterfaceGenerator, Source, Types, WorldGenerator, + abi::{self, Bindgen, Bitcast, WasmType}, + wit_parser::*, +}; + +type DType = String; +#[derive(Default, Debug)] +struct DSig { + static_member: bool, + result: DType, + arguments: Vec<(String, DType)>, + name: String, + implicit_self: bool, +} + +#[derive(Default)] +struct D { + root_pkg: String, + common_module: String, + + used_interfaces: HashSet<(WorldKey, InterfaceId)>, + export_stubs: Vec, + + interface_imports: Vec, + interface_exports: Vec, + type_imports_src: Source, + function_imports_src: Source, + function_exports_src: Source, + export_stubs_src: Source, + + opts: Opts, + + world_id: Option, + world_fqn: String, + interface_fqns: HashMap, + + cur_interface: Option, + + types: Types, +} + +#[derive(Default, Debug)] +struct InterfaceFQNSet { + import: Option, + export: Option, + common: Option, +} + +#[derive(Default, Debug, Clone)] +#[cfg_attr(feature = "clap", derive(clap::Parser))] +pub struct Opts { + /// Where to place output files + #[cfg_attr(feature = "clap", arg(skip))] + out_dir: Option, + + /// Whether stubs/declarations for exports should be emitted + /// Only for testing purposes. + #[cfg_attr(feature = "clap", arg(long, default_value_t = false))] + emit_export_stubs: bool, + + /// Add the specified suffix to the name of the custom section containing + /// the component type. + #[cfg_attr(feature = "clap", arg(long, value_name = "STRING"))] + pub type_section_suffix: Option, + + /// Choose root package other than `wit` to nest everything under. + #[cfg_attr(feature = "clap", arg(long, value_name = "STRING"))] + pub root_package: Option, + + // TODO: find new home for wit_common; dub package? + /* + /// Whether the generated bindings should be self-contained + /// + /// Instead of relying on DRuntime (and wasi-libc) to define + /// the common types, and `cabi_realloc`, `wit.common` is + /// emitted alongside the bindings. + #[cfg_attr(feature = "clap", arg(long, default_value_t = false))] + pub self_contained: bool, + */ + /// A series of D versions that all the generated bindings + /// will be gated behind. + #[cfg_attr(feature = "clap", arg(long, value_name = "VERSION"))] + pub required_d_versions: Vec, +} + +impl Opts { + pub fn build(mut self, out_dir: Option<&PathBuf>) -> Box { + let mut r = D::default(); + self.out_dir = out_dir.cloned(); + r.opts = self.clone(); + Box::new(r) + } +} + +fn escape_d_identifier(name: &str) -> &str { + match name { + // Escape D keywords. + // Source: https://dlang.org/spec/lex.html#keywords + "abstract" => "abstract_", + "alias" => "alias_", + "align" => "align_", + "asm" => "asm_", + "assert" => "assert_", + "auto" => "auto_", + + "body" => "body_", + "bool" => "bool_", + "break" => "break_", + "byte" => "byte_", + + "case" => "case_", + "cast" => "cast_", + "catch" => "catch_", + "cdouble" => "cdouble_", + "cent" => "cent_", + "cfloat" => "cfloat_", + "char" => "char_", + "class" => "class_", + "const" => "const_", + "continue" => "continue_", + "creal" => "creal_", + + "dchar" => "dchar_", + "debug" => "debug_", + "default" => "default_", + "delegate" => "delegate_", + "delete" => "delete_", + "deprecated" => "deprecated_", + "do" => "do_", + "double" => "double_", + + "else" => "else_", + "enum" => "enum_", + "export" => "export_", + "extern" => "extern_", + + "false" => "false_", + "final" => "final_", + "finally" => "finally_", + "float" => "float_", + "for" => "for_", + "foreach" => "foreach_", + "foreach_reverse" => "foreach_reverse_", + "function" => "function_", + + "goto" => "goto_", + + "idouble" => "idouble_", + "if" => "if_", + "ifloat" => "ifloat_", + "immutable" => "immutable_", + "import" => "import_", + "in" => "in_", + "inout" => "inout_", + "int" => "int_", + "interface" => "interface_", + "invariant" => "invariant_", + "ireal" => "ireal_", + "is" => "is_", + + "lazy" => "lazy_", + "long" => "long_", + + "macro" => "macro_", + "mixin" => "mixin_", + "module" => "module_", + + "new" => "new_", + "nothrow" => "nothrow_", + "null" => "null_", + + "out" => "out_", + "override" => "override_", + + "package" => "package_", + "pragma" => "pragma_", + "private" => "private_", + "protected" => "protected_", + "public" => "public_", + "pure" => "pure_", + + "real" => "real_", + "ref" => "ref_", + "return" => "return_", + + "scope" => "scope_", + "shared" => "shared_", + "short" => "short_", + "static" => "static_", + "struct" => "struct_", + "super" => "super_", + "switch" => "switch_", + "synchronized" => "synchronized_", + + "template" => "template_", + "this" => "this_", + "throw" => "throw_", + "true" => "true_", + "try" => "try_", + "typeid" => "typeid_", + "typeof" => "typeof_", + + "ubyte" => "ubyte_", + "ucent" => "ucent_", + "uint" => "uint_", + "ulong" => "ulong_", + "union" => "union_", + "unittest" => "unittest_", + "ushort" => "ushort_", + + "version" => "version_", + "void" => "void_", + + "wchar" => "wchar_", + "while" => "while_", + "with" => "with_", + + // Common DRuntime & Phobos symbols + "Object" => "Object_", + "Error" => "Error_", + "Throwable" => "Throwable_", + "Exception" => "Exception_", + "TypeInfo" => "TypeInfo_", + + // Symbols we define as part of the bindings we want to avoid creating conflicts with + "WitList" => "WitList_", + "WitString" => "WitString_", + "WitFlags" => "WitFlags_", + "Option" => "Option_", + "Result" => "Result_", + "bits" => "bits_", // part of WitFlags + "borrow" => "borrow_", // part of the expansion of `resource` + "rep" => "rep_", // part of the expansion of `resource` + "makeNew" => "makeNew_", // part of the expansion of `resource` + "constructor" => "constructor_", // part of the expansion of `resource` + + s => s, + } +} + +pub fn wasm_type(ty: WasmType) -> &'static str { + match ty { + WasmType::I32 => "uint", + WasmType::I64 => "ulong", + WasmType::F32 => "float", + WasmType::F64 => "double", + WasmType::Pointer => "void*", + WasmType::PointerOrI64 => "ulong", + WasmType::Length => "size_t", + } +} + +fn get_package_fqn(root_pkg: &str, id: PackageId, resolve: &Resolve) -> String { + let pkg = &resolve.packages[id]; + let pkg_has_multiple_versions = resolve.packages.iter().any(|(_, p)| { + p.name.namespace == pkg.name.namespace + && p.name.name == pkg.name.name + && p.name.version != pkg.name.version + }); + + format!( + "{root_pkg}.{}.{}{}", + escape_d_identifier(&pkg.name.namespace.to_snake_case()), + escape_d_identifier(&pkg.name.name.to_snake_case()), + if pkg_has_multiple_versions { + if let Some(version) = &pkg.name.version { + let version = version + .to_string() + .replace('.', "_") + .replace('-', "_") + .replace('+', "_"); + format!("_{version}") + } else { + String::default() + } + } else { + String::default() + } + ) +} + +fn get_interface_fqn( + root_pkg: &str, + interface_id: &WorldKey, + world_fqn: &str, + resolve: &Resolve, + direction: Option, +) -> String { + match interface_id { + WorldKey::Name(name) => { + format!( + "{}.{}.{}", + world_fqn, + match direction { + None => panic!( + "Inline interfaces can only generate `import` or `export` module variant" + ), + Some(Direction::Import) => "imports", + Some(Direction::Export) => "exports", + }, + escape_d_identifier(&name.to_snake_case()) + ) + } + WorldKey::Interface(id) => { + let iface = &resolve.interfaces[*id]; + + format!( + "{}.{}.{}", + get_package_fqn(root_pkg, iface.package.unwrap(), resolve), + escape_d_identifier(&iface.name.as_ref().unwrap().to_snake_case()), + match direction { + None => "common", + Some(Direction::Import) => "imports", + Some(Direction::Export) => "exports", + }, + ) + } + } +} + +fn get_world_fqn(root_pkg: &str, id: WorldId, resolve: &Resolve) -> String { + let world = &resolve.worlds[id]; + format!( + "{}.{}", + get_package_fqn(root_pkg, world.package.unwrap(), resolve), + escape_d_identifier(&world.name.to_snake_case()) + ) +} + +impl D { + fn interface<'a>( + &'a mut self, + resolve: &'a Resolve, + direction: Option, + name: Option<&'a WorldKey>, + wasm_import_module: Option<&'a str>, + ) -> DInterfaceGenerator<'a> { + let mut sizes = SizeAlign::default(); + sizes.fill(resolve); + + DInterfaceGenerator { + src: Source::default(), + stub_src: Source::default(), + stubs: Vec::default(), + fqn: "", + r#gen: self, + resolve, + interface: None, + name: name, + sizes, + direction, + + wasm_import_module, + + return_pointer_area_size: Default::default(), + return_pointer_area_align: Default::default(), + } + } + + fn lookup_interface_fqn(&self, id: InterfaceId, direction: Option) -> Option<&str> { + let all_fqns = &self.interface_fqns[&id]; + match direction { + None => all_fqns.common.as_deref(), + Some(Direction::Import) => all_fqns.import.as_deref(), + Some(Direction::Export) => all_fqns.export.as_deref(), + } + } +} + +impl WorldGenerator for D { + fn uses_nominal_type_ids(&self) -> bool { + false + } + + fn preprocess(&mut self, resolve: &Resolve, world_id: WorldId) -> Result<()> { + self.root_pkg = self.opts.root_package.as_deref().unwrap_or("wit").into(); + self.common_module = format!("{}.common", self.root_pkg); + + self.world_fqn = get_world_fqn(&self.root_pkg, world_id, resolve); + self.world_id = Some(world_id); + self.types.analyze(resolve); + + let world = &resolve.worlds[world_id]; + + for (name, import) in world.imports.iter() { + match import { + WorldItem::Interface { id, .. } => { + let fqns = self.interface_fqns.entry(*id).or_insert_with(|| { + let mut result = InterfaceFQNSet::default(); + + match name { + WorldKey::Interface(_) => { + result.common = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + None, + )); + } + WorldKey::Name(_) => { + // For anonymous/inline imports, the common types are in the same file as the imports + result.common = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + Some(Direction::Import), + )); + } + } + + result + }); + (*fqns).import = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + Some(Direction::Import), + )) + } + _ => {} + } + } + + for (name, export) in world.exports.iter() { + match export { + WorldItem::Interface { id, .. } => { + let fqns = self.interface_fqns.entry(*id).or_insert_with(|| { + let mut result = InterfaceFQNSet::default(); + + match name { + WorldKey::Interface(_) => { + result.common = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + None, + )); + } + WorldKey::Name(_) => { + // For anonymous/inline exports, the common types are in the same file as the exports + result.common = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + Some(Direction::Export), + )); + } + } + + result + }); + (*fqns).export = Some(get_interface_fqn( + &self.root_pkg, + &name, + &self.world_fqn, + resolve, + Some(Direction::Export), + )) + } + _ => {} + } + } + + Ok(()) + } + + fn import_interface( + &mut self, + resolve: &Resolve, + name: &WorldKey, + id: InterfaceId, + files: &mut Files, + ) -> Result<()> { + self.used_interfaces.insert((name.clone(), id)); + + self.cur_interface = Some(id); + + let fqn = self.interface_fqns[&id].import.as_ref().unwrap().clone(); + + self.interface_imports.push(fqn.clone()); + + let wasm_import_module = resolve.name_world_key(name); + let mut r#gen = self.interface( + resolve, + Some(Direction::Import), + Some(name), + Some(&wasm_import_module), + ); + r#gen.fqn = &fqn; + r#gen.interface = Some(id); + r#gen.prologue(); + + if let WorldKey::Name(_) = name { + // We have an inline interface imported in a world. + // Emit the "common" types as well + + r#gen.direction = None; + r#gen.types(id); + r#gen.direction = Some(Direction::Import); + } + + r#gen.types(id); + + for (_name, func) in &resolve.interfaces[id].functions { + match func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => { + r#gen.import_func(func); + } + _ => {} + } + } + + let mut interface_filepath = PathBuf::from_iter( + ["wit"] + .into_iter() + .chain(fqn.split(".").skip(r#gen.r#gen.root_pkg.split(".").count())), + ); + interface_filepath.set_extension("d"); + + files.push(interface_filepath.to_str().unwrap(), r#gen.src.as_bytes()); + + //self.interface_imports.push(interface_src.fqn.clone()); + //interface_src.src.push_str("\n// Function imports\n"); + //interface_src.src.append_src(&tmp_src); + + self.cur_interface = None; + Ok(()) + } + + fn import_types( + &mut self, + resolve: &Resolve, + _world: WorldId, + types: &[(&str, TypeId)], + _files: &mut Files, + ) { + let fqn = self.world_fqn.clone(); + let mut r#gen = self.interface(resolve, Some(Direction::Import), None, Some("$root")); + r#gen.fqn = &fqn; + + for (name, id) in types.iter() { + r#gen.define_type(name, *id); + } + + self.type_imports_src = take(&mut r#gen.src); + } + + fn import_funcs( + &mut self, + resolve: &Resolve, + _world: WorldId, + funcs: &[(&str, &Function)], + _files: &mut Files, + ) { + let fqn = self.world_fqn.clone(); + let mut r#gen = self.interface(resolve, Some(Direction::Import), None, Some("$root")); + r#gen.fqn = &fqn; + + for (_name, func) in funcs { + r#gen.import_func(func); + } + + self.function_imports_src = take(&mut r#gen.src); + } + + fn export_interface( + &mut self, + resolve: &Resolve, + name: &WorldKey, + id: InterfaceId, + files: &mut Files, + ) -> Result<()> { + self.used_interfaces.insert((name.clone(), id)); + + self.cur_interface = Some(id); + + let fqn = self.interface_fqns[&id].export.as_ref().unwrap().clone(); + + self.interface_exports.push(fqn.clone()); + + let wasm_import_module = resolve.name_world_key(name); + let emit_exports_stubs = self.opts.emit_export_stubs; + + let mut r#gen = self.interface( + resolve, + Some(Direction::Export), + Some(name), + Some(&wasm_import_module), + ); + r#gen.fqn = &fqn; + r#gen.interface = Some(id); + r#gen.prologue(); + + if let WorldKey::Name(_) = name { + // We have an inline interface exported in a world. + // Emit the "common" types as well + + r#gen.direction = None; + r#gen.types(id); + r#gen.direction = Some(Direction::Export); + } + + r#gen.types(id); + + r#gen.src.push_str(&format!( + "\npackage({}) template Exports(Impl...) {{\n", + r#gen.r#gen.root_pkg + )); + + for (_name, func) in &resolve.interfaces[id].functions { + match func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => { + r#gen.export_func(func); + } + _ => {} + } + } + + for (type_name, type_id) in &resolve.interfaces[id].types { + let ty = &resolve.types[*type_id]; + + match &ty.kind { + TypeDefKind::Resource => { + let upper_name = ty.name.as_ref().unwrap().to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + r#gen.src.push_str(&format!( + "\n/++\n{}\n+/\n", + ty.docs.contents.as_deref().unwrap_or_default() + )); + + r#gen + .src + .push_str(&format!("/// ditto\nstruct {escaped_name}_Wrappers {{\n")); + + r#gen.src.push_str(&format!( + "alias _Resource_Impl = findWitExportResource!(\"{wasm_import_module}\", \"{type_name}\", Impl);\n" + )); + + if emit_exports_stubs { + r#gen.stub_src.push_str(&format!( + "@witExport(\"{}\", \"{}\")\nstruct {escaped_name}_STUB {{\n", + wasm_import_module, + ty.name.as_ref().unwrap() + )); + + r#gen.stubs.push(escaped_name.to_owned() + "_STUB"); + } + + for (_, func) in &resolve.interfaces[id].functions { + match func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => {} + FunctionKind::Method(owner) + | FunctionKind::AsyncMethod(owner) + | FunctionKind::Constructor(owner) + | FunctionKind::Static(owner) + | FunctionKind::AsyncStatic(owner) => { + if owner == *type_id { + r#gen.export_func(func); + } + } + } + } + + r#gen.src.push_str(&format!( + "\n@wasmExport!(\"{}#[dtor]{}\")\n", + wasm_import_module, + ty.name.as_ref().unwrap() + )); + r#gen.src.push_str(&format!( + "pragma(mangle, \"__wit_export_{}__:dtor:{}\")\n", + wasm_import_module.replace("/", "__").replace("-", "_"), + ty.name.as_ref().unwrap().replace("-", "_") + )); + r#gen.src.push_str( + "static private extern(C) void __export_dtor(void* ptr) { + (*cast(_Resource_Impl*)ptr).destroy!false; + free(ptr); + } + ", + ); + + r#gen.src.push_str("}\n"); + + if emit_exports_stubs { + r#gen.stub_src.push_str("}\n"); + } + } + _ => {} + } + } + + let ret_area_decl = r#gen.emit_ret_area_if_needed(); + + r#gen.src.push_str(&ret_area_decl); + r#gen.src.push_str("}\n\n"); + + let DInterfaceGenerator { + mut src, + stub_src, + stubs, + .. + } = r#gen; + + if self.opts.emit_export_stubs { + src.append_src(&stub_src); + + src.push_str("alias STUBS = AliasSeq!(\n"); + src.indent(1); + src.push_str(&stubs.join(",\n")); + src.deindent(1); + src.push_str("\n);\n"); + + self.export_stubs.push(format!("{fqn}.STUBS")); + } + + let mut interface_filepath = PathBuf::from_iter( + ["wit"] + .into_iter() + .chain(fqn.split(".").skip(self.root_pkg.split(".").count())), + ); + interface_filepath.set_extension("d"); + + files.push(interface_filepath.to_str().unwrap(), src.as_bytes()); + + self.cur_interface = None; + Ok(()) + } + + fn export_funcs( + &mut self, + resolve: &Resolve, + _world: WorldId, + funcs: &[(&str, &Function)], + _files: &mut Files, + ) -> Result<()> { + let fqn = self.world_fqn.clone(); + let mut r#gen = self.interface(resolve, Some(Direction::Export), None, Some("$root")); + r#gen.fqn = &fqn; + + for (_name, func) in funcs { + match func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => { + r#gen.export_func(func); + } + _ => {} + } + } + + let ret_area_decl = r#gen.emit_ret_area_if_needed(); + + let DInterfaceGenerator { + src, + stub_src, + mut stubs, + .. + } = r#gen; + + self.function_exports_src = src; + self.function_exports_src.push_str(&ret_area_decl); + + if self.opts.emit_export_stubs { + self.export_stubs_src.append_src(&stub_src); + self.export_stubs.append(&mut stubs); + } + + Ok(()) + } + + fn finish(&mut self, resolve: &Resolve, world_id: WorldId, files: &mut Files) -> Result<()> { + for (name, id) in take(&mut self.used_interfaces) { + if let WorldKey::Interface(_) = name { + let fqn = self.interface_fqns[&id].common.as_ref().unwrap().clone(); + + let wasm_import_module = resolve.name_world_key(&name); + let mut r#gen = + self.interface(resolve, None, Some(&name), Some(&wasm_import_module)); + r#gen.fqn = &fqn; + r#gen.interface = Some(id); + r#gen.prologue(); + r#gen.types(id); + + let mut interface_filepath = PathBuf::from_iter( + ["wit"] + .into_iter() + .chain(fqn.split(".").skip(r#gen.r#gen.root_pkg.split(".").count())), + ); + interface_filepath.set_extension("d"); + + files.push(interface_filepath.to_str().unwrap(), r#gen.src.as_bytes()); + } + } + + let mut world_src = Source::default(); + + let world = &resolve.worlds[world_id]; + + world_src.push_str(&format!( + "/++\n{}\n+/\n", + world.docs.contents.as_deref().unwrap_or_default() + )); + + world_src.push_str(&format!("module {};\n\n", self.world_fqn)); + world_src.push_str(&format!("import {};\n\n", self.common_module)); + world_src.push_str( + &self + .interface_imports + .iter() + .map(|fqn| format!("public import {fqn};")) + .collect::>() + .join("\n"), + ); + + world_src.push_str("\n"); + + world_src.push_str( + &self + .interface_exports + .iter() + .map(|fqn| format!("public import {fqn};")) + .collect::>() + .join("\n"), + ); + + world_src.push_str("\n"); + + world_src.append_src(&self.type_imports_src); + + world_src.append_src(&self.function_imports_src); + + world_src.push_str("\n\nprivate alias AliasSeq(T...) = T;\n"); + world_src.push_str("template Exports(Impl...) {\n"); + world_src.push_str("alias InterfaceExports = AliasSeq!(\n"); + world_src.indent(1); + world_src.push_str( + &self + .interface_exports + .iter() + .map(|fqn| format!("{fqn}.Exports!Impl")) + .collect::>() + .join(",\n"), + ); + world_src.deindent(1); + world_src.push_str("\n);\n"); + + world_src.push_str(&self.function_exports_src.as_str()); + world_src.push_str("}\n"); + + if self.opts.emit_export_stubs { + self.export_stubs_src.push_str("alias STUBS = AliasSeq!(\n"); + self.export_stubs_src.indent(1); + self.export_stubs_src + .push_str(&self.export_stubs.join(",\n")); + self.export_stubs_src.deindent(1); + self.export_stubs_src.push_str("\n);\n"); + + self.export_stubs_src + .push_str("alias Exports_STUB_INVOKE = Exports!(STUBS);\n"); + + world_src.append_src(&self.export_stubs_src); + } + + // Linker `component-type` section + { + let opts_suffix = self.opts.type_section_suffix.as_deref().unwrap_or(""); + let world = &resolve.worlds[world_id]; + let world_name = &world.name; + let pkg = &resolve.packages[world.package.unwrap()].name; + let version = env!("CARGO_PKG_VERSION"); + + let mut producers = wasm_metadata::Producers::empty(); + producers.add( + "processed-by", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + ); + + let component_type = wit_component::metadata::encode( + resolve, + world_id, + wit_component::StringEncoding::UTF8, + Some(&producers), + ) + .unwrap(); + + world_src.push_str(&format!( + " + pragma(inline, false) + package({}) void __wit_bindgen_component_type_force_link() pure @nogc nothrow {{}} + + + package({0}) void __wit_bindgen_component_type() {{ + imported!\"ldc.llvmasm\".__irEx!( + \"\", + \"\", + `!wasm.custom_sections = !{{!0}} + !0 = !{{!\"component-type:wit-bindgen:{version}:{pkg}:{world_name}:{opts_suffix}\", !\"{}\"}}`, + void + ); + }} + ", + self.root_pkg, + &component_type + .iter() + .map(|b| format!("\\{b:02X}")) + .enumerate() + .fold(String::default(), |a, (i, b)| { + if (i % 24) == 0 { a + "`\n~`" + &b } else { a + &b } + }) + )); + } + + let mut world_filepath = PathBuf::from_iter( + ["wit"].into_iter().chain( + get_world_fqn(&self.root_pkg, world_id, resolve) + .split(".") + .skip(self.root_pkg.split(".").count()), + ), + ); + world_filepath.push("package.d"); + + files.push(world_filepath.to_str().unwrap(), world_src.as_bytes()); + + let mut wit_common_file = format!("module {};\n\n", self.common_module).into_bytes(); + wit_common_file.extend_from_slice(include_bytes!("wit_common.d").as_slice()); + files.push("wit/common.d", &wit_common_file); + + Ok(()) + } +} + +struct DInterfaceGenerator<'a> { + src: Source, + stub_src: Source, + stubs: Vec, + direction: Option, + r#gen: &'a mut D, + resolve: &'a Resolve, + interface: Option, + name: Option<&'a WorldKey>, + wasm_import_module: Option<&'a str>, + fqn: &'a str, + + sizes: SizeAlign, + + return_pointer_area_size: ArchitectureSize, + return_pointer_area_align: Alignment, +} + +impl<'a> DInterfaceGenerator<'a> { + fn scoped_type_name(&self, id: TypeId, from_module_fqn: &str) -> String { + let ty = &self.resolve.types[id]; + + let owner_fqn = self + .type_owner_fqn(&ty.owner, self.r#gen.types.get(id).has_resource) + .unwrap(); + + let upper_name = ty.name.as_ref().unwrap().to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + if from_module_fqn == owner_fqn { + escaped_name.into() + } else { + format!("{owner_fqn}.{escaped_name}") + } + } + fn type_name(&self, ty: &Type, from_module_fqn: &str) -> Cow<'static, str> { + match ty { + Type::Bool => Cow::Borrowed("bool"), + Type::Char => Cow::Borrowed("dchar"), + Type::U8 => Cow::Borrowed("ubyte"), + Type::S8 => Cow::Borrowed("byte"), + Type::U16 => Cow::Borrowed("ushort"), + Type::S16 => Cow::Borrowed("short"), + Type::U32 => Cow::Borrowed("uint"), + Type::S32 => Cow::Borrowed("int"), + Type::U64 => Cow::Borrowed("ulong"), + Type::S64 => Cow::Borrowed("long"), + Type::F32 => Cow::Borrowed("float"), + Type::F64 => Cow::Borrowed("double"), + Type::String => Cow::Borrowed("WitString"), + Type::Id(id) => { + let typedef = &self.resolve.types[*id]; + + match typedef.owner { + TypeOwner::None => match &typedef.kind { + TypeDefKind::Record(_) => { + Cow::Owned(self.scoped_type_name(*id, from_module_fqn)) + } + TypeDefKind::Resource => { + Cow::Owned(self.scoped_type_name(*id, from_module_fqn)) + } + TypeDefKind::Handle(Handle::Own(id)) => { + Cow::Owned(self.scoped_type_name(*id, from_module_fqn)) + } + TypeDefKind::Handle(Handle::Borrow(id)) => { + Cow::Owned(self.scoped_type_name(*id, from_module_fqn) + ".Borrow") + } + TypeDefKind::Tuple(t) => Cow::Owned(format!( + "Tuple!({})", + t.types + .iter() + .map(|ty| self.type_name(ty, from_module_fqn).into_owned()) + .collect::>() + .join(", ") + )), + TypeDefKind::Option(o) => { + Cow::Owned(format!("Option!({})", self.type_name(o, from_module_fqn))) + } + TypeDefKind::Result(r) => Cow::Owned(format!( + "Result!({}, {})", + self.optional_type_name(r.ok.as_ref(), from_module_fqn), + self.optional_type_name(r.err.as_ref(), from_module_fqn), + )), + TypeDefKind::List(ty) => Cow::Owned(format!( + "WitList!({})", + self.type_name(&ty, from_module_fqn) + )), + TypeDefKind::Future(_) => { + todo!("type_name of `future`") + } + TypeDefKind::Stream(_) => { + todo!("type_name of `stream`") + } + TypeDefKind::FixedLengthList(ty, size) => { + Cow::Owned(format!("{}[{size}]", self.type_name(ty, from_module_fqn))) + } + TypeDefKind::Map(_, _) => todo!("type_name of `map`"), + TypeDefKind::Unknown => unimplemented!(), + unhandled => { + panic!( + "Encountered unexpected `type_name` invocation of ownerless typedef: {unhandled:?}." + ); + } + }, + _ => Cow::Owned(self.scoped_type_name(*id, from_module_fqn)), + } + } + Type::ErrorContext => todo!(), + } + } + + fn optional_type_name(&self, ty: Option<&Type>, from_module_fqn: &str) -> Cow<'static, str> { + match ty { + Some(ty) => self.type_name(ty, from_module_fqn), + None => Cow::Borrowed("void"), + } + } + + fn type_owner_fqn(&self, owner: &TypeOwner, imports_instead_of_common: bool) -> Option<&str> { + match &owner { + TypeOwner::None => None, + TypeOwner::Interface(interface_id) => match self.direction { + Some(_) => self + .r#gen + .lookup_interface_fqn(*interface_id, self.direction) + .or_else(|| { + if !imports_instead_of_common || self.direction != Some(Direction::Import) { + self.r#gen.lookup_interface_fqn( + *interface_id, + if imports_instead_of_common { + Some(Direction::Import) + } else { + None + }, + ) + } else { + None + } + }), + None => self.r#gen.lookup_interface_fqn(*interface_id, None), + }, + TypeOwner::World(world_id) => { + if *world_id != self.r#gen.world_id.unwrap() { + panic!("Dealing with type from different world?"); + } + + Some(&self.r#gen.world_fqn) + } + } + } + + fn prologue(&mut self) { + let id = self.interface.unwrap(); + + let interface = &self.resolve.interfaces[self.interface.unwrap()]; + + self.src.push_str(&format!( + "/++\n{}\n+/\n", + interface.docs.contents.as_deref().unwrap_or_default() + )); + + self.src.push_str(&format!("module {};\n\n", self.fqn)); + + for version in &self.r#gen.opts.required_d_versions { + self.src.push_str(&format!("version({version}):\n")); + } + + self.src + .push_str(&format!("\nimport {};\n\n", self.r#gen.common_module)); + if self.direction.is_some() + && let Some(WorldKey::Interface(_)) = self.name + { + self.src.push_str("public import "); + self.src + .push_str(self.r#gen.lookup_interface_fqn(id, None).unwrap()); + self.src.push_str(";\n\n"); + } + + let mut deps = BTreeSet::new(); + + for dep_id in self.resolve.interface_direct_deps(id) { + deps.insert(dep_id); + } + + for dep_id in deps { + let common_fqn = self.r#gen.lookup_interface_fqn(dep_id, None).unwrap(); + let directional_fqn = self.r#gen.lookup_interface_fqn(dep_id, self.direction); + + if let Some(WorldKey::Interface(_)) = self.name { + self.src.push_str(&format!( + "static import {};\n", + match self.direction { + Some(_) => directional_fqn.unwrap_or(common_fqn), + None => common_fqn, + } + )); + + if self.direction == Some(Direction::Export) { + if let Some(import_fqn) = self + .r#gen + .lookup_interface_fqn(dep_id, Some(Direction::Import)) + { + self.src.push_str("static import "); + self.src.push_str(import_fqn); + self.src.push_str(";\n"); + } + } + } else { + self.src.push_str(&format!("static import {common_fqn};\n")); + + if let Some(fqn) = directional_fqn { + self.src.push_str(&format!("static import {fqn};\n")); + }; + } + } + self.src.push_str("\n"); + self.src.push_str(&format!("package ({}) void __wit_bindgen_component_type_force_link() pure @nogc nothrow => imported!\"{}\".__wit_bindgen_component_type_force_link();\n", self.r#gen.root_pkg, self.r#gen.world_fqn)); + } + + fn type_is_direction_sensitive(&self, id: TypeId) -> bool { + let type_info = &self.r#gen.types.get(id); + + type_info.has_resource + } + + fn param_qualifier_for_type(&self, param_type: &Type) -> String { + match param_type { + Type::ErrorContext => todo!(), + Type::String => "in ".to_owned(), + Type::Id(id) => match &self.resolve.types[*id].kind { + TypeDefKind::Enum(_) | TypeDefKind::Flags(_) | TypeDefKind::Handle(_) => { + "".to_owned() + } + TypeDefKind::Future(_) | TypeDefKind::Map(_, _) | TypeDefKind::Stream(_) => { + todo!() + } + TypeDefKind::Type(r#type) => self.param_qualifier_for_type(r#type), + _ => { + if matches!(self.direction, Some(Direction::Export)) + && self.r#gen.types.get(*id).has_resource + { + "scope ref ".to_owned() + } else { + "in ".to_owned() + } + } + }, + _ => "".to_owned(), + } + } + + fn get_d_signature(&mut self, func: &Function) -> DSig { + match &func.kind { + FunctionKind::Freestanding + | FunctionKind::Method(_) + | FunctionKind::Static(_) + | FunctionKind::Constructor(_) => {} + + FunctionKind::AsyncFreestanding + | FunctionKind::AsyncMethod(_) + | FunctionKind::AsyncStatic(_) => { + todo!() + } + } + + let mut res = DSig::default(); + + let split_name = match &func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => &func.name, + FunctionKind::Constructor(_) => "", + FunctionKind::Method(_) + | FunctionKind::Static(_) + | FunctionKind::AsyncMethod(_) + | FunctionKind::AsyncStatic(_) => func.name.split(".").skip(1).next().unwrap(), + }; + + let lower_name = split_name.to_lower_camel_case(); + let escaped_name = if let FunctionKind::Constructor(_) = &func.kind { + match self.direction { + Some(Direction::Import) => "makeNew", + _ => "constructor", + } + } else { + escape_d_identifier(&lower_name) + }; + + res.name = escaped_name.into(); + res.static_member = match &func.kind { + FunctionKind::Static(_) => true, + FunctionKind::Constructor(_) => true, + _ => false, + }; + + res.result + .push_str(&(self.optional_type_name(func.result.as_ref(), self.fqn))); + + for ( + i, + Param { + name, ty: param, .. + }, + ) in func.params.iter().enumerate() + { + if i == 0 && name == "self" { + match &func.kind { + FunctionKind::Method(_) => { + res.implicit_self = true; + continue; + } + _ => {} + } + } + + let lower_param_name = name.to_lower_camel_case(); + let escaped_param_name = escape_d_identifier(&lower_param_name); + + let qualifier = self.param_qualifier_for_type(param); + + res.arguments.push(( + escaped_param_name.into(), + qualifier + &self.type_name(¶m, self.fqn), + )); + } + + res + } + + fn import_func(&mut self, func: &Function) { + match &func.kind { + FunctionKind::Freestanding + | FunctionKind::Constructor(_) + | FunctionKind::Method(_) + | FunctionKind::Static(_) => {} + kind => { + todo!("Import {kind:?} - {}\n", func.name); + } + } + + let wasm_sig = self + .resolve + .wasm_signature(abi::AbiVariant::GuestImport, func); + + let d_sig = self.get_d_signature(func); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + func.docs.contents.as_deref().unwrap_or_default() + )); + + if d_sig.static_member { + self.src.push_str("static "); + } + self.src.push_str(&format!( + "{} {}({}) @trusted nothrow {{\n", + d_sig.result, + d_sig.name, + d_sig + .arguments + .iter() + .map(|(name, ty)| ty.to_owned() + " " + name) + .collect::>() + .join(", ") + )); + + let mut params = Vec::new(); + + if d_sig.implicit_self { + params.push("this"); + } + for (arg, _ty) in &d_sig.arguments { + params.push(arg); + } + + let mut f = FunctionBindgen::new(self, ¶ms); + abi::call( + f.r#gen.resolve, + abi::AbiVariant::GuestImport, + abi::LiftLower::LowerArgsLiftResults, + func, + &mut f, + false, + ); + let ret_area_decl = f.emit_ret_area_if_needed(); + + let FunctionBindgen { + src, + needs_deallocate, + .. + } = f; + self.src.push_str(&ret_area_decl); + if needs_deallocate { + self.src.push_str(&format!( + "{}.DeallocateBuffer deallocate;\n", + self.r#gen.common_module + )); + } + self.src.push_str(&src); + + self.src.push_str("}\n"); + + self.src.push_str("/// ditto\n"); + self.src.push_str(&format!( + "@wasmImport!(\"{}\", \"{}\")\n", + self.wasm_import_module.unwrap(), + func.name + )); + + // The mangle is not important, as long as it won't conflict with other symbols + // WebAssembly symbol identifiers are much more permissive than C (can be any UTF-8). + // Yet, LDC before 1.42 doesn't allow full use of this fact. We make some substitutions. + self.src.push_str(&format!( + "pragma(mangle, \"__wit_import_{}__{}\")\n", + self.wasm_import_module + .unwrap() + .replace("/", "__") + .replace("-", "_"), + func.name + .replace("-", "_") + .replace("[", ":") + .replace("]", ":") + )); + + if d_sig.implicit_self || d_sig.static_member { + self.src.push_str("static "); + } + self.src.push_str(&format!( + "private extern(C) {} __import_{}({}) nothrow;\n", + match wasm_sig.results.len() { + 0 => "void", + 1 => wasm_type(wasm_sig.results[0]), + _ => unimplemented!("multi-value return not supported"), + }, + d_sig.name, + wasm_sig + .params + .iter() + .map(|param| wasm_type(*param)) + .collect::>() + .join(", ") + )); + } + + fn export_func(&mut self, func: &Function) { + match &func.kind { + FunctionKind::Freestanding + | FunctionKind::Constructor(_) + | FunctionKind::Method(_) + | FunctionKind::Static(_) => {} + kind => { + todo!("Export {kind:?} - {}\n", func.name); + } + } + + let wasm_sig = self + .resolve + .wasm_signature(abi::AbiVariant::GuestExport, func); + + let d_sig = self.get_d_signature(func); + + let mut params_data = Vec::new(); + let mut params = Vec::new(); + + if d_sig.implicit_self { + params.push("self"); + } + for (arg, _ty) in wasm_sig.params.iter().enumerate() { + params_data.push(format!("arg{arg}")); + } + for param in ¶ms_data { + params.push(¶m); + } + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + func.docs.contents.as_deref().unwrap_or_default() + )); + + self.src.push_str(&format!( + "alias {}_Sig = {} function({});\n", + d_sig.name, + d_sig.result, + d_sig + .arguments + .iter() + .map(|(name, ty)| ty.to_owned() + " " + name) + .collect::>() + .join(", ") + )); + + self.src.push_str(&format!( + "/// ditto\nalias {}_Impl = findWitExportFunc!(\"{}\", \"{}\", {0}_Sig, {}, {});\n", + d_sig.name, + self.wasm_import_module.unwrap(), + func.name, + d_sig.implicit_self, + match &func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => "Impl", + _ => { + "witExportsIn!_Resource_Impl" + } + } + )); + + if self.r#gen.opts.emit_export_stubs { + self.stub_src.push_str(&format!( + "@witExport(\"{}\", \"{}\")\n", + self.wasm_import_module.unwrap(), + func.name + )); + if d_sig.static_member { + self.stub_src.push_str("static "); + } + self.stub_src.push_str(&format!( + "{} {}_STUB({});\n", + d_sig.result, + d_sig.name, + d_sig + .arguments + .iter() + .map(|(name, ty)| ty.to_owned() + " " + name) + .collect::>() + .join(", ") + )); + + match func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => { + self.stubs.push(d_sig.name.clone() + "_STUB"); + } + _ => {} + } + } + + let core_module_name = self.name.map(|s| self.resolve.name_world_key(s)); + let export_name = func.legacy_core_export_name(core_module_name.as_deref()); + + self.src.push_str("/// ditto\n"); + self.src + .push_str(&format!("@wasmExport!(\"{export_name}\")\n")); + + self.src.push_str(&format!( + "pragma(mangle, \"__wit_export_{}\")\n", + export_name + .replace("/", "__") + .replace("-", "_") + .replace("[", ":") + .replace("]", ":") + .replace("#", "::") + )); + + if d_sig.implicit_self || d_sig.static_member { + self.src.push_str("static "); + } + self.src.push_str(&format!( + "private extern(C) {} __export_{}({}) {{\n", + match wasm_sig.results.len() { + 0 => "void", + 1 => wasm_type(wasm_sig.results[0]), + _ => unimplemented!("multi-value return not supported"), + }, + d_sig.name, + wasm_sig + .params + .iter() + .zip(¶ms) + .map(|(ty, name)| format!("{} {name}", wasm_type(*ty))) + .collect::>() + .join(", ") + )); + + let mut f = FunctionBindgen::new(self, ¶ms); + abi::call( + f.r#gen.resolve, + abi::AbiVariant::GuestExport, + abi::LiftLower::LiftArgsLowerResults, + func, + &mut f, + false, + ); + + let ret_area_decl = f.emit_ret_area_if_needed(); + + let FunctionBindgen { + src, + return_pointer_area_size, + return_pointer_area_align, + needs_deallocate, + .. + } = f; + self.return_pointer_area_size = self.return_pointer_area_size.max(return_pointer_area_size); + self.return_pointer_area_align = self + .return_pointer_area_align + .max(return_pointer_area_align); + + self.src.push_str(&ret_area_decl); + if needs_deallocate { + self.src.push_str(&format!( + "{}.DeallocateBuffer deallocate;\n", + self.r#gen.common_module + )); + } + self.src.push_str(&src); + + self.src.push_str("}\n"); + + if abi::guest_export_needs_post_return(self.resolve, func) { + let mut param_data = Vec::new(); + let mut params = Vec::<&str>::new(); + + for (arg, _ty) in wasm_sig.results.iter().enumerate() { + param_data.push(format!("arg{arg}")); + } + for param in ¶m_data { + params.push(¶m); + } + + self.src + .push_str(&format!("@wasmExport!(\"cabi_post_{export_name}\")\n")); + + self.src.push_str(&format!( + "pragma(mangle, \"__wit_cabi_post_{}\")\n", + export_name + .replace("/", "__") + .replace("-", "_") + .replace("[", ":") + .replace("]", ":") + .replace("#", "::") + )); + + if d_sig.implicit_self || d_sig.static_member { + self.src.push_str("static "); + } + self.src.push_str(&format!( + "private extern(C) void __cabi_post_{}({}) {{\n", + d_sig.name, + wasm_sig + .results + .iter() + .zip(¶ms) + .map(|(ty, name)| format!("{} {name}", wasm_type(*ty))) + .collect::>() + .join(", ") + )); + + let mut f = FunctionBindgen::new(self, ¶ms); + abi::post_return(f.r#gen.resolve, func, &mut f); + + let ret_area_decl = f.emit_ret_area_if_needed(); + + let FunctionBindgen { + src, + return_pointer_area_size, + return_pointer_area_align, + .. + } = f; + self.return_pointer_area_size = + self.return_pointer_area_size.max(return_pointer_area_size); + self.return_pointer_area_align = self + .return_pointer_area_align + .max(return_pointer_area_align); + + self.src.push_str(&ret_area_decl); + self.src.push_str(&src); + + self.src.push_str("}\n"); + } + } + + fn emit_ret_area_if_needed(&self) -> String { + if !self.return_pointer_area_size.is_empty() { + format!( + "\nalign({}) private void[{}] _exportsRetArea;\n", + self.return_pointer_area_align.format("size_t.sizeof"), + self.return_pointer_area_size.format("size_t.sizeof") + ) + } else { + String::new() + } + } + + fn needs_wit_free(&self, ty: Type) -> bool { + match ty { + Type::String => true, + Type::Id(id) => { + let typeinfo = &self.r#gen.types.get(id); + typeinfo.has_list + } + _ => false, + } + } + + fn needs_wit_drop(&self, ty: Type) -> bool { + match ty { + Type::Id(id) => { + let typeinfo = &self.r#gen.types.get(id); + typeinfo.has_resource + } + _ => false, + } + } + + fn can_have_wit_clone(&self, _ty: Type) -> bool { + true + } +} + +impl<'a> InterfaceGenerator<'a> for DInterfaceGenerator<'a> { + fn resolve(&self) -> &'a Resolve { + self.resolve + } + + // Override `types` to filter by `self.direction` + fn types(&mut self, iface: InterfaceId) { + let iface = &self.resolve().interfaces[iface]; + for (name, id) in iface.types.iter() { + if self.direction.is_some() == self.type_is_direction_sensitive(*id) { + self.define_type(name, *id); + } + } + } + + fn type_record(&mut self, id: TypeId, name: &str, record: &Record, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap() + .to_string(); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!("struct {escaped_name} {{\n")); + + let mut is_first = true; + for field in &record.fields { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + if is_first { + is_first = false; + } else { + self.src.push_str("\n"); + } + + self.src.push_str(&format!( + "/++\n{}\n+/\n", + field.docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!( + "{} {escaped_name};\n", + self.type_name(&field.ty, &owner_fqn) + )); + } + + self.src.push_str("\nvoid witFree() @nogc nothrow {\n"); + for field in &record.fields { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + if self.needs_wit_free(field.ty) { + self.src.push_str(&format!("{escaped_name}.witFree;\n")); + } + } + self.src.push_str("}\n"); + + self.src.push_str("\nvoid witDrop() @nogc nothrow {\n"); + for field in &record.fields { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + if self.needs_wit_drop(field.ty) { + self.src.push_str(&format!("{escaped_name}.witDrop;\n")); + } + } + self.src.push_str("}\n"); + + if self.can_have_wit_clone(Type::Id(id)) { + self.src.push_str(&format!( + "\n{escaped_name} witClone() const @nogc nothrow {{\n" + )); + self.src + .push_str(&format!("{escaped_name} clone = void;\n")); + for field in &record.fields { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + self.src.push_str(&format!( + "clone.{escaped_name} = this.{escaped_name}.witClone;\n" + )); + } + self.src.push_str("return clone;\n"); + self.src.push_str("}\n"); + } + + self.src.push_str("}\n"); + } + + fn type_resource(&mut self, id: TypeId, name: &str, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + let ty = &self.resolve.types[id]; + + match self.direction { + None => panic!("Resources can only be generated for imports, or exports. Not common."), + Some(Direction::Import) => { + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + self.src.push_str(&format!( + "struct {escaped_name} {{ + package({}) uint __handle = 0; + + package({0}) this(uint handle) @safe @nogc nothrow {{ + __handle = handle; + }} + + ", + self.r#gen.root_pkg + )); + + match ty.owner { + TypeOwner::Interface(owner_id) => { + for (_, func) in &self.resolve.interfaces[owner_id].functions { + if match &func.kind { + FunctionKind::Freestanding => false, + FunctionKind::Method(_) => false, + FunctionKind::Static(mid) => *mid == id, + FunctionKind::Constructor(mid) => *mid == id, + FunctionKind::AsyncFreestanding => false, + FunctionKind::AsyncMethod(_) => false, + FunctionKind::AsyncStatic(_) => todo!(), + } { + self.import_func(func); + } + } + } + TypeOwner::World(owner_id) => { + for (_, import) in &self.resolve.worlds[owner_id].imports { + match &import { + WorldItem::Function(func) => { + if match &func.kind { + FunctionKind::Freestanding => false, + FunctionKind::Method(_) => false, + FunctionKind::Static(mid) => *mid == id, + FunctionKind::Constructor(mid) => *mid == id, + FunctionKind::AsyncFreestanding => false, + FunctionKind::AsyncMethod(_) => false, + FunctionKind::AsyncStatic(_) => todo!(), + } { + self.import_func(func); + } + } + _ => {} + } + } + } + TypeOwner::None => { + panic!("Resource definition without owner?"); + } + } + + self.src.push_str( + "\nvoid witDrop() @trusted @nogc nothrow {\nif (!__handle) return; __import_drop(__handle); __handle = 0;\n}\n", + ); + self.src.push_str(&format!( + "@wasmImport!(\"{}\", \"[resource-drop]{}\")\n", + self.wasm_import_module.unwrap(), + name + )); + self.src.push_str(&format!( + "pragma(mangle, \"__wit_import_{}__:resource_drop:{}\")\n", + self.wasm_import_module + .unwrap() + .replace("/", "__") + .replace("-", "_"), + name.replace("-", "_") + )); + self.src.push_str( + "static private extern(C) void __import_drop(uint) @nogc nothrow;\n\n", + ); + self.src.push_str("void witFree() @safe @nogc nothrow {}\n"); + + self.src.push_str("typeof(this) witClone() const @safe @nogc nothrow { return typeof(this)(__handle); }\n"); + + self.src.push_str(&format!( + "// TODO: make RAII? disable copy for the own + + Borrow borrow() => Borrow(__handle); + alias borrow this; + + struct Borrow {{ + package({}) uint __handle = 0; + + package({0}) this(uint handle) @safe @nogc nothrow {{ + __handle = handle; + }} + + void witFree() @safe @nogc nothrow {{}} + void witDrop() @trusted @nogc nothrow {{ + if (!__handle) return; __import_drop(__handle); __handle = 0; + }} + Borrow witClone() const @safe @nogc nothrow {{ return Borrow(__handle); }} + ", + self.r#gen.root_pkg + )); + + match ty.owner { + TypeOwner::Interface(owner_id) => { + for (_, func) in &self.resolve.interfaces[owner_id].functions { + if match &func.kind { + FunctionKind::Freestanding => false, + FunctionKind::Method(mid) => *mid == id, + FunctionKind::Static(_) => false, + FunctionKind::Constructor(_) => false, + FunctionKind::AsyncFreestanding => false, + FunctionKind::AsyncMethod(_) => todo!(), + FunctionKind::AsyncStatic(_) => false, + } { + self.import_func(func); + } + } + } + TypeOwner::World(owner_id) => { + for (_, import) in &self.resolve.worlds[owner_id].imports { + match &import { + WorldItem::Function(func) => { + if match &func.kind { + FunctionKind::Freestanding => false, + FunctionKind::Method(mid) => *mid == id, + FunctionKind::Static(_) => false, + FunctionKind::Constructor(_) => false, + FunctionKind::AsyncFreestanding => false, + FunctionKind::AsyncMethod(_) => todo!(), + FunctionKind::AsyncStatic(_) => false, + } { + self.import_func(func); + } + } + _ => {} + } + } + } + TypeOwner::None => { + panic!("Resource definition without owner?"); + } + } + self.src.push_str("}\n"); + self.src.push_str("}\n"); + } + Some(Direction::Export) => match ty.owner { + TypeOwner::Interface(owner_id) => { + if let Some(cur_interface) = self.interface + && cur_interface == owner_id + { + } else { + panic!("Emitting resource from `interface` outside that interface?"); + } + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + self.src.push_str(&format!( + "struct {escaped_name} {{ + package({}) uint __handle = 0; + + package({0}) this(uint handle) @safe @nogc nothrow {{ + __handle = handle; + }} + +", + self.r#gen.root_pkg + )); + + self.src.push_str(&format!( + " + static {escaped_name} makeNew(T)(scope void delegate(out T) dg) if (is(T == struct)) {{ + if (dg is null) return {escaped_name}.init; + + auto ptr = cast(T*)malloc(T.sizeof); + if (ptr is null) return {escaped_name}.init; + + dg(*ptr); + return {escaped_name}(__import_makeNew(ptr)); + }} + ", + )); + self.src.push_str(&format!( + "@wasmImport!(\"[export]{}\", \"[resource-new]{}\")\n", + self.wasm_import_module.unwrap(), + name + )); + self.src.push_str(&format!( + "pragma(mangle, \"__wit_import_:export:{}__:resource_new:{}\")\n", + self.wasm_import_module + .unwrap() + .replace("/", "__") + .replace("-", "_"), + name.replace("-", "_") + )); + self.src + .push_str("static private extern(C) uint __import_makeNew(void*);\n\n"); + + self.src + .push_str("T* rep(T)() const @nogc nothrow if (is(T == struct)) {\nreturn cast(T*)__import_rep(__handle);\n}\n"); + self.src.push_str(&format!( + "@wasmImport!(\"[export]{}\", \"[resource-rep]{}\")\n", + self.wasm_import_module.unwrap(), + name + )); + self.src.push_str(&format!( + "pragma(mangle, \"__wit_import_:export:{}__:resource_rep:{}\")\n", + self.wasm_import_module + .unwrap() + .replace("/", "__") + .replace("-", "_"), + name.replace("-", "_") + )); + self.src.push_str( + "static private extern(C) void* __import_rep(uint) @nogc nothrow;\n\n", + ); + + self.src.push_str( + "void witDrop() @trusted @nogc nothrow {\nif (!__handle) return; __import_drop(__handle); __handle = 0;\n}\n", + ); + self.src.push_str(&format!( + "@wasmImport!(\"[export]{}\", \"[resource-drop]{}\")\n", + self.wasm_import_module.unwrap(), + name + )); + self.src.push_str(&format!( + "pragma(mangle, \"__wit_import_:export:{}__:resource_drop:{}\")\n", + self.wasm_import_module + .unwrap() + .replace("/", "__") + .replace("-", "_"), + name.replace("-", "_") + )); + self.src.push_str( + "static private extern(C) void __import_drop(uint) @nogc nothrow;\n\n", + ); + self.src.push_str("void witFree() @safe @nogc nothrow {}\n"); + + self.src.push_str("typeof(this) witClone() const @safe @nogc nothrow { return typeof(this)(__handle); }\n"); + + self.src.push_str(&format!( + "// TODO: make RAII? disable copy for the own + Borrow borrow() const @trusted @nogc nothrow => Borrow(__import_rep(__handle)); + //alias borrow this; + + struct Borrow {{ + package({}) void* __handle = null; + + package({0}) this(void* handle) @safe @nogc nothrow {{ + __handle = handle; + }} + + package({0}) this(uint handle) @safe @nogc nothrow {{ + __handle = cast(void*)handle; + }} + + void witFree() @safe @nogc nothrow {{}} + void witDrop() @safe @nogc nothrow {{}} + Borrow witClone() const @trusted @nogc nothrow {{ return Borrow(cast(void*)__handle); }} + + ", + self.r#gen.root_pkg + )); + + self.src + .push_str("T* rep(T)() const @nogc nothrow if (is(T == struct)) {\nreturn cast(T*)__handle;\n}\n"); + + self.src.push_str("}\n"); + + self.src.push_str("}\n"); + } + TypeOwner::World(_) => unimplemented!("resource exports in worlds"), + TypeOwner::None => { + panic!("Resource definition without owner?"); + } + }, + } + } + + fn type_tuple(&mut self, id: TypeId, name: &str, tuple: &Tuple, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(); + self.src.push_str(&format!( + "alias {escaped_name} = Tuple!({});", + tuple + .types + .iter() + .map(|ty| self.type_name(ty, owner_fqn).into_owned()) + .collect::>() + .join(", ") + )); + } + + fn type_flags(&mut self, _id: TypeId, name: &str, flags: &Flags, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + let storage_type = match flags.repr() { + FlagsRepr::U8 => "ubyte", + FlagsRepr::U16 => "ushort", + FlagsRepr::U32(1) => "uint", + FlagsRepr::U32(2) => "ulong", + repr => todo!("flags {repr:?}"), + }; + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!("struct {escaped_name} {{\n")); + + self.src + .push_str(&format!("mixin WitFlags!{storage_type};\n\n")); + + for (index, flag) in flags.flags.iter().enumerate() { + if index != 0 { + self.src.push_str("\n"); + } + self.src.push_str(&format!( + "/++\n{}\n+/\n", + flag.docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!( + "enum {} = {escaped_name}[{index}];\n", + escape_d_identifier(&flag.name.to_lower_camel_case()) + )); + } + self.src.push_str(&format!("}}\n")); + } + + fn type_variant(&mut self, id: TypeId, name: &str, variant: &Variant, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + let storage_type = match variant.tag() { + Int::U8 => "ubyte", + Int::U16 => "ushort", + Int::U32 => "uint", + Int::U64 => "ulong", + }; + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap() + .to_string(); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!("struct {escaped_name} {{\n")); + + self.src.push_str("mixin WitVariant!(\n"); + self.src.indent(1); + + for case in &variant.cases { + self.src.push_str(&format!( + "{}, // {}\n", + self.optional_type_name(case.ty.as_ref(), &owner_fqn), + escape_d_identifier(&case.name.to_lower_camel_case()) + )); + } + + self.src.deindent(1); + self.src.push_str(");\n"); + + self.src.deindent(1); + //self.src.push_str("@safe @nogc nothrow:\n"); + self.src.indent(1); + + self.src.deindent(1); + self.src.push_str("\npublic:\n"); + self.src.indent(1); + + self.src + .push_str(&format!("enum Tag : {storage_type} {{\n")); + + let mut is_first = true; + for case in &variant.cases { + if is_first { + is_first = false; + } else { + self.src.push_str("\n"); + } + self.src.push_str(&format!( + "/++\n{}\n+/\n", + case.docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!( + "{},\n", + escape_d_identifier(&case.name.to_lower_camel_case()) + )); + } + + self.src.push_str("}\n"); + + self.src + .push_str("Tag tag() const @safe @nogc nothrow pure => _tag;\n"); + + for case in &variant.cases { + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + case.docs.contents.as_deref().unwrap_or_default() + )); + let upper_case_name = case.name.to_upper_camel_case(); + let escaped_upper_case_name = escape_d_identifier(&upper_case_name); + + let lower_case_name = case.name.to_lower_camel_case(); + let escaped_lower_case_name = escape_d_identifier(&lower_case_name); + + self.src.push_str(&format!( + "alias {escaped_lower_case_name} = _create!(Tag.{escaped_lower_case_name});\n", + )); + self.src.push_str(&format!( + "/// ditto\nbool is{escaped_upper_case_name}() const => _tag == Tag.{escaped_lower_case_name};\n", + )); + + if case.ty.is_some() { + self.src.push_str(&format!( + "///ditto\nalias get{escaped_upper_case_name} = _get!(Tag.{escaped_lower_case_name});\n", + )); + } + } + + self.src.push_str("\nvoid witFree() @nogc nothrow {\n"); + if self.needs_wit_free(Type::Id(id)) { + self.src.push_str("switch (_tag) with (Tag) {\n"); + for case in &variant.cases { + let lower_case_name = case.name.to_lower_camel_case(); + let escaped_lower_case_name = escape_d_identifier(&lower_case_name); + + if case.ty.is_some() && self.needs_wit_free(case.ty.unwrap()) { + self.src.push_str(&format!( + "case {escaped_lower_case_name}: _get!(Tag.{escaped_lower_case_name}).witFree; break;\n", + )); + } + } + self.src.push_str("default: break;\n"); + self.src.push_str("}\n"); + } + self.src.push_str("}\n"); + + self.src.push_str("\nvoid witDrop() @nogc nothrow {\n"); + if self.needs_wit_drop(Type::Id(id)) { + self.src.push_str("switch (_tag) with (Tag) {\n"); + for case in &variant.cases { + let lower_case_name = case.name.to_lower_camel_case(); + let escaped_lower_case_name = escape_d_identifier(&lower_case_name); + + if case.ty.is_some() && self.needs_wit_drop(case.ty.unwrap()) { + self.src.push_str(&format!( + "case {escaped_lower_case_name}: _get!(Tag.{escaped_lower_case_name}).witDrop; break;\n", + )); + } + } + self.src.push_str("default: break;\n"); + self.src.push_str("}\n"); + } + self.src.push_str("}\n"); + + if self.can_have_wit_clone(Type::Id(id)) { + self.src.push_str(&format!( + "\n{escaped_name} witClone() const @nogc nothrow {{\n" + )); + self.src.push_str("final switch (_tag) {\n"); + for case in &variant.cases { + let lower_case_name = case.name.to_lower_camel_case(); + let escaped_lower_case_name = escape_d_identifier(&lower_case_name); + + if case.ty.is_some() { + self.src.push_str(&format!( + "case Tag.{escaped_lower_case_name}: return _create!(Tag.{escaped_lower_case_name})(this._get!(Tag.{escaped_lower_case_name}).witClone); break;\n", + )); + } else { + self.src.push_str(&format!( + "case Tag.{escaped_lower_case_name}: return _create!(Tag.{escaped_lower_case_name}); break;\n", + )); + } + } + self.src.push_str("}\n"); + self.src.push_str("}\n"); + } + + self.src.push_str("}\n"); + } + + fn type_option(&mut self, id: TypeId, name: &str, payload: &Type, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(); + self.src.push_str(&format!( + "alias {escaped_name} = Option!({});", + self.type_name(payload, owner_fqn) + )); + } + + fn type_result(&mut self, id: TypeId, name: &str, result: &Result_, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(); + self.src.push_str(&format!( + "alias {escaped_name} = Result!({}, {});", + self.optional_type_name(result.ok.as_ref(), owner_fqn), + self.optional_type_name(result.err.as_ref(), owner_fqn), + )); + } + + fn type_enum(&mut self, _id: TypeId, name: &str, enum_: &Enum, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + let storage_type = match enum_.tag() { + Int::U8 => "ubyte", + Int::U16 => "ushort", + Int::U32 => "uint", + Int::U64 => "ulong", + }; + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + self.src + .push_str(&format!("enum {escaped_name} : {storage_type} {{\n")); + + let mut is_first = true; + for case in &enum_.cases { + if is_first { + is_first = false; + } else { + self.src.push_str("\n"); + } + self.src.push_str(&format!( + "/++\n{}\n+/\n", + case.docs.contents.as_deref().unwrap_or_default() + )); + self.src.push_str(&format!( + "{},\n", + escape_d_identifier(&case.name.to_lower_camel_case()) + )); + } + + self.src.push_str("}"); + } + + fn type_alias(&mut self, id: TypeId, name: &str, alias_ty: &Type, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let typename = self.type_name( + alias_ty, + self.type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(), + ); + + self.src + .push_str(&format!("alias {escaped_name} = {typename};\n")); + } + + fn type_list(&mut self, id: TypeId, name: &str, ty: &Type, docs: &Docs) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(); + self.src.push_str(&format!( + "alias {escaped_name} = WitList!({});", + self.type_name(ty, owner_fqn) + )); + } + + fn type_fixed_length_list( + &mut self, + id: TypeId, + name: &str, + ty: &Type, + size: u32, + docs: &Docs, + ) { + let upper_name = name.to_upper_camel_case(); + let escaped_name = escape_d_identifier(&upper_name); + + self.src.push_str(&format!( + "\n/++\n{}\n+/\n", + docs.contents.as_deref().unwrap_or_default() + )); + + let owner_fqn = self + .type_owner_fqn(&self.resolve.types[id].owner, false) + .unwrap(); + self.src.push_str(&format!( + "alias {escaped_name} = {}[{size}];", + self.type_name(ty, owner_fqn) + )); + } + + fn type_map(&mut self, _id: TypeId, name: &str, _key: &Type, _value: &Type, _docs: &Docs) { + todo!("def of `map` - {name}"); + } + + fn type_future(&mut self, _id: TypeId, name: &str, _ty: &Option, _docs: &Docs) { + todo!("def of `future` - {name}"); + } + + fn type_stream(&mut self, _id: TypeId, name: &str, _ty: &Option, _docs: &Docs) { + todo!("def of `stream` - {name}"); + } + + fn type_builtin(&mut self, _id: TypeId, name: &str, _ty: &Type, _docs: &Docs) { + todo!("def of `builtin` - {name}"); + } +} + +struct Block { + body: String, + results: Vec, + element: String, + base: String, +} + +struct BlockStorage { + body: Source, + element: String, + base: String, +} + +struct FunctionBindgen<'a, 'b> { + r#gen: &'b mut DInterfaceGenerator<'a>, + params: &'b [&'b str], + tmp: usize, + src: Source, + block_storage: Vec, + /// intermediate calculations for contained objects + blocks: Vec, + payloads: Vec, + return_pointer_area_size: ArchitectureSize, + return_pointer_area_align: Alignment, + needs_deallocate: bool, +} + +fn tempname(base: &str, idx: usize) -> String { + format!("{base}{idx}") +} + +impl<'a, 'b> FunctionBindgen<'a, 'b> { + fn new(r#gen: &'b mut DInterfaceGenerator<'a>, params: &'b [&'b str]) -> Self { + Self { + r#gen, + params, + tmp: 0, + src: Default::default(), + block_storage: Default::default(), + blocks: Default::default(), + payloads: Default::default(), + return_pointer_area_size: Default::default(), + return_pointer_area_align: Default::default(), + needs_deallocate: false, + } + } + + fn tmp(&mut self) -> usize { + let ret = self.tmp; + self.tmp += 1; + ret + } + + fn push_str(&mut self, s: &str) { + self.src.push_str(s); + } + + fn load( + &mut self, + ty: &str, + offset: ArchitectureSize, + operands: &[String], + results: &mut Vec, + ) { + results.push(format!( + "*(cast({}*)({} + {}))", + ty, + operands[0], + offset.format("size_t.sizeof") + )); + } + + fn load_ext( + &mut self, + ty: &str, + offset: ArchitectureSize, + operands: &[String], + results: &mut Vec, + ) { + self.load(ty, offset, operands, results); + let result = results.pop().unwrap(); + results.push(format!("cast(uint)({result})")); + } + + fn store(&mut self, ty: &str, offset: ArchitectureSize, operands: &[String]) { + self.push_str(&format!( + "*cast({ty}*)({} + {}) = cast({ty})({});\n", + operands[1], + offset.format("size_t.sizeof"), + operands[0] + )); + } + + /// Emits a shared return area declaration if needed by this function. + /// + /// During code generation, `return_pointer()` may be called multiple times for: + /// - Indirect parameter storage (when too many/large params) + /// - Return value storage (when return type is too large) + /// + /// **Safety:** This is safe because return pointers are used sequentially: + /// 1. Parameter marshaling (before call) + /// 2. Function execution + /// 3. Return value unmarshaling (after call) + /// + /// The scratch space is reused but never accessed simultaneously. + fn emit_ret_area_if_needed(&self) -> String { + if !self.return_pointer_area_size.is_empty() { + match self.r#gen.direction { + Some(Direction::Import) => format!( + "align({}) void[{}] _retArea = void;\n", + self.return_pointer_area_align.format("size_t.sizeof"), + self.return_pointer_area_size.format("size_t.sizeof") + ), + Some(Direction::Export) => "alias _retArea = _exportsRetArea;\n".to_string(), + None => { + unreachable!(); + } + } + } else { + String::new() + } + } +} + +fn perform_cast(op: &str, cast: &Bitcast) -> String { + match cast { + Bitcast::I32ToF32 | Bitcast::I64ToF32 => { + format!("(cast(uint){op}).reinterpretCast!float") + } + Bitcast::F32ToI32 | Bitcast::F32ToI64 => { + format!("({op}).reinterpretCast!uint") + } + Bitcast::I64ToF64 => { + format!("({op}).reinterpretCast!double") + } + Bitcast::F64ToI64 => { + format!("({op}).reinterpretCast!ulong") + } + Bitcast::I32ToI64 | Bitcast::LToI64 | Bitcast::PToP64 => { + format!("cast(ulong)({op})") + } + Bitcast::I64ToI32 | Bitcast::PToI32 | Bitcast::LToI32 => { + format!("cast(uint)({op})") + } + Bitcast::P64ToI64 | Bitcast::None | Bitcast::I64ToP64 => op.to_string(), + Bitcast::P64ToP | Bitcast::I32ToP | Bitcast::LToP => { + format!("cast(void*)({op})") + } + Bitcast::PToL | Bitcast::I32ToL | Bitcast::I64ToL => { + format!("cast(size_t)({op})") + } + Bitcast::Sequence(sequence) => { + let [first, second] = &**sequence; + let inner = perform_cast(op, first); + perform_cast(&inner, second) + } + } +} + +impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { + type Operand = String; + + fn emit( + &mut self, + _resolve: &Resolve, + inst: &wit_bindgen_core::abi::Instruction<'_>, + operands: &mut Vec, + results: &mut Vec, + ) { + let mut top_as = |cvt: &str| { + results.push(format!("cast({cvt})({})", operands.pop().unwrap())); + }; + + match inst { + abi::Instruction::GetArg { nth } => { + if *nth == 0 && &self.params[0] == &"self" { + results.push("this".into()); + } else { + results.push(self.params[*nth].into()); + } + } + + abi::Instruction::I32Const { val } => results.push(val.to_string()), + abi::Instruction::Bitcasts { casts } => { + for (cast, op) in casts.iter().zip(operands) { + let op = perform_cast(op, cast); + results.push(op); + } + } + abi::Instruction::ConstZero { tys } => { + for ty in tys.iter() { + results.push( + match ty { + WasmType::Pointer => "null", + _ => "0", + } + .to_string(), + ); + } + } + + abi::Instruction::I32Load { offset } => self.load("uint", *offset, operands, results), + abi::Instruction::I32Load8U { offset } => { + self.load_ext("ubyte", *offset, operands, results) + } + abi::Instruction::I32Load8S { offset } => { + self.load_ext("byte", *offset, operands, results) + } + abi::Instruction::I32Load16U { offset } => { + self.load_ext("ushort", *offset, operands, results) + } + abi::Instruction::I32Load16S { offset } => { + self.load_ext("short", *offset, operands, results) + } + abi::Instruction::I64Load { offset } => self.load("ulong", *offset, operands, results), + abi::Instruction::F32Load { offset } => self.load("float", *offset, operands, results), + abi::Instruction::F64Load { offset } => self.load("double", *offset, operands, results), + + abi::Instruction::PointerLoad { offset } => { + self.load("void*", *offset, operands, results) + } + abi::Instruction::LengthLoad { offset } => { + self.load("size_t", *offset, operands, results) + } + + abi::Instruction::I32Store { offset } => self.store("uint", *offset, operands), + abi::Instruction::I32Store8 { offset } => self.store("ubyte", *offset, operands), + abi::Instruction::I32Store16 { offset } => self.store("ushort", *offset, operands), + + abi::Instruction::I64Store { offset } => self.store("ulong", *offset, operands), + abi::Instruction::F32Store { offset } => self.store("float", *offset, operands), + abi::Instruction::F64Store { offset } => self.store("double", *offset, operands), + + abi::Instruction::PointerStore { offset } => self.store("void*", *offset, operands), + abi::Instruction::LengthStore { offset } => self.store("size_t", *offset, operands), + + abi::Instruction::I32FromChar + | abi::Instruction::I32FromBool + | abi::Instruction::I32FromU8 + | abi::Instruction::I32FromS8 + | abi::Instruction::I32FromU16 + | abi::Instruction::I32FromS16 + | abi::Instruction::I32FromS32 => top_as("uint"), + abi::Instruction::I32FromU32 => results.push(operands.pop().unwrap()), + + abi::Instruction::I64FromU64 => results.push(operands.pop().unwrap()), + abi::Instruction::I64FromS64 => top_as("ulong"), + abi::Instruction::CoreF32FromF32 => results.push(operands.pop().unwrap()), + abi::Instruction::CoreF64FromF64 => results.push(operands.pop().unwrap()), + + abi::Instruction::S8FromI32 => top_as("byte"), + abi::Instruction::U8FromI32 => top_as("ubyte"), + abi::Instruction::S16FromI32 => top_as("short"), + abi::Instruction::U16FromI32 => top_as("ushort"), + abi::Instruction::S32FromI32 => top_as("int"), + abi::Instruction::U32FromI32 => results.push(operands.pop().unwrap()), + abi::Instruction::S64FromI64 => top_as("long"), + abi::Instruction::U64FromI64 => results.push(operands.pop().unwrap()), + abi::Instruction::CharFromI32 => top_as("dchar"), + abi::Instruction::F32FromCoreF32 => results.push(operands.pop().unwrap()), + abi::Instruction::F64FromCoreF64 => results.push(operands.pop().unwrap()), + abi::Instruction::BoolFromI32 => results.push(format!("({}) != 0", operands[0])), + + abi::Instruction::ListCanonLower { .. } | abi::Instruction::StringLower { .. } => { + results.push(format!("cast(void*)({}.ptr)", operands[0])); + results.push(format!("{}.length", operands[0])); + } + abi::Instruction::ListLower { element, .. } => { + let Block { + body, + element: block_element, + base, + .. + } = self.blocks.pop().unwrap(); + let tmp = self.tmp(); + + let size = self.r#gen.sizes.size(element); + let size_str = size.format("size_t.sizeof"); + + let list = tempname("_list", tmp); + let list_src = tempname("_listSrc", tmp); + + self.push_str(&format!( + "auto {list_src} = {}; + auto {list} = {list_src}.length ? {}.malloc({list_src}.length * ({size_str})) : null; + assert(!{list_src}.length || {list});\n", + operands[0], self.r#gen.r#gen.common_module + )); + + if matches!(self.r#gen.direction, Some(Direction::Import)) { + self.needs_deallocate = true; + self.push_str(&format!("if ({list_src}.length) deallocate ~= {list};\n")); + } + + self.push_str(&format!( + "foreach ({block_element}_idx, ref {block_element}; {list_src}) {{\n" + )); + self.push_str(&format!( + "auto {base} = {list} + {block_element}_idx * ({size_str});\n" + )); + self.push_str(&body); + //self.push_str(&format!("_targetElem = {};", body.1[0])); + self.push_str("\n}\n"); + + if !matches!(self.r#gen.direction, Some(Direction::Import)) { + self.push_str(&format!( + "if ({list_src}.length) {}.free({list_src}.ptr);\n", + self.r#gen.r#gen.common_module + )); + } + + results.push(format!("{list}")); + results.push(format!("{}.length", operands[0])); + } + + abi::Instruction::ListCanonLift { element, ty, .. } => { + let list_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + let elem_name = self.r#gen.type_name(element, self.r#gen.fqn); + let tmp = self.tmp(); + + let ptr = tempname("_ptr", tmp); + let len = tempname("_len", tmp); + + self.push_str(&format!( + "auto {len} = {}; + auto {ptr} = {len} ? cast({elem_name}*)({}) : null; + ", + operands[1], operands[0] + )); + + if matches!(self.r#gen.direction, Some(Direction::Export)) { + self.needs_deallocate = true; + self.push_str(&format!("if ({len}) deallocate ~= cast(){ptr};\n")); + } + + let result = format!("{list_name}({ptr}[0..{len}])"); + + let tmpvar = tempname("_list", self.tmp()); + self.push_str(&format!("auto {tmpvar} = {result};\n")); + results.push(tmpvar); + } + abi::Instruction::StringLift => { + let tmp = self.tmp(); + + let ptr = tempname("_ptr", tmp); + let len = tempname("_len", tmp); + + self.push_str(&format!( + "auto {len} = {}; + auto {ptr} = {len} ? cast(char*)({}) : null; + ", + operands[1], operands[0] + )); + + if matches!(self.r#gen.direction, Some(Direction::Export)) { + self.needs_deallocate = true; + self.push_str(&format!("if ({len}) deallocate ~= cast(){ptr};\n")); + } + + let result = format!("WitString({ptr}[0..{len}])"); + + let tmpvar = tempname("_list", self.tmp()); + self.push_str(&format!("auto {tmpvar} = {result};\n")); + results.push(tmpvar); + } + abi::Instruction::ListLift { ty, element, .. } => { + let Block { + body, + results: block_results, + element: block_element, + base, + } = self.blocks.pop().unwrap(); + let tmp = self.tmp(); + let size = self.r#gen.sizes.size(element); + let size_str = size.format("size_t.sizeof"); + let elem_type_name = self.r#gen.type_name(element, self.r#gen.fqn); + + let list = tempname("_list", tmp); + let list_len = tempname("_listLen", tmp); + let list_src = tempname("_listSrcPtr", tmp); + self.push_str(&format!("auto {list_src} = {};\n", operands[0])); + self.push_str(&format!("auto {list_len} = {};\n", operands[1])); + self.push_str(&format!( + "auto {list} = {list_len} ? {}.mallocSlice!({elem_type_name})({list_len}) : []; + assert(!{list_len} || {list}.ptr);\n", + self.r#gen.r#gen.common_module + )); + + if matches!(self.r#gen.direction, Some(Direction::Export)) { + self.needs_deallocate = true; + self.push_str(&format!( + "if ({list_len}) deallocate ~= cast(void*){list}.ptr;\n" + )); + } + + self.push_str(&format!( + "foreach ({block_element}_idx, ref {block_element}; {list}) {{\n", + )); + self.push_str(&format!( + "const auto {base} = {list_src} + {block_element}_idx * {size_str};\n" + )); + self.push_str(&body); + self.push_str(&format!("{block_element} = {};", block_results[0])); + self.push_str("\n}\n"); + + if matches!(self.r#gen.direction, Some(Direction::Import)) { + self.push_str(&format!( + "if ({list_len}) {}.free({list_src});\n", + self.r#gen.r#gen.common_module + )); + } else { + self.needs_deallocate = true; + self.push_str(&format!("if ({list_len}) deallocate ~= {list_src};\n")); + } + + let list_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + let result = format!("{list_name}({list})"); + + let tmpvar = tempname("_witList", self.tmp()); + self.push_str(&format!("auto {tmpvar} = {result};\n")); + results.push(tmpvar); + } + + abi::Instruction::FixedLengthListLift { size, id, .. } => { + let result = tempname("_arr", self.tmp()); + let type_name = self.r#gen.type_name(&Type::Id(*id), self.r#gen.fqn); + self.push_str(&format!("{type_name} {result} = [\n",)); + self.src.indent(1); + for op in operands.drain(0..(*size as usize)) { + self.push_str(&op); + self.push_str(", \n"); + } + self.src.deindent(1); + self.push_str("];\n"); + results.push(result); + } + abi::Instruction::FixedLengthListLower { size, .. } => { + for i in 0..(*size as usize) { + results.push(format!("{}[{i}]", operands[0])); + } + } + abi::Instruction::FixedLengthListLowerToMemory { element, .. } => { + let Block { + body, + results: _, + element: block_element, + base, + } = self.blocks.pop().unwrap(); + let arr_src = &operands[0]; + let arr_dst = &operands[1]; + let size_str = self.r#gen.sizes.size(element).format("size_t.sizeof"); + + self.push_str(&format!( + "foreach ({block_element}_idx, ref {block_element}; {arr_src}) {{\n" + )); + self.push_str(&format!( + "const auto {base} = {arr_dst} + {block_element}_idx * {size_str};\n" + )); + self.push_str(&body); + self.push_str("\n}\n"); + } + abi::Instruction::FixedLengthListLiftFromMemory { id, element, .. } => { + let Block { + body, + results: block_results, + element: block_element, + base, + } = self.blocks.pop().unwrap(); + let arr_src = &operands[0]; + let type_name = self.r#gen.type_name(&Type::Id(*id), self.r#gen.fqn); + let size_str = self.r#gen.sizes.size(element).format("size_t.sizeof"); + + let result = tempname("_arr", self.tmp()); + self.push_str(&format!("{type_name} {result} = void;\n")); + + self.push_str(&format!( + "foreach ({block_element}_idx, ref {block_element}; {result}) {{\n" + )); + self.push_str(&format!( + "const auto {base} = {arr_src} + {block_element}_idx * {size_str};\n" + )); + self.push_str(&body); + self.push_str(&format!("{block_element} = {};", block_results[0])); + self.push_str("\n}\n"); + + results.push(result); + } + + abi::Instruction::IterElem { .. } => { + results.push(self.block_storage.last().unwrap().element.clone()) + } + abi::Instruction::IterBasePointer => { + results.push(self.block_storage.last().unwrap().base.clone()) + } + + abi::Instruction::RecordLower { record, .. } => { + for field in record.fields.iter() { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + results.push(format!("{}.{escaped_name}", operands[0])); + } + } + abi::Instruction::RecordLift { ty, record, .. } => { + let name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + + let tmpvar = tempname("_record", self.tmp()); + + self.push_str(&format!("{name} {tmpvar} = {{\n")); + for (field, op) in record.fields.iter().zip(operands.iter()) { + let lower_name = field.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + self.push_str(&format!("{escaped_name}: {op},\n")); + } + self.push_str("};\n"); + + results.push(tmpvar); + } + + abi::Instruction::HandleLower { .. } => { + let op = &operands[0]; + results.push(format!("{op}.__handle")) + } + abi::Instruction::HandleLift { ty, .. } => { + let name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + let result = format!("{name}({})", operands[0]); + + if matches!(self.r#gen.direction, Some(Direction::Export)) && operands[0] == "this" + { + results.push(result); + } else { + let tmpvar = tempname("_handle", self.tmp()); + self.push_str(&format!("auto {tmpvar} = {result};\n")); + results.push(tmpvar); + } + } + + abi::Instruction::TupleLower { tuple, .. } => { + for i in 0..tuple.types.len() { + results.push(format!("{}[{i}]", &operands[0])); + } + } + abi::Instruction::TupleLift { ty, .. } => { + let name = tempname("_tuple", self.tmp()); + self.push_str(&format!( + "auto {name} = {}(\n", + self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn), + )); + self.src.indent(1); + for op in operands.iter() { + self.push_str(op); + self.push_str(",\n"); + } + self.src.deindent(1); + self.push_str(");\n"); + results.push(name); + } + + abi::Instruction::FlagsLower { flags, .. } => match flags.repr() { + FlagsRepr::U8 | FlagsRepr::U16 | FlagsRepr::U32(1) => { + results.push(format!("cast(uint)({}.bits)", operands.pop().unwrap())); + } + FlagsRepr::U32(2) => { + let tempname = tempname("_flags", self.tmp()); + + self.push_str(&format!("auto {tempname} = {};", operands[0])); + results.push(format!("cast(uint)({tempname}.bits & 0xffffffff)")); + results.push(format!("cast(uint)(({tempname}.bits >> 32) & 0xffffffff)")); + } + _ => todo!(), + }, + abi::Instruction::FlagsLift { flags, ty, .. } => { + let type_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + + match flags.repr() { + FlagsRepr::U8 => { + results.push(format!( + "{type_name}(cast(ubyte)({}))", + operands.pop().unwrap() + )); + } + FlagsRepr::U16 => { + results.push(format!( + "{type_name}(cast(ushort)({}))", + operands.pop().unwrap() + )); + } + FlagsRepr::U32(1) => { + results.push(format!("{type_name}({})", operands.pop().unwrap())); + } + FlagsRepr::U32(2) => { + results.push(format!( + "({type_name}({}) | {type_name}({} << 32))", + operands[0], operands[1] + )); + } + _ => todo!(), + } + } + + abi::Instruction::VariantPayloadName => { + let name = tempname("_payload", self.tmp()); + results.push(name.clone()); + self.payloads.push(name); + } + abi::Instruction::VariantLower { + ty, + variant, + results: result_types, + .. + } => { + let blocks = self + .blocks + .drain(self.blocks.len() - variant.cases.len()..) + .collect::>(); + let payloads = self + .payloads + .drain(self.payloads.len() - variant.cases.len()..) + .collect::>(); + + let mut variant_results = Vec::with_capacity(result_types.len()); + for res_ty in result_types.iter() { + let name = tempname("_variantPart", self.tmp()); + results.push(name.clone()); + self.src + .push_str(&format!("{} {name} = void;\n", wasm_type(*res_ty))); + variant_results.push(name); + } + + let ty_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + + let tag_type = tempname("_Tag", self.tmp()); + + self.push_str(&format!("alias {tag_type} = {ty_name}.Tag;\n")); + self.push_str(&format!("final switch ({}.tag) {{\n", operands[0])); + + for ((case, block), payload) in variant.cases.iter().zip(blocks).zip(payloads) { + let lower_name = case.name.to_lower_camel_case(); + let lower_escaped_name = escape_d_identifier(&lower_name); + + let uppper_name = case.name.to_upper_camel_case(); + let upper_escaped_name = escape_d_identifier(&uppper_name); + + self.push_str(&format!("case {tag_type}.{lower_escaped_name}: {{\n")); + if let Some(ty) = case.ty.as_ref() { + let ty_name = self.r#gen.type_name(ty, self.r#gen.fqn); + self.push_str(&format!( + "{}ref {ty_name} {payload} = {}.get{upper_escaped_name}();\n", + if matches!(self.r#gen.direction, Some(Direction::Import)) { + "const " + } else { + "" + }, + operands[0], + )); + } + self.src.push_str(&block.body); + + for (name, result) in variant_results.iter().zip(&block.results) { + self.push_str(&format!("{name} = {result};\n")); + } + self.src.push_str("break;\n}\n"); + } + + self.src.push_str("}\n"); + } + abi::Instruction::VariantLift { variant, ty, .. } => { + let blocks = self + .blocks + .drain(self.blocks.len() - variant.cases.len()..) + .collect::>(); + + let ty = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + + let tmp = self.tmp(); + let result = tempname("_variant", tmp); + let tag = tempname("_tag", tmp); + let tag_type = tempname("_Tag", tmp); + + self.push_str(&format!("{ty} {result} = void;\n")); + self.push_str(&format!("auto {tag} = {};\n", operands[0])); + + self.push_str(&format!("alias {tag_type} = {ty}.Tag;\n")); + self.push_str(&format!("final switch (cast({ty}.Tag){tag}) {{\n")); + for (case, block) in variant.cases.iter().zip(blocks) { + let lower_name = case.name.to_lower_camel_case(); + let escaped_name = escape_d_identifier(&lower_name); + + let payload = tempname("_payload", self.tmp()); + + self.push_str(&format!("case {tag_type}.{escaped_name}: {{\n")); + self.src.push_str(&block.body); + assert!(block.results.len() == (case.ty.is_some() as usize)); + + let val = if let Some(_) = case.ty.as_ref() { + self.push_str(&format!("auto {payload} = {};\n", block.results[0])); + &payload + } else { + "" + }; + self.push_str(&format!("{result} = {ty}.{escaped_name}({val});\n")); + self.src.push_str("break;\n}\n"); + } + self.src.push_str("}\n"); + results.push(result); + } + + abi::Instruction::EnumLower { .. } => { + results.push(format!("cast(uint)({})", operands[0])) + } + abi::Instruction::EnumLift { ty, .. } => { + let type_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + results.push(format!("cast({})({})", type_name, operands.pop().unwrap())) + } + + abi::Instruction::OptionLower { + results: result_types, + .. + } => { + let Block { + body: mut some, + results: some_results, + .. + } = self.blocks.pop().unwrap(); + let Block { + body: mut none, + results: none_results, + .. + } = self.blocks.pop().unwrap(); + let some_payload = self.payloads.pop().unwrap(); + let _none_payload = self.payloads.pop().unwrap(); + + for (i, ty) in result_types.iter().enumerate() { + let name = tempname("_option", self.tmp()); + results.push(name.clone()); + self.push_str(&format!("{} {name} = void;\n", wasm_type(*ty))); + let some_result = &some_results[i]; + some.push_str(&format!("{name} = {some_result};\n")); + let none_result = &none_results[i]; + none.push_str(&format!("{name} = {none_result};\n")); + } + + let bind_some = format!("ref {some_payload} = {}.unwrap();", operands[0]); + + self.push_str(&format!( + "\ + if ({}.isSome) {{ + {bind_some} + {some}}} else {{ + {none}}} + ", + operands[0] + )); + } + abi::Instruction::OptionLift { ty, .. } => { + let Block { + body: some, + results: some_results, + .. + } = self.blocks.pop().unwrap(); + let Block { + results: none_results, + .. + } = self.blocks.pop().unwrap(); + assert!(none_results.is_empty()); + assert!(some_results.len() == 1); + + let type_name = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + let op0 = &operands[0]; + + let tmp = self.tmp(); + let resultname = tempname("_option", tmp); + let is_some = tempname("_isSome", tmp); + let some_value = &some_results[0]; + self.push_str(&format!( + "{type_name} {resultname} = void; + bool {is_some} = ({op0}) != 0; + if ({is_some}) {{ + {some} + {resultname} = {type_name}.makeSome({some_value}); + }} else {{ + {resultname} = {type_name}.makeNone; + }} + " + )); + results.push(format!("{resultname}")); + } + + abi::Instruction::ResultLower { + results: result_types, + result, + .. + } => { + let Block { + body: mut err, + results: err_results, + .. + } = self.blocks.pop().unwrap(); + let Block { + body: mut ok, + results: ok_results, + .. + } = self.blocks.pop().unwrap(); + let err_payload = self.payloads.pop().unwrap(); + let ok_payload = self.payloads.pop().unwrap(); + + for (i, ty) in result_types.iter().enumerate() { + let tmp = self.tmp(); + let name = tempname("_resultPart", tmp); + results.push(name.clone()); + self.src.push_str(wasm_type(*ty)); + self.src.push_str(" "); + self.src.push_str(&name); + self.src.push_str(";\n"); + let ok_result = &ok_results[i]; + ok.push_str(&format!("{name} = {ok_result};\n")); + let err_result = &err_results[i]; + err.push_str(&format!("{name} = {err_result};\n")); + } + + let op0 = &operands[0]; + let bind_ok = if let Some(_ok) = result.ok.as_ref() { + format!("ref {ok_payload} = {op0}.unwrap();") + } else { + String::new() + }; + let bind_err = if let Some(_err) = result.err.as_ref() { + format!("ref {err_payload} = {op0}.unwrapErr();") + } else { + String::new() + }; + + self.push_str(&format!( + "\ + if ({op0}.isErr) {{ + {bind_err} + {err}}} else {{ + {bind_ok} + {ok}}} + " + )); + } + abi::Instruction::ResultLift { result, ty, .. } => { + let Block { + body: err, + results: err_results, + .. + } = self.blocks.pop().unwrap(); + assert!(err_results.len() == (result.err.is_some() as usize)); + let Block { + body: ok, + results: ok_results, + .. + } = self.blocks.pop().unwrap(); + assert!(ok_results.len() == (result.ok.is_some() as usize)); + + let full_type = self.r#gen.type_name(&Type::Id(*ty), self.r#gen.fqn); + let op0 = &operands[0]; + + let tmp = self.tmp(); + let resultname = tempname("_result", tmp); + let is_err = tempname("_isErr", tmp); + + let ok_value = if result.ok.is_some() { + &ok_results[0] + } else { + "" + }; + + let err_value = if result.err.is_some() { + &err_results[0] + } else { + "" + }; + + self.push_str(&format!( + "{full_type} {resultname} = void; + bool {is_err} = ({op0}) != 0; + if ({is_err}) {{ + {err} + {resultname} = {full_type}.makeErr({err_value}); + }} else {{ + {ok} + {resultname} = {full_type}.makeOk({ok_value}); + }}\n" + )); + results.push(resultname); + } + + abi::Instruction::CallWasm { name, sig } => { + let split_name = if name.contains('.') { + name.split(".").skip(1).next().unwrap() + } else { + name + }; + + let lower_name = split_name.to_lower_camel_case(); + let escaped_name = if name.starts_with("[constructor]") { + "makeNew" + } else { + escape_d_identifier(&lower_name) + }; + + if !sig.results.is_empty() { + self.src.push_str("auto _ret = "); + results.push("_ret".to_string()); + } + self.push_str(&format!( + "__import_{escaped_name}({});\n", + operands.iter().cloned().collect::>().join(", ") + )); + + if self.needs_deallocate { + self.push_str(&format!("deallocate.purge();\n")); + } + } + abi::Instruction::CallInterface { func, async_ } => { + if *async_ { + todo!("CallInterface async"); + } + + if func.result.is_some() { + self.src.push_str("auto _ret = "); + results.push("_ret".to_string()); + } + + let split_name = match &func.kind { + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => &func.name, + FunctionKind::Constructor(_) => "", + FunctionKind::Method(_) + | FunctionKind::Static(_) + | FunctionKind::AsyncMethod(_) + | FunctionKind::AsyncStatic(_) => func.name.split(".").skip(1).next().unwrap(), + }; + + let lower_name = split_name.to_lower_camel_case(); + let escaped_name = if let FunctionKind::Constructor(_) = &func.kind { + "constructor" + } else { + escape_d_identifier(&lower_name) + }; + + let implicit_self = match &func.kind { + FunctionKind::Freestanding + | FunctionKind::AsyncFreestanding + | FunctionKind::Static(_) + | FunctionKind::AsyncStatic(_) + | FunctionKind::Constructor(_) => { + self.src.push_str(&format!("{escaped_name}_Impl(")); + false + } + FunctionKind::Method(_) | FunctionKind::AsyncMethod(_) => { + self.src.push_str(&format!( + "__traits(child, cast(_Resource_Impl*)self, {escaped_name}_Impl)(" + )); + true + } + }; + self.src.push_str( + &operands + .iter() + .skip(if implicit_self { 1 } else { 0 }) + .cloned() + .collect::>() + .join(", "), + ); + self.src.push_str(");\n"); + + if self.needs_deallocate { + self.push_str(&format!("deallocate.purge();\n")); + } + } + abi::Instruction::Return { amt, .. } => match amt { + 0 => {} + _ => { + assert!(*amt == operands.len()); + + if *amt == 1 { + self.push_str("return "); + self.src.push_str(&operands[0]); + self.push_str(";\n"); + } else { + todo!(); + } + } + }, + + abi::Instruction::Malloc { .. } => { + todo!("instr: Malloc") + } + abi::Instruction::GuestDeallocate { .. } => { + self.push_str(&format!("free({});", operands[0])); + } + abi::Instruction::GuestDeallocateString { .. } => { + self.push_str(&format!("if ({} > 0) {{\n", operands[1])); + self.push_str(&format!("free({});\n", operands[0])); + self.push_str("}\n"); + } + abi::Instruction::GuestDeallocateList { element } => { + let Block { + body, + results: _, + element: block_element, + base, + } = self.blocks.pop().unwrap(); + let tmp = self.tmp(); + let size = self.r#gen.sizes.size(element); + let size_str = size.format("size_t.sizeof"); + + let list_len = tempname("_listLen", tmp); + let list_src = tempname("_listSrcPtr", tmp); + self.push_str(&format!("auto {list_src} = {};\n", operands[0])); + self.push_str(&format!("auto {list_len} = {};\n", operands[1])); + + self.push_str(&format!( + "foreach ({block_element}_idx; 0..{list_len}) {{\n", + )); + self.push_str(&format!( + "const auto {base} = {list_src} + {block_element}_idx * {size_str};\n" + )); + self.push_str(&body); + self.push_str("\n}\n"); + + self.push_str(&format!("if ({} > 0) {{\n", operands[1])); + self.push_str(&format!("free({});\n", operands[0])); + self.push_str("}\n"); + } + abi::Instruction::GuestDeallocateVariant { + blocks: block_count, + } => { + let blocks = self + .blocks + .drain(self.blocks.len() - block_count..) + .collect::>(); + + self.push_str(&format!("switch ({}) {{\n", operands[0])); + for (i, block) in blocks.into_iter().enumerate() { + assert!(results.is_empty()); + + self.push_str(&format!("case {i}: {{\n")); + self.src.push_str(&block.body); + self.src.push_str("break;\n}\n"); + } + self.src.push_str("default: break;\n}\n"); + } + abi::Instruction::DropHandle { .. } => { + todo!("instr: DropHandle") + } + + abi::Instruction::Flush { amt } => { + for op in operands.iter().take(*amt) { + let result = tempname("_flush", self.tmp()); + self.push_str(&format!("auto {result} = {op};\n")); + results.push(result); + } + } + + unk => todo!("emit instruction: {unk:?}"), + } + } + + fn return_pointer(&mut self, size: ArchitectureSize, align: Alignment) -> Self::Operand { + // Track maximum return area requirements + self.return_pointer_area_size = self.return_pointer_area_size.max(size); + self.return_pointer_area_align = self.return_pointer_area_align.max(align); + + "_retArea.ptr".into() + } + + fn push_block(&mut self) { + let tmp = self.tmp(); + + self.block_storage.push(BlockStorage { + body: take(&mut self.src), + element: tempname("_elem", tmp), + base: tempname("_base", tmp), + }); + } + + fn finish_block(&mut self, operands: &mut Vec) { + let BlockStorage { + body, + element, + base, + } = self.block_storage.pop().unwrap(); + + let src = replace(&mut self.src, body); + self.blocks.push(Block { + body: src.into(), + results: take(operands), + element, + base, + }); + } + + fn sizes(&self) -> &SizeAlign { + &self.r#gen.sizes + } + + fn is_list_canonical(&self, _resolve: &Resolve, ty: &Type) -> bool { + self.r#gen.resolve.all_bits_valid(ty) + } +} diff --git a/crates/d/src/wit_common.d b/crates/d/src/wit_common.d new file mode 100644 index 000000000..e8230add7 --- /dev/null +++ b/crates/d/src/wit_common.d @@ -0,0 +1,595 @@ +import core.attribute : mustuse; +import ldc.attributes : llvmAttr; + +alias wasmImport(string mod, string name) = AliasSeq!( + llvmAttr("wasm-import-module", mod), + llvmAttr("wasm-import-name", name) +); + +enum wasmExport(string name) = llvmAttr("wasm-export-name", name); + +struct witExport { string mod; string name; } + +/// Thin CABI compliant wrapper over `T[]` +struct WitList(T) { +@safe @nogc pure nothrow: + T* ptr; + size_t length; + + this(inout T[] slice) inout @trusted { + ptr = slice.ptr; + length = slice.length; + } + + void opAssign(T[] slice) @trusted { + ptr = slice.ptr; + length = slice.length; + } + + alias asSlice this; + inout(T)[] asSlice() @trusted inout { + return (ptr && length) ? ptr[0..length] : null; + } + + bool opEquals(in T[] other) const => this[] == other; + size_t toHash() const => this[].hashOf; +} +auto witList(T)(inout T[] slice) => inout WitList!T(slice); + +// WIT ABI for string matches List, +// except list in WIT is actually List!(dchar) +// +// We assume UTF-8 data (as D native strings are UTF-8) +alias WitString = WitList!(char); + +// TODO: split this file up and give Tuple a full port of the Phobos version? +/// adapted from Phobos std.typecons.Tuple +/// No support for naming members. +struct Tuple(Types...) if (is(Types)) { + Types expand; + alias expand this; +} + +inout(Tuple!Types) tuple(Types...)(inout Types vals) => inout Tuple!Types(vals); + +mixin template WitFlags(T) if (__traits(isUnsigned, T)) { + private alias F = typeof(this); + + T bits; + + @safe nothrow @nogc pure: + + static typeof(this) opIndex(size_t i) + in(i < T.sizeof*8) => F(cast(T)(1 << i)); + + auto opUnary(string op : "~")() const => F(~bits); + + auto ref opOpAssign(string op)(F rhs) + if (op == "|" || op == "&" || op == "^") + { + mixin("bits "~op~"= rhs.bits;"); + return this; + } + + auto opBinary(string op)(F flags) const + if (op == "|" || op == "&" || op == "^") + { + F result = this; + result.opOpAssign!op(flags); + return result; + } + + typeof(this) witClone() const { return this; } +} + + +mixin template WitVariant(Types...) { +private: + static assert(is(typeof(this).Tag)); + static assert(is(Tag U == enum) && __traits(isIntegral, U)); + + static assert(__traits(allMembers, Tag).length == Types.length); + static foreach (i, M; __traits(allMembers, Tag)) { + static assert(i == __traits(getMember, Tag, M)); + } + + union Storage { + template ReplacedTypes() { + alias ReplacedTypes = AliasSeq!(); + + static foreach (T; Types) { + static if (is(T == void)) + ReplacedTypes = AliasSeq!(ReplacedTypes, void[0]); + else + ReplacedTypes = AliasSeq!(ReplacedTypes, T); + } + } + + ubyte __zeroinit = 0; + ReplacedTypes!() members; + } + + Tag _tag; + Storage _storage; + + + @disable this(); + + this(Tag tag, inout Storage storage = Storage.init) inout @nogc nothrow @trusted { + _tag = tag; + _storage = storage; + } + + + static auto _create(Tag tag)() if (is(Types[tag] == void)) { + return typeof(this)(tag); + } + static auto _create(Tag tag)(inout Types[tag] val) if (!is(Types[tag] == void)) { + Storage storage = Storage.init; + storage.tupleof[tag+1] = cast(Types[tag])val; + return inout typeof(this)(tag, cast(inout(Storage))storage); + } + + ref auto _get(Tag tag)() inout return if (!is(Types[tag] == void)) + in (_tag == tag) do { return cast(inout)_storage.tupleof[tag+1]; } +} + +/// Based on Rust's Option +struct Option(T) { +private: + bool _present = false; + T _value; + + this(bool present, inout T value) inout @safe @nogc nothrow { + _present = present; + _value = value; + } +public: + static inout(Option) makeSome(inout T value) @safe @nogc nothrow { + return inout Option(true, value); + } + + static Option makeNone() @safe @nogc nothrow { + return Option(false, T.init); + } + + bool isSome() const @safe @nogc nothrow => _present; + alias isSome this; // implicit conversion to bool + + bool isNone() const @safe @nogc nothrow => !_present; + + ref inout(T) unwrap() inout @trusted @nogc nothrow return + in (_present) do { return _value; } + + T unwrapOr(T fallback) @trusted @nogc nothrow => _present ? _value : fallback; + + T unwrapOrElse(D)(scope D fallback) + if (is(D R == return) && is(R : T) && is(D == __parameters)) + { return _present ? _value : fallback(); } + + bool opEquals(in Option rhs) const { + if (isSome != rhs.isSome) return false; + + if (isSome) return unwrap == rhs.unwrap; + else return true; + } + + size_t toHash() const @safe pure nothrow + { + if (isSome) return this.unwrap.hashOf(true.hashOf); + return false.hashOf; + } +} + +auto some(T)(inout T value) @safe @nogc nothrow { + return Option!T.makeSome(value); +} + +auto none(T)() @safe @nogc nothrow { + return Option!T.makeNone; +} + +/// Based on Rust's Result +@mustuse +struct Result(T = void, E = void) { +private: + bool _hasError; + union Storage { + ubyte __zeroinit = 0; + static if (!is(T == void)) { + T value; + } + static if (!is(E == void)) { + E error; + } + } + Storage _storage; + + this(bool hasError, inout(Storage) storage) inout @safe @nogc nothrow { + _hasError = hasError; + _storage = storage; + } + +public: + static if (is(T == void)) { + static Result makeOk() @safe @nogc nothrow => Result(false, Storage.init); + } else { + static inout(Result) makeOk(inout(T) value) @trusted @nogc nothrow { + Storage newStorage = Storage.init; + newStorage.value = cast(T)value; + + return inout Result(false, cast(inout Storage)newStorage); + } + } + + static if (is(E == void)) { + static Result makeErr() @safe @nogc nothrow => Result(true, Storage.init); + } else { + static inout(Result) makeErr(inout(E) error) @trusted @nogc nothrow { + Storage newStorage = Storage.init; + newStorage.error = cast(E)error; + + return inout Result(true, cast(inout Storage)newStorage); + } + } + + bool isOk() const @safe @nogc nothrow => !_hasError; + + bool isErr() const @safe @nogc nothrow => _hasError; + alias isErr this; // implicit conversion to bool + + static if (!is(T == void)) { + ref inout(T) unwrap() inout @trusted @nogc nothrow return + in (isOk) do { return _storage.value; } + + T unwrapOr(T fallback) @trusted @nogc nothrow => isOk ? _storage.value : fallback; + + T unwrapOrElse(D)(scope D fallback) + if (is(D R == return) && is(R : T) && is(D == __parameters)) + { return isOk ? _storage.value : fallback(); } + } + + static if (!is(E == void)) { + ref inout(E) unwrapErr() inout @trusted @nogc nothrow return + in (isErr) do { return _storage.error; } + } + + bool opEquals(in Result rhs) const { + if (isErr != rhs.isErr) return false; + + if (isErr) { + static if (!is(E == void)) return unwrapErr == rhs.unwrapErr; + else return true; + } + + static if (!is(T == void)) return unwrap == rhs.unwrap; + else return true; + } + + size_t toHash() const @safe pure nothrow + { + if (isErr) { + static if (!is(E == void)) return this.unwrapErr.hashOf(true.hashOf); + return true.hashOf; + } + + static if (!is(T == void)) return this.unwrap.hashOf(false.hashOf); + else return false.hashOf; + } +} + +auto ok(E, T)(inout T value) @safe @nogc nothrow { + return Result!(T, E).makeOk(value); +} +auto ok(E)() @safe @nogc nothrow { + return Result!(void, E).makeOk(); +} + +auto err(T, E)(inout E value) @safe @nogc nothrow { + return Result!(T, E).makeErr(value); +} +auto err(T)() @safe @nogc nothrow { + return Result!(T, void).makeErr(); +} + +void witFree(T)(scope ref T val) if (__traits(isArithmetic, T)) { + // no-op +} +void witDrop(T)(scope ref T val) if (__traits(isArithmetic, T)) { + // no-op +} +T witClone(T)(in T val) if (__traits(isArithmetic, T)) { + return val; +} + +void witFree(T : Option!U, U)(scope ref T val) { + static if (!is(U == void)) if (val.isSome) val.unwrap.witFree; +} +void witDrop(T : Option!U, U)(scope ref T val) { + static if (!is(U == void)) if (val.isSome) val.unwrap.witDrop; +} +T witClone(T : Option!U, U)(in T val) { + if (val.isSome) { + static if (!is(U == void)) { + return T.makeSome(val.unwrap.witClone); + } else { + return T.makeSome; + } + } else { + return T.makeNone; + } +} + +void witFree(T : Result!(U, V), U, V)(scope ref T val) { + if (val.isErr) { + static if (!is(V == void)) val.unwrapErr.witFree; + } else { + static if (!is(U == void)) val.unwrap.witFree; + } +} +void witDrop(T : Result!(U, V), U, V)(scope ref T val) { + if (val.isErr) { + static if (!is(V == void)) val.unwrapErr.witDrop; + } else { + static if (!is(U == void)) val.unwrap.witDrop; + } +} +T witClone(T : Result!(U, V), U, V)(in T val) { + if (val.isErr) { + static if (!is(V == void)) { + return T.makeErr(val.unwrapErr.witClone); + } else { + return T.makeErr; + } + } else { + static if (!is(U == void)) { + return T.makeOk(val.unwrap.witClone); + } else { + return T.makeOk; + } + } +} + +void witFree(T : WitList!U, U)(scope ref T val) { + foreach (ref e; val) { + e.witFree; + } + if (val.ptr && val.length) free(val.ptr); + val = null; +} +void witDrop(T : WitList!U, U)(scope ref T val) { + foreach (ref e; val) { + e.witDrop; + } + val = null; +} +T witClone(T : WitList!U, U)(in T val) @trusted { + if (val.ptr == null || val.length == 0) return T(null); + + auto clone = mallocSlice!U(val.length); + + foreach (i, ref e; clone) { + e = val[i].witClone; + } + + return clone.witList; +} + +void witFree(T : Tuple!U, U...)(scope ref T val) { + static foreach (F; T.tupleof) { + __traits(child, val, F).witFree; + } +} +void witDrop(T : Tuple!U, U...)(scope ref T val) { + static foreach (F; T.tupleof) { + __traits(child, val, F).witDrop; + } +} +T witClone(T : Tuple!U, U...)(in T val) { + T clone = void; + static foreach (F; T.tupleof) { + __traits(child, clone, F) = __traits(child, val, F).witClone; + } + return clone; +} + + +void witFree(T, size_t L)(scope ref T[L] val) { + foreach (ref e; val) { + e.witFree; + } +} +void witDrop(T, size_t L)(scope ref T[L] val) { + foreach (ref e; val) { + e.witDrop; + } +} +T[L] witClone(T, size_t L)(in T[L] val) { + T[L] clone; + foreach (i, ref e; clone) { + e = val[i].witClone; + } + return clone; +} + +package: + +extern(C) @nogc nothrow { + void* malloc(size_t size); + void* realloc(void* ptr, size_t newSize); + void free(void* ptr); + noreturn abort(); +} + +// from https://github.com/Inochi2D/numem/blob/main/source/numem/casting.d +// Copyright © 2023-2025, Kitsunebi Games +// Copyright © 2023-2025, Inochi2D Project +// License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) +// Authors: Luna Nielsen +pragma(inline, true) +auto ref T reinterpretCast(T, U)(auto ref U from) @trusted if (T.sizeof == U.sizeof) { + union tmp { U from; T to; } + return tmp(from).to; +} + +T[] mallocSlice(T)(size_t count) @nogc nothrow { + if (count == 0) return []; + auto ptr = malloc(count*T.sizeof); + if (ptr is null) return []; + + return (cast(T*)ptr)[0..count]; +} + +// from std.meta +alias AliasSeq(T...) = T; + + +template findWitExportFunc(string mod, string name, Sig, bool implicitSelf, Impl...) { + static foreach(Func; Impl) { + static foreach(uda; __traits(getAttributes, Func)) { + static if (!is(uda) && is(typeof(uda) == witExport) && uda == witExport(mod, name)) { + static assert( + !is(Func) && + (is(typeof(Func) == function)), + "The implementation of '", mod, "#", name, "' ", + "`", __traits(fullyQualifiedName, findWitExportFunc), "` ", + "must be a function or method." + ); + + static assert( + !is(typeof(findWitExportFunc) == void) || __traits(isSame, findWitExportFunc, Func), + "There must be only one implementation of '", mod, "#", name, "'. ", + "Found at least `", __traits(fullyQualifiedName, findWitExportFunc), + "` and `", __traits(fullyQualifiedName, Func), "`." + ); + alias findWitExportFunc = Func; + } + } + } + + static assert( + !is(typeof(findWitExportFunc) == void), + "Could not find implementation for '", mod, "#", name, "'" + ); + + static assert( + is(typeof(&findWitExportFunc) : Sig) && __traits(isStaticFunction, findWitExportFunc) != implicitSelf, + "The implementation of '", mod, "#", name, "' ", + "`", __traits(fullyQualifiedName, findWitExportFunc), "` ", + "must conform to the necessary signature. ", + "Found `", typeof(&findWitExportFunc), "`", + ", but expected `", Sig, "`" + ); +} + +template findWitExportResource(string mod, string name, Impl...) { + static foreach(Resource; Impl) { + static foreach(uda; __traits(getAttributes, Resource)) { + static if (!is(uda) && is(typeof(uda) == witExport) && uda == witExport(mod, name)) { + static assert( + is(Resource == struct), + "The implementation of '", mod, "#", name, "' ", + "`", __traits(fullyQualifiedName, findWitExportResource), "` ", + "must be a struct." + ); + + static assert( + !is(typeof(findWitExportResource) == void) || __traits(isSame, findWitExportResource, Resource), + "There must be only one implementation of '", mod, "#", name, "'. ", + "Found at least `", __traits(fullyQualifiedName, findWitExportResource), + "` and `", __traits(fullyQualifiedName, Resource), "`." + ); + alias findWitExportResource = Resource; + } + } + } + + static assert( + !is(typeof(findWitExportResource) == void), + "Could not find implementation for '", mod, "#", name, "'" + ); +} + + +template witExportsIn(T) { + alias witExportsIn = AliasSeq!(); + + static foreach(M; __traits(allMembers, T)) { + static foreach(Export; __traits(getOverloads, T, M)) { + static foreach(uda; __traits(getAttributes, Export)) { + static if (!is(uda) && is(typeof(uda) == witExport)) { + witExportsIn = AliasSeq!(witExportsIn, Export); + } + } + } + } +} + +struct DeallocateBuffer { + @nogc nothrow: + struct Page { + void*[32] slots; + static assert(slots.length < 256); + + ubyte cursor; + Page* next; + } + + Page first; + Page* head; + + @disable this(this); + + private void allocNewPage() { + Page* page = cast(Page*)malloc(Page.sizeof); + if (page is null) abort(); + + *page = Page.init; + page.next = head; + head = page; + } + + void opOpAssign(string op: "~")(void* ptr) { + import core.builtins : unlikely; + + if (unlikely(ptr is null)) return; + if (/*unlikely?*/(head is null)) head = &first; + + if (head.cursor >= head.slots.length) allocNewPage(); + + head.slots[head.cursor++] = ptr; + } + + void purge() { + auto page = head; + while (page) { + foreach (ptr; page.slots[0..page.cursor]) free(ptr); + + auto next = page.next; + if (page != &first) free(page); + page = next; + } + + first = Page.init; + head = null; + } + + ~this() { + purge(); + } +} + +version (CRuntime_WASI) { + version (WASIp1) {} + else version = LibcDefinesCABIRealloc; +} + +version (LibcDefinesCABIRealloc) {} +else +@wasmExport!("cabi_realloc") +void* cabi_realloc(void *ptr, size_t oldSize, size_t alignment, size_t newSize) { + if (newSize == 0) return cast(void*)alignment; + void *ret = realloc(ptr, newSize); + if (!ret) abort(); + return ret; +} diff --git a/crates/test/d-test-support/runtime.d b/crates/test/d-test-support/runtime.d new file mode 100644 index 000000000..c03ed2c9f --- /dev/null +++ b/crates/test/d-test-support/runtime.d @@ -0,0 +1,31 @@ +extern(C) @nogc nothrow: + +noreturn abort() { + import ldc.intrinsics : llvm_trap; + llvm_trap(); + while(true) {} +} + +private int memcmp(const void* ptr1, const void* ptr2, size_t size) +{ + auto data1 = cast(const(ubyte)*)ptr1; + auto data2 = cast(const(ubyte)*)ptr2; + + foreach (i; 0..size) { + auto b1 = data1[i]; + auto b2 = data2[i]; + if (b1 != b2) return b1-b2; + } + + return 0; +} + +void _d_array_slice_copy(void* dst, size_t dstlen, void* src, size_t srclen, size_t elemsz) +{ + import ldc.intrinsics : llvm_memcpy; + + //enforceRawArraysConformable("copy", elemsz, src[0..srclen], dst[0..dstlen]); + assert(srclen == dstlen); + + llvm_memcpy!size_t(dst, src, dstlen * elemsz, 0); +} diff --git a/crates/test/d-test-support/walloc.d b/crates/test/d-test-support/walloc.d new file mode 100644 index 000000000..83bbc3147 --- /dev/null +++ b/crates/test/d-test-support/walloc.d @@ -0,0 +1,548 @@ +// From https://github.com/Inochi2D/numem/blob/main/modules/hookset-wasm/source/walloc.d +// Modified to include double-free detection + +/** + A small malloc implementation for use in WebAssembly targets + + Copyright (c) 2023-2025, Kitsunebi Games + Copyright (c) 2023-2025, Inochi2D Project + Copyright (c) 2020, Igalia, S.L. + + Distributed under an MIT-style License. + (See accompanying LICENSE file or copy at + https://github.com/wingo/walloc/blob/master/LICENSE.md) +*/ + +module walloc; +import ldc.intrinsics : + llvm_wasm_memory_grow, + llvm_wasm_memory_size, + llvm_memmove; + +extern(C) @nogc nothrow: + +/// MODIFIED FOR wit-bindgen TESTS +enum MAX_ALLOCATIONS = 2048; +extern(D) void*[MAX_ALLOCATIONS] activePointers; +extern(D) size_t[MAX_ALLOCATIONS] activeAllocSizes; + +// extern(C) to make it "public" for the `lists` test +extern(C) size_t walloc_allocated_bytes = 0; +/// END + +void* malloc(size_t size) @nogc nothrow @system { + if (size == 0) + return null; + + size_t granules = size_to_granules(size); + chunk_kind kind = granules_to_chunk_kind(granules); + + /// MODIFIED FOR wit-bindgen TESTS + auto result = (kind == chunk_kind.LARGE_OBJECT) ? allocate_large(size) : allocate_small(kind); + assert(result !is null); + foreach (i, ref ptr; activePointers) { + if (ptr !is null) continue; + ptr = result; + activeAllocSizes[i] = size; + walloc_allocated_bytes += size; + return result; + } + assert(0); + /// END +} + +export +void free(void *ptr) @nogc nothrow @system { + /// MODIFIED FOR wit-bindgen TESTS + assert(ptr !is null); + + bool found = false; + foreach (i, ref existingPtr; activePointers) { + if (ptr !is existingPtr) continue; + existingPtr = null; + walloc_allocated_bytes -= activeAllocSizes[i]; + found = true; + break; + } + assert(found); + /// END + + _page_t* page = get_page(ptr); + size_t chunk = get_chunk_index(ptr); + ubyte kind = page.header.chunk_kinds[chunk]; + if (kind == chunk_kind.LARGE_OBJECT) { + _large_object_t* obj = get_large_object(ptr); + obj.next = large_objects; + large_objects = obj; + allocate_chunk(page, chunk, chunk_kind.FREE_LARGE_OBJECT); + pending_large_object_compact = 1; + } else { + size_t granules = kind; + _freelist_t** loc = get_small_object_freelist(cast(chunk_kind)granules); + _freelist_t* obj = cast(_freelist_t*)ptr; + obj.next = *loc; + *loc = obj; + } +} + +export +void* realloc(void* ptr, size_t newSize) @nogc nothrow @system { + if (!ptr) + return malloc(newSize); + + size_t oldSize = get_alloc_size(ptr); + if (newSize <= oldSize) + return ptr; + + // Size is bigger, realloc just to be sure. + void* n_mem = malloc(newSize); + llvm_memmove(n_mem, ptr, oldSize, true); + free(ptr); + return n_mem; +} + +private: + +size_t get_alloc_size(void* ptr) { + _page_t* page = get_page(ptr); + size_t chunk = get_chunk_index(ptr); + chunk_kind kind = cast(chunk_kind)page.header.chunk_kinds[chunk]; + + if (kind == chunk_kind.LARGE_OBJECT) { + _large_object_t* obj = get_large_object(ptr); + return obj.size; + } + + if (kind < chunk_kind.SMALL_OBJECT_CHUNK_KINDS) { + ptrdiff_t granules = chunk_kind_to_granules(kind); + return granules * GRANULE_SIZE; + } + + return 0; +} + +extern __gshared void* __heap_base; +__gshared size_t walloc_heap_size; +__gshared _freelist_t*[chunk_kind.SMALL_OBJECT_CHUNK_KINDS] small_object_freelists; +__gshared _large_object_t* large_objects; + + +pragma(inline, true) +size_t _max(size_t a, size_t b) { return a < b ? b : a; } + +pragma(inline, true) +size_t _alignv(size_t val, size_t alignment) { return (val + alignment - 1) & ~(alignment - 1); } + +pragma(inline, true) +extern(D) +void __assert_aligned(T, Y)(T x, Y y) { + assert(cast(size_t)x == _alignv(cast(size_t)x, cast(size_t)y)); +} + +enum size_t CHUNK_SIZE = 256; +enum size_t CHUNK_SIZE_LOG_2 = 8; +enum size_t CHUNK_MASK = (CHUNK_SIZE - 1); +enum size_t PAGE_SIZE = 65536; +enum size_t PAGE_SIZE_LOG_2 = 16; +enum size_t PAGE_MASK = (PAGE_SIZE - 1); +enum size_t CHUNKS_PER_PAGE = 256; +enum size_t GRANULE_SIZE = 8; +enum size_t GRANULE_SIZE_LOG_2 = 3; +enum size_t LARGE_OBJECT_THRESHOLD = 256; +enum size_t LARGE_OBJECT_GRANULE_THRESHOLD = 32; +enum size_t FIRST_ALLOCATABLE_CHUNK = 1; +enum size_t PAGE_HEADER_SIZE = _page_header_t.sizeof; +enum size_t LARGE_OBJECT_HEADER_SIZE = _large_object_t.sizeof; + +static assert(PAGE_SIZE == CHUNK_SIZE * CHUNKS_PER_PAGE); +static assert(CHUNK_SIZE == 1 << CHUNK_SIZE_LOG_2); +static assert(PAGE_SIZE == 1 << PAGE_SIZE_LOG_2); +static assert(GRANULE_SIZE == 1 << GRANULE_SIZE_LOG_2); +static assert(LARGE_OBJECT_THRESHOLD == + LARGE_OBJECT_GRANULE_THRESHOLD * GRANULE_SIZE); + +struct _chunk_t { + void[CHUNK_SIZE] data; +} + +enum chunk_kind : ubyte { + GRANULES_1, + GRANULES_2, + GRANULES_3, + GRANULES_4, + GRANULES_5, + GRANULES_6, + GRANULES_8, + GRANULES_10, + GRANULES_16, + GRANULES_32, + + SMALL_OBJECT_CHUNK_KINDS, + FREE_LARGE_OBJECT = 254, + LARGE_OBJECT = 255 +} + +__gshared const ubyte[] small_object_granule_sizes = [ + 1, 2, 3, 4, 5, 6, 8, 10, 16, 32 +]; + +pragma(inline, true) +chunk_kind granules_to_chunk_kind(size_t granules) { + static foreach(gsize; small_object_granule_sizes) { + if (granules <= gsize) + return mixin(q{chunk_kind.GRANULES_}, cast(int)gsize); + } + return chunk_kind.LARGE_OBJECT; +} + +pragma(inline, true) +ubyte chunk_kind_to_granules(chunk_kind kind) { + static foreach(gsize; small_object_granule_sizes) { + if (kind == mixin(q{chunk_kind.GRANULES_}, cast(int)gsize)) + return gsize; + } + return cast(ubyte)-1; +} + +struct _page_header_t { + ubyte[CHUNKS_PER_PAGE] chunk_kinds; +} + +struct _page_t { + union { + _page_header_t header; + _chunk_t[CHUNKS_PER_PAGE] chunks; + } +} + +pragma(inline, true) +_page_t* get_page(void *ptr) { + return cast(_page_t*)cast(void*)((cast(size_t) ptr) & ~PAGE_MASK); +} + +pragma(inline, true) +static size_t get_chunk_index(void *ptr) { + return ((cast(size_t) ptr) & PAGE_MASK) / CHUNK_SIZE; +} + +struct _freelist_t { + _freelist_t *next; +} + +struct _large_object_t { + _large_object_t* next; + size_t size; +} + +pragma(inline, true) +void* get_large_object_payload(_large_object_t *obj) { + return (cast(void*)obj) + LARGE_OBJECT_HEADER_SIZE; +} + +pragma(inline, true) +_large_object_t* get_large_object(void *ptr) { + return cast(_large_object_t*)(ptr - LARGE_OBJECT_HEADER_SIZE); +} + +_page_t* allocate_pages(size_t payloadSize, size_t* allocated) { + size_t needed = payloadSize + PAGE_HEADER_SIZE; + size_t heap_size = llvm_wasm_memory_size(0) * PAGE_SIZE; + size_t base = heap_size; + size_t preallocated = 0, grow = 0; + + if (!walloc_heap_size) { + // We are allocating the initial pages, if any. We skip the first 64 kB, + // then take any additional space up to the memory size. + size_t heap_base = _alignv(cast(size_t)&__heap_base, PAGE_SIZE); + preallocated = heap_size - heap_base; // Preallocated pages. + walloc_heap_size = preallocated; + base -= preallocated; + } + + if (preallocated < needed) { + // Always grow the walloc heap at least by 50%. + grow = _alignv(_max(walloc_heap_size / 2, needed - preallocated), + PAGE_SIZE); + + assert(grow); + if (llvm_wasm_memory_grow(0, cast(int)(grow >> PAGE_SIZE_LOG_2)) == -1) { + return null; + } + + walloc_heap_size += grow; + } + + _page_t* ret = cast(_page_t*)base; + size_t size = grow + preallocated; + + assert(size); + assert(size == _alignv(size, PAGE_SIZE)); + *allocated = size / PAGE_SIZE; + return ret; +} + +void* allocate_chunk(_page_t* page, size_t idx, chunk_kind kind) { + page.header.chunk_kinds[idx] = kind; + return page.chunks[idx].data.ptr; +} + +// It's possible for splitting to produce a large object of size 248 (256 minus +// the header size) -- i.e. spanning a single chunk. In that case, push the +// chunk back on the GRANULES_32 small object freelist. +void maybe_repurpose_single_chunk_large_objects_head() { + if (large_objects.size < CHUNK_SIZE) { + size_t idx = get_chunk_index(large_objects); + void* ptr = allocate_chunk(get_page(large_objects), idx, chunk_kind.GRANULES_32); + large_objects = large_objects.next; + _freelist_t* head = cast(_freelist_t*)ptr; + head.next = small_object_freelists[chunk_kind.GRANULES_32]; + small_object_freelists[chunk_kind.GRANULES_32] = head; + } +} + +// If there have been any large-object frees since the last large object +// allocation, go through the freelist and merge any adjacent objects. +__gshared int pending_large_object_compact = 0; +_large_object_t** maybe_merge_free_large_object(_large_object_t** prev) { + _large_object_t* obj = *prev; + + while(true) { + void* end = get_large_object_payload(obj) + obj.size; + __assert_aligned(end, CHUNK_SIZE); + + size_t chunk = get_chunk_index(end); + if (chunk < FIRST_ALLOCATABLE_CHUNK) { + // Merging can't create a large object that newly spans the header chunk. + // This check also catches the end-of-heap case. + return prev; + } + _page_t* page = get_page(end); + if (page.header.chunk_kinds[chunk] != chunk_kind.FREE_LARGE_OBJECT) { + return prev; + } + _large_object_t* next = cast(_large_object_t*)end; + + _large_object_t** prev_prev = &large_objects; + _large_object_t* walk = large_objects; + while(true) { + assert(walk); + if (walk == next) { + obj.size += LARGE_OBJECT_HEADER_SIZE + walk.size; + *prev_prev = walk.next; + if (prev == &walk.next) { + prev = prev_prev; + } + break; + } + prev_prev = &walk.next; + walk = walk.next; + } + } +} + +void maybe_compact_free_large_objects() { + if (pending_large_object_compact) { + pending_large_object_compact = 0; + _large_object_t** prev = &large_objects; + while (*prev) { + prev = &(*maybe_merge_free_large_object(prev)).next; + } + } +} + +// Allocate a large object with enough space for SIZE payload bytes. Returns a +// large object with a header, aligned on a chunk boundary, whose payload size +// may be larger than SIZE, and whose total size (header included) is +// chunk-aligned. Either a suitable allocation is found in the large object +// freelist, or we ask the OS for some more pages and treat those pages as a +// large object. If the allocation fits in that large object and there's more +// than an aligned chunk's worth of data free at the end, the large object is +// split. +// +// The return value's corresponding chunk in the page as starting a large +// object. +_large_object_t* allocate_large_object(size_t size) { + maybe_compact_free_large_objects(); + + _large_object_t* best = null; + _large_object_t** best_prev = &large_objects; + size_t best_size = -1; + + _large_object_t** prev = &large_objects; + _large_object_t* walk = large_objects; + while (walk) { + if (walk.size >= size && walk.size < best_size) { + best_size = walk.size; + best = walk; + best_prev = prev; + + // Not going to do any better than this; just return it. + if (best_size + LARGE_OBJECT_HEADER_SIZE == _alignv(size + LARGE_OBJECT_HEADER_SIZE, CHUNK_SIZE)) + break; + } + + prev = &walk.next; + walk = walk.next; + } + + if (!best) { + // The large object freelist doesn't have an object big enough for this + // allocation. Allocate one or more pages from the OS, and treat that new + // sequence of pages as a fresh large object. It will be split if + // necessary. + size_t size_with_header = size + _large_object_t.sizeof; + size_t n_allocated = 0; + _page_t* page = allocate_pages(size_with_header, &n_allocated); + if (!page) { + return null; + } + + void* ptr = allocate_chunk(page, FIRST_ALLOCATABLE_CHUNK, chunk_kind.LARGE_OBJECT); + best = cast(_large_object_t*)ptr; + size_t page_header = ptr - cast(void*)page; + + best.next = large_objects; + best.size = best_size = n_allocated * PAGE_SIZE - page_header - LARGE_OBJECT_HEADER_SIZE; + assert(best_size >= size_with_header); + } + + allocate_chunk(get_page(best), get_chunk_index(best), chunk_kind.LARGE_OBJECT); + + _large_object_t* next = best.next; + *best_prev = next; + + size_t tail_size = (best_size - size) & ~CHUNK_MASK; + if (tail_size) { + // The best-fitting object has 1 or more aligned chunks free after the + // requested allocation; split the tail off into a fresh aligned object. + _page_t* start_page = get_page(best); + void* start = get_large_object_payload(best); + void* end = start + best_size; + + if (start_page == get_page(end - tail_size - 1)) { + + // The allocation does not span a page boundary; yay. + __assert_aligned(end, CHUNK_SIZE); + } else if (size < PAGE_SIZE - LARGE_OBJECT_HEADER_SIZE - CHUNK_SIZE) { + + // If the allocation itself smaller than a page, split off the head, then + // fall through to maybe split the tail. + assert(cast(size_t)end == _alignv(cast(size_t)end, PAGE_SIZE)); + + size_t first_page_size = PAGE_SIZE - (cast(size_t)start & PAGE_MASK); + _large_object_t* head = best; + allocate_chunk(start_page, get_chunk_index(start), chunk_kind.FREE_LARGE_OBJECT); + head.size = first_page_size; + head.next = large_objects; + large_objects = head; + + maybe_repurpose_single_chunk_large_objects_head(); + + _page_t* next_page = start_page + 1; + void* ptr = allocate_chunk(next_page, FIRST_ALLOCATABLE_CHUNK, chunk_kind.LARGE_OBJECT); + best = cast(_large_object_t*)ptr; + best.size = best_size = best_size - first_page_size - CHUNK_SIZE - LARGE_OBJECT_HEADER_SIZE; + assert(best_size >= size); + + start = get_large_object_payload(best); + tail_size = (best_size - size) & ~CHUNK_MASK; + } else { + + // A large object that spans more than one page will consume all of its + // tail pages. Therefore if the split traverses a page boundary, round up + // to page size. + __assert_aligned(end, PAGE_SIZE); + size_t first_page_size = PAGE_SIZE - (cast(size_t)start & PAGE_MASK); + size_t tail_pages_size = _alignv(size - first_page_size, PAGE_SIZE); + size = first_page_size + tail_pages_size; + tail_size = best_size - size; + } + best.size -= tail_size; + + size_t tail_idx = get_chunk_index(end - tail_size); + while (tail_idx < FIRST_ALLOCATABLE_CHUNK && tail_size) { + + // We would be splitting in a page header; don't do that. + tail_size -= CHUNK_SIZE; + tail_idx++; + } + + if (tail_size) { + _page_t *page = get_page(end - tail_size); + void* tail_ptr = allocate_chunk(page, tail_idx, chunk_kind.FREE_LARGE_OBJECT); + _large_object_t* tail = cast(_large_object_t*) tail_ptr; + tail.next = large_objects; + tail.size = tail_size - LARGE_OBJECT_HEADER_SIZE; + + debug { + size_t payloadsz = cast(size_t)get_large_object_payload(tail) + tail.size; + assert(payloadsz == _alignv(payloadsz, CHUNK_SIZE)); + } + + large_objects = tail; + maybe_repurpose_single_chunk_large_objects_head(); + } + } + + debug { + size_t payloadsz = cast(size_t)get_large_object_payload(best) + best.size; + assert(payloadsz == _alignv(payloadsz, CHUNK_SIZE)); + } + return best; +} + +_freelist_t* obtain_small_objects(chunk_kind kind) { + _freelist_t** whole_chunk_freelist = &small_object_freelists[chunk_kind.GRANULES_32]; + void *chunk; + if (*whole_chunk_freelist) { + chunk = *whole_chunk_freelist; + *whole_chunk_freelist = (*whole_chunk_freelist).next; + } else { + chunk = allocate_large_object(0); + if (!chunk) { + return null; + } + } + + void* ptr = allocate_chunk(get_page(chunk), get_chunk_index(chunk), kind); + void* end = ptr + CHUNK_SIZE; + _freelist_t* next = null; + size_t size = chunk_kind_to_granules(kind) * GRANULE_SIZE; + for (size_t i = size; i <= CHUNK_SIZE; i += size) { + _freelist_t* head = cast(_freelist_t*)(end - i); + head.next = next; + next = head; + } + return next; +} + +pragma(inline, true) +size_t size_to_granules(size_t size) { + return (size + GRANULE_SIZE - 1) >> GRANULE_SIZE_LOG_2; +} + +pragma(inline, true) +_freelist_t** get_small_object_freelist(chunk_kind kind) { + assert(kind < chunk_kind.SMALL_OBJECT_CHUNK_KINDS); + return &small_object_freelists[kind]; +} + +void* allocate_small(chunk_kind kind) { + _freelist_t** loc = get_small_object_freelist(kind); + if (!*loc) { + _freelist_t* freelist = obtain_small_objects(kind); + if (!freelist) + return null; + + *loc = freelist; + } + + _freelist_t* ret = *loc; + *loc = ret.next; + return cast(void*)ret; +} + +void* allocate_large(size_t size) { + _large_object_t* obj = allocate_large_object(size); + return obj ? get_large_object_payload(obj) : null; +} diff --git a/crates/test/src/d.rs b/crates/test/src/d.rs new file mode 100644 index 000000000..43563ce2b --- /dev/null +++ b/crates/test/src/d.rs @@ -0,0 +1,168 @@ +use crate::{Compile, LanguageMethods, Runner, Verify}; +use anyhow::{Context, Result}; +use clap::Parser; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Default, Debug, Clone, Parser)] +pub struct DOpts {} + +pub struct D; + +fn ldc2(_runner: &Runner) -> PathBuf { + format!("ldc2").into() +} + +impl LanguageMethods for D { + fn display(&self) -> &str { + "d" + } + + fn comment_prefix_for_test_config(&self) -> Option<&str> { + Some("//@") + } + + fn should_fail_verify( + &self, + _runner: &Runner, + name: &str, + config: &crate::config::WitConfig, + _args: &[String], + ) -> bool { + config.async_ || config.error_context || name == "map.wit" || name == "issue1642.wit" + } + + fn default_bindgen_args_for_codegen(&self) -> &[&str] { + &["--emit-export-stubs"] + } + + fn prepare(&self, runner: &mut Runner) -> Result<()> { + prepare(runner, ldc2(runner)) + } + + fn compile(&self, runner: &Runner, c: &Compile<'_>) -> Result<()> { + compile(runner, c, ldc2(runner)) + } + + fn verify(&self, runner: &Runner, v: &Verify<'_>) -> Result<()> { + verify(runner, v, ldc2(runner)) + } +} + +fn prepare(runner: &mut Runner, compiler: PathBuf) -> Result<()> { + let cwd = env::current_dir()?; + let dir = cwd.join(&runner.opts.artifacts).join("d"); + + super::write_if_different(&dir.join("test.d"), "extern(C) void _start() {}")?; + + println!("Testing if `{}` works...", compiler.display()); + runner + .run_command( + Command::new(&compiler) + .current_dir(&dir) + .arg("-mtriple=wasm32-unknown-unknown") + .arg("-betterC") + .arg("test.d"), + ) + .inspect_err(|_| { + eprintln!("Error: failed to find `{}`.", compiler.display()); + })?; + + Ok(()) +} + +fn search_for_world_package(bindings_root: &Path) -> Option { + // Look for a package.d generated from a world nested at wit/*/*/*/package.d + + // TODO: If we had access to the full package+version of the world being + // generated, we wouldn't need to search. + + // ./wit/* + fs::read_dir(bindings_root.join("wit")) + .ok()? + .flatten() + .map(|e| e.path()) + .filter(|p| p.is_dir()) + // ./wit/*/* + .filter_map(|p| fs::read_dir(p).ok()) + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| p.is_dir()) + // ./wit/*/*/* + .filter_map(|p| fs::read_dir(p).ok()) + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| p.is_dir()) + // ./wit/*/*/*/package.d + .filter_map(|p| fs::read_dir(p).ok()) + .flatten() + .flatten() + .map(|e| e.path()) + .find(|p| p.is_file() && p.file_name().unwrap() == "package.d") +} + +fn compile(runner: &Runner, compile: &Compile<'_>, compiler: PathBuf) -> Result<()> { + let mut cmd = Command::new(compiler); + + let output = compile.output.with_extension("core.wasm"); + + std::fs::write( + compile.artifacts_dir.join("runtime.d"), + include_bytes!("../d-test-support/runtime.d"), + )?; + std::fs::write( + compile.artifacts_dir.join("walloc.d"), + include_bytes!("../d-test-support/walloc.d"), + )?; + + cmd.arg(&compile.component.path) + .arg("-betterC") // don't allow features needing DRuntime + .arg("-mtriple=wasm32-unknown-unknown") + .arg("-I") + .arg(&compile.bindings_dir) + .arg("-i") // compile included dependencies + .arg("--de") // deperecations are errors + .arg("-w") // warnings are errors + .arg("-L--no-entry") + .arg("-L--no-export-dynamic") // important to make sure unused symbols don't get linked + .arg("--checkaction=halt") // to trap instead of using libc __assert + .arg("-g") // debug info + .arg("--preview=in") // `in` is more restricting with this on; test it + .arg("-of") + .arg(&output) + .arg(compile.artifacts_dir.join("runtime.d")) + .arg(compile.artifacts_dir.join("walloc.d")); + + runner.run_command(&mut cmd)?; + + runner + .convert_p1_to_component(&output, compile) + .with_context(|| format!("failed to convert {output:?}"))?; + + Ok(()) +} + +fn verify(runner: &Runner, verify: &Verify<'_>, compiler: PathBuf) -> Result<()> { + let mut cmd = Command::new(compiler); + + let world_path = search_for_world_package(verify.bindings_dir).unwrap(); + + cmd.arg(world_path) + .arg("-betterC") + .arg("-mtriple=wasm32-unknown-unknown") + .arg("-I") + .arg(&verify.bindings_dir) + .arg("-i") // compile included dependencies + .arg("-c") // compile only + .arg("--de") // deperecations are errors + .arg("-w") // warnigns are errors + .arg("--preview=in") + .arg("-of") + .arg(verify.artifacts_dir.join("tmp.o")); + runner.run_command(&mut cmd)?; + Ok(()) +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 7a54f4612..de496275b 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -17,6 +17,7 @@ mod config; mod cpp; mod csharp; mod custom; +mod d; mod go; mod moonbit; mod runner; @@ -229,6 +230,7 @@ enum Language { Csharp, MoonBit, Go, + D, Custom(custom::Language), } @@ -451,6 +453,7 @@ impl Runner { "cs" => Language::Csharp, "mbt" => Language::MoonBit, "go" => Language::Go, + "d" => Language::D, other => Language::Custom(custom::Language::lookup(self, other)?), }; @@ -1322,6 +1325,7 @@ impl Language { Language::Csharp, Language::MoonBit, Language::Go, + Language::D, ]; fn obj(&self) -> &dyn LanguageMethods { @@ -1333,6 +1337,7 @@ impl Language { Language::Csharp => &csharp::Csharp, Language::MoonBit => &moonbit::MoonBit, Language::Go => &go::Go, + Language::D => &d::D, Language::Custom(custom) => custom, } } diff --git a/src/bin/wit-bindgen.rs b/src/bin/wit-bindgen.rs index 04e38f7f0..6f0ff8fed 100644 --- a/src/bin/wit-bindgen.rs +++ b/src/bin/wit-bindgen.rs @@ -74,6 +74,15 @@ enum Opt { args: Common, }, + /// Generates bindings for D guest modules. + #[cfg(feature = "d")] + D { + #[clap(flatten)] + opts: wit_bindgen_d::Opts, + #[clap(flatten)] + args: Common, + }, + // doc-comments are present on `wit_bindgen_test::Opts` for clap to use. Test { #[clap(flatten)] @@ -150,6 +159,8 @@ fn main() -> Result<()> { Opt::Go { opts, args } => (opts.build(), args), #[cfg(feature = "csharp")] Opt::Csharp { opts, args } => (opts.build(), args), + #[cfg(feature = "d")] + Opt::D { opts, args } => (opts.build(args.out_dir.as_ref()), args), Opt::Test { opts } => return opts.run(std::env::args_os().nth(0).unwrap().as_ref()), }; diff --git a/tests/runtime/common-types/leaf.d b/tests/runtime/common-types/leaf.d new file mode 100644 index 000000000..b78d4a248 --- /dev/null +++ b/tests/runtime/common-types/leaf.d @@ -0,0 +1,24 @@ +import wit.test.common.leaf; +import wit.common; + +@witExport("test:common/to-test", "wrap") +R1 wrap(F1 flag) { + switch (flag.bits) with (F1) { + case a.bits: + return R1(1, flag); + case b.bits: + return R1(2, flag); + default: + assert(0); + } +} + +@witExport("test:common/to-test", "var-f") +V1 varF() { + return V1.b(42); +} + +alias Exports = wit.test.common.leaf.Exports!( + wrap, + varF +); diff --git a/tests/runtime/common-types/middle.d b/tests/runtime/common-types/middle.d new file mode 100644 index 000000000..50868f65e --- /dev/null +++ b/tests/runtime/common-types/middle.d @@ -0,0 +1,19 @@ +import wit.test.common.middle; +import wit.common; + +import imps = wit.test.common.to_test.imports; + +@witExport("test:common/to-test", "wrap") +R1 wrap(F1 flag) { + return imps.wrap(flag); +} + +@witExport("test:common/to-test", "var-f") +V1 varF() { + return imps.varF; +} + +alias Exports = wit.test.common.middle.Exports!( + wrap, + varF +); diff --git a/tests/runtime/common-types/runner.d b/tests/runtime/common-types/runner.d new file mode 100644 index 000000000..b94bb5f66 --- /dev/null +++ b/tests/runtime/common-types/runner.d @@ -0,0 +1,21 @@ +import wit.test.common.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + R1 res = wrap(F1.a); + assert(res.b == F1.a); + assert(res.a == 1); + + R1 res2 = wrap(F1.b); + assert(res2.b == F1.b); + assert(res2.a == 2); + + V1 res3 = varF(); + assert(res3.isB); + assert(res3.getB == 42); +} + +alias Exports = wit.test.common.runner.Exports!( + run +); diff --git a/tests/runtime/demo/runner.d b/tests/runtime/demo/runner.d new file mode 100644 index 000000000..848742806 --- /dev/null +++ b/tests/runtime/demo/runner.d @@ -0,0 +1,11 @@ +import wit.a.b.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + x(); +} + +alias Exports = wit.a.b.runner.Exports!( + run +); diff --git a/tests/runtime/demo/test.d b/tests/runtime/demo/test.d new file mode 100644 index 000000000..647d2e3fa --- /dev/null +++ b/tests/runtime/demo/test.d @@ -0,0 +1,10 @@ +import wit.a.b.test; +import wit.common; + +@witExport("a:b/the-test", "x") +void x() { +} + +alias Exports = wit.a.b.test.Exports!( + x +); diff --git a/tests/runtime/fixed-length-lists/runner.d b/tests/runtime/fixed-length-lists/runner.d new file mode 100644 index 000000000..0b9ee34de --- /dev/null +++ b/tests/runtime/fixed-length-lists/runner.d @@ -0,0 +1,68 @@ +//@ wasmtime-flags = '-Wcomponent-model-fixed-length-lists' + +import wit.test.fixed_length_lists.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + listParam([1, 2, 3, 4]); + listParam2([[1, 2], [3, 4]]); + listParam3([ + -1, 2, -3, 4, -5, 6, -7, 8, -9, 10, -11, 12, -13, 14, -15, 16, -17, 18, -19, 20, + ]); + { + auto result = listResult(); + assert(result == ['0', '1', 'A', 'B', 'a', 'b', 128, 255]); + } + { + auto result = listMinmax16([0, 1024, 32768, 65535], [1, 2048, -32767, -2]); + assert(result == Tuple!(ushort[4], short[4])([0, 1024, 32768, 65535], [1, 2048, -32767, -2])); + } + { + auto result = listMinmaxFloat([2.0, -42.0], [0.25, -0.125]); + assert(result == Tuple!(float[2], float[2])([2.0, -42.0], [0.25, -0.125])); + } + { + auto result = listRoundtrip(['a', 'b', 'c', 'd', 0, 1, 2, 3, 'A', 'B', 'Y', 'Z']); + assert(result == ['a', 'b', 'c', 'd', 0, 1, 2, 3, 'A', 'B', 'Y', 'Z']); + } + { + auto result = nestedRoundtrip([[1, 5], [42, 1_000_000]], [[-1, 3], [-2_000_000, 4711]]); + assert( + result == + Tuple!(uint[2][2], int[2][2])([[1, 5], [42, 1_000_000]], [[-1, 3], [-2_000_000, 4711]]) + ); + } + { + auto result = largeRoundtrip( + [[1, 5], [42, 1_000_000]], + [ + [-1, 3, -2, 4], + [-2_000_000, 4711, 99_999, -5], + [-6, 7, 8, -9], + [50, -5, 500, -5000], + ], + ); + assert( + result == + Tuple!(uint[2][2], int[4][4])( + [[1, 5], [42, 1_000_000]], + [ + [-1, 3, -2, 4], + [-2_000_000, 4711, 99_999, -5], + [-6, 7, 8, -9], + [50, -5, 500, -5000] + ] + ) + ); + } + { + auto result = nightmareOnCpp([Nested(l: [1, -1]), Nested(l: [2, -2])]); + assert(result[0].l == [1, -1]); + assert(result[1].l == [2, -2]); + } +} + +alias Exports = wit.test.fixed_length_lists.runner.Exports!( + run +); diff --git a/tests/runtime/fixed-length-lists/test.d b/tests/runtime/fixed-length-lists/test.d new file mode 100644 index 000000000..88a201de7 --- /dev/null +++ b/tests/runtime/fixed-length-lists/test.d @@ -0,0 +1,63 @@ +import wit.test.fixed_length_lists.test; +import wit.common; + +@witExport("test:fixed-length-lists/to-test", "list-param") +void listParam(in uint[4] a) { + assert(a == [1, 2, 3, 4]); +} + +@witExport("test:fixed-length-lists/to-test", "list-param2") +void listParam2(in uint[2][2] a) { + enum uint[2][2] v = [[1, 2], [3, 4]]; + assert(a == v); +} + +@witExport("test:fixed-length-lists/to-test", "list-param3") +void listParam3(in int[20] a) { + assert(a == [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10, -11, 12, -13, 14, -15, 16, -17, 18, -19, 20]); +} + +@witExport("test:fixed-length-lists/to-test", "list-minmax16") +Tuple!(ushort[4], short[4]) listMinmax16(in ushort[4] a, in short[4] b) { + return tuple(a, b); +} + + +@witExport("test:fixed-length-lists/to-test", "list-minmax-float") +Tuple!(float[2], double[2]) listMinmaxFloat(in float[2] a, in double[2] b) { + return tuple(a, b); +} + +@witExport("test:fixed-length-lists/to-test", "list-roundtrip") +ubyte[12] listRoundtrip(in ubyte[12] a) => a; + +@witExport("test:fixed-length-lists/to-test", "list-result") +ubyte[8] listResult() => ['0', '1', 'A', 'B', 'a', 'b', 128, 255]; + +@witExport("test:fixed-length-lists/to-test", "nested-roundtrip") +Tuple!(uint[2][2], int[2][2]) nestedRoundtrip(in uint[2][2] a, in int[2][2] b) { + return tuple(a, b); +} + +@witExport("test:fixed-length-lists/to-test", "large-roundtrip") +Tuple!(uint[2][2], int[4][4]) largeRoundtrip(in uint[2][2] a, in int[4][4] b) { + return tuple(a, b); +} + +@witExport("test:fixed-length-lists/to-test", "nightmare-on-cpp") +Nested[2] nightmareOnCpp(in Nested[2] a) { + return a; +} + +alias Exports = wit.test.fixed_length_lists.test.Exports!( + listParam, + listParam2, + listParam3, + listMinmax16, + listMinmaxFloat, + listRoundtrip, + listResult, + nestedRoundtrip, + largeRoundtrip, + nightmareOnCpp +); diff --git a/tests/runtime/flavorful/runner.d b/tests/runtime/flavorful/runner.d new file mode 100644 index 000000000..43f575c9b --- /dev/null +++ b/tests/runtime/flavorful/runner.d @@ -0,0 +1,93 @@ +import wit.test.flavorful.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + fListInRecord1(ListInRecord1(a: cast(WitString)"list_in_record1".witList)); + + { + auto result = fListInRecord2(); + scope(exit) result.witFree; + + assert(result.a == "list_in_record2"); + } + + { + auto result = fListInRecord3(const ListInRecord3("list_in_record3 input".witList)); + scope(exit) result.witFree; + + assert( + result.a + == "list_in_record3 output" + ); + } + + { + auto result = fListInRecord4(const ListInAlias("input4".witList)); + scope(exit) result.witFree; + + assert( + result.a + == "result4" + ); + } + + fListInVariant1(some("foo".witList), err!void("bar".witList)); + + { + auto result = fListInVariant2(); + scope(exit) result.witFree; + + + assert( + result + == some("list_in_variant2".witList) + ); + } + + { + auto result = fListInVariant3(some("input3".witList)); + scope(exit) result.witFree; + + assert( + result + == some("output3".witList) + ); + } + + { + auto errno = errnoResult(); + assert(errno.isErr && errno.unwrapErr == MyErrno.b); + } + assert(errnoResult().isOk); + + { + immutable WitString[1] input = ["typedef2".witList]; + auto result = listTypedefs("typedef1".witList, input[].witList); + scope(exit) result.witFree; + + assert(result[0] == (cast(ubyte[])"typedef3").witList); + assert(result[1].length == 1); + assert(result[1][0] == "typedef4"); + } + + { + static immutable bool[] input1 = [true, false]; + static immutable Result!()[] input2 = [ok!void, err!void]; + static immutable MyErrno[] input3 = [MyErrno.success, MyErrno.a]; + + auto result = listOfVariants(input1[].witList, input2[].witList, input3[].witList); + scope(exit) result.witFree; + + static immutable bool[] output1 = [false, true]; + static immutable Result!()[] output2 = [err!void, ok!void]; + static immutable MyErrno[] output3 = [MyErrno.a, MyErrno.b]; + assert(result[0] == output1); + assert(result[1] == output2); + assert(result[2] == output3); + } +} + +alias Exports = wit.test.flavorful.runner.Exports!( + run +); diff --git a/tests/runtime/flavorful/test.d b/tests/runtime/flavorful/test.d new file mode 100644 index 000000000..a384b790b --- /dev/null +++ b/tests/runtime/flavorful/test.d @@ -0,0 +1,106 @@ +import wit.test.flavorful.test; +import wit.common; + +@witExport("test:flavorful/to-test", "f-list-in-record1") +void fListInRecord1(in ListInRecord1 a) { + assert(a.a == "list_in_record1"); +} + +@witExport("test:flavorful/to-test", "f-list-in-record2") +ListInRecord2 fListInRecord2() { + return (const ListInRecord2("list_in_record2".witList)).witClone; +} + +@witExport("test:flavorful/to-test", "f-list-in-record3") +ListInRecord3 fListInRecord3(in ListInRecord3 a) { + assert(a.a == "list_in_record3 input"); + return (const ListInRecord3("list_in_record3 output".witList)).witClone; +} + +@witExport("test:flavorful/to-test", "f-list-in-record4") +ListInAlias fListInRecord4(in ListInAlias a) { + assert(a.a == "input4"); + return (const ListInAlias("result4".witList)).witClone; +} + +@witExport("test:flavorful/to-test", "f-list-in-variant1") +void fListInVariant1(in ListInVariant1V1 a, in ListInVariant1V2 b) { + assert(a.unwrap() == "foo"); + assert(b.unwrapErr() == "bar"); +} + +@witExport("test:flavorful/to-test", "f-list-in-variant2") +Option!WitString fListInVariant2() { + return some("list_in_variant2".witList).witClone; +} + +@witExport("test:flavorful/to-test", "f-list-in-variant3") +Option!WitString fListInVariant3(in ListInVariant3 a) { + assert(a.unwrap() == "input3"); + return some("output3".witList).witClone; +} + +@witExport("test:flavorful/to-test", "errno-result") +Result!(void, MyErrno) errnoResult() { + static bool first = true; + + if (first) { + first = false; + return MyErrno.b.err!void; + } else { + return ok!MyErrno; + } +} + + +@witExport("test:flavorful/to-test", "list-typedefs") +Tuple!(ListTypedef2, ListTypedef3) listTypedefs(in ListTypedef a, in ListTypedef3 b) { + assert(a == "typedef1"); + assert(b.length == 1); + assert(b[0] == "typedef2"); + + WitString[1] strings = [ + cast(WitString)"typedef4".witList + ]; + + return tuple( + (cast(immutable ubyte[])"typedef3").witList, + strings[].witList + ).witClone; +} + + + +@witExport("test:flavorful/to-test", "list-of-variants") +Tuple!(WitList!bool, WitList!(Result!()), WitList!MyErrno) listOfVariants(in WitList!bool bools, in WitList!(Result!()) results, in WitList!MyErrno enums) { + static immutable bool[] boolsCmp = [true, false]; + assert(bools == boolsCmp[]); + + static immutable Result!()[] resultsCmp = [ok!void, err!void]; + assert(results == resultsCmp[]); + + static immutable MyErrno[] enumsCmp = [MyErrno.success, MyErrno.a]; + assert(enums == enumsCmp[]); + + static immutable bool[] boolsOut = [false, true]; + static immutable Result!(void)[] resultsOut = [err!void, ok!void]; + static immutable MyErrno[] enumsOut = [MyErrno.a, MyErrno.b]; + return tuple( + boolsOut.witList, + resultsOut.witList, + enumsOut.witList + ).witClone; +} + +alias Exports = wit.test.flavorful.test.Exports!( + fListInRecord1, + fListInRecord2, + fListInRecord3, + fListInRecord4, + fListInVariant1, + fListInVariant2, + fListInVariant3, + errnoResult, + listTypedefs, + listOfVariants +); diff --git a/tests/runtime/gated-features/runner.d b/tests/runtime/gated-features/runner.d new file mode 100644 index 000000000..14c95ca9f --- /dev/null +++ b/tests/runtime/gated-features/runner.d @@ -0,0 +1,14 @@ +//@ args = '--features y' + +import wit.foo.bar.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + y(); + z(); +} + +alias Exports = wit.foo.bar.runner.Exports!( + run +); diff --git a/tests/runtime/gated-features/test.d b/tests/runtime/gated-features/test.d new file mode 100644 index 000000000..39a1c7eff --- /dev/null +++ b/tests/runtime/gated-features/test.d @@ -0,0 +1,15 @@ +//@ args = '--features y' + +import wit.foo.bar.test; +import wit.common; + +@witExport("foo:bar/bindings@1.2.3", "y") +void y() {} + +@witExport("foo:bar/bindings@1.2.3", "z") +void z() {} + +alias Exports = wit.foo.bar.test.Exports!( + y, + z +); diff --git a/tests/runtime/list-in-variant/runner.d b/tests/runtime/list-in-variant/runner.d new file mode 100644 index 000000000..34e85cec6 --- /dev/null +++ b/tests/runtime/list-in-variant/runner.d @@ -0,0 +1,88 @@ +import wit.test.list_in_variant.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + const WitString[2] hw = ["hello".witList, "world".witList]; + { + auto result = listInOption(hw[].witList.some); + scope(exit) result.witFree; + + assert(result == "hello,world"); + } + { + auto result = listInOption(none!(WitList!WitString)); + scope(exit) result.witFree; + + assert(result == "none"); + } + + const WitString[3] fbb_data = ["foo".witList, "bar".witList, "baz".witList]; + auto fbb = PayloadOrEmpty.withData(fbb_data.witList); + { + auto result = listInVariant(fbb); + scope(exit) result.witFree; + + assert(result == "foo,bar,baz"); + } + { + auto result = listInVariant(PayloadOrEmpty.empty); + scope(exit) result.witFree; + + assert(result == "empty"); + } + + const WitString[3] abc = ["a".witList, "b".witList, "c".witList]; + { + auto result = listInResult(abc[].witList.ok!WitString); + scope(exit) result.witFree; + + assert(result == "a,b,c"); + } + { + auto result = listInResult("oops".witList.err!(WitList!WitString)); + scope(exit) result.witFree; + + assert(result == "err:oops"); + } + + const WitString[2] hw2 = ["hello".witList, "world".witList]; + auto s1 = listInOptionWithReturn(hw2.witList.some); + { + auto result = s1.count; + scope(exit) result.witFree; + + assert(result == 2); + } + { + auto result = s1.label; + scope(exit) result.witFree; + + assert(result == "hello,world"); + } + auto s2 = listInOptionWithReturn(none!(WitList!WitString)); + { + auto result = s2.count; + scope(exit) result.witFree; + + assert(result == 0); + } + { + auto result = s2.label; + scope(exit) result.witFree; + + assert(result == "none"); + } + + const WitString[3] xyz = ["x".witList, "y".witList, "z".witList]; + { + auto result = topLevelList(xyz.witList); + scope(exit) result.witFree; + + assert(result == "x,y,z"); + } +} + +alias Exports = wit.test.list_in_variant.runner.Exports!( + run +); diff --git a/tests/runtime/list-in-variant/test.d b/tests/runtime/list-in-variant/test.d new file mode 100644 index 000000000..c1946a57c --- /dev/null +++ b/tests/runtime/list-in-variant/test.d @@ -0,0 +1,94 @@ +import wit.test.list_in_variant.test; +import wit.common; + +// Allocates directly with `malloc`, so no witClone needed. +extern(C) void* malloc(size_t size); + +char[] commaJoin(in WitString[] strs) { + if (strs.length == 0) return null; + + size_t total = 0; + foreach (i, str; strs) { + total += str.length; + + if (i+1 != strs.length) { + total += 1; // comma + } + } + + void* ptr = malloc(total); + assert(ptr); + char[] chars = cast(char[])ptr[0..total]; + + size_t cursor = 0; + foreach (i, str; strs) { + foreach (chr; str) { + chars[cursor++] = chr; + } + + if (i+1 != strs.length) { + chars[cursor++] = ','; + } + } + + return chars; +} + +@witExport("test:list-in-variant/to-test", "list-in-option") +WitString listInOption(in Option!(WitList!WitString) data) { + if (data.isSome) { + return data.unwrap.commaJoin.witList; // no clone + } + return "none".witList.witClone; +} + +@witExport("test:list-in-variant/to-test", "list-in-variant") +WitString listInVariant(in PayloadOrEmpty data) { + if (data.isWithData) { + return data.getWithData.commaJoin.witList; // no clone + } + return "empty".witList.witClone; +} + +@witExport("test:list-in-variant/to-test", "list-in-result") +WitString listInResult(in Result!(WitList!WitString, WitString) data) { + if (data.isOk) { + return data.unwrap.commaJoin.witList; + } + + + auto errStr = data.unwrapErr; + void* ptr = malloc(errStr.length+4); + assert(ptr); + char[] chars = cast(char[])ptr[0..errStr.length+4]; + + chars[0..4] = "err:"; + foreach (i, ref chr; chars[4..$]) { + chr = errStr[i]; + } + + return chars.witList; // no clone +} + +@witExport("test:list-in-variant/to-test", "list-in-option-with-return") +Summary listInOptionWithReturn(in Option!(WitList!WitString) data) { + if (data.isSome) { + auto items = data.unwrap(); + return Summary(items.length, items.commaJoin.witList); // no clone + } + + return Summary(0, "none".witList.witClone); +} + +@witExport("test:list-in-variant/to-test", "top-level-list") +WitString topLevelList(in WitList!WitString data) { + return data.commaJoin.witList; // no clone +} + +alias Exports = wit.test.list_in_variant.test.Exports!( + listInOption, + listInVariant, + listInResult, + listInOptionWithReturn, + topLevelList +); diff --git a/tests/runtime/lists-alias/runner.d b/tests/runtime/lists-alias/runner.d new file mode 100644 index 000000000..282c8fe2b --- /dev/null +++ b/tests/runtime/lists-alias/runner.d @@ -0,0 +1,16 @@ +import wit.my.lists.runner; +import cat = wit.my.lists.runner.imports.cat; +import wit.common; + +@witExport("$root", "run") +void run() { + cat.foo((cast(immutable ubyte[])"hello").witList); + + WitList!ubyte t = cat.bar(); + scope(exit) t.witFree; + assert(t == (cast(immutable ubyte[])"world").witList); +} + +alias Exports = wit.my.lists.runner.Exports!( + run +); diff --git a/tests/runtime/lists-alias/test.d b/tests/runtime/lists-alias/test.d new file mode 100644 index 000000000..f0ebf6f18 --- /dev/null +++ b/tests/runtime/lists-alias/test.d @@ -0,0 +1,17 @@ +import wit.my.lists.test; +import wit.common; + +@witExport("cat", "foo") +void foo(in WitList!ubyte x) { + assert(x == (cast(immutable ubyte[])"hello").witList); +} + +@witExport("cat", "bar") +WitList!ubyte bar() { + return (cast(immutable ubyte[])"world").witList.witClone; +} + +alias Exports = wit.my.lists.test.Exports!( + foo, + bar +); diff --git a/tests/runtime/lists/runner.d b/tests/runtime/lists/runner.d new file mode 100644 index 000000000..d252b9ee5 --- /dev/null +++ b/tests/runtime/lists/runner.d @@ -0,0 +1,422 @@ +import wit.test.lists.runner; +import wit.common; + +extern extern(C) size_t walloc_allocated_bytes; + +extern(C) void* malloc(size_t size); +extern(C) void free(void* ptr); + +@witExport("$root", "run") +void run() { + auto allocedAtFuncStart = walloc_allocated_bytes; + auto allocedAtFuncStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtFuncStart + && allocatedBytes == allocedAtFuncStart2 + ); + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + emptyListParam(WitList!ubyte()); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + emptyStringParam("".witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + assert(!emptyListResult().length); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + assert(!emptyStringResult().length); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[4] inputs = [1, 2, 3, 4]; + listParam(inputs.witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + listParam2("foo".witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + immutable WitString[3] inputs = ["foo".witList, "bar".witList, "baz".witList]; + listParam3(inputs.witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + immutable WitString[2] inputs = ["foo".witList, "bar".witList]; + immutable WitString[1] inputs2 = ["baz".witList]; + + immutable WitList!WitString[2] inputs3 = [inputs.witList, inputs2.witList]; + listParam4(inputs3.witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + immutable Tuple!(ubyte, uint, ubyte)[2] inputs = [ + tuple(ubyte(1), uint(2), ubyte(3)), + tuple(ubyte(4), uint(5), ubyte(6)) + ]; + + listParam5(inputs.witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + enum len = 1000; + auto ptr = cast(WitString*)malloc(WitString.sizeof*len); + assert(ptr); + scope(exit) free(ptr); + + foreach (ref str; ptr[0..len]) { + str = cast()"string".witList; + } + + listParamLarge(ptr[0..len].witList); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + auto result = listResult(); + scope(exit) result.witFree; + + static immutable ubyte[5] outputs = [1, 2, 3, 4, 5]; + assert(result == outputs); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + auto result = listResult2(); + scope(exit) result.witFree; + + assert(result == "hello!"); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + auto result = listResult3(); + scope(exit) result.witFree; + + immutable WitString[2] outputs = ["hello,".witList, "world!".witList]; + assert(result == outputs); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[0] inputs = []; + + auto result = listRoundtrip(inputs.witList); + scope(exit) result.witFree; + + assert(result == inputs); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[1] inputs = ['x']; + + auto result = listRoundtrip(inputs.witList); + scope(exit) result.witFree; + + assert(result == inputs); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[5] inputs = ['h', 'e', 'l', 'l', 'o']; + + auto result = listRoundtrip(inputs.witList); + scope(exit) result.witFree; + + assert(result == inputs); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable string input = "x"; + + auto result = stringRoundtrip(input.witList); + scope(exit) result.witFree; + + assert(result == input); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable string input = ""; + + auto result = stringRoundtrip(input.witList); + scope(exit) result.witFree; + + assert(result == input); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable string input = "hello"; + + auto result = stringRoundtrip(input.witList); + scope(exit) result.witFree; + + assert(result == input); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable string input = "hello âš‘ world"; + + auto result = stringRoundtrip(input.witList); + scope(exit) result.witFree; + + assert(result == input); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[2] inputs1 = [ubyte.min, ubyte.max]; + static immutable byte[2] inputs2 = [byte.min, byte.max]; + + auto result = listMinmax8(inputs1.witList, inputs2.witList); + scope(exit) result.witFree; + + assert(result[0] == inputs1); + assert(result[1] == inputs2); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ushort[2] inputs1 = [ushort.min, ushort.max]; + static immutable short[2] inputs2 = [short.min, short.max]; + + auto result = listMinmax16(inputs1.witList, inputs2.witList); + scope(exit) result.witFree; + + assert(result[0] == inputs1); + assert(result[1] == inputs2); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable uint[2] inputs1 = [uint.min, uint.max]; + static immutable int[2] inputs2 = [int.min, int.max]; + + auto result = listMinmax32(inputs1.witList, inputs2.witList); + scope(exit) result.witFree; + + assert(result[0] == inputs1); + assert(result[1] == inputs2); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ulong[2] inputs1 = [ulong.min, ulong.max]; + static immutable long[2] inputs2 = [long.min, long.max]; + + auto result = listMinmax64(inputs1.witList, inputs2.witList); + scope(exit) result.witFree; + + assert(result[0] == inputs1); + assert(result[1] == inputs2); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable float[2] inputs1 = [-float.infinity, float.infinity]; + static immutable double[2] inputs2 = [-double.infinity, double.infinity]; + + auto result = listMinmaxFloat(inputs1.witList, inputs2.witList); + scope(exit) result.witFree; + + assert(result[0] == inputs1); + assert(result[1] == inputs2); + } + + { + auto allocedAtScopeStart = walloc_allocated_bytes; + auto allocedAtScopeStart2 = allocatedBytes; + scope(exit) assert( + walloc_allocated_bytes == allocedAtScopeStart + && allocatedBytes == allocedAtScopeStart2 + ); + + static immutable ubyte[10] textPlain = ['t', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n']; + static immutable ubyte[9] notFound = ['N', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd']; + + immutable Tuple!(WitString, WitList!ubyte)[2] headers = [ + tuple("Content-Type".witList, textPlain.witList), + tuple("Content-Length".witList, notFound.witList) + ]; + + auto result = wasiHttpHeadersRoundtrip(headers.witList); + scope(exit) result.witFree; + + assert(result[0][0] == "Content-Type"); + assert(result[0][1] == textPlain); + assert(result[1][0] == "Content-Length"); + assert(result[1][1] == notFound); + } +} + +alias Exports = wit.test.lists.runner.Exports!( + run +); diff --git a/tests/runtime/lists/test.d b/tests/runtime/lists/test.d new file mode 100644 index 000000000..a7a788106 --- /dev/null +++ b/tests/runtime/lists/test.d @@ -0,0 +1,122 @@ +import wit.test.lists.test; +import wit.common; + +@witExport("test:lists/to-test", "empty-list-param") +void emptyListParam(in WitList!ubyte a) { +} + +@witExport("test:lists/to-test", "empty-string-param") +void emptyStringParam(in WitString a) { +} + +@witExport("test:lists/to-test", "empty-list-result") +WitList!ubyte emptyListResult() { + return WitList!ubyte(); +} + +@witExport("test:lists/to-test", "empty-string-result") +WitString emptyStringResult() { + return WitString(); +} + +@witExport("test:lists/to-test", "list-param") +void listParam(in WitList!ubyte a) { +} + +@witExport("test:lists/to-test", "list-param2") +void listParam2(in WitString a) { +} + +@witExport("test:lists/to-test", "list-param3") +void listParam3(in WitList!WitString a) { +} + +@witExport("test:lists/to-test", "list-param4") +void listParam4(in WitList!(WitList!WitString) a) { +} + +@witExport("test:lists/to-test", "list-param5") +void listParam5(in WitList!(Tuple!(ubyte, uint, ubyte)) a) { +} + +@witExport("test:lists/to-test", "list-param-large") +void listParamLarge(in WitList!WitString a) { +} + +@witExport("test:lists/to-test", "list-result") +WitList!ubyte listResult() { + immutable ubyte[5] outputs = [1, 2, 3, 4, 5]; + return outputs.witList.witClone; +} + +@witExport("test:lists/to-test", "list-result2") +WitString listResult2() { + return "hello!".witList.witClone; +} + +@witExport("test:lists/to-test", "list-result3") +WitList!WitString listResult3() { + immutable WitString[2] outputs = ["hello,".witList, "world!".witList]; + return outputs.witList.witClone; +} + +template listMinmax(T, U, string suffix) { + @witExport("test:lists/to-test", "list-minmax"~suffix) + Tuple!(WitList!T, WitList!U) listMinmax(in WitList!T a, in WitList!U b) { + return tuple(a, b).witClone; + } +} + +@witExport("test:lists/to-test", "list-roundtrip") +WitList!ubyte listRoundtrip(in WitList!ubyte a) { + return a.witClone; +} + +@witExport("test:lists/to-test", "string-roundtrip") +WitString stringRoundtrip(in WitString a) { + return a.witClone; +} + +@witExport("test:lists/to-test", "wasi-http-headers-roundtrip") +WitList!(Tuple!(WitString, WitList!ubyte)) wasiHttpHeadersRoundtrip(in WitList!(Tuple!(WitString, WitList!ubyte)) a) { + return a.witClone; +} + + +extern extern(C) size_t walloc_allocated_bytes; +@witExport("test:lists/to-test", "allocated-bytes") +size_t allocatedBytes() { + return walloc_allocated_bytes; +} + + +alias Exports = wit.test.lists.test.Exports!( + emptyListParam, + emptyStringParam, + emptyListResult, + emptyStringResult, + + listParam, + listParam2, + listParam3, + listParam4, + listParam5, + listParamLarge, + listResult, + listResult2, + listResult3, + + listMinmax!(ubyte, byte, "8"), + listMinmax!(ushort, short, "16"), + listMinmax!(uint, int, "32"), + listMinmax!(ulong, long, "64"), + listMinmax!(float, double, "-float"), + + listRoundtrip, + + stringRoundtrip, + + wasiHttpHeadersRoundtrip, + + allocatedBytes +); diff --git a/tests/runtime/many-arguments/runner.d b/tests/runtime/many-arguments/runner.d new file mode 100644 index 000000000..0cc1df1a8 --- /dev/null +++ b/tests/runtime/many-arguments/runner.d @@ -0,0 +1,11 @@ +import wit.test.many_arguments.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + manyArguments(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); +} + +alias Exports = wit.test.many_arguments.runner.Exports!( + run +); diff --git a/tests/runtime/many-arguments/test.d b/tests/runtime/many-arguments/test.d new file mode 100644 index 000000000..053122ece --- /dev/null +++ b/tests/runtime/many-arguments/test.d @@ -0,0 +1,16 @@ +import wit.test.many_arguments.test; +import wit.common; + +import std.meta : Repeat, AliasSeq; + +@witExport("test:many-arguments/to-test", "many-arguments") +void manyArguments(Repeat!(16, ulong) args) { + assert(args == AliasSeq!( + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16 + )); +} + +alias Exports = wit.test.many_arguments.test.Exports!( + manyArguments +); diff --git a/tests/runtime/numbers/runner.d b/tests/runtime/numbers/runner.d new file mode 100644 index 000000000..336856e31 --- /dev/null +++ b/tests/runtime/numbers/runner.d @@ -0,0 +1,50 @@ +import wit.test.numbers.runner; +import wit.common; + +void doAsserts(alias func)() { + static if(is(typeof(func) P == function)) { + alias T = P[0]; + static if (is(T == dchar)) { + enum T a = 'a'; + enum T b = ' '; + enum T c = '🚩'; + } else static if (__traits(isFloating, T)) { + enum T a = 1.0; + enum T b = -T.infinity; + enum T c = T.infinity; + } else { + enum T a = 1; + enum T b = T.min; + enum T c = T.max; + } + } + + assert(func(a) == a); + assert(func(b) == b); + assert(func(c) == c); +} + +@witExport("$root", "run") +void run() { + doAsserts!roundtripU8; + doAsserts!roundtripS8; + doAsserts!roundtripU16; + doAsserts!roundtripS16; + doAsserts!roundtripU32; + doAsserts!roundtripS32; + doAsserts!roundtripU64; + doAsserts!roundtripS64; + doAsserts!roundtripF32; + doAsserts!roundtripF64; + doAsserts!roundtripChar; + + setScalar(2); + assert(getScalar() == 2); + + setScalar(4); + assert(getScalar() == 4); +} + +alias Exports = wit.test.numbers.runner.Exports!( + run +); diff --git a/tests/runtime/numbers/test.d b/tests/runtime/numbers/test.d new file mode 100644 index 000000000..4f4b27d88 --- /dev/null +++ b/tests/runtime/numbers/test.d @@ -0,0 +1,32 @@ +import wit.test.numbers.test; +import wit.common; + +template roundtrip(T, string suffix) { + @witExport("test:numbers/numbers", "roundtrip-"~suffix) + T roundtrip(T val) => val; +} + +uint scalar; + +@witExport("test:numbers/numbers", "get-scalar") +auto getScalar() => scalar; + +@witExport("test:numbers/numbers", "set-scalar") +void setScalar(uint val) { scalar = val; } + +alias Exports = wit.test.numbers.test.Exports!( + roundtrip!(ubyte, "u8"), + roundtrip!(byte, "s8"), + roundtrip!(ushort, "u16"), + roundtrip!(short, "s16"), + roundtrip!(uint, "u32"), + roundtrip!(int, "s32"), + roundtrip!(ulong, "u64"), + roundtrip!(long, "s64"), + roundtrip!(float, "f32"), + roundtrip!(double, "f64"), + roundtrip!(dchar, "char"), + + getScalar, + setScalar +); diff --git a/tests/runtime/options/runner.d b/tests/runtime/options/runner.d new file mode 100644 index 000000000..c1eb9c0d7 --- /dev/null +++ b/tests/runtime/options/runner.d @@ -0,0 +1,43 @@ +import wit.test.options.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + optionNoneParam(none!WitString); + optionSomeParam("foo".witList.some); + assert(optionNoneResult().isNone); + { + auto result = optionSomeResult(); + scope(exit) result.witFree; + + assert(result == "foo".witList.some); + } + { + auto result = optionRoundtrip("foo".witList.some); + scope(exit) result.witFree; + + assert(result == "foo".witList.some); + } + { + auto result = doubleOptionRoundtrip(uint(42).some.some); + scope(exit) result.witFree; + + assert(result == uint(42).some.some); + } + { + auto result = doubleOptionRoundtrip(none!uint.some); + scope(exit) result.witFree; + + assert(result == none!uint.some); + } + { + auto result = doubleOptionRoundtrip(none!(Option!uint)); + scope(exit) result.witFree; + + assert(result == none!(Option!uint)); + } +} + +alias Exports = wit.test.options.runner.Exports!( + run +); diff --git a/tests/runtime/options/test.d b/tests/runtime/options/test.d new file mode 100644 index 000000000..eba2ffd81 --- /dev/null +++ b/tests/runtime/options/test.d @@ -0,0 +1,42 @@ +import wit.test.options.test; +import wit.common; + +import std.meta : Repeat, AliasSeq; + +@witExport("test:options/to-test", "option-none-param") +void optionNoneParam(in Option!WitString a) { +} + +@witExport("test:options/to-test", "option-some-param") +void optionSomeParam(in Option!WitString a) { +} + +@witExport("test:options/to-test", "option-none-result") +Option!WitString optionNoneResult() { + return none!WitString; +} + +@witExport("test:options/to-test", "option-some-result") +Option!WitString optionSomeResult() { + return "foo".witList.witClone.some; +} + +@witExport("test:options/to-test", "option-roundtrip") +Option!WitString optionRoundtrip(in Option!WitString a) { + return a.witClone; +} +@witExport("test:options/to-test", "double-option-roundtrip") +Option!(Option!uint) doubleOptionRoundtrip(in Option!(Option!uint) a) { + return a.witClone; +} + + +alias Exports = wit.test.options.test.Exports!( + optionNoneParam, + optionSomeParam, + optionNoneResult, + optionSomeResult, + + optionRoundtrip, + doubleOptionRoundtrip +); diff --git a/tests/runtime/package-with-version/runner.d b/tests/runtime/package-with-version/runner.d new file mode 100644 index 000000000..d11db58d3 --- /dev/null +++ b/tests/runtime/package-with-version/runner.d @@ -0,0 +1,11 @@ +import wit.my.inline.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + Bar.makeNew().witDrop; +} + +alias Exports = wit.my.inline.runner.Exports!( + run +); diff --git a/tests/runtime/package-with-version/test.d b/tests/runtime/package-with-version/test.d new file mode 100644 index 000000000..4d50ac49c --- /dev/null +++ b/tests/runtime/package-with-version/test.d @@ -0,0 +1,17 @@ +import wit.my.inline.test; +import wit.common; + +import std.meta : Repeat, AliasSeq; + +@witExport("my:inline/foo@0.0.0", "bar") +struct BarImpl { + @witExport("my:inline/foo@0.0.0", "[constructor]bar") + static Bar constructor() { + return Bar.makeNew((out typeof(this) self) { + }); + } +} + +alias Exports = wit.my.inline.test.Exports!( + BarImpl +); diff --git a/tests/runtime/records/runner.d b/tests/runtime/records/runner.d new file mode 100644 index 000000000..d1476e0d5 --- /dev/null +++ b/tests/runtime/records/runner.d @@ -0,0 +1,47 @@ +import wit.test.records.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + assert(multipleResults() == tuple(ubyte(4), ushort(5))); + + assert(swapTuple(tuple(ubyte(1), uint(2))) == tuple(uint(2), ubyte(1))); + assert(roundtripFlags1(F1.a) == F1.a); + assert(roundtripFlags1(F1()) == F1()); + assert(roundtripFlags1(F1.b) == F1.b); + assert(roundtripFlags1(F1.a | F1.b) == (F1.a | F1.b)); + + assert(roundtripFlags2(F2.c) == F2.c); + assert(roundtripFlags2(F2()) == F2()); + assert(roundtripFlags2(F2.d) == F2.d); + assert(roundtripFlags2(F2.c | F2.e) == (F2.c | F2.e)); + + assert( + roundtripFlags3(Flag8.b0, Flag16.b1, Flag32.b2) == + tuple(Flag8.b0, Flag16.b1, Flag32.b2) + ); + + { + auto r = roundtripRecord1(R1( + a: 8, + b: F1() + )); + assert(r.a == 8); + assert(r.b == F1()); + } + + { + auto r = roundtripRecord1(R1( + a: 0, + b: F1.a | F1.b + )); + assert(r.a == 0); + assert(r.b == (F1.a | F1.b)); + } + + assert(tuple1(tuple(ubyte(1))) == tuple(1)); +} + +alias Exports = wit.test.records.runner.Exports!( + run +); diff --git a/tests/runtime/records/test.d b/tests/runtime/records/test.d new file mode 100644 index 000000000..474ff070a --- /dev/null +++ b/tests/runtime/records/test.d @@ -0,0 +1,50 @@ +import wit.test.records.test; +import wit.common; + +import std.meta : Repeat, AliasSeq; + +@witExport("test:records/to-test", "multiple-results") +Tuple!(ubyte, ushort) multipleResults() { + return tuple(ubyte(4), ushort(5)); +} + +@witExport("test:records/to-test", "swap-tuple") +Tuple!(uint, ubyte) swapTuple(in Tuple!(ubyte, uint) a) { + return tuple(a[1], a[0]); +} + +@witExport("test:records/to-test", "roundtrip-flags1") +F1 roundtripFlags1(F1 a) { + return a; +} + +@witExport("test:records/to-test", "roundtrip-flags2") +F2 roundtripFlags2(F2 a) { + return a; +} + +@witExport("test:records/to-test", "roundtrip-flags3") +Tuple!(Flag8, Flag16, Flag32) roundtripFlags3(Flag8 a, Flag16 b, Flag32 c) { + return tuple(a, b, c); +} + +@witExport("test:records/to-test", "roundtrip-record1") +R1 roundtripRecord1(in R1 a) { + return a; +} + +@witExport("test:records/to-test", "tuple1") +Tuple!(ubyte) tuple1(in Tuple!(ubyte) a) { + return tuple(a[0]); +} + + +alias Exports = wit.test.records.test.Exports!( + multipleResults, + swapTuple, + roundtripFlags1, + roundtripFlags2, + roundtripFlags3, + roundtripRecord1, + tuple1 +); diff --git a/tests/runtime/resource-borrow/runner.d b/tests/runtime/resource-borrow/runner.d new file mode 100644 index 000000000..e68553c70 --- /dev/null +++ b/tests/runtime/resource-borrow/runner.d @@ -0,0 +1,14 @@ +import wit.test.resource_borrow.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + auto thing = Thing.makeNew(42); + scope(exit) thing.witDrop; + + assert(foo(thing) == 42 + 1 + 2); +} + +alias Exports = wit.test.resource_borrow.runner.Exports!( + run +); diff --git a/tests/runtime/resource-borrow/test.d b/tests/runtime/resource-borrow/test.d new file mode 100644 index 000000000..73e14f2dc --- /dev/null +++ b/tests/runtime/resource-borrow/test.d @@ -0,0 +1,24 @@ +import wit.test.resource_borrow.test; +import wit.common; + +@witExport("test:resource-borrow/to-test", "thing") +struct ThingImpl { + uint val; + + @witExport("test:resource-borrow/to-test", "[constructor]thing") + static Thing constructor(uint v) { + return Thing.makeNew((out typeof(this) self) { + self.val = v + 1; + }); + } +} + +@witExport("test:resource-borrow/to-test", "foo") +uint foo(Thing.Borrow v) { + return v.rep!ThingImpl.val + 2; +} + +alias Exports = wit.test.resource_borrow.test.Exports!( + ThingImpl, + foo +); diff --git a/tests/runtime/resource-import-and-export/intermediate.d b/tests/runtime/resource-import-and-export/intermediate.d new file mode 100644 index 000000000..649157f9b --- /dev/null +++ b/tests/runtime/resource-import-and-export/intermediate.d @@ -0,0 +1,61 @@ +import wit.test.resource_import_and_export.intermediate; +import wit.test.resource_import_and_export.test.imports : ThingImport = Thing; +import wit.test.resource_import_and_export.test.exports : ThingExport = Thing; + +import wit.common; + +@witExport("test:resource-import-and-export/test", "thing") +struct ThingImpl { + ThingImport thing; + + @witExport("test:resource-import-and-export/test", "[constructor]thing") + static ThingExport constructor(uint v) { + return ThingExport.makeNew((out typeof(this) self) { + self.thing = ThingImport.makeNew(v + 1); + }); + } + + @witExport("test:resource-import-and-export/test", "[method]thing.foo") + uint foo() { + return thing.foo + 2; + } + + @witExport("test:resource-import-and-export/test", "[method]thing.bar") + void bar(uint v) { + thing.bar(v + 3); + } + + @witExport("test:resource-import-and-export/test", "[static]thing.baz") + static ThingExport baz(ThingExport a, ThingExport b) { + scope(exit) { + a.witDrop; + b.witDrop; + } + + auto aRep = a.rep!ThingImpl; + auto bRep = b.rep!ThingImpl; + + auto result = ThingImport.baz( + aRep.thing, + bRep.thing + ); + aRep.thing = Thing.init; // consumed by `baz` + bRep.thing = Thing.init; // consumed by `baz` + scope(exit) result.witDrop; + + return ThingImpl.constructor(result.foo + 4); + } +} + +@witExport("$root", "toplevel-export") +ThingImport toplevelExport(ThingImport input) { + // `input` not dropped b/c ownership transferred + // to `toplevelImport` + + return toplevelImport(input); +} + +alias Exports = wit.test.resource_import_and_export.intermediate.Exports!( + ThingImpl, + toplevelExport +); diff --git a/tests/runtime/resource-import-and-export/leaf-thing.d b/tests/runtime/resource-import-and-export/leaf-thing.d new file mode 100644 index 000000000..15c5fce1f --- /dev/null +++ b/tests/runtime/resource-import-and-export/leaf-thing.d @@ -0,0 +1,38 @@ +module leaf_thing; + +import wit.test.resource_import_and_export.leaf_thing; + +import wit.common; + +@witExport("test:resource-import-and-export/test", "thing") +struct ThingImpl { + uint val; + + @witExport("test:resource-import-and-export/test", "[constructor]thing") + static Thing constructor(uint v) { + return Thing.makeNew((out typeof(this) self) { + self.val = v + 1; + }); + } + + @witExport("test:resource-import-and-export/test", "[method]thing.foo") + uint foo() { + return val + 2; + } + + @witExport("test:resource-import-and-export/test", "[method]thing.bar") + void bar(uint v) { + val = v + 3; + } + + @witExport("test:resource-import-and-export/test", "[static]thing.baz") + static Thing baz(Thing a, Thing b) { + return ThingImpl.constructor( + a.rep!ThingImpl.foo + b.rep!ThingImpl.foo + 4 + ); + } +} + +alias Exports = wit.test.resource_import_and_export.leaf_thing.Exports!( + ThingImpl +); diff --git a/tests/runtime/resource-import-and-export/leaf-toplevel.d b/tests/runtime/resource-import-and-export/leaf-toplevel.d new file mode 100644 index 000000000..fc10e773b --- /dev/null +++ b/tests/runtime/resource-import-and-export/leaf-toplevel.d @@ -0,0 +1,17 @@ +module leaf_toplevel; + +import wit.test.resource_import_and_export.leaf_toplevel; + +import wit.common; + +@witExport("$root", "toplevel-export") +Thing toplevelExport(Thing input) { + // `input` not dropped b/c ownership transferred + // via return + + return input; +} + +alias Exports = wit.test.resource_import_and_export.leaf_toplevel.Exports!( + toplevelExport +); diff --git a/tests/runtime/resource-import-and-export/runner.d b/tests/runtime/resource-import-and-export/runner.d new file mode 100644 index 000000000..a8b13dccd --- /dev/null +++ b/tests/runtime/resource-import-and-export/runner.d @@ -0,0 +1,27 @@ +import wit.test.resource_import_and_export.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + auto thing1 = Thing.makeNew(42); + scope(exit) thing1.witDrop; + + // 42 + 1 (constructor) + 1 (constructor) + 2 (foo) + 2 (foo) + assert(thing1.foo == 48); + + // 33 + 3 (bar) + 3 (bar) + 2 (foo) + 2 (foo) + thing1.bar(33); + assert(thing1.foo() == 43); + + auto thing2 = Thing.makeNew(81); + scope(exit) thing2.witDrop; + + auto thing3 = Thing.baz(thing1, thing2); + thing1 = Thing.init; // thing1 consumed by `baz` + thing2 = Thing.init; // thing2 consumed by `baz` + scope(exit) thing3.witDrop; +} + +alias Exports = wit.test.resource_import_and_export.runner.Exports!( + run +); diff --git a/tests/runtime/resource_aggregates/runner.d b/tests/runtime/resource_aggregates/runner.d new file mode 100644 index 000000000..8d4282b8c --- /dev/null +++ b/tests/runtime/resource_aggregates/runner.d @@ -0,0 +1,65 @@ +import wit.test.resource_aggregates.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + auto r2Thing = Thing.makeNew(1); + scope(exit) r2Thing.witDrop; + + auto r3Thing1 = Thing.makeNew(2); + scope(exit) r3Thing1.witDrop; + + auto t2Thing = Thing.makeNew(6); + scope(exit) t2Thing.witDrop; + + auto v2Thing = Thing.makeNew(8); + scope(exit) v2Thing.witDrop; + + auto l2Thing1 = Thing.makeNew(11); + scope(exit) l2Thing1.witDrop; + + auto l2Thing2 = Thing.makeNew(12); + scope(exit) l2Thing2.witDrop; + + auto o2Thing = Thing.makeNew(14); + scope(exit) o2Thing.witDrop; + + auto result2Thing = Thing.makeNew(16); + scope(exit) result2Thing.witDrop; + + immutable Thing[2] l1Elems = [ + Thing.makeNew(9), + Thing.makeNew(10), + ]; + + immutable Thing.Borrow[2] l2Elems = [ + l2Thing1, + l2Thing2, + ]; + + assert(foo( + R1(thing: Thing.makeNew(0)), + R2(thing: r2Thing), + R3( + thing1: r3Thing1, + thing2: Thing.makeNew(3) + ), + tuple( + Thing.makeNew(4), + R1(thing: Thing.makeNew(5)) + ), + tuple(t2Thing.borrow), + V1.thing(Thing.makeNew(7)), + V2.thing(v2Thing), + l1Elems.witList, + l2Elems.witList, + Thing.makeNew(13).some, + o2Thing.borrow.some, + Thing.makeNew(15).ok!void, + result2Thing.borrow.ok!void + ) == 156); +} + +alias Exports = wit.test.resource_aggregates.runner.Exports!( + run +); diff --git a/tests/runtime/resource_aggregates/test.d b/tests/runtime/resource_aggregates/test.d new file mode 100644 index 000000000..932e1ad77 --- /dev/null +++ b/tests/runtime/resource_aggregates/test.d @@ -0,0 +1,66 @@ +import wit.test.resource_aggregates.test; + +import wit.common; +import std.algorithm.iteration : sum, map; + +@witExport("test:resource-aggregates/to-test", "thing") +struct ThingImpl { + uint val; + + @witExport("test:resource-aggregates/to-test", "[constructor]thing") + static Thing constructor(uint v) { + return Thing.makeNew((out typeof(this) self) { + self.val = v + 1; + }); + } +} + +@witExport("test:resource-aggregates/to-test", "foo") +uint foo( + scope ref R1 r1, scope ref R2 r2, scope ref R3 r3, + scope ref T1 t1, scope ref T2 t2, + scope ref V1 v1, scope ref V2 v2, + scope ref L1 l1, scope ref L2 l2, + scope ref Option!Thing o1, scope ref Option!(Thing.Borrow) o2, + scope ref Result!(Thing, void) result1, scope ref Result!(Thing.Borrow, void) result2 +) { + scope(exit) { + r1.witDrop; + r2.witDrop; + r3.witDrop; + t1.witDrop; + t2.witDrop; + v1.witDrop; + v2.witDrop; + l1.witDrop; + l2.witDrop; + o1.witDrop; + o2.witDrop; + result1.witDrop; + result2.witDrop; + } + + return ( + r1.thing.rep!ThingImpl.val + + r2.thing.rep!ThingImpl.val + + r3.thing1.rep!ThingImpl.val + + r3.thing2.rep!ThingImpl.val + + t1[0].rep!ThingImpl.val + + t1[1].thing.rep!ThingImpl.val + + t2[0].rep!ThingImpl.val + + v1.getThing.rep!ThingImpl.val + + v2.getThing.rep!ThingImpl.val + + l1[].map!((a) => a.rep!ThingImpl.val).sum + + l2[].map!((a) => a.rep!ThingImpl.val).sum + + o1.unwrap.rep!ThingImpl.val + + o2.unwrap.rep!ThingImpl.val + + result1.unwrap.rep!ThingImpl.val + + result2.unwrap.rep!ThingImpl.val + + 3 + ); +} + +alias Exports = wit.test.resource_aggregates.test.Exports!( + ThingImpl, + foo +); diff --git a/tests/runtime/resource_alias/runner.d b/tests/runtime/resource_alias/runner.d new file mode 100644 index 000000000..420ff52c4 --- /dev/null +++ b/tests/runtime/resource_alias/runner.d @@ -0,0 +1,36 @@ +import wit.test.resource_alias.runner; + +import wit.test.resource_alias.e1.imports : a1 = a, Foo1 = Foo, X; +import wit.test.resource_alias.e2.imports : a2 = a, Foo2 = Foo; + +import wit.common; + +@witExport("$root", "run") +void run() { + auto fooE1 = Foo1( + x: X.makeNew(42) + ); + + // consumed later + + a1(fooE1); + + auto fooE2 = Foo2( + x: X.makeNew(7) + ); + // consumed later + + auto barE2 = Foo1( + x: X.makeNew(8) + ); + // consumed later + + auto y = X.makeNew(8); + scope(exit) y.witDrop; + + a2(fooE2, barE2, y); +} + +alias Exports = wit.test.resource_alias.runner.Exports!( + run +); diff --git a/tests/runtime/resource_alias/test.d b/tests/runtime/resource_alias/test.d new file mode 100644 index 000000000..7d42b10f0 --- /dev/null +++ b/tests/runtime/resource_alias/test.d @@ -0,0 +1,45 @@ +import wit.test.resource_alias.test; + +import wit.test.resource_alias.e1.exports : Foo1 = Foo, X; +import wit.test.resource_alias.e2.exports : Foo2 = Foo, Bar, Y; + +import wit.common; + +@witExport("test:resource-alias/e1", "x") +struct XImpl { + uint val; + + @witExport("test:resource-alias/e1", "[constructor]x") + static X constructor(uint v) { + return X.makeNew((out typeof(this) self) { + self.val = v; + }); + } +} + +@witExport("test:resource-alias/e1", "a") +WitList!X a1(ref scope Foo1 f) { + // `f.x` consumed by return + + immutable X[1] ret = [f.x]; + + return ret.witList.witClone; +} + +@witExport("test:resource-alias/e2", "a") +WitList!Y a2(ref scope Foo2 f, ref scope Bar g, Y.Borrow h) { + // `f.x` consumed by return + // `f.g` consumed by return + //scope(exit) h.witDrop; + + immutable X[2] ret = [f.x, g.x]; + + return ret.witList.witClone; +} + + +alias Exports = wit.test.resource_alias.test.Exports!( + XImpl, + a1, + a2 +); diff --git a/tests/runtime/resource_alias_redux/runner.d b/tests/runtime/resource_alias_redux/runner.d new file mode 100644 index 000000000..109fb8f5a --- /dev/null +++ b/tests/runtime/resource_alias_redux/runner.d @@ -0,0 +1,78 @@ +import wit.test.resource_alias_redux.runner; + +import wit.test.resource_alias_redux.runner.imports.the_test : Thing1 = Thing; +import wit.test.resource_alias_redux.resource_alias1.imports : Foo1 = Foo, Thing2 = Thing; +import wit.test.resource_alias_redux.resource_alias2.imports : Foo2 = Foo; + +import wit.common; + +@witExport("$root", "run") +void run() { + auto thing1 = Thing.makeNew("Ni Hao".witList); + scope(exit) thing1.witDrop; + + { + Thing1[1] things = [thing1]; + auto result = test(things.witList); + thing1 = Thing1.init; // consumed + scope(exit) result.witFree; + assert(result.length == 1); + + { + auto str = result[0].get; + scope(exit) str.witFree; + + assert(str == "Ni Hao GuestThing GuestThing.get"); + } + } + + + auto thing2 = Thing2.makeNew("Ciao".witList); + scope(exit) thing2.witDrop; + + { + auto result = a(Foo1(thing: thing2)); + thing2 = Thing2.init; // consumed + scope(exit) result.witFree; + assert(result.length == 1); + + { + auto str = result[0].get; + scope(exit) str.witFree; + + assert(str == "Ciao GuestThing GuestThing.get"); + } + } + + + auto thing3 = Thing2.makeNew("Ciao".witList); + scope(exit) thing3.witDrop; + auto thing4 = Thing2.makeNew("Aloha".witList); + scope(exit) thing4.witDrop; + + { + auto result = b(Foo2(thing: thing3), Bar(thing: thing4)); + thing3 = Thing2.init; // consumed + thing4 = Thing2.init; // consumed + scope(exit) result.witFree; + assert(result.length == 2); + + { + auto str = result[0].get; + scope(exit) str.witFree; + + assert(str == "Ciao GuestThing GuestThing.get"); + } + + { + auto str = result[1].get; + scope(exit) str.witFree; + + assert(str == "Aloha GuestThing GuestThing.get"); + } + } +} + +alias Exports = wit.test.resource_alias_redux.runner.Exports!( + run +); diff --git a/tests/runtime/resource_alias_redux/test.d b/tests/runtime/resource_alias_redux/test.d new file mode 100644 index 000000000..cb77b2dd8 --- /dev/null +++ b/tests/runtime/resource_alias_redux/test.d @@ -0,0 +1,75 @@ +import wit.test.resource_alias_redux.test; + +import wit.test.resource_alias_redux.test.exports.the_test : Thing1 = Thing; +import wit.test.resource_alias_redux.resource_alias1.exports : Foo1 = Foo, Thing2 = Thing; +import wit.test.resource_alias_redux.resource_alias2.exports : Foo2 = Foo; +import wit.common; + +extern(C) void* malloc(size_t size); +extern(C) void free(void* ptr); + +char[] concat(in char[] a, in char[] b) { + auto ptr = cast(char*)malloc(a.length + b.length); + assert((a.length + b.length) == 0 || ptr); + + if (!ptr) return null; + + ptr[0..a.length] = a[]; + ptr[a.length..a.length+b.length] = b[]; + + return ptr[0..a.length+b.length]; +} + +@witExport("test:resource-alias-redux/resource-alias1", "thing") +struct ThingImpl { + const(char)[] str; + + @witExport("test:resource-alias-redux/resource-alias1", "[constructor]thing") + static Thing1 constructor(in WitString msg) { + return Thing1.makeNew((out typeof(this) self) { + self.str = concat(msg, " GuestThing"); + }); + } + + @witExport("test:resource-alias-redux/resource-alias1", "[method]thing.get") + WitString get() { + return concat(str, " GuestThing.get").witList; // no clone; already on C heap + } +} + +@witExport("test:resource-alias-redux/resource-alias1", "a") +WitList!Thing1 a(scope ref Foo1 f) { + scope(exit) f.witDrop; + + Thing1[1] things = [f.thing]; + f.thing = Thing1.init; // consumed + + return things.witList.witClone; +} + +@witExport("test:resource-alias-redux/resource-alias2", "b") +WitList!Thing2 b(scope ref Foo2 f, scope ref Bar g) { + scope(exit) { + f.witDrop; + g.witDrop; + } + + Thing1[2] things = [f.thing, g.thing]; + f.thing = Thing2.init; // consumed + g.thing = Thing2.init; // consumed + + return things.witList.witClone; +} + +@witExport("the-test", "test") +WitList!Thing1 test(scope ref WitList!Thing1 things) { + return things.witClone; +} + + +alias Exports = wit.test.resource_alias_redux.test.Exports!( + ThingImpl, + a, + b, + test +); diff --git a/tests/runtime/resource_borrow_in_record/runner.d b/tests/runtime/resource_borrow_in_record/runner.d new file mode 100644 index 000000000..02f44b30e --- /dev/null +++ b/tests/runtime/resource_borrow_in_record/runner.d @@ -0,0 +1,31 @@ +import wit.test.resource_borrow_in_record.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + auto thing1 = Thing.makeNew("Bonjour".witList); + scope(exit) thing1.witDrop; + + auto thing2 = Thing.makeNew("mon cher".witList); + scope(exit) thing2.witDrop; + + Foo[2] things = [ + Foo(thing1), + Foo(thing2) + ]; + + auto result = test(things.witList); + scope(exit) { + result.witDrop; + result.witFree; + } + + assert(result.length == 2); + assert(result[0].get == "Bonjour new test get"); + assert(result[1].get == "mon cher new test get"); +} + +alias Exports = wit.test.resource_borrow_in_record.runner.Exports!( + run +); diff --git a/tests/runtime/resource_borrow_in_record/test.d b/tests/runtime/resource_borrow_in_record/test.d new file mode 100644 index 000000000..190e40133 --- /dev/null +++ b/tests/runtime/resource_borrow_in_record/test.d @@ -0,0 +1,61 @@ +import wit.test.resource_borrow_in_record.test; + +import wit.common; + +extern(C) void* malloc(size_t size); +extern(C) void free(void* ptr); + +char[] concat(in char[] a, in char[] b) { + auto ptr = cast(char*)malloc(a.length + b.length); + assert((a.length + b.length) == 0 || ptr); + + if (!ptr) return null; + + ptr[0..a.length] = a[]; + ptr[a.length..a.length+b.length] = b[]; + + return ptr[0..a.length+b.length]; +} + +@witExport("test:resource-borrow-in-record/to-test", "thing") +struct ThingImpl { + const(char)[] contents; + + @witExport("test:resource-borrow-in-record/to-test", "[constructor]thing") + static Thing constructor(in WitString v) { + return Thing.makeNew((out typeof(this) self) { + self.contents = concat(v, " new"); + }); + } + + @witExport("test:resource-borrow-in-record/to-test", "[method]thing.get") + WitString get() { + return concat(contents, " get").witList; + } +} + +@witExport("test:resource-borrow-in-record/to-test", "test") +WitList!Thing test(ref scope WitList!Foo list) { + if (list.length == 0) return WitList!Thing(); + + auto ptr = cast(Thing*)malloc(Thing.sizeof*list.length); + assert(ptr); + + auto things = ptr[0..list.length]; + + foreach (i, ref thing; things) { + auto orig = list[i].thing.rep!ThingImpl.contents; + auto str = concat(orig, " test"); + + thing = Thing.makeNew((out ThingImpl self) { + self.contents = str; + }); + } + + return things.witList; // already on C heap; no clone +} + +alias Exports = wit.test.resource_borrow_in_record.test.Exports!( + ThingImpl, + test +); diff --git a/tests/runtime/resource_floats/intermediate.d b/tests/runtime/resource_floats/intermediate.d new file mode 100644 index 000000000..125709f37 --- /dev/null +++ b/tests/runtime/resource_floats/intermediate.d @@ -0,0 +1,55 @@ +import wit.test.resource_floats.intermediate; + +import wit.test.resource_floats.intermediate.exports.exports : EFloat = Float; + +import wit.test.resource_floats.intermediate.imports.imports : IFloat1 = Float; +import wit.test.resource_floats.test.imports : IFloat2 = Float; + +import wit.common; + +@witExport("exports", "float") +struct FloatImpl { + IFloat1 val; + + @witExport("exports", "[constructor]float") + static EFloat constructor(double v) { + return EFloat.makeNew((out typeof(this) self) { + self.val = IFloat1.makeNew(v + 1); + }); + } + + @witExport("exports", "[method]float.get") + double get() { + return val.get + 3; + } + + @witExport("exports", "[static]float.add") + static EFloat add(EFloat a, double b) { + scope(exit) a.witDrop; + + auto added = IFloat1.add(a.rep!FloatImpl.val, b); + scope(exit) added.witDrop; + + return FloatImpl.constructor( + added.get + 5 + ); + } +} + + +@witExport("$root", "add") +static IFloat2 add(IFloat2.Borrow a, IFloat2.Borrow b) { + scope(exit) { + a.witDrop; + b.witDrop; + } + + return IFloat2.makeNew( + a.get + b.get + 5 + ); +} + +alias Exports = wit.test.resource_floats.intermediate.Exports!( + FloatImpl, + add +); diff --git a/tests/runtime/resource_floats/leaf.d b/tests/runtime/resource_floats/leaf.d new file mode 100644 index 000000000..8c39af757 --- /dev/null +++ b/tests/runtime/resource_floats/leaf.d @@ -0,0 +1,54 @@ +import wit.test.resource_floats.leaf; + +import wit.test.resource_floats.leaf.exports.imports : Float; + +import wit.test.resource_floats.test.exports : Float2 = Float; +import wit.common; + +@witExport("imports", "float") +struct FloatImpl { + double val; + + @witExport("imports", "[constructor]float") + static Float constructor(double v) { + return Float.makeNew((out typeof(this) self) { + self.val = v + 2; + }); + } + + @witExport("imports", "[method]float.get") + double get() { + return val + 4; + } + + @witExport("imports", "[static]float.add") + static Float add(Float a, double b) { + scope(exit) a.witDrop; + + return FloatImpl.constructor( + a.rep!FloatImpl.val + b + 6 + ); + } +} + +@witExport("test:resource-floats/test", "float") +struct Float2Impl { + double val; + + @witExport("test:resource-floats/test", "[constructor]float") + static Float2 constructor(double v) { + return Float2.makeNew((out typeof(this) self) { + self.val = v + 1; + }); + } + + @witExport("test:resource-floats/test", "[method]float.get") + double get() { + return val + 3; + } +} + +alias Exports = wit.test.resource_floats.leaf.Exports!( + FloatImpl, + Float2Impl +); diff --git a/tests/runtime/resource_floats/runner.d b/tests/runtime/resource_floats/runner.d new file mode 100644 index 000000000..cac87830f --- /dev/null +++ b/tests/runtime/resource_floats/runner.d @@ -0,0 +1,31 @@ +import wit.test.resource_floats.runner; + +import wit.test.resource_floats.runner.imports.exports : Float2 = Float; + +import wit.common; + +@witExport("$root", "run") +void run() { + auto float1 = Float.makeNew(42); + scope(exit) float1.witDrop; + + auto float2 = Float.makeNew(55); + scope(exit) float2.witDrop; + + auto float3 = add(float1, float2); + scope(exit) float3.witDrop; + assert(float3.get == 114.0); + + auto float4 = Float2.makeNew(22); + scope(exit) float4.witDrop; + assert(float4.get == 22.0 + 1.0 + 2.0 + 4.0 + 3.0); + + auto res = Float2.add(float4, 7.0); + scope(exit) res.witDrop; + float4 = Float2.init; // consumed + assert(res.get == 59.0); +} + +alias Exports = wit.test.resource_floats.runner.Exports!( + run +); diff --git a/tests/runtime/resource_with_lists/leaf.d b/tests/runtime/resource_with_lists/leaf.d new file mode 100644 index 000000000..eb7718ead --- /dev/null +++ b/tests/runtime/resource_with_lists/leaf.d @@ -0,0 +1,61 @@ +import wit.test.resource_with_lists.leaf; + +import wit.common; + +extern(C) void* malloc(size_t size); +extern(C) void free(void* ptr); + +ubyte[] concat(in ubyte[] a, in char[] b) { + auto ptr = cast(ubyte*)malloc(a.length + b.length); + assert((a.length + b.length) == 0 || ptr); + + if (!ptr) return null; + + ptr[0..a.length] = a[]; + ptr[a.length..a.length+b.length] = cast(const(ubyte[]))b[]; + + return ptr[0..a.length+b.length]; +} + +@witExport("test:resource-with-lists/test", "thing") +struct ThingImpl { + ubyte[] val; + + @witExport("test:resource-with-lists/test", "[constructor]thing") + static Thing constructor(in WitList!ubyte a) { + return Thing.makeNew((out typeof(this) self) { + auto result = a.concat(" HostThing"); + + self.val = result; + }); + } + + @witExport("test:resource-with-lists/test", "[method]thing.foo") + WitList!ubyte foo() { + auto result = val.concat(" HostThing.foo"); + return result.witList; // no clone, no free; already on C heap + } + + @witExport("test:resource-with-lists/test", "[method]thing.bar") + auto bar(in WitList!ubyte l) { + auto result = l.concat(" HostThing.bar"); + + if (val.ptr) free(val.ptr); + val = result; + } + + + @witExport("test:resource-with-lists/test", "[static]thing.baz") + static WitList!ubyte baz(in WitList!ubyte l) { + auto result = l.concat(" HostThing.baz"); + return result.witList; // no clone, no free; already on C heap + } + + ~this() { + if (val.ptr) free(val.ptr); + } +} + +alias Exports = wit.test.resource_with_lists.leaf.Exports!( + ThingImpl +); diff --git a/tests/runtime/resource_with_lists/resource-with-lists.d b/tests/runtime/resource_with_lists/resource-with-lists.d new file mode 100644 index 000000000..123361b71 --- /dev/null +++ b/tests/runtime/resource_with_lists/resource-with-lists.d @@ -0,0 +1,75 @@ +module resource_with_lists; + +import wit.test.resource_with_lists.resource_with_lists; +import wit.test.resource_with_lists.test.imports : IThing = Thing; +import wit.test.resource_with_lists.test.exports : EThing = Thing; + +import wit.common; + +extern(C) void* malloc(size_t size); +extern(C) void free(void* ptr); + +ubyte[] concat(in ubyte[] a, in char[] b) { + auto ptr = cast(ubyte*)malloc(a.length + b.length); + assert((a.length + b.length) == 0 || ptr); + + if (!ptr) return null; + + ptr[0..a.length] = a[]; + ptr[a.length..a.length+b.length] = cast(const(ubyte[]))b[]; + + return ptr[0..a.length+b.length]; +} + +@witExport("test:resource-with-lists/test", "thing") +struct ThingImpl { + IThing val; + + @witExport("test:resource-with-lists/test", "[constructor]thing") + static EThing constructor(in WitList!ubyte a) { + return EThing.makeNew((out typeof(this) self) { + auto result = a.concat(" Thing"); + scope(exit) free(result.ptr); + + self.val = IThing.makeNew(result.witList); + }); + } + + @witExport("test:resource-with-lists/test", "[method]thing.foo") + WitList!ubyte foo() { + auto list = val.foo; + scope(exit) list.witFree; + + auto result = list.concat(" Thing.foo"); + return result.witList; // no clone, no free; already on C heap + } + + @witExport("test:resource-with-lists/test", "[method]thing.bar") + auto bar(in WitList!ubyte l) { + auto result = l.concat(" Thing.bar"); + scope(exit) free(result.ptr); + + val.bar(result.witList); + } + + + @witExport("test:resource-with-lists/test", "[static]thing.baz") + static WitList!ubyte baz(in WitList!ubyte l) { + auto input = l.concat(" Thing.baz"); + scope(exit) free(input.ptr); + + auto impBaz = IThing.baz(input.witList); + scope(exit) impBaz.witFree; + + auto result = impBaz.concat(" Thing.baz again"); + return result.witList; + } + + ~this() { + val.witDrop; + } +} + +alias Exports = wit.test.resource_with_lists.resource_with_lists.Exports!( + ThingImpl +); diff --git a/tests/runtime/resource_with_lists/runner.d b/tests/runtime/resource_with_lists/runner.d new file mode 100644 index 000000000..23783700a --- /dev/null +++ b/tests/runtime/resource_with_lists/runner.d @@ -0,0 +1,36 @@ +import wit.test.resource_with_lists.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + auto thingInstance = Thing.makeNew((cast(immutable ubyte[])"Hi").witList); + scope(exit) thingInstance.witDrop; + + { + auto result = thingInstance.foo(); + scope(exit) result.witFree; + + assert(cast(char[])result[] == "Hi Thing HostThing HostThing.foo Thing.foo"); + } + + thingInstance.bar((cast(immutable ubyte[])"Hola").witList); + + { + auto result = thingInstance.foo(); + scope(exit) result.witFree; + + assert(cast(char[])result[] == "Hola Thing.bar HostThing.bar HostThing.foo Thing.foo"); + } + + { + auto result = thingInstance.baz((cast(immutable ubyte[])"Ohayo Gozaimas").witList); + scope(exit) result.witFree; + + assert(cast(char[])result[] == "Ohayo Gozaimas Thing.baz HostThing.baz Thing.baz again"); + } +} + +alias Exports = wit.test.resource_with_lists.runner.Exports!( + run +); diff --git a/tests/runtime/resources/leaf.d b/tests/runtime/resources/leaf.d new file mode 100644 index 000000000..59b19aaca --- /dev/null +++ b/tests/runtime/resources/leaf.d @@ -0,0 +1,38 @@ +import wit.test.resources.leaf; + +import wit.common; + +@witExport("imports", "y") +struct YImpl { + int val; + + @witExport("imports", "[constructor]y") + static Y constructor(int a) { + return Y.makeNew((out typeof(this) self) { + self.val = a; + }); + } + + @witExport("imports", "[method]y.get-a") + int getA() { + return val; + } + + @witExport("imports", "[method]y.set-a") + void setA(int a) { + val = a; + } + + @witExport("imports", "[static]y.add") + static Y add(Y y, int a) { + scope(exit) y.witDrop; + + return Y.makeNew((out typeof(this) self) { + self.val = y.rep!YImpl.getA + a; + }); + } +} + +alias Exports = wit.test.resources.leaf.Exports!( + YImpl +); diff --git a/tests/runtime/resources/resources.d b/tests/runtime/resources/resources.d new file mode 100644 index 000000000..2c61c706d --- /dev/null +++ b/tests/runtime/resources/resources.d @@ -0,0 +1,160 @@ +import wit.test.resources.resources; + +import wit.common; + +@witExport("exports", "x") +struct XImpl { + int val; + + @witExport("exports", "[constructor]x") + static X constructor(int a) { + return X.makeNew((out typeof(this) self) { + self.val = a; + }); + } + + @witExport("exports", "[method]x.get-a") + int getA() { + return val; + } + + @witExport("exports", "[method]x.set-a") + void setA(int a) { + val = a; + } + + @witExport("exports", "[static]x.add") + static X add(X x, int a) { + scope(exit) x.witDrop; + + return X.makeNew((out typeof(this) self) { + self.val = x.rep!XImpl.getA + a; + }); + } +} + +@witExport("exports", "z") +struct ZImpl { + static uint numDropped = 0; + + int val; + + @witExport("exports", "[constructor]z") + static Z constructor(int a) { + return Z.makeNew((out typeof(this) self) { + self.val = a; + }); + } + + @witExport("exports", "[method]z.get-a") + int getA() { + return val; + } + + @witExport("exports", "[static]z.num-dropped") + static uint getNumDropped() { + return numDropped + 1; + } + + ~this() { + numDropped += 1; + } +} + +@witExport("exports", "kebab-case") +struct KebabCaseImpl { + uint val; + + @witExport("exports", "[constructor]kebab-case") + static KebabCase constructor(uint a) { + return KebabCase.makeNew((out typeof(this) self) { + self.val = a; + }); + } + + @witExport("exports", "[method]kebab-case.get-a") + uint getA() { + return val; + } + + @witExport("exports", "[static]kebab-case.take-owned") + static uint takeOwned(KebabCase k) { + scope(exit) k.witDrop; + + return k.rep!KebabCaseImpl.getA; + } +} + + +@witExport("exports", "add") +Z add(Z.Borrow a, Z.Borrow b) { + scope(exit) { + a.witDrop; + b.witDrop; + } + + return ZImpl.constructor(a.rep!ZImpl.val + b.rep!ZImpl.val); +} + + +@witExport("exports", "consume") +void consume(X x) { + x.witDrop; +} + + +@witExport("exports", "test-imports") +Result!(void, WitString) testImports() { + { + auto y = Y.makeNew(10); + scope(exit) y.witDrop; + assert(y.getA == 10); + y.setA(20); + assert(y.getA == 20); + + auto y2 = Y.add(y, 20); + scope(exit) y2.witDrop; + y = Y.init; // consumed + assert(y2.getA == 40); + } + + { + auto y1 = Y.makeNew(1); + scope(exit) y1.witDrop; + auto y2 = Y.makeNew(2); + scope(exit) y2.witDrop; + + assert(y1.getA == 1); + assert(y2.getA == 2); + + y1.setA(10); + y2.setA(20); + assert(y1.getA == 10); + assert(y2.getA == 20); + + auto y3 = Y.add(y1, 20); + scope(exit) y3.witDrop; + y1 = Y.init; // consumed + assert(y3.getA == 30); + + auto y4 = Y.add(y2, 30); + scope(exit) y4.witDrop; + y2 = Y.init; // consumed + assert(y4.getA == 50); + } + + + return ok!WitString; +} + + + +alias Exports = wit.test.resources.resources.Exports!( + XImpl, + ZImpl, + KebabCaseImpl, + + add, + consume, + testImports +); diff --git a/tests/runtime/resources/runner.d b/tests/runtime/resources/runner.d new file mode 100644 index 000000000..db4bdd5d9 --- /dev/null +++ b/tests/runtime/resources/runner.d @@ -0,0 +1,52 @@ +import wit.test.resources.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + { + auto result = testImports(); + scope(exit) result.witFree; + + assert(result.isOk); + } + + auto x = X.makeNew(5); + scope(exit) x.witDrop; + assert(x.getA == 5); + x.setA(10); + assert(x.getA == 10); + + auto z1 = Z.makeNew(10); + scope(exit) z1.witDrop; + assert(z1.getA() == 10); + + auto z2 = Z.makeNew(20); + scope(exit) z2.witDrop; + assert(z2.getA() == 20); + + auto xadd = X.add(x, 5); + scope(exit) xadd.witDrop; + x = X.init; // consumed + assert(xadd.getA() == 15); + + auto zadd = add(z1, z2); + scope(exit) zadd.witDrop; + assert(zadd.getA() == 30); + + auto droppedZsStart = Z.numDropped; + z1.witDrop; + z2.witDrop; + + consume(xadd); + xadd = X.init; // consumed + + auto droppedZsEnd = Z.numDropped; + if (droppedZsStart != 0) { + assert(droppedZsEnd == droppedZsStart + 2); + } +} + +alias Exports = wit.test.resources.runner.Exports!( + run +); diff --git a/tests/runtime/results/intermediate.d b/tests/runtime/results/intermediate.d new file mode 100644 index 000000000..2bc6a8ee9 --- /dev/null +++ b/tests/runtime/results/intermediate.d @@ -0,0 +1,43 @@ +import wit.test.results.intermediate; +import imports = wit.test.results.test.imports; + +import wit.common; + +@witExport("test:results/test", "string-error") +Result!(float, WitString) stringError(float a) { + return imports.stringError(a); +} + +@witExport("test:results/test", "enum-error") +Result!(float, E) enumError(float a) { + return imports.enumError(a); +} + +@witExport("test:results/test", "record-error") +Result!(float, E2) recordError(float a) { + return imports.recordError(a); +} + +@witExport("test:results/test", "variant-error") +Result!(float, E3) variantError(float a) { + return imports.variantError(a); +} + +@witExport("test:results/test", "empty-error") +Result!(uint, void) emptyError(uint a) { + return imports.emptyError(a); +} + +@witExport("test:results/test", "double-error") +Result!(Result!(void, WitString), WitString) doubleError(uint a) { + return imports.doubleError(a); +} + +alias Exports = wit.test.results.intermediate.Exports!( + stringError, + enumError, + recordError, + variantError, + emptyError, + doubleError +); diff --git a/tests/runtime/results/leaf.d b/tests/runtime/results/leaf.d new file mode 100644 index 000000000..7b4bb7991 --- /dev/null +++ b/tests/runtime/results/leaf.d @@ -0,0 +1,85 @@ +import wit.test.results.leaf; + +import wit.common; + +@witExport("test:results/test", "string-error") +Result!(float, WitString) stringError(float a) { + if (a == 0.0) { + return "zero".witList.witClone.err!float; + } + + return a.ok!WitString; +} + +@witExport("test:results/test", "enum-error") +Result!(float, E) enumError(float a) { + if (a == 0.0) { + return E.a.err!float; + } + + return a.ok!E; +} + +@witExport("test:results/test", "record-error") +Result!(float, E2) recordError(float a) { + if (a == 0.0) { + return E2( + line: 420, + column: 0 + ).err!float; + } else if (a == 1.0) { + return E2( + line: 77, + column: 2 + ).err!float; + } + + return a.ok!E2; +} + +@witExport("test:results/test", "variant-error") +Result!(float, E3) variantError(float a) { + if (a == 0.0) { + return E3.e2(E2( + line: 420, + column: 0 + )).err!float; + } else if (a == 1.0) { + return E3.e1(E.b).err!float; + } else if (a == 2.0) { + return E3.e1(E.c).err!float; + } + + return a.ok!E3; +} + +@witExport("test:results/test", "empty-error") +Result!(uint, void) emptyError(uint a) { + if (a == 0) { + return err!uint; + } else if (a == 1) { + return 42u.ok!void; + } + + return a.ok!void; +} + +@witExport("test:results/test", "double-error") +Result!(Result!(void, WitString), WitString) doubleError(uint a) { + if (a == 0) { + return ok!WitString.ok!WitString; + } else if (a == 1) { + return "one".witList.witClone.err!void.ok!WitString; + } + + return "two".witList.witClone.err!(Result!(void, WitString)); +} + +alias Exports = wit.test.results.leaf.Exports!( + stringError, + enumError, + recordError, + variantError, + emptyError, + doubleError +); diff --git a/tests/runtime/results/runner.d b/tests/runtime/results/runner.d new file mode 100644 index 000000000..426c5c75e --- /dev/null +++ b/tests/runtime/results/runner.d @@ -0,0 +1,69 @@ +import wit.test.results.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + { + auto result = stringError(0.0); + scope(exit) result.witFree; + + assert(result == "zero".witList.err!float); + } + + { + auto result = stringError(1.0); + scope(exit) result.witFree; + + assert(result == 1.0f.ok!WitString); + } + + assert(enumError(0.0) == E.a.err!float); + assert(enumError(1.0) == 1.0f.ok!E); + + assert(recordError(0.0) == E2( + line: 420, + column: 0 + ).err!float); + assert(recordError(1.0) == E2( + line: 77, + column: 2 + ).err!float); + assert(recordError(2.0).isOk); + + assert(variantError(0.0) == E3.e2(E2( + line: 420, + column: 0 + )).err!float); + assert(variantError(1.0) == E3.e1(E.b).err!float); + assert(variantError(2.0) == E3.e1(E.c).err!float); + + assert(emptyError(0) == err!uint); + assert(emptyError(1) == 42u.ok!void); + assert(emptyError(2) == 2u.ok!void); + + { + auto result = doubleError(0); + scope(exit) result.witFree; + + assert(result == ok!WitString.ok!WitString); + } + + { + auto result = doubleError(1); + scope(exit) result.witFree; + + assert(result == "one".witList.err!void.ok!WitString); + } + + { + auto result = doubleError(2); + scope(exit) result.witFree; + + assert(result == "two".witList.err!(Result!(void, WitString))); + } +} + +alias Exports = wit.test.results.runner.Exports!( + run +); diff --git a/tests/runtime/strings-alias/runner.d b/tests/runtime/strings-alias/runner.d new file mode 100644 index 000000000..52ae6ae83 --- /dev/null +++ b/tests/runtime/strings-alias/runner.d @@ -0,0 +1,17 @@ +import wit.my.strings.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + foo("hello".witList); + + auto str = bar(); + scope(exit) str.witFree; + + assert(str == "world"); +} + +alias Exports = wit.my.strings.runner.Exports!( + run +); diff --git a/tests/runtime/strings-alias/test.d b/tests/runtime/strings-alias/test.d new file mode 100644 index 000000000..2817234d3 --- /dev/null +++ b/tests/runtime/strings-alias/test.d @@ -0,0 +1,18 @@ +import wit.my.strings.test; + +import wit.common; + +@witExport("cat", "foo") +void foo(in MyString str) { + assert(str == "hello"); +} + +@witExport("cat", "bar") +MyString bar() { + return "world".witList.witClone; +} + +alias Exports = wit.my.strings.test.Exports!( + foo, + bar +); diff --git a/tests/runtime/strings-simple/runner.d b/tests/runtime/strings-simple/runner.d new file mode 100644 index 000000000..52ae6ae83 --- /dev/null +++ b/tests/runtime/strings-simple/runner.d @@ -0,0 +1,17 @@ +import wit.my.strings.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + foo("hello".witList); + + auto str = bar(); + scope(exit) str.witFree; + + assert(str == "world"); +} + +alias Exports = wit.my.strings.runner.Exports!( + run +); diff --git a/tests/runtime/strings-simple/test.d b/tests/runtime/strings-simple/test.d new file mode 100644 index 000000000..dde48ffdb --- /dev/null +++ b/tests/runtime/strings-simple/test.d @@ -0,0 +1,18 @@ +import wit.my.strings.test; + +import wit.common; + +@witExport("cat", "foo") +void foo(in WitString str) { + assert(str == "hello"); +} + +@witExport("cat", "bar") +WitString bar() { + return "world".witList.witClone; +} + +alias Exports = wit.my.strings.test.Exports!( + foo, + bar +); diff --git a/tests/runtime/strings/runner.d b/tests/runtime/strings/runner.d new file mode 100644 index 000000000..fb9b6195b --- /dev/null +++ b/tests/runtime/strings/runner.d @@ -0,0 +1,33 @@ +import wit.test.strings.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + takeBasic("latin utf16".witList); + + { + auto str = returnUnicode(); + scope(exit) str.witFree; + + assert(str == "🚀🚀🚀 𠈄𓀀"); + } + + { + auto str = returnEmpty(); + scope(exit) str.witFree; + + assert(str == ""); + } + + { + auto str = roundtrip("🚀🚀🚀 𠈄𓀀".witList); + scope(exit) str.witFree; + + assert(str == "🚀🚀🚀 𠈄𓀀"); + } +} + +alias Exports = wit.test.strings.runner.Exports!( + run +); diff --git a/tests/runtime/strings/test.d b/tests/runtime/strings/test.d new file mode 100644 index 000000000..62ee4cc9d --- /dev/null +++ b/tests/runtime/strings/test.d @@ -0,0 +1,31 @@ +import wit.test.strings.test; + +import wit.common; + +@witExport("test:strings/to-test", "take-basic") +void takeBasic(in WitString str) { + assert(str == "latin utf16"); +} + +@witExport("test:strings/to-test", "return-unicode") +WitString returnUnicode() { + return "🚀🚀🚀 𠈄𓀀".witList.witClone; +} + +@witExport("test:strings/to-test", "return-empty") +WitString returnEmpty() { + return WitString(); +} + +@witExport("test:strings/to-test", "roundtrip") +WitString roundtrip(in WitString str) { + return str.witClone; +} + + +alias Exports = wit.test.strings.test.Exports!( + takeBasic, + returnUnicode, + returnEmpty, + roundtrip +); diff --git a/tests/runtime/symbol-conflicts/runner.d b/tests/runtime/symbol-conflicts/runner.d new file mode 100644 index 000000000..79f104033 --- /dev/null +++ b/tests/runtime/symbol-conflicts/runner.d @@ -0,0 +1,15 @@ +import wit.my.inline.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + wit.my.inline.foo1.imports.foo(); + wit.my.inline.foo2.imports.foo(); + wit.my.inline.bar1.imports.bar(); + wit.my.inline.bar2.imports.bar(); +} + +alias Exports = wit.my.inline.runner.Exports!( + run +); diff --git a/tests/runtime/symbol-conflicts/test.d b/tests/runtime/symbol-conflicts/test.d new file mode 100644 index 000000000..1d9b0997d --- /dev/null +++ b/tests/runtime/symbol-conflicts/test.d @@ -0,0 +1,23 @@ +import wit.my.inline.test; + +import wit.common; + +@witExport("my:inline/foo1", "foo") +void foo1() {} + +@witExport("my:inline/foo2", "foo") +void foo2() {} + +@witExport("my:inline/bar1", "bar") +WitString bar1() { return WitString(); } + +@witExport("my:inline/bar2", "bar") +WitString bar2() { return WitString(); } + + +alias Exports = wit.my.inline.test.Exports!( + foo1, + foo2, + bar1, + bar2 +); diff --git a/tests/runtime/unused-types/runner.d b/tests/runtime/unused-types/runner.d new file mode 100644 index 000000000..81a55e9ac --- /dev/null +++ b/tests/runtime/unused-types/runner.d @@ -0,0 +1,14 @@ +import wit.foo.bar.runner; + +import wit.foo.bar.component.common : UnusedEnum, UnusedRecord, UnusedVariant; + +import wit.common; + +@witExport("$root", "run") +void run() { + foo(); +} + +alias Exports = wit.foo.bar.runner.Exports!( + run +); diff --git a/tests/runtime/unused-types/test.d b/tests/runtime/unused-types/test.d new file mode 100644 index 000000000..bf0bba35a --- /dev/null +++ b/tests/runtime/unused-types/test.d @@ -0,0 +1,12 @@ +import wit.foo.bar.test; + +import wit.foo.bar.component.common : UnusedEnum, UnusedRecord, UnusedVariant; + +import wit.common; + +@witExport("foo:bar/component", "foo") +void foo() {} + +alias Exports = wit.foo.bar.test.Exports!( + foo +); diff --git a/tests/runtime/variants/runner.d b/tests/runtime/variants/runner.d new file mode 100644 index 000000000..406e81a46 --- /dev/null +++ b/tests/runtime/variants/runner.d @@ -0,0 +1,93 @@ +import wit.test.variants.runner; + +import wit.common; + +@witExport("$root", "run") +void run() { + assert(roundtripOption(1.0f.some) == ubyte(1).some); + assert(roundtripOption(none!float) == none!ubyte); + assert(roundtripOption(2.0f.some) == ubyte(2).some); + assert(roundtripOption(4.0f.some) == ubyte(4).some); + assert(roundtripOption(5.3f.some) == ubyte(5).some); + + assert(roundtripEnum(E1.a) == E1.a); + assert(roundtripEnum(E1.b) == E1.b); + + assert(invertBool(true) == false); + assert(invertBool(false) == true); + + { + auto result = variantCasts(tuple( + C1.a(1), + C2.a(2), + C3.a(3), + C4.a(4), + C5.a(5), + C6.a(6.0), + )); + + assert(result[0] == C1.a(1)); + assert(result[1] == C2.a(2)); + assert(result[2] == C3.a(3)); + assert(result[3] == C4.a(4)); + assert(result[4] == C5.a(5)); + assert(result[5] == C6.a(6.0)); + } + + { + auto result = variantCasts(tuple( + C1.b(1), + C2.b(2.0), + C3.b(3.0), + C4.b(4.0), + C5.b(5.0), + C6.b(6.0), + )); + + assert(result[0] == C1.b(1)); + assert(result[1] == C2.b(2.0)); + assert(result[2] == C3.b(3.0)); + assert(result[3] == C4.b(4.0)); + assert(result[4] == C5.b(5.0)); + assert(result[5] == C6.b(6.0)); + } + + { + auto result = variantZeros(tuple( + Z1.a(1), + Z2.a(2), + Z3.a(3.0), + Z4.a(4.0), + )); + + assert(result[0] == Z1.a(1)); + assert(result[1] == Z2.a(2)); + assert(result[2] == Z3.a(3.0)); + assert(result[3] == Z4.a(4.0)); + } + + { + auto result = variantZeros(tuple( + Z1.b, + Z2.b, + Z3.b, + Z4.b, + )); + + assert(result[0] == Z1.b); + assert(result[1] == Z2.b); + assert(result[2] == Z3.b); + assert(result[3] == Z4.b); + } + + variantTypedefs(none!uint, false, err!uint); + + assert( + variantEnums(true, ok!void, MyErrno.success) + == tuple(true, ok!void, MyErrno.success) + ); +} + +alias Exports = wit.test.variants.runner.Exports!( + run +); diff --git a/tests/runtime/variants/test.d b/tests/runtime/variants/test.d new file mode 100644 index 000000000..2da0702a8 --- /dev/null +++ b/tests/runtime/variants/test.d @@ -0,0 +1,47 @@ +import wit.test.variants.test; + +import wit.common; + +@witExport("test:variants/to-test", "roundtrip-option") +Option!ubyte roundtripOption(in Option!float a) { + if (a.isSome) return (cast(ubyte)a.unwrap).some; + return none!ubyte; +} + +@witExport("test:variants/to-test", "roundtrip-result") +Result!(double, ubyte) roundtripResult(in Result!(uint, float) a) { + if (a.isOk) return (cast(double)a.unwrap).ok!ubyte; + return (cast(ubyte)a.unwrapErr).err!double; +} + +@witExport("test:variants/to-test", "roundtrip-enum") +E1 roundtripEnum(E1 a) => a; + +@witExport("test:variants/to-test", "invert-bool") +bool invertBool(bool a) => !a; + +@witExport("test:variants/to-test", "variant-casts") +Casts variantCasts(in Casts a) => a; + +@witExport("test:variants/to-test", "variant-zeros") +Zeros variantZeros(in Zeros a) => a; + +@witExport("test:variants/to-test", "variant-typedefs") +void variantTypedefs(in Option!uint, bool, in Result!uint) {} + + +@witExport("test:variants/to-test", "variant-enums") +Tuple!(bool, Result!void, MyErrno) variantEnums(bool a, in Result!void b, MyErrno c) { + return tuple(a, b, c); +} + +alias Exports = wit.test.variants.test.Exports!( + roundtripOption, + roundtripResult, + roundtripEnum, + invertBool, + variantCasts, + variantZeros, + variantTypedefs, + variantEnums +); diff --git a/tests/runtime/versions/runner.d b/tests/runtime/versions/runner.d new file mode 100644 index 000000000..7b62cf5ee --- /dev/null +++ b/tests/runtime/versions/runner.d @@ -0,0 +1,18 @@ +import wit.test.versions.runner; +import wit.common; + +@witExport("$root", "run") +void run() { + import v1 = wit.test.dep_0_1_0.test.imports; + + assert(v1.x() == 1.0); + assert(v1.y(1.0) == 2.0); + + import v2 = wit.test.dep_0_2_0.test.imports; + assert(v2.x() == 2.0); + assert(v2.z(1.0, 1.0) == 4.0); +} + +alias Exports = wit.test.versions.runner.Exports!( + run +); diff --git a/tests/runtime/versions/test.d b/tests/runtime/versions/test.d new file mode 100644 index 000000000..19879bd68 --- /dev/null +++ b/tests/runtime/versions/test.d @@ -0,0 +1,21 @@ +import wit.test.versions.test; +import wit.common; + +@witExport("test:dep/test@0.1.0", "x") +float x_v1() => 1.0; + +@witExport("test:dep/test@0.1.0", "y") +float y(float a) => 1.0 + a; + +@witExport("test:dep/test@0.2.0", "x") +float x_v2() => 2.0; + +@witExport("test:dep/test@0.2.0", "z") +float z(float a, float b) => 2.0 + a + b; + +alias Exports = wit.test.versions.test.Exports!( + x_v1, + y, + x_v2, + z +);