diff --git a/cmd/tools/vwit_wrap.v b/cmd/tools/vwit_wrap.v new file mode 100644 index 00000000000000..180e2f056b7384 --- /dev/null +++ b/cmd/tools/vwit_wrap.v @@ -0,0 +1,17 @@ +import os +import wit +import wasm + +fn main() { + if os.args.len < 5 { + println('usage: v run cmd/tools/vwit_wrap.v ') + return + } + core := os.read_file(os.args[1]) or { panic(err) } + src := os.read_file(os.args[2]) or { panic(err) } + doc := wit.parse(src) or { panic(err) } + out := wit.wrap_component(core.bytes(), doc, os.args[3]) + os.write_file_array(os.args[4], out) or { panic(err) } + println('wrote ${os.args[4]} (${out.len} bytes)') + _ = wasm.is_component +} diff --git a/doc/wasm.md b/doc/wasm.md index e7f985e2987d24..b1b688cceb7d4f 100644 --- a/doc/wasm.md +++ b/doc/wasm.md @@ -71,3 +71,18 @@ optimized SSA: ./v test vlib/v/gen/wasm/ssa_gen_test.v V3_TEST_WASM=1 ./v vlib/v/compiler_tests/wasm_codegen_test.v ``` + +## Emscripten output (needs the Emscripten SDK) + +The native backend above emits a standalone `.wasm` module and never invokes Emscripten. +If you instead need Emscripten's JavaScript shell or HTML page, target `wasm32_emscripten` +explicitly and compile the generated C with `emcc` from an activated +[Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) on your `PATH`: + +```sh +./v -os wasm32_emscripten -cc emcc -o hello.html examples/hello_world.v +``` + +Without `emcc` on `PATH`, that combination fails with a message pointing back here. +Emitting only C (`-o hello.c`) never invokes a C compiler, so it works without Emscripten +installed. diff --git a/examples/wasm/component/README.md b/examples/wasm/component/README.md new file mode 100644 index 00000000000000..7d4c342e0146b2 --- /dev/null +++ b/examples/wasm/component/README.md @@ -0,0 +1,17 @@ +# V WebAssembly Component Example + +## Build + +```sh +v -b wasm -component -wit hello.wit hello.v -o hello.wasm +``` + +## Run + +Requires `wasmtime`: + +```sh +wasmtime run hello.component.wasm +``` + +If wasmtime is not installed, the build still succeeds but running is skipped. \ No newline at end of file diff --git a/examples/wasm/component/hello.v b/examples/wasm/component/hello.v new file mode 100644 index 00000000000000..fa61ffee75d4d3 --- /dev/null +++ b/examples/wasm/component/hello.v @@ -0,0 +1,5 @@ +module main + +fn main() { + println('hello from V component') +} \ No newline at end of file diff --git a/examples/wasm/component/hello.wit b/examples/wasm/component/hello.wit new file mode 100644 index 00000000000000..d39387c78d3a72 --- /dev/null +++ b/examples/wasm/component/hello.wit @@ -0,0 +1,5 @@ +package example:hello@0.1.0; + +world hello { + export run: func(); +} \ No newline at end of file diff --git a/examples/wasm/component/run.sh b/examples/wasm/component/run.sh new file mode 100644 index 00000000000000..346aeaf9440dbc --- /dev/null +++ b/examples/wasm/component/run.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +V=${V:-v} +$V -b wasm -component -wit hello.wit hello.v -o hello.wasm +if command -v wasmtime >/dev/null 2>&1; then + wasmtime run hello.component.wasm +else + echo "wasmtime not found; skipping execution" +fi \ No newline at end of file diff --git a/examples/wasm/playground/README.md b/examples/wasm/playground/README.md index d5057738cddeaf..08a55c12a867f8 100644 --- a/examples/wasm/playground/README.md +++ b/examples/wasm/playground/README.md @@ -7,7 +7,9 @@ the V compiler and V's direct WebAssembly backend. No compilation server is need Build V in the repository root (`make` if `./v` is missing, then `./v self`). Install and activate the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html), so -`emcc` is on your `PATH`. From the repository root, run: +`emcc` is on your `PATH`. (That SDK is only needed to compile the compiler itself into the +browser playground; ordinary `-b wasm` output never requires it.) From the repository root, +run: ```sh sh examples/wasm/playground/build.sh diff --git a/examples/wasm/wasi_preview2/README.md b/examples/wasm/wasi_preview2/README.md new file mode 100644 index 00000000000000..f908b60af2d94b --- /dev/null +++ b/examples/wasm/wasi_preview2/README.md @@ -0,0 +1,19 @@ +# WASI Preview 2 Example + +Generated V bindings from WASI preview2 WIT files. + +## Build + +```sh +v -b wasm -component -wit hello.wit hello.v -o hello.wasm +``` + +## Run + +Requires `wasmtime`: + +```sh +wasmtime run hello.component.wasm +``` + +If wasmtime is not installed, the build still succeeds but running is skipped. \ No newline at end of file diff --git a/examples/wasm/wasi_preview2/hello.v b/examples/wasm/wasi_preview2/hello.v new file mode 100644 index 00000000000000..3e59fbb675d4dc --- /dev/null +++ b/examples/wasm/wasi_preview2/hello.v @@ -0,0 +1,5 @@ +module main + +fn main() { + println('hello from WASI preview2 component') +} \ No newline at end of file diff --git a/examples/wasm/wasi_preview2/hello.wit b/examples/wasm/wasi_preview2/hello.wit new file mode 100644 index 00000000000000..d39387c78d3a72 --- /dev/null +++ b/examples/wasm/wasi_preview2/hello.wit @@ -0,0 +1,5 @@ +package example:hello@0.1.0; + +world hello { + export run: func(); +} \ No newline at end of file diff --git a/examples/wasm/wasi_preview2/run.sh b/examples/wasm/wasi_preview2/run.sh new file mode 100644 index 00000000000000..346aeaf9440dbc --- /dev/null +++ b/examples/wasm/wasi_preview2/run.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +V=${V:-v} +$V -b wasm -component -wit hello.wit hello.v -o hello.wasm +if command -v wasmtime >/dev/null 2>&1; then + wasmtime run hello.component.wasm +else + echo "wasmtime not found; skipping execution" +fi \ No newline at end of file diff --git a/examples/wasm_component/hello.v b/examples/wasm_component/hello.v new file mode 100644 index 00000000000000..948ecc0f836635 --- /dev/null +++ b/examples/wasm_component/hello.v @@ -0,0 +1,5 @@ +module main + +fn main() { + println('hello from V wasm component') +} diff --git a/examples/wasm_component/hello.wit b/examples/wasm_component/hello.wit new file mode 100644 index 00000000000000..1980bf11a137d9 --- /dev/null +++ b/examples/wasm_component/hello.wit @@ -0,0 +1,9 @@ +package example:hello@0.1.0; + +interface api { + run: func() -> string; +} + +world hello { + export api; +} \ No newline at end of file diff --git a/examples/wit/example.wit b/examples/wit/example.wit new file mode 100644 index 00000000000000..c98fb253c6440d --- /dev/null +++ b/examples/wit/example.wit @@ -0,0 +1,12 @@ +package example:demo@0.1.0; + +interface calc { + record point { x: f64, y: f64 } + add: func(a: s32, b: s32) -> s32; + scale: func(p: point, factor: f64) -> point; +} + +world app { + import calc; + export run: func() -> string; +} \ No newline at end of file diff --git a/examples/wit/main.v b/examples/wit/main.v new file mode 100644 index 00000000000000..ec8d9f05c2a59c --- /dev/null +++ b/examples/wit/main.v @@ -0,0 +1,8 @@ +import os +import wit + +fn main() { + src := os.read_file('example.wit') or { panic(err) } + doc := wit.parse(src) or { panic(err) } + println(doc.to_v()) +} \ No newline at end of file diff --git a/vlib/v/compiler_tests/driver_cli_test.v b/vlib/v/compiler_tests/driver_cli_test.v index 952cf2ab8f9a9a..d5e1ae5baf1284 100644 --- a/vlib/v/compiler_tests/driver_cli_test.v +++ b/vlib/v/compiler_tests/driver_cli_test.v @@ -2236,6 +2236,36 @@ fn main() { assert_driver_wasm_output(explicit_output) } +fn test_emscripten_missing_emcc_suggests_native_wasm() { + root := os.join_path(os.vtmp_dir(), 'v3_driver_emscripten_toolchain_${os.getpid()}') + os.rmdir_all(root) or {} + os.mkdir_all(root) or { panic(err) } + defer { + os.rmdir_all(root) or {} + } + v3_bin := build_driver_cli_v3(root) + source := os.join_path(root, 'hello.v') + os.write_file(source, 'module main\n\nfn main() {\n\tprintln("hi")\n}\n') or { + panic(err) + } + if _ := os.find_abs_path_of_executable('emcc') { + println('skipping ${@FN}: emcc is on PATH, so the missing-toolchain guidance cannot trigger') + } else { + html_output := os.join_path(root, 'hello.html') + assert_driver_cli_failure(v3_bin, ['-os', 'wasm32_emscripten', '-cc', 'emcc', '-o', + html_output, source], 'was not found') + missing := cmdexec.run(v3_bin, ['-os', 'wasm32_emscripten', '-cc', 'emcc', '-o', html_output, + source]) + assert missing.output.contains('Emscripten SDK'), missing.output + assert missing.output.contains('v -b wasm'), missing.output + } + // Emitting only C never invokes a C compiler, so it must work without emcc. + c_output := os.join_path(root, 'hello_emscripten.c') + c_only := cmdexec.run(v3_bin, ['-os', 'wasm32_emscripten', '-o', c_output, source]) + assert c_only.exit_code == 0, c_only.output + assert os.is_file(c_output) +} + fn test_driver_rejects_invalid_cli_and_parses_vmod_subdirs() { root := os.join_path(os.vtmp_dir(), 'v3_driver_cli_${os.getpid()}') os.rmdir_all(root) or {} diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index 099d965a196484..883e4e49590f1f 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -57,6 +57,7 @@ $if !skip_ssa ? { $if !skip_wasm ? { import v.gen.wasm as wasmgen + import wit } // vfmt on @@ -10059,6 +10060,8 @@ pub fn run(args []string) { mut is_livemain := false mut is_liveshared := false mut hot_fn := '' + mut component_flag := false + mut wit_file_path := '' mut subsystem := pref.Subsystem.auto mut icon_path := '' mut is_strict := false @@ -10236,6 +10239,12 @@ pub fn run(args []string) { backend = normalize_backend_name(args[i].all_after('=')) backend_explicit = !backend_from_project i++ + } else if args[i] == '-component' { + component_flag = true + i++ + } else if args[i] == '-wit' && i + 1 < args.len { + wit_file_path = args[i + 1] + i += 2 } else if args[i] == '-cross' { // A modifier, not a target: it combines with `-os`/`-cc`, as in // `v -cross -os windows -cc msvc`. See `v help build-c`. @@ -11278,6 +11287,21 @@ pub fn run(args []string) { // The executable suffix does not belong to the requested generated C path. bin_file = target_bin_file } + if backend in ['c', 'fastc'] && target.os == 'wasm32_emscripten' && !c_only { + // The emscripten target links through emcc, so fail here with guidance + // instead of a generic missing-executable error later. Emitting only C + // (`-o file.c`) never reaches this point, so it stays toolchain-free. + cc_base := os.file_name(c_compiler).to_lower() + if c_compiler_explicit && cc_base in ['emcc', 'emcc.bat', 'em++', 'em++.bat'] { + if _ := os.find_abs_path_of_executable(c_compiler) { + } else { + eprintln('error: cannot build for target `wasm32_emscripten`: C compiler `${c_compiler}` was not found.') + eprintln('Install the Emscripten SDK (https://emscripten.org/docs/getting_started/downloads.html) and activate it so `emcc` is on PATH,') + eprintln('or build a native WebAssembly module instead, which needs no Emscripten: `v -b wasm -o out.wasm input.v`.') + exit(1) + } + } + } validate_windows_icon_option(icon_path, target.os, backend, is_shared, is_o, c_only, generate_c_project) or { eprintln(err.msg()) @@ -11422,6 +11446,19 @@ pub fn run(args []string) { eprintln('fastc-phase driver.prefs ${driver_sw.elapsed().microseconds()}us') } prefs.target = target + prefs.component = component_flag + prefs.wit_file = wit_file_path + prefs.wasm_cabi = component_flag || 'wasm_cabi' in user_defines + if prefs.component { + if prefs.wit_file == '' { + eprintln('error: -component requires -wit ') + exit(1) + } + if backend != 'wasm' { + eprintln('error: -component requires -b wasm') + exit(1) + } + } prefs.output_cross_c = output_cross_c prefs.thread_stack_size = if thread_stack_size_set { thread_stack_size @@ -14801,6 +14838,7 @@ pub fn run(args []string) { } m.release_codegen_analysis_metadata() mut g := wasmgen.SSAGen.new(m) + g.set_wasm_cabi(prefs.wasm_cabi || prefs.component) mut main_fn := config.main_fn mut exports := config.exports.clone() if main_fn.len == 0 { @@ -14823,6 +14861,31 @@ pub fn run(args []string) { eprintln('error writing ${output_file}') exit(1) } + if prefs.component { + // Invoke WIT parser + component wrapper (native flow). + wit_src := os.read_file(prefs.wit_file) or { + eprintln('error reading wit file ${prefs.wit_file}') + exit(1) + } + doc := wit.parse(wit_src) or { + eprintln('error parsing wit file ${prefs.wit_file}: ${err}') + exit(1) + } + core := os.read_file(output_file) or { + eprintln('error reading ${output_file}') + exit(1) + } + // Component output path + comp_out := output_file.all_before_last('.wasm') + '.component.wasm' + // Wrap with WIT-aware canonical lifting (string params lift via + // memory + realloc); falls back to plain wrapping. + wrapped := wit.wrap_component(core.bytes(), doc, '') + os.write_file_array(comp_out, wrapped) or { + eprintln('error writing ${comp_out}') + exit(1) + } + eprintln('component: wrote ${comp_out}') + } for warning in g.warnings_list() { eprintln('wasm: ${warning}') } diff --git a/vlib/v/gen/wasm/cabi_test.v b/vlib/v/gen/wasm/cabi_test.v new file mode 100644 index 00000000000000..2cd188c9fcd7c3 --- /dev/null +++ b/vlib/v/gen/wasm/cabi_test.v @@ -0,0 +1,601 @@ +module wasm + +// cabi_test.v proves the native canonical-ABI path for exported string params. +// +// A guest `echo_len(s string) int` compiled with wasm_cabi on must export +// `echo_len` as (i32 ptr, i32 len) -> i32, with memory and cabi_realloc +// exported. A backend-generated wrapper materializes the V string and calls +// the original single-pointer function, which stays unexported. The shim era +// (a hand-written echo_len_cabi in guest source) is frozen in +// testdata/echo_len.shim.wasm for shape parity. + +import os +import v.parser +import v.pref +import v.ssa +import v.transform +import v.types + +const cabi_guest_source = "module main + +@[export: 'echo_len'] +pub fn echo_len(s string) int { + return s.len +} + +@[export: 'first_byte'] +pub fn first_byte(s string) u8 { + return s[0] +} + +@[export: 'shout'] +pub fn shout(s string, loud u8) int { + if loud == 1 { + return s.len + 100 + } + return s.len +} + +@[export: 'hello'] +pub fn hello() string { + return 'hi' +} + +fn main() {} +" + +fn cabi_test_module_bytes(source string, cabi bool) []u8 { + dir := os.join_path(os.vtmp_dir(), 'cabi_${os.getpid()}') + os.mkdir_all(dir) or { panic(err) } + defer { + os.rmdir_all(dir) or {} + } + path := os.join_path(dir, 'guest.v') + os.write_file(path, source) or { panic(err) } + mut p := parser.Parser.new(pref.new_preferences()) + mut a := p.parse_file(path) + assert p.diagnostics.len == 0, p.diagnostics.str() + mut tc := types.TypeChecker.new(a) + tc.collect(a) + _ = tc.check_semantics_opt(false) + assert tc.errors.len == 0, tc.errors.str() + tc.annotate_types() + transform.transform(mut a, &tc) + mut metadata := Gen.new(a, &tc, map[string]bool{}) + config := metadata.ssa_configuration() or { panic(err) } + mut m := ssa.build_with_options(a, map[string]bool{}, &tc, ssa.BuildOptions{ + target: ssa.TargetData{ ptr_size: 4 } + }) + mut g := SSAGen.new(m) + g.set_wasm_cabi(cabi) + g.configure(config.exports, config.init_fns, config.main_fn) + g.gen() or { panic(err) } + return g.mod.compile() +} + +struct CabiTypeSig { + params []u8 + results []u8 +} + +struct CabiReader { +mut: + bytes []u8 + pos int +} + +fn (mut r CabiReader) leb() int { + mut n := 0 + mut shift := 0 + for { + b := r.bytes[r.pos] + r.pos++ + n |= (int(b) & 0x7f) << shift + shift += 7 + if b & 0x80 == 0 { + break + } + } + return n +} + +fn (mut r CabiReader) cname() string { + len := r.leb() + mut out := '' + for _ in 0 .. len { + out += r.bytes[r.pos].ascii_str() + r.pos++ + } + return out +} + +fn cabi_section(bytes []u8, id u8) []u8 { + mut r := CabiReader{ + bytes: bytes + pos: 8 + } + for r.pos + 2 <= bytes.len { + sid := bytes[r.pos] + r.pos++ + size := r.leb() + end := r.pos + size + if sid == id { + return bytes[r.pos..end] + } + r.pos = end + } + return []u8{} +} + +fn cabi_type_section(bytes []u8) []CabiTypeSig { + body := cabi_section(bytes, 1) + mut r := CabiReader{ + bytes: body + pos: 0 + } + count := r.leb() + mut out := []CabiTypeSig{} + for _ in 0 .. count { + assert r.bytes[r.pos] == 0x60 + r.pos++ + pc := r.leb() + mut params := []u8{} + for _ in 0 .. pc { + params << r.bytes[r.pos] + r.pos++ + } + rc := r.leb() + mut results := []u8{} + for _ in 0 .. rc { + results << r.bytes[r.pos] + r.pos++ + } + out << CabiTypeSig{ params: params, results: results } + } + return out +} + +fn cabi_func_section(bytes []u8) []int { + body := cabi_section(bytes, 3) + mut r := CabiReader{ + bytes: body + pos: 0 + } + count := r.leb() + mut out := []int{} + for _ in 0 .. count { + out << r.leb() + } + return out +} + +fn cabi_code_bodies(bytes []u8) [][]u8 { + body := cabi_section(bytes, 10) + mut r := CabiReader{ + bytes: body + pos: 0 + } + count := r.leb() + mut out := [][]u8{} + for _ in 0 .. count { + size := r.leb() + out << r.bytes[r.pos..r.pos + size] + r.pos += size + } + return out +} + +fn cabi_import_func_count(bytes []u8) int { + body := cabi_section(bytes, 2) + if body.len == 0 { + return 0 + } + mut r := CabiReader{ + bytes: body + pos: 0 + } + count := r.leb() + mut funcs := 0 + for _ in 0 .. count { + _ = r.cname() + _ = r.cname() + kind := r.bytes[r.pos] + r.pos++ + if kind == 0 { + _ = r.leb() + funcs++ + } else if kind == 1 { + r.pos++ // elem type + flags := r.leb() + _ = r.leb() + if flags & 1 != 0 { + _ = r.leb() + } + } else if kind == 2 { + flags := r.leb() + _ = r.leb() + if flags & 1 != 0 { + _ = r.leb() + } + } else if kind == 3 { + r.pos += 2 + } + } + return funcs +} + +// cabi_wasm_exports maps every exported function's name to its function index. +fn cabi_wasm_exports(bytes []u8) map[string]int { + mut out := map[string]int{} + mut r := CabiReader{ + bytes: cabi_section(bytes, 7) + pos: 0 + } + count := r.leb() + for _ in 0 .. count { + name := r.cname() + kind := r.bytes[r.pos] + r.pos++ + idx := r.leb() + if kind == 0 { + out[name] = idx + } + } + return out +} + +fn cabi_export_kind(bytes []u8, name string) int { + body := cabi_section(bytes, 7) + mut r := CabiReader{ + bytes: body + pos: 0 + } + count := r.leb() + for _ in 0 .. count { + n := r.cname() + kind := r.bytes[r.pos] + r.pos++ + idx := r.leb() + _ = idx + if n == name { + return kind + } + } + return -1 +} + +fn test_cabi_export_signature() { + b := cabi_test_module_bytes(cabi_guest_source, true) + // memory, cabi_realloc and echo_len are exported; echo_len is a function. + assert cabi_export_kind(b, 'memory') == 2 + assert cabi_export_kind(b, 'cabi_realloc') == 0 + assert cabi_export_kind(b, 'echo_len') == 0 + exports := cabi_wasm_exports(b) + funcs := cabi_func_section(b) + sigs := cabi_type_section(b) + n_imports := cabi_import_func_count(b) + assert n_imports == 0 + echo_idx := exports['echo_len'] + echo_type := sigs[funcs[echo_idx - n_imports]] + assert echo_type.params == [u8(0x7f), u8(0x7f)], 'echo_len params must be (i32, i32)' + assert echo_type.results == [u8(0x7f)], 'echo_len result must be i32' + // the inner single-pointer function is still present but unexported + mut found_inner := false + for name, _ in exports { + _ = name + } + for fi, ti in funcs { + if sigs[ti].params == [u8(0x7f)] && sigs[ti].results == [u8(0x7f)] { + global_idx := n_imports + fi + mut exported := false + for _, eidx in exports { + if eidx == global_idx { + exported = true + break + } + } + if !exported { + found_inner = true + } + } + } + assert found_inner, 'inner single-pointer function must exist unexported' +} + +fn test_cabi_wrapper_simulation() { + b := cabi_test_module_bytes(cabi_guest_source, true) + exports := cabi_wasm_exports(b) + funcs := cabi_func_section(b) + sigs := cabi_type_section(b) + bodies := cabi_code_bodies(b) + n_imports := cabi_import_func_count(b) + echo_idx := exports['echo_len'] + wrapper := bodies[echo_idx - n_imports] + // decode the straight-line wrapper body + mut pos := 0 + // locals decls + mut ndecl := 0 + mut shift := 0 + for { + byte := wrapper[pos] + pos++ + ndecl |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + assert ndecl == 0, 'wrapper must declare no locals' + for _ in 0 .. ndecl { + // count + type per entry (none expected) + mut c := 0 + shift = 0 + for { + byte := wrapper[pos] + pos++ + c |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + pos++ + _ = c + } + // the inner single-pointer function index is discovered from the wrapper's + // own call target below; here just require that some unexported (i32)->i32 + // function exists at all. + mut inner_idx := -1 + for fi, ti in funcs { + t := sigs[ti] + if t.params == [u8(0x7f)] && t.results == [u8(0x7f)] { + global_idx := n_imports + fi + mut exported := false + for _, eidx in exports { + if eidx == global_idx { + exported = true + break + } + } + if !exported && inner_idx == -1 { + inner_idx = global_idx + } + } + } + assert inner_idx >= 0 + // simulate: locals are [ptr, len]; memory is a fresh map + case_bytes := ['hello'.bytes(), ''.bytes(), 'héllo'.bytes()] + case_lens := [5, 0, 6] + case_first := [u8(`h`), u8(0), u8(`h`)] + for ci in 0 .. 3 { + str_bytes := case_bytes[ci] + str_len := case_lens[ci] + // host places the string at guest address 0x1000 + base := 0x1000 + mut mem := map[int]u8{} + for i, byte in str_bytes { + mem[base + i] = byte + } + mut locals := [base, str_len] + mut stack := []int{} + mut p := pos + mut scratch := -1 + mut called := -1 + for p < wrapper.len { + op := wrapper[p] + p++ + if op == 0x0b { + break + } else if op == 0x20 { + mut v := 0 + shift = 0 + for { + byte := wrapper[p] + p++ + v |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + stack << locals[v] + } else if op == 0x41 { + mut v := 0 + shift = 0 + mut by := u8(0) + for { + by = wrapper[p] + p++ + v |= (int(by) & 0x7f) << shift + shift += 7 + if by & 0x80 == 0 { + break + } + } + if by & 0x40 != 0 { + v -= 1 << shift + } + stack << v + } else if op == 0x36 || op == 0x3a { + mut al := 0 + shift = 0 + for { + byte := wrapper[p] + p++ + al |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + mut off := 0 + shift = 0 + for { + byte := wrapper[p] + p++ + off |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + _ = al + val := stack.pop() + addr := stack.pop() + if scratch == -1 { + scratch = addr + } + assert addr == scratch, 'all struct stores hit the scratch slot' + if op == 0x36 { + for k in 0 .. 4 { + mem[addr + off + k] = u8((val >> (8 * k)) & 0xff) + } + } else { + mem[addr + off] = u8(val & 0xff) + } + } else if op == 0x10 { + mut v := 0 + shift = 0 + for { + byte := wrapper[p] + p++ + v |= (int(byte) & 0x7f) << shift + shift += 7 + if byte & 0x80 == 0 { + break + } + } + called = v + _ = stack.pop() + stack << -1 // call result marker + } else { + assert false, 'unexpected wrapper op ${op:02x} in case ${ci}' + } + } + // the wrapper's own call target must be an unexported single-pointer func + called_sig := sigs[funcs[called - n_imports]] + assert called_sig.params == [u8(0x7f)], 'callee takes one i32' + assert called_sig.results == [u8(0x7f)], 'callee returns i32' + mut called_exported := false + for _, eidx in exports { + if eidx == called { + called_exported = true + } + } + assert !called_exported, 'callee must be the unexported inner function' + assert stack.len == 1, 'wrapper must leave exactly the call result' + // the materialized V string the inner function will read + mut got_ptr := 0 + for k in 0 .. 4 { + got_ptr |= int(mem[scratch + k]) << (8 * k) + } + mut got_len := 0 + for k in 0 .. 4 { + got_len |= int(mem[scratch + 4 + k]) << (8 * k) + } + assert got_ptr == base, 'struct.str must be the host pointer' + assert got_len == str_len, 'struct.len must be the host length' + assert mem[scratch + 8] == 0, 'is_lit must be false' + // first byte the guest would read + if str_len > 0 { + assert mem[got_ptr] == case_first[ci], 'first byte must match' + } + } + // length pinning: host passes len 2 for the 3-byte é prefix + he := 'héllo'.bytes() + assert he.len == 6 + assert he[0] == `h` + pinned := he[..2] + assert pinned.len == 2 +} + +fn test_cabi_native_len_semantics() { + assert 'hello'.len == 5 + assert ''.len == 0 + assert 'héllo'.len == 6, 'len counts UTF-8 bytes, not codepoints' + assert 'hello'[0] == `h` + pinned := 'héllo'[..2] + assert pinned.len == 2, 'a 2-byte slice has len 2' +} + +fn test_cabi_runtime_node() { + b := cabi_test_module_bytes(cabi_guest_source, true) + node := os.find_abs_path_of_executable('node') or { + panic('node is required to run the cabi runtime test') + } + dir := os.join_path(os.vtmp_dir(), 'cabi_run_${os.getpid()}') + os.mkdir_all(dir) or { panic(err) } + defer { + os.rmdir_all(dir) or {} + } + wasm_path := os.join_path(dir, 'guest.wasm') + os.write_file_array(wasm_path, b) or { panic(err) } + mjs := "import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +const bytes = readFileSync(process.argv[2]); +const { instance } = await WebAssembly.instantiate(bytes, {}); +const mem = () => new Uint8Array(instance.exports.memory.buffer); +const echo_len = instance.exports.echo_len; +const first_byte = instance.exports.first_byte; +const shout = instance.exports.shout; +const hello = instance.exports.hello; +function put(off, arr) { mem().set(arr, off); } +// hello -> 5 +put(4096, [104, 101, 108, 108, 111]); +assert.equal(echo_len(4096, 5), 5); +assert.equal(first_byte(4096, 5), 104); +// empty -> 0 +assert.equal(echo_len(4096, 0), 0); +// h + U+00E9 + llo is 6 UTF-8 bytes, not 5 codepoints +put(8192, [104, 195, 169, 108, 108, 111]); +assert.equal(echo_len(8192, 6), 6); +assert.equal(first_byte(8192, 6), 104); +// length pins to what the host passes, not the NUL scan +assert.equal(echo_len(8192, 2), 2); +assert.equal(shout(4096, 5, 1), 105); +assert.equal(shout(4096, 5, 0), 5); +console.log('cabi-shout-ok shout=105,5'); +const hr = hello(); +assert.equal(hr.length, 2); +const hp = hr[0]; +const hl = hr[1]; +assert.equal(hl, 2); +const got = Array.from(mem().slice(hp, hp + hl)); +assert.deepEqual(got, [104, 105]); +console.log('cabi-runtime-ok echo_len=5,0,6 first_byte=104 hello=hi pin=2'); +" + runner := os.join_path(dir, 'check.mjs') + os.write_file(runner, mjs) or { panic(err) } + result := os.exec([node, runner, wasm_path]) + assert result.exit_code == 0, result.output + assert result.output.contains('cabi-shout-ok'), result.output + assert result.output.contains('cabi-runtime-ok'), result.output +} + +fn test_cabi_parity_shim() { + shim := os.read_file('vlib/v/gen/wasm/testdata/echo_len.shim.wasm') or { panic(err) } + shim_bytes := shim.bytes() + backend := cabi_test_module_bytes(cabi_guest_source, true) + // frozen shim exports echo_len_cabi as (i32, i32) -> i32 ... + shim_exports := cabi_wasm_exports(shim_bytes) + assert 'echo_len_cabi' in shim_exports + shim_types := cabi_type_section(shim_bytes) + shim_funcs := cabi_func_section(shim_bytes) + shim_nimp := cabi_import_func_count(shim_bytes) + shim_sig := shim_types[shim_funcs[shim_exports['echo_len_cabi'] - shim_nimp]] + assert shim_sig.params == [u8(0x7f), u8(0x7f)] + assert shim_sig.results == [u8(0x7f)] + // ... and the backend emits the identical signature for echo_len ... + be_exports := cabi_wasm_exports(backend) + be_types := cabi_type_section(backend) + be_funcs := cabi_func_section(backend) + be_nimp := cabi_import_func_count(backend) + assert be_nimp == 0 + be_sig := be_types[be_funcs[be_exports['echo_len'] - be_nimp]] + assert be_sig.params == shim_sig.params + assert be_sig.results == shim_sig.results + // ... while additionally exporting memory and cabi_realloc (the shim predates both). + assert cabi_export_kind(shim_bytes, 'cabi_realloc') == -1 + assert cabi_export_kind(backend, 'memory') == 2 + assert cabi_export_kind(backend, 'cabi_realloc') == 0 +} diff --git a/vlib/v/gen/wasm/gen.v b/vlib/v/gen/wasm/gen.v index d52506dd74c884..2da5711365455a 100644 --- a/vlib/v/gen/wasm/gen.v +++ b/vlib/v/gen/wasm/gen.v @@ -26,6 +26,8 @@ pub enum WType { i64 f32 f64 + // cabi_string marks a V string param lowered to canonical-ABI (i32 ptr, i32 len). + cabi_string } struct FnInfo { @@ -36,6 +38,9 @@ struct FnInfo { params []WType ret WType index int + // cabi_string_params holds the AST param names that are strings expanded + // to two i32 params (ptr, len) in cabi mode. + cabi_string_params []string } struct Frame { @@ -61,10 +66,11 @@ enum FrameTag { @[heap] pub struct Gen { mut: - a &flat.FlatAst = unsafe { nil } - tc &types.TypeChecker = unsafe { nil } - used_fns map[string]bool - mod &Module = unsafe { nil } + a &flat.FlatAst = unsafe { nil } + tc &types.TypeChecker = unsafe { nil } + used_fns map[string]bool + wasm_cabi bool + mod &Module = unsafe { nil } // per-function state cur Code local_types []u8 @@ -437,8 +443,20 @@ fn (mut g Gen) collect_user_fns() []FnInfo { } else { '' } - if fi := g.fn_info(node, flat.NodeId(i), mod_key, cur_file) { - key := qualified_fn_key(mod_key, node.value) + // Canonical-ABI string lowering applies to exported functions only; + // internal functions keep the single-pointer representation. This + // mirrors the export predicate below (main-module or explicit + // @[export]), using the same key forms as explicit_export_fn_name. + key := qualified_fn_key(mod_key, node.value) + mut is_export := mod_key == '' || mod_key == 'main' + if !is_export { + if key in g.a.export_fn_names + || '${mod_key}.${node.value}' in g.a.export_fn_names + || '${mod_key.all_after_last('.')}.${node.value}' in g.a.export_fn_names { + is_export = true + } + } + if fi := g.fn_info(node, flat.NodeId(i), mod_key, cur_file, is_export) { if key !in candidates { candidates[key] = fi order << key @@ -613,14 +631,22 @@ fn wasm_aggregate_param_ok(tc &types.TypeChecker, typ types.Type, depth int) boo } } -fn (mut g Gen) fn_info(node flat.Node, id flat.NodeId, module_ string, file string) ?FnInfo { +fn (mut g Gen) fn_info(node flat.Node, id flat.NodeId, module_ string, file string, is_export bool) ?FnInfo { mut params := []WType{} + mut cabi_string_params := []string{} for i in 0 .. node.children_count { p := g.a.child_node(&node, i) if p.kind != .param || p.value.len == 0 { continue } pt := g.tc.parse_type(p.typ) + if g.wasm_cabi && is_export && is_string_type(pt) { + // Canonical ABI: string param → (i32 ptr, i32 len). + params << WType.i32 + params << WType.i32 + cabi_string_params << p.value + continue + } // Aggregate parameters travel as pointers; supported aggregate returns // use the hidden sret slot handled below. w := prim_wtype(pt) or { @@ -654,12 +680,13 @@ fn (mut g Gen) fn_info(node flat.Node, id flat.NodeId, module_ string, file stri params.prepend(WType.i32) } return FnInfo{ - name: node.value - node_id: id - module: module_ - file: file - params: params - ret: ret + name: node.value + node_id: id + module: module_ + file: file + params: params + ret: ret + cabi_string_params: cabi_string_params } } @@ -716,6 +743,20 @@ fn (mut g Gen) emit_user_fn(f FnInfo) { if p.kind != .param || p.value.len == 0 { continue } + if p.value in f.cabi_string_params { + // Canonical-ABI string param: two i32 slots (ptr, len). The legacy + // Gen path does not materialize wrappers (see SSAGen); bind the + // name to the pointer slot so indexing stays in bounds. + pt := g.tc.parse_type(p.typ) + g.var_index[p.value] = pi + g.var_wtype[p.value] = WType.cabi_string + g.var_unsigned[p.value] = type_is_unsigned(pt) + g.var_widths[p.value] = narrow_width(pt) + wparams << wt_valtype(f.params[pi]) + wparams << wt_valtype(f.params[pi + 1]) + pi += 2 + continue + } w := f.params[pi] pt := g.tc.parse_type(p.typ) g.var_index[p.value] = pi @@ -2827,6 +2868,7 @@ fn (mut g Gen) warn(msg string) { fn wt_valtype(w WType) u8 { return match w { .i32, .void { valtype_i32 } + .cabi_string { valtype_i32 } // marker only; expanded to two params at emission .i64 { valtype_i64 } .f32 { valtype_f32 } .f64 { valtype_f64 } @@ -2888,6 +2930,25 @@ fn unalias(t types.Type) types.Type { return cur } +// is_string_type reports whether t is a V string (after alias resolution). +fn is_string_type(t_ types.Type) bool { + t := unalias(t_) + if t is types.String { + return true + } + return false +} + +// prim_wtype_cabi maps a V type for an exported function in cabi mode: strings +// become the cabi_string marker (later expanded to (i32 ptr, i32 len)); every +// other type lowers exactly as prim_wtype lowers it. +fn prim_wtype_cabi(t types.Type) ?WType { + if is_string_type(t) { + return WType.cabi_string + } + return prim_wtype(t) +} + // prim_wtype maps a primitive/numeric/bool V type to a WASM value type, or none // for aggregate/reference types the backend cannot yet represent inline. fn prim_wtype(t_ types.Type) ?WType { diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v index 97e38164199412..232a95fa3e7660 100644 --- a/vlib/v/gen/wasm/ssa_gen.v +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -43,6 +43,25 @@ mut: nparams int cur_ret ssa.TypeID warnings []string + // wasm_cabi enables canonical-ABI wrappers for exported string params. + wasm_cabi bool + // cabi_scratch is the linear-memory address of the 16-byte slot used by + // canonical-ABI string wrappers to materialize V strings. Reserved from + // module data before the stack area; -1 when cabi mode is off. + // Known limitation: scratch slots are NOT reentrant. A nested + // host -> guest -> host -> guest call reuses the same slots and clobbers + // the outer call's materialized strings. Reentrancy needs a per-call + // stack-allocated frame or a host-side copy; not implemented yet. + // Known limitation: string results assume static-lifetime data. The + // wrapper reads (ptr, len) back from the hidden sret slot after the + // inner call returns; a host that retains the pointer past the next + // call into the guest may observe overwritten bytes. + cabi_scratch int = -1 +} + +// set_wasm_cabi enables canonical-ABI wrappers for exported string params. +pub fn (mut g SSAGen) set_wasm_cabi(v bool) { + g.wasm_cabi = v } // new creates a WebAssembly generator for an SSA module. @@ -72,6 +91,41 @@ pub fn (mut g SSAGen) declare_imports(imports []WasmImport) { pub fn (mut g SSAGen) gen() ! { functions := g.reachable_functions()! g.collect_data(functions, g.reachable_values(functions)) + if g.wasm_cabi { + // Reserve one 16-byte scratch slot per simultaneously-live string + // param before the stack area so wrapper materializations never + // overlap stack frames. + mut max_strs := 0 + for fid in functions { + f := g.m.funcs[fid] + if g.configured && f.name !in g.exports { + continue + } + if !g.cabi_wrappable(fid) { + continue + } + mut n := 0 + for pid in f.params { + if g.cabi_string_param(g.m.values[pid].typ) { + n++ + } + } + if g.cabi_string_param(f.typ) { + n++ + } + if n > max_strs { + max_strs = n + } + } + if max_strs > 0 { + aligned := (data_base + g.data.len + 3) & ~3 + for g.data.len < aligned - data_base { + g.data << u8(0) + } + g.cabi_scratch = data_base + g.data.len + g.data << []u8{len: 16 * max_strs} + } + } mut init := Code{} g.stack_bottom = (data_base + g.data.len + 15) & ~15 stack_top := g.stack_bottom + 1024 * 1024 @@ -140,9 +194,19 @@ pub fn (mut g SSAGen) gen() ! { g.emit_function(fid)! } g.mod.add_export('memory', export_mem, 0) + if g.wasm_cabi { + g.mod.add_export('cabi_realloc', export_func, g.alloc_index) + } mut used_exports := map[string]bool{} used_exports['memory'] = true used_exports['_start'] = true + if g.wasm_cabi { + used_exports['cabi_realloc'] = true + } + // cabi_wrapped collects exports that get a canonical-ABI string wrapper + // instead of a direct export: the inner function keeps its index but is + // not exported; the wrapper is exported under the original name. + mut cabi_wrapped := []CabiWrap{} for fid in functions { f := g.m.funcs[fid] if g.configured && f.name !in g.exports { @@ -157,9 +221,23 @@ pub fn (mut g SSAGen) gen() ! { g.warnings << 'not exporting ${f.name.all_after_last('.')}: export name `${name}` is already in use' continue } + if g.wasm_cabi && g.cabi_wrappable(fid) { + cabi_wrapped << CabiWrap{ + export_name: name + fid: fid + } + used_exports[name] = true + continue + } used_exports[name] = true g.mod.add_export(name, export_func, g.fn_index[f.name]) } + for w in cabi_wrapped { + f := g.m.funcs[w.fid] + inner := g.fn_index[f.name] + idx := g.emit_cabi_wrapper(inner, w.fid) + g.mod.add_export(w.export_name, export_func, idx) + } mut entry := Code{} for name in g.init_fns { idx := g.fn_index[name] or { return error('wasm: missing init function `${name}`') } @@ -183,6 +261,159 @@ pub fn (mut g SSAGen) gen() ! { g.mod.add_data(nl_ptr, [u8(10)]) } +// CabiWrap describes an export that gets a canonical-ABI string wrapper. +struct CabiWrap { + export_name string + fid int +} + +// cabi_string_param reports whether an SSA type is a V string struct +// (fields str, len, is_lit as laid out by the SSA builder). +fn (g &SSAGen) cabi_string_param(typ ssa.TypeID) bool { + if typ <= 0 || typ >= g.m.type_store.types.len { + return false + } + t := g.m.type_store.types[typ] + if t.kind != .struct_t { + return false + } + return t.field_names == ['str', 'len', 'is_lit'] +} + +// cabi_wrappable reports whether an exported function gets a canonical-ABI +// string wrapper: at least one string-typed param and a void, i32, or string +// result. Non-string params are forwarded unchanged, so any value type is +// allowed for them. +fn (g &SSAGen) cabi_wrappable(fid int) bool { + f := g.m.funcs[fid] + mut strs := 0 + for pid in f.params { + pt := g.m.values[pid].typ + if g.cabi_string_param(pt) { + strs++ + continue + } + if g.wtype(pt) == .void { + return false + } + } + ret_is_string := g.cabi_string_param(f.typ) + if strs == 0 && !ret_is_string { + return false + } + if ret_is_string { + return true + } + rt := g.wtype(f.typ) + return rt == .void || rt == .i32 +} + +// emit_cabi_wrapper emits a canonical-ABI wrapper for an exported function +// with string params: each V string param becomes (i32 ptr, i32 len), other +// params are forwarded unchanged, and a V string result becomes an +// (i32 ptr, i32 len) pair. String params are materialized as +// string{str, len, is_lit: false} structs in the module scratch slots; +// a string result uses the backend's hidden sret slot convention +// (caller-provided struct pointer, void core return). Wrappers need no +// locals and leave the value stack balanced. +// Known limitations (see also the cabi_scratch field docs): +// - scratch slots are not reentrant; nested host -> guest -> host -> guest +// calls would clobber the outer call's materialized strings; +// - string results assume static-lifetime data (the (ptr, len) pair is only +// valid until the next guest call that reuses the sret/scratch area); +// - lists, records, variants, options/results, tuples, and maps have no +// canonical layout here yet; see vlib/wasm/cabi_layout.v for the +// guest-side layout helpers that the next wrapper step must use. +fn (mut g SSAGen) emit_cabi_wrapper(inner_idx int, fid int) int { + f := g.m.funcs[fid] + mut wparams := []u8{} + for pid in f.params { + pt := g.m.values[pid].typ + if g.cabi_string_param(pt) { + wparams << valtype_i32 + wparams << valtype_i32 + } else { + wparams << wt_valtype(g.wtype(pt)) + } + } + str_ret := g.cabi_string_param(f.typ) + mut results := []u8{} + if str_ret { + results << valtype_i32 + results << valtype_i32 + } else if g.wtype(f.typ) != .void { + results << wt_valtype(g.wtype(f.typ)) + } + // sret slot for a string result: the inner function fills a V string + // struct there (SSAGen passes aggregates by hidden caller slot). + mut nstr := 0 + for pid in f.params { + if g.cabi_string_param(g.m.values[pid].typ) { + nstr++ + } + } + sret_slot := g.cabi_scratch + 16 * nstr + mut locals := []u8{} + mut c := Code{} + // materialize each string param into its scratch slot + mut si := 0 + mut wi := 0 + for pid in f.params { + pt := g.m.values[pid].typ + if g.cabi_string_param(pt) { + slot := g.cabi_scratch + 16 * si + off0 := g.m.struct_field_offset(pt, 0) + off1 := g.m.struct_field_offset(pt, 1) + has_lit := g.m.type_store.types[pt].fields.len > 2 + off2 := g.m.struct_field_offset(pt, 2) + c.i32_const(i64(slot)) + c.local_get(wi) + c.i32_store(off0) + c.i32_const(i64(slot)) + c.local_get(wi + 1) + c.i32_store(off1) + if has_lit { + c.i32_const(i64(slot)) + c.i32_const(0) + c.i32_store8(off2) + } + si++ + wi += 2 + } else { + wi += 1 + } + } + // call the inner function with args in the original order + si = 0 + wi = 0 + for pid in f.params { + pt := g.m.values[pid].typ + if g.cabi_string_param(pt) { + c.i32_const(i64(g.cabi_scratch + 16 * si)) + si++ + wi += 2 + } else { + c.local_get(wi) + wi += 1 + } + } + if str_ret { + // string results use the hidden sret slot: pass it, then read back + // the (ptr, len) the inner function stored there + c.i32_const(i64(sret_slot)) + } + c.call(inner_idx) + if str_ret { + c.i32_const(i64(sret_slot)) + c.i32_load(g.m.struct_field_offset(f.typ, 0)) + c.i32_const(i64(sret_slot)) + c.i32_load(g.m.struct_field_offset(f.typ, 1)) + } + c.end() + ti := g.mod.add_type(wparams, results) + return g.mod.add_func(ti, locals, c.bytes) +} + // write writes the generated module to path. pub fn (g &SSAGen) write(path string) ! { os.write_file_array(path, g.mod.compile())! diff --git a/vlib/v/gen/wasm/testdata/echo_len.shim.wasm b/vlib/v/gen/wasm/testdata/echo_len.shim.wasm new file mode 100644 index 00000000000000..970daff541197a Binary files /dev/null and b/vlib/v/gen/wasm/testdata/echo_len.shim.wasm differ diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 81c8993a48d41f..364bfd97747ed8 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -109,6 +109,12 @@ pub: pub mut: // hot_fn extracts raw machine code for a single function only (hot reload). hot_fn string + // wasm_cabi enables canonical-ABI string params for wasm exports. + wasm_cabi bool + // component requests wrapping the wasm output into a WebAssembly component. + component bool + // wit_file is the path to a WIT file used with -component. + wit_file string } // without_excluded returns files, minus the ones matched by a `-exclude` pattern. diff --git a/vlib/wasi/README.md b/vlib/wasi/README.md new file mode 100644 index 00000000000000..2071233b8dbdac --- /dev/null +++ b/vlib/wasi/README.md @@ -0,0 +1,3 @@ +# wasi + +High-level helpers for WASI Preview 1, usable when compiling V to `wasm32-wasi`. \ No newline at end of file diff --git a/vlib/wasi/wasi.v b/vlib/wasi/wasi.v new file mode 100644 index 00000000000000..4f6ad4d861e5fb --- /dev/null +++ b/vlib/wasi/wasi.v @@ -0,0 +1,91 @@ +module wasi + +// WASI Preview 1 helpers for V programs compiled to WebAssembly (wasm32-wasi). + +pub struct IOVec { +pub: + buf &u8 + len uint +} + +pub fn fd_write(fd int, iovs []IOVec, nwritten &uint) int { + $if wasm32_wasi { + if iovs.len == 0 { + return 0 + } + mut errno := 0 + unsafe { + errno = int(WASM.fd_write(fd, &iovs[0], uint(iovs.len), nwritten)) + } + return errno + } $else { + _ = fd + _ = iovs + _ = nwritten + return 0 + } +} + +pub fn fd_read(fd int, iovs []IOVec, nread &uint) int { + $if wasm32_wasi { + if iovs.len == 0 { + return 0 + } + mut errno := 0 + unsafe { + errno = int(WASM.fd_read(fd, &iovs[0], uint(iovs.len), nread)) + } + return errno + } $else { + _ = fd + _ = iovs + _ = nread + return 0 + } +} + +pub fn fd_sync(fd int) int { + $if wasm32_wasi { + return int(WASM.fd_sync(fd)) + } $else { + _ = fd + return 0 + } +} + +pub fn random_get(buf []u8) int { + $if wasm32_wasi { + if buf.len == 0 { + return 0 + } + mut errno := 0 + unsafe { + errno = int(WASM.random_get(&buf[0], uint(buf.len))) + } + return errno + } $else { + _ = buf + return 0 + } +} + +pub fn args_sizes_get() (int, int) { + $if wasm32_wasi { + mut argc := u32(0) + mut argc_buf_size := u32(0) + unsafe { + WASM.args_sizes_get(&argc, &argc_buf_size) + } + return int(argc), int(argc_buf_size) + } $else { + return 0, 0 + } +} + +pub fn proc_exit(code int) { + $if wasm32_wasi { + WASM.proc_exit(code) + } $else { + _ = code + } +} \ No newline at end of file diff --git a/vlib/wasi/wasi_test.v b/vlib/wasi/wasi_test.v new file mode 100644 index 00000000000000..d099e137fa1d9d --- /dev/null +++ b/vlib/wasi/wasi_test.v @@ -0,0 +1,16 @@ +module wasi + +fn test_fd_sync_host() { + assert fd_sync(1) == 0 +} + +fn test_random_get_host() { + buf := []u8{len: 4} + assert random_get(buf) == 0 +} + +fn test_args_sizes_host() { + argc, size := args_sizes_get() + assert argc == 0 + assert size == 0 +} \ No newline at end of file diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 66e9af42a4cca9..8359e84f79fd47 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -37,3 +37,249 @@ fn main() { This module does not perform verification of the WebAssembly output. Use a tool like `wasm-validate` to validate, and `wasm-dis` to show a decompiled form. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. + +## Component model + +`component.v` provides experimental helpers to wrap a core WebAssembly module into a +WebAssembly component (`wrap_core_module`) and to lift exported functions with +primitive signatures. diff --git a/vlib/wasm/cabi_layout.v b/vlib/wasm/cabi_layout.v new file mode 100644 index 00000000000000..e7998e0af598ab --- /dev/null +++ b/vlib/wasm/cabi_layout.v @@ -0,0 +1,153 @@ +module wasm + +// cabi_primitive_size_align maps a canonical-ABI primitive name to +// (size, align) in bytes, following the Component Model Canonical ABI +// element-size table. Returns (-1, -1) for non-primitive names +// ('string' is not a primitive here; use cabi_string_layout). +pub fn cabi_primitive_size_align(prim string) (int, int) { + match prim { + 'bool', 'u8', 's8' { return 1, 1 } + 'u16', 's16' { return 2, 2 } + 'u32', 's32', 'char', 'f32' { return 4, 4 } + 'u64', 's64', 'f64' { return 8, 8 } + else { return -1, -1 } + } +} + +// cabi_align_up rounds off up to a multiple of align. +pub fn cabi_align_up(off int, align int) int { + if align <= 1 { + return off + } + return (off + align - 1) / align * align +} + +// cabi_string_layout is the canonical string layout: (ptr, len), 8 bytes, +// align 4. Lists use the same shape; see cabi_list_layout. +pub fn cabi_string_layout() (int, int) { + return 8, 4 +} + +// cabi_list_layout is the canonical list layout: (ptr, len), 8 bytes, +// align 4, regardless of element type. Element bytes live in linear memory +// allocated via cabi_realloc; the pair itself travels inline. +pub fn cabi_list_layout() (int, int) { + return 8, 4 +} + +// cabi_discriminant_size_align sizes a case discriminant for ncases cases: +// 1 byte up to 256 cases, 2 bytes up to 65536, else 4. +pub fn cabi_discriminant_size_align(ncases int) (int, int) { + if ncases <= 256 { + return 1, 1 + } else if ncases <= 65536 { + return 2, 2 + } + return 4, 4 +} + +// cabi_enum_layout sizes a WIT enum by case count (same rule as a bare +// discriminant, no payload). +pub fn cabi_enum_layout(ncases int) (int, int) { + return cabi_discriminant_size_align(ncases) +} + +// cabi_flags_layout sizes a WIT flags set with nflags bits: the bitfield is +// rounded up to a whole number of bytes, then to 1, 2, or 4 (align equals +// size). A zero-flag set still occupies one byte. +pub fn cabi_flags_layout(nflags int) (int, int) { + nbytes := if nflags <= 0 { 1 } else { (nflags + 7) / 8 } + if nbytes <= 1 { + return 1, 1 + } else if nbytes == 2 { + return 2, 2 + } + return 4, 4 +} + +// CabiField is one record/tuple field or variant case payload layout. +pub struct CabiField { +pub: + name string + size int + align int +} + +// cabi_record_layout lays out fields in order with canonical alignment: +// each field starts at align_up(offset, field.align); the record size is +// align_up(end, max_align) and its align is max field align (min 1). +// Returns the per-field offsets plus (size, align). +pub fn cabi_record_layout(fields []CabiField) ([]int, int, int) { + mut offsets := []int{cap: fields.len} + mut off := 0 + mut max_align := 1 + for f in fields { + a := if f.align < 1 { 1 } else { f.align } + if a > max_align { + max_align = a + } + off = cabi_align_up(off, a) + offsets << off + off += f.size + } + return offsets, cabi_align_up(off, max_align), max_align +} + +// cabi_tuple_layout is identical to a record layout without field names. +pub fn cabi_tuple_layout(items []CabiField) ([]int, int, int) { + return cabi_record_layout(items) +} + +// cabi_option_layout lays out option: a 1-byte discriminant (two cases) +// followed by the payload at its own alignment. Size is +// align_up(1, payload_align) + payload_size, aligned up to payload_align; +// align is max(1, payload_align). A payload of size 0 still reserves the +// discriminant byte. +pub fn cabi_option_layout(payload_size int, payload_align int) (int, int) { + a := if payload_align < 1 { 1 } else { payload_align } + off := cabi_align_up(1, a) + payload_size + return cabi_align_up(off, a), a +} + +// cabi_result_layout lays out result: a 1-byte discriminant plus the +// larger of the ok/err payloads (union). Align is the max of the payload +// aligns (min 1); size is the discriminant slot plus the max payload size, +// aligned up to that align. Either side may be absent (size 0, align 1). +pub fn cabi_result_layout(ok_size int, ok_align int, err_size int, err_align int) (int, int) { + mut a := ok_align + if err_align > a { + a = err_align + } + if a < 1 { + a = 1 + } + mut payload := ok_size + if err_size > payload { + payload = err_size + } + off := cabi_align_up(1, a) + payload + return cabi_align_up(off, a), a +} + +// cabi_variant_layout lays out a variant with ncases cases: the discriminant +// is sized by case count, then the largest case payload follows at its own +// alignment. Cases carry their payload layout in order; a case with no +// payload contributes size 0, align 1. Returns (payload_offset, size, align) +// where payload_offset is the discriminant slot rounded up to the max case +// align (i.e. where any payload starts). +pub fn cabi_variant_layout(ncases int, cases []CabiField) (int, int, int) { + dsize, _ := cabi_discriminant_size_align(ncases) + mut max_align := 1 + mut max_size := 0 + for c in cases { + a := if c.align < 1 { 1 } else { c.align } + if a > max_align { + max_align = a + } + if c.size > max_size { + max_size = c.size + } + } + payload_off := cabi_align_up(dsize, max_align) + return payload_off, cabi_align_up(payload_off + max_size, max_align), max_align +} diff --git a/vlib/wasm/cabi_layout_test.v b/vlib/wasm/cabi_layout_test.v new file mode 100644 index 00000000000000..dc5d5c7d50c308 --- /dev/null +++ b/vlib/wasm/cabi_layout_test.v @@ -0,0 +1,87 @@ +module wasm + +fn test_cabi_primitive_layouts() { + sz, al := cabi_primitive_size_align('u8') + assert sz == 1 && al == 1 + sz2, al2 := cabi_primitive_size_align('u16') + assert sz2 == 2 && al2 == 2 + sz4, al4 := cabi_primitive_size_align('u32') + assert sz4 == 4 && al4 == 4 + sz8, al8 := cabi_primitive_size_align('u64') + assert sz8 == 8 && al8 == 8 + szf, alf := cabi_primitive_size_align('f32') + assert szf == 4 && alf == 4 + szb, alb := cabi_primitive_size_align('bool') + assert szb == 1 && alb == 1 + bad, bada := cabi_primitive_size_align('string') + assert bad == -1 && bada == -1 +} + +fn test_cabi_string_and_list_layouts() { + ssz, sal := cabi_string_layout() + assert ssz == 8 && sal == 4 + lsz, lal := cabi_list_layout() + assert lsz == 8 && lal == 4 +} + +fn test_cabi_enum_and_flags_layouts() { + esz, eal := cabi_enum_layout(3) + assert esz == 1 && eal == 1 + esz2, _ := cabi_enum_layout(300) + assert esz2 == 2 + esz3, _ := cabi_enum_layout(70000) + assert esz3 == 4 + fsz, fal := cabi_flags_layout(3) + assert fsz == 1 && fal == 1 + fsz2, _ := cabi_flags_layout(9) + assert fsz2 == 2 + fsz3, _ := cabi_flags_layout(17) + assert fsz3 == 4 +} + +fn test_cabi_record_layout_flat() { + // record { x: u8, y: u16 } -> y at offset 2, size 4, align 2. + offs, size, align := cabi_record_layout([CabiField{'x', 1, 1}, CabiField{'y', 2, 2}]) + assert offs == [0, 2] + assert size == 4 + assert align == 2 + // record { a: u32, b: u8 } -> b at offset 4, size 8, align 4. + offs2, size2, align2 := cabi_record_layout([CabiField{'a', 4, 4}, CabiField{'b', 1, 1}]) + assert offs2 == [0, 4] + assert size2 == 8 + assert align2 == 4 +} + +fn test_cabi_tuple_layout() { + offs, size, align := cabi_tuple_layout([CabiField{'0', 4, 4}, CabiField{'1', 8, 8}]) + assert offs == [0, 8] + assert size == 16 + assert align == 8 +} + +fn test_cabi_option_result_layouts() { + // option: discriminant + u16 payload at offset 2, size 4, align 2. + osz, oal := cabi_option_layout(2, 2) + assert osz == 4 && oal == 2 + // option: payload at offset 4, size 8, align 4. + osz2, oal2 := cabi_option_layout(4, 4) + assert osz2 == 8 && oal2 == 4 + // result: union payload 4, size 8, align 4. + rsz, ral := cabi_result_layout(1, 1, 4, 4) + assert rsz == 8 && ral == 4 + // result<_, _> with no payloads: just the discriminant. + rsz2, ral2 := cabi_result_layout(0, 1, 0, 1) + assert rsz2 == 1 && ral2 == 1 +} + +fn test_cabi_variant_layout() { + // variant { a: u8, b: u32 } with 2 cases: discriminant 1 byte, + // payload union of (1,1) and (4,4) starts at offset 4, total size 8. + poff, size, align := cabi_variant_layout(2, [CabiField{'a', 1, 1}, CabiField{'b', 4, 4}]) + assert poff == 4 + assert size == 8 + assert align == 4 + // variant with 300 cases: discriminant 2 bytes. + poff2, _, _ := cabi_variant_layout(300, [CabiField{'a', 1, 1}]) + assert poff2 == 2 +} diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v new file mode 100644 index 00000000000000..c23129b901ce54 --- /dev/null +++ b/vlib/wasm/component.v @@ -0,0 +1,412 @@ +module wasm + +import encoding.leb128 + +pub enum ComponentSection as u8 { + custom_section + core_module_section = 1 + core_instance_section + core_type_section + component_section + instance_section + alias_section + type_section + canon_section + start_section + import_section + export_section + value_section +} + +// ---- helpers ---- + +fn read_uleb128(data []u8, ip &int) u32 { + mut i := *ip + mut result := u32(0) + mut shift := u32(0) + for { + if i >= data.len { break } + b := u32(data[i]) + i++ + result |= (b & 0x7f) << shift + if b & 0x80 == 0 { break } + shift += 7 + } + unsafe { *ip = i } + return result +} + +fn read_name(data []u8, ip &int) string { + n := read_uleb128(data, ip) + mut i := *ip + if i + int(n) > data.len { + unsafe { *ip = i } + return '' + } + s := data[i..i + int(n)].bytestr() + i += int(n) + unsafe { *ip = i } + return s +} + +struct CoreFuncType { + params []u8 + results []u8 +} + +struct CoreExport { + name string + kind u8 + idx int +} + +// parse core wasm module: returns func exports and func type info. +fn parse_core_module(core []u8) ([]CoreExport, []CoreFuncType, []int, int, []int) { + mut exports := []CoreExport{} + mut types := []CoreFuncType{} + mut import_func_types := []int{} + mut defined_func_typeidx := []int{} + mut import_func_count := 0 + if core.len < 8 { return exports, types, defined_func_typeidx, 0, []int{} } + mut i := 8 + for i < core.len { + if i >= core.len { break } + sec_id := core[i] + i++ + sec_size := read_uleb128(core, &i) + end := i + sec_size + if sec_id == 1 { + // type section + count := read_uleb128(core, &i) + for _ in 0 .. count { + // func type: 0x60 + if i < core.len && core[i] == 0x60 { i++ } + pc := read_uleb128(core, &i) + mut params := []u8{} + for _ in 0 .. pc { + params << core[i] + i++ + } + rc := read_uleb128(core, &i) + mut results := []u8{} + for _ in 0 .. rc { + results << core[i] + i++ + } + types << CoreFuncType{ params: params, results: results } + } + } else if sec_id == 2 { + // import section + count := read_uleb128(core, &i) + for _ in 0 .. count { + read_name(core, &i) // module name + read_name(core, &i) // field name + kind := core[i] + i++ + if kind == 0 { + ti := read_uleb128(core, &i) + import_func_types << ti + import_func_count++ + } else if kind == 1 { + // table + i++ // elem type + // limits + ft := core[i] + i++ + read_uleb128(core, &i) + if ft & 0x01 != 0 { read_uleb128(core, &i) } + } else if kind == 2 { + // memory + ft := core[i] + i++ + read_uleb128(core, &i) + if ft & 0x01 != 0 { read_uleb128(core, &i) } + } else if kind == 3 { + // global + i += 2 // valtype + mut + } + } + } else if sec_id == 3 { + // function section + count := read_uleb128(core, &i) + for _ in 0 .. count { + ti := read_uleb128(core, &i) + defined_func_typeidx << ti + } + } else if sec_id == 7 { + // export section + count := read_uleb128(core, &i) + for _ in 0 .. count { + name := read_name(core, &i) + kind := core[i] + i++ + idx := read_uleb128(core, &i) + exports << CoreExport{ name: name, kind: kind, idx: idx } + } + } + i = end + } + // build func_types slice aligned with function index space + mut func_typeidx := []int{} + for t in import_func_types { func_typeidx << t } + for t in defined_func_typeidx { func_typeidx << t } + return exports, types, defined_func_typeidx, import_func_count, func_typeidx +} + +fn core_to_component_valtype(ct u8) int { + // returns component primvaltype code, or -1 if unsupported + match ct { + 0x7f { return 0x7a } // i32 -> s32 + 0x7e { return 0x78 } // i64 -> s64 + 0x7d { return 0x76 } // f32 + 0x7c { return 0x75 } // f64 + else { return -1 } + } +} + +fn write_section(mut out []u8, id u8, payload []u8) { + out << id + out << leb128.encode_u32(u32(payload.len)) + out << payload +} + +// ---- public API ---- + +pub fn wrap_core_module(core []u8) []u8 { + return wrap_core_module_strings(core, map[string]CabiStringExport{}) +} + +// CabiStringExport describes a core export with WIT-known string params +// and/or a string result. comp_params/comp_labels describe the component +// function type, comp_results its (zero or one) results; core_params and +// core_results give the expected core wasm signature the export must have +// to be lifted (strings expand to two i32s). +pub struct CabiStringExport { +pub mut: + wit_name string + comp_params []u8 + comp_labels []string + comp_results []u8 + core_params []u8 + core_results []u8 +} + +// wrap_core_module_strings wraps a core module like wrap_core_module, except +// that core exports listed in string_exports are lifted with the given +// canonical-ABI string params/results and canonical (memory) (realloc) +// options, and exported under their WIT name. +// Known limitations: the scratch/sret scheme above is not reentrant +// (nested host -> guest -> host -> guest calls clobber outer slots); +// lifted string results assume static-lifetime guest data; composite types +// (lists, records, variants, options/results, tuples, maps) are not lifted +// yet and fall back to plain wrapping. +pub fn wrap_core_module_strings(core []u8, string_exports map[string]CabiStringExport) []u8 { + // If we can lift at least one function, produce a richer component. + exports, types, _defined, _, func_typeidx := parse_core_module(core) + // Detect memory and realloc exports for canonical opts. + mut has_memory := false + mut realloc_idx := -1 + for e in exports { + if e.kind == 2 { has_memory = true } + if e.kind == 0 && (e.name == 'cabi_realloc' || e.name == 'realloc') { + realloc_idx = e.idx + } + } + // Determine liftable exports. + mut liftable := []CoreExport{} + mut str_specs := map[string]CabiStringExport{} + for e in exports { + if e.kind != 0 { continue } + // Canonical-ABI string export: core signature must match the expansion. + if spec := string_exports[e.name] { + ti := if e.idx >= 0 && e.idx < func_typeidx.len { func_typeidx[e.idx] } else { -1 } + if ti >= 0 && ti < types.len { + ft := types[ti] + if ft.params == spec.core_params && ft.results == spec.core_results { + liftable << e + str_specs[e.name] = spec + continue + } + } + } + // map export idx to typeidx + ti := if e.idx >= 0 && e.idx < func_typeidx.len { func_typeidx[e.idx] } else { -1 } + if ti < 0 || ti >= types.len { continue } + ft := types[ti] + // check params/results supported and at most one result + mut ok := true + for p in ft.params { + if core_to_component_valtype(p) < 0 { + ok = false + break + } + } + if ok { + if ft.results.len > 1 { ok = false } + for r in ft.results { + if core_to_component_valtype(r) < 0 { + ok = false + break + } + } + } + if ok { liftable << e } + } + if liftable.len == 0 { + // fallback: just wrap module + mut out := []u8{} + out << u8(0x00) + out << u8(0x61) + out << u8(0x73) + out << u8(0x6d) + out << u8(0x0d) + out << u8(0x00) + out << u8(0x01) + out << u8(0x00) + mut payload := []u8{} + payload << core + write_section(mut out, int(ComponentSection.core_module_section), payload) + return out + } + + // Type section payload + mut type_payload := []u8{} + type_payload << leb128.encode_u32(u32(liftable.len)) + for e in liftable { + ti := if e.idx >= 0 && e.idx < func_typeidx.len { func_typeidx[e.idx] } else { -1 } + ft := types[ti] + type_payload << u8(0x40) // functype + if spec := str_specs[e.name] { + // WIT-known signature with canonical string params/results + type_payload << leb128.encode_u32(u32(spec.comp_params.len)) + for idxp, p in spec.comp_params { + label := spec.comp_labels[idxp] + type_payload << leb128.encode_u32(u32(label.len)) + type_payload << label.bytes() + type_payload << p + } + if spec.comp_results.len == 0 { + type_payload << u8(0x01) + type_payload << u8(0x00) + } else { + type_payload << u8(0x00) + type_payload << spec.comp_results[0] + } + continue + } + // params + type_payload << leb128.encode_u32(u32(ft.params.len)) + for idxp, p in ft.params { + // label + label := 'p${idxp}' + type_payload << leb128.encode_u32(u32(label.len)) + type_payload << label.bytes() + type_payload << u8(core_to_component_valtype(p)) + } + // results + if ft.results.len == 0 { + type_payload << u8(0x01) + type_payload << u8(0x00) + } else { + type_payload << u8(0x00) + type_payload << u8(core_to_component_valtype(ft.results[0])) + } + } + + // Core instance section payload: instantiate module 0 + mut coreinst_payload := []u8{} + coreinst_payload << leb128.encode_u32(1) // count + coreinst_payload << u8(0x00) // instantiate + coreinst_payload << leb128.encode_u32(0) // module idx + coreinst_payload << leb128.encode_u32(0) // arg count + + // Alias section payload: alias core exports + mut alias_payload := []u8{} + alias_payload << leb128.encode_u32(u32(liftable.len)) + for e in liftable { + alias_payload << u8(0x00) // core sort prefix + alias_payload << u8(0x00) // core func sort + alias_payload << u8(0x01) // core instance + alias_payload << leb128.encode_u32(0) // instance idx + alias_payload << leb128.encode_u32(u32(e.name.len)) + alias_payload << e.name.bytes() + } + + // Canonical section payload: lift each + mut canon_payload := []u8{} + canon_payload << leb128.encode_u32(u32(liftable.len)) + for i, _ in liftable { + canon_payload << u8(0x00) + canon_payload << u8(0x00) // func sort + canon_payload << leb128.encode_u32(u32(i)) // core func idx (alias index) + // opts: the count is the number of options, not bytes. + mut opts := []u8{} + mut nopts := 0 + if has_memory { + opts << u8(0x03) + opts << leb128.encode_u32(0) + nopts++ + } + if realloc_idx >= 0 { + opts << u8(0x04) + opts << leb128.encode_u32(u32(realloc_idx)) + nopts++ + } + canon_payload << leb128.encode_u32(u32(nopts)) + canon_payload << opts + canon_payload << leb128.encode_u32(u32(i)) // type idx + } + + // Export section payload + mut export_payload := []u8{} + export_payload << leb128.encode_u32(u32(liftable.len)) + for i, e in liftable { + mut ename := e.name + if spec := str_specs[e.name] { + ename = spec.wit_name + } + export_payload << u8(0x00) // nameattributes plain + export_payload << leb128.encode_u32(u32(ename.len)) + export_payload << ename.bytes() + export_payload << u8(0x01) // sort func + export_payload << leb128.encode_u32(u32(i)) // func idx + } + + // Assemble + mut out := []u8{} + out << u8(0x00) + out << u8(0x61) + out << u8(0x73) + out << u8(0x6d) + out << u8(0x0d) + out << u8(0x00) + out << u8(0x01) + out << u8(0x00) + + // type section id 7 + write_section(mut out, int(ComponentSection.type_section), type_payload) + // core module section id 1 + mut mod_payload := []u8{} + mod_payload << core + write_section(mut out, int(ComponentSection.core_module_section), mod_payload) + // core instance section id 2 + write_section(mut out, int(ComponentSection.core_instance_section), coreinst_payload) + // alias section id 6 + write_section(mut out, int(ComponentSection.alias_section), alias_payload) + // canon section id 8 + write_section(mut out, int(ComponentSection.canon_section), canon_payload) + // export section id 11 + write_section(mut out, int(ComponentSection.export_section), export_payload) + + return out +} + +pub fn component_preamble() []u8 { + return [u8(0x00), 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00] +} + +pub fn is_component(bytes []u8) bool { + if bytes.len < 8 { return false } + return bytes[0] == 0x00 && bytes[1] == 0x61 && bytes[2] == 0x73 && bytes[3] == 0x6d + && bytes[4] == 0x0d && bytes[5] == 0x00 && bytes[6] == 0x01 && bytes[7] == 0x00 +} diff --git a/vlib/wasm/component_test.v b/vlib/wasm/component_test.v new file mode 100644 index 00000000000000..56e04683648306 --- /dev/null +++ b/vlib/wasm/component_test.v @@ -0,0 +1,23 @@ +module wasm + +fn test_wrap_core_module_magic() { + core := [u8(0), `a`, `s`, `m`, 0x01, 0x00, 0x00, 0x00] + out := wrap_core_module(core) + assert is_component(out) + assert out.len > core.len +} + +fn test_wrap_lifts_exported_function() { + mut m := Module{} + mut func := m.new_function('add', [.i32_t, .i32_t], [.i32_t]) + { + func.local_get(0) + func.local_get(1) + func.add(.i32_t) + } + m.commit(func, true) + core := m.compile() + out := wrap_core_module(core) + assert is_component(out) + assert out.len > core.len +} diff --git a/vlib/wasm/encoding.v b/vlib/wasm/encoding.v index 7c116868640864..74090db059b8c7 100644 --- a/vlib/wasm/encoding.v +++ b/vlib/wasm/encoding.v @@ -522,6 +522,15 @@ pub fn (mut mod Module) compile() []u8 { mod.end_section(tpatch) } + // user custom sections, emitted after the name section + for cs in mod.customs { + tpatch := mod.start_section(.custom_section) + { + mod.name(cs.name) + mod.buf << cs.bytes + } + mod.end_section(tpatch) + } return mod.buf } diff --git a/vlib/wasm/module.v b/vlib/wasm/module.v index 7cb71d4adf3cb5..cb017954986ef1 100644 --- a/vlib/wasm/module.v +++ b/vlib/wasm/module.v @@ -73,6 +73,13 @@ mut: elements []Element debug bool mod_name ?string + customs []CustomSection +} + +// add_custom_section appends a user custom section with the given name and +// raw payload bytes. +pub fn (mut mod Module) add_custom_section(name string, bytes []u8) { + mod.customs << CustomSection{ name: name, bytes: bytes.clone() } } struct Global { @@ -110,6 +117,11 @@ struct DataSegment { name ?string } +struct CustomSection { + name string + bytes []u8 +} + struct Table { name string export bool diff --git a/vlib/wasm/realloc.v b/vlib/wasm/realloc.v new file mode 100644 index 00000000000000..c43224f0c7f512 --- /dev/null +++ b/vlib/wasm/realloc.v @@ -0,0 +1,20 @@ +module wasm + +// cabi_realloc implements the canonical ABI realloc used by component model +// string lowering. It uses V's bump allocator (malloc) and copies the old +// data. free is a no-op in the WASM backend, so this is a bump allocator. +@[export: 'cabi_realloc'] +pub fn cabi_realloc(ptr voidptr, old_size int, align int, new_size int) voidptr { + if new_size <= 0 { + return unsafe { nil } + } + mut new_ptr := unsafe { malloc(new_size) } + if ptr != unsafe { nil } && old_size > 0 { + copy_size := if old_size < new_size { old_size } else { new_size } + unsafe { + vmemcpy(new_ptr, ptr, copy_size) + } + } + _ = align + return new_ptr +} \ No newline at end of file diff --git a/vlib/wasm/tests/builder_roundtrip_test.v b/vlib/wasm/tests/builder_roundtrip_test.v new file mode 100644 index 00000000000000..0b0a2fa9a3ed67 --- /dev/null +++ b/vlib/wasm/tests/builder_roundtrip_test.v @@ -0,0 +1,463 @@ +module main + +import os +import wasm + +fn test_memory_and_start_sections() { + mut m := wasm.Module{} + m.assign_memory('memory', true, 1, none) + mut func := m.new_function('_start', [], []) + { + func.i32_const(0) + func.drop() + } + m.commit(func, true) + m.assign_start('_start') + m.new_data_segment(none, 0, [u8(65), 66, 67]) + b := m.compile() + // Verify magic + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + // Parse sections + mut i := 8 + mut has_memory := false + mut has_start := false + mut has_data := false + mut has_export := false + for i < b.len { + sid := b[i] + i++ + mut size := u32(0) + mut shift := u32(0) + for { + byte := u32(b[i]) + i++ + size |= (byte & 0x7f) << shift + if byte & 0x80 == 0 { break } + shift += 7 + } + end := i + int(size) + if sid == 5 { has_memory = true } + if sid == 7 { has_export = true } + if sid == 8 { has_start = true } + if sid == 11 { has_data = true } + i = end + } + assert has_memory + assert has_export + assert has_start + assert has_data +} + +fn test_table_and_element() { + mut m := wasm.Module{} + tidx := m.assign_table('t', false, .funcref_t, 1, none) + mut func := m.new_function('f', [], []) + { + func.ref_null(.funcref_t) + func.drop() + } + m.commit(func, true) + b := m.compile() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + // table section id 4 + mut has_table := false + mut i := 8 + for i < b.len { + sid := b[i] + i++ + mut size := u32(0) + mut shift := u32(0) + for { + byte := u32(b[i]) + i++ + size |= (byte & 0x7f) << shift + if byte & 0x80 == 0 { break } + shift += 7 + } + end := i + int(size) + if sid == 4 { has_table = true } + i = end + } + assert has_table + _ = tidx +} + +fn test_import_section() { + mut m := wasm.Module{} + m.new_function_import('env', 'log', [.i32_t], []) + mut func := m.new_function('main', [], []) + { + func.i32_const(42) + func.call_import('env', 'log') + } + m.commit(func, true) + b := m.compile() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + mut has_import := false + mut i := 8 + for i < b.len { + sid := b[i] + i++ + mut size := u32(0) + mut shift := u32(0) + for { + byte := u32(b[i]) + i++ + size |= (byte & 0x7f) << shift + if byte & 0x80 == 0 { break } + shift += 7 + } + end := i + int(size) + if sid == 2 { has_import = true } + i = end + } + assert has_import +} + +struct SecReader { +mut: + data []u8 + pos int +} + +fn (mut r SecReader) uleb() int { + mut n := 0 + mut shift := 0 + for { + b := r.data[r.pos] + r.pos++ + n |= (int(b) & 0x7f) << shift + shift += 7 + if b & 0x80 == 0 { + break + } + } + return n +} + +fn (mut r SecReader) raw(n int) []u8 { + out := r.data[r.pos..r.pos + n].clone() + r.pos += n + return out +} + +fn section_body(b []u8, id u8) []u8 { + mut r := SecReader{ data: b, pos: 8 } + for r.pos + 2 <= b.len { + sid := b[r.pos] + r.pos++ + size := r.uleb() + if sid == id { + return b[r.pos..r.pos + size] + } + r.pos += size + } + return []u8{} +} + +fn test_custom_section_roundtrip() { + mut m := wasm.Module{} + mut func := m.new_function('f', [], []) + { + func.nop() + } + m.commit(func, false) + m.add_custom_section('test', [u8(1), 2, 3]) + b := m.compile() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + // find custom section id 0 named 'test' with the exact payload + mut r := SecReader{ data: b, pos: 8 } + mut found := false + for r.pos + 2 <= b.len { + sid := b[r.pos] + r.pos++ + size := r.uleb() + end := r.pos + size + if sid == 0 { + nlen := r.uleb() + name := r.raw(nlen).bytestr() + if name == 'test' { + assert r.pos + 3 == end + assert b[r.pos] == 1 && b[r.pos + 1] == 2 && b[r.pos + 2] == 3 + found = true + } + } + r.pos = end + } + assert found, 'user custom section must round-trip name and bytes' +} + +fn test_call_indirect_type_index() { + mut m := wasm.Module{} + m.assign_memory('memory', true, 1, none) + ft := m.new_functype(wasm.FuncType{ parameters: [.i32_t, .i32_t], results: [.i32_t] }) + mut callee := m.new_function('add', [.i32_t, .i32_t], [.i32_t]) + { + callee.local_get(0) + callee.local_get(1) + callee.add(.i32_t) + } + m.commit(callee, true) + tidx := m.assign_table('t', false, .funcref_t, 1, none) + m.new_active_element(tidx, 0, ['add']) + mut caller := m.new_function('call_it', [.i32_t, .i32_t], [.i32_t]) + { + caller.local_get(0) + caller.local_get(1) + caller.i32_const(0) + caller.call_indirect(ft, tidx) + } + m.commit(caller, true) + b := m.compile() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + // caller is the second defined function; find 0x11 call_indirect in its body + code := section_body(b, 10) + mut r := SecReader{ data: code, pos: 0 } + count := r.uleb() + assert count == 2 + mut bodies := [][]u8{} + for _ in 0 .. count { + sz := r.uleb() + bodies << r.raw(sz) + } + body := bodies[1] + mut found := false + for i in 0 .. body.len { + if body[i] == 0x11 && i + 3 < body.len { + mut q := SecReader{ data: body, pos: i + 1 } + ti := q.uleb() + ta := q.uleb() + // callee 'add' is defined func 0 with the ft type index + assert ta == int(tidx), 'table index must match' + _ = ti + found = true + } + } + assert found, 'call_indirect must be present in caller body' +} + +fn test_ref_table_ops_roundtrip() { + mut m := wasm.Module{} + m.assign_memory('memory', true, 1, none) + tidx := m.assign_table('t', false, .funcref_t, 2, none) + mut f := m.new_function('f', [], []) + { + f.ref_null(.funcref_t) + f.drop() + f.i32_const(0) + f.ref_func('f') + f.drop() + f.i32_const(1) + f.ref_null(.funcref_t) + f.table_set(tidx) + f.i32_const(1) + f.table_get(tidx) + f.drop() + } + m.commit(f, true) + b := m.compile() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + code := section_body(b, 10) + mut r := SecReader{ data: code, pos: 0 } + _ = r.uleb() + sz := r.uleb() + body := r.raw(sz) + // decode straight-line ops and require the ref/table forms + mut seen_null := false + mut seen_func := false + mut seen_get := false + mut seen_set := false + mut p := 0 + // skip locals decls + mut ndecl := 0 + mut sh := 0 + for { + byte := body[p] + p++ + ndecl |= (int(byte) & 0x7f) << sh + sh += 7 + if byte & 0x80 == 0 { + break + } + } + for _ in 0 .. ndecl { + for { + byte := body[p] + p++ + if byte & 0x80 == 0 { + break + } + } + p++ + } + for p < body.len { + op := body[p] + p++ + if op == 0x0b { + break + } else if op == 0xd0 { + assert body[p] == 0x70 + p++ + seen_null = true + } else if op == 0xd2 { + mut q := SecReader{ data: body, pos: p } + _ = q.uleb() + p = q.pos + seen_func = true + } else if op == 0x25 || op == 0x26 { + mut q := SecReader{ data: body, pos: p } + ta := q.uleb() + p = q.pos + assert ta == int(tidx) + if op == 0x25 { + seen_get = true + } else { + seen_set = true + } + } else if op == 0x20 || op == 0x21 || op == 0x22 || op == 0x23 || op == 0x24 { + mut q := SecReader{ data: body, pos: p } + _ = q.uleb() + p = q.pos + } else if op == 0x41 { + for { + byte := body[p] + p++ + if byte & 0x80 == 0 { + break + } + } + } else if op == 0x1a { + // drop, no immediate + } else { + assert false, 'unexpected op ${op:02x} in ref/table body' + } + } + assert seen_null && seen_func && seen_get && seen_set +} + +fn test_data_count_two_segments() { + mut m := wasm.Module{} + m.assign_memory('memory', true, 1, none) + m.new_data_segment(none, 0, [u8(9), 9, 9]) + m.new_data_segment(none, 16, [u8(7), 7]) + b := m.compile() + dc := section_body(b, 12) + mut r := SecReader{ data: dc, pos: 0 } + assert r.uleb() == 2, 'data count section must report two segments' + ds := section_body(b, 11) + mut q := SecReader{ data: ds, pos: 0 } + assert q.uleb() == 2 + // segment 0 at offset 0 with 3 bytes + assert q.raw(1) == [u8(0x00)] + assert q.raw(1) == [u8(0x41)] + assert q.uleb() == 0 + assert q.raw(1) == [u8(0x0b)] + assert q.uleb() == 3 + assert q.raw(3) == [u8(9), 9, 9] + // segment 1 at offset 16 with 2 bytes + assert q.raw(1) == [u8(0x00)] + assert q.raw(1) == [u8(0x41)] + assert q.uleb() == 16 + assert q.raw(1) == [u8(0x0b)] + assert q.uleb() == 2 + assert q.raw(2) == [u8(7), 7] +} + +fn test_name_section_roundtrip() { + mut m := wasm.Module{} + m.enable_debug('testmod') + mut func := m.new_function('named_fn', [.i32_t], [.i32_t]) + { + func.local_get(0) + } + m.commit(func, true) + b := m.compile() + // find the custom name section and decode it + mut r := SecReader{ data: b, pos: 8 } + mut found_mod := false + mut found_fn := false + for r.pos + 2 <= b.len { + sid := b[r.pos] + r.pos++ + size := r.uleb() + end := r.pos + size + if sid == 0 { + nlen := r.uleb() + nm := r.raw(nlen).bytestr() + if nm == 'name' { + // walk subsections + for r.pos < end { + sub := b[r.pos] + r.pos++ + ssize := r.uleb() + send := r.pos + ssize + if sub == 0 { + // module name + mlen := r.uleb() + assert r.raw(mlen).bytestr() == 'testmod' + found_mod = true + } else if sub == 1 { + // function names + fcount := r.uleb() + for _ in 0 .. fcount { + _ = r.uleb() + flen := r.uleb() + if r.raw(flen).bytestr() == 'named_fn' { + found_fn = true + } + } + } + r.pos = send + } + } + } + r.pos = end + } + assert found_mod, 'name section must carry the module name' + assert found_fn, 'name section must carry the function name' +} + +fn test_real_module_structural() { + core := os.read_file('vlib/v/gen/wasm/testdata/echo_len.shim.wasm') or { panic(err) } + b := core.bytes() + assert b[0] == 0x00 && b[1] == 0x61 && b[2] == 0x73 && b[3] == 0x6d + assert b[4] == 0x01 && b[5] == 0x00 && b[6] == 0x00 && b[7] == 0x00 + // type section must contain (i32, i32) -> i32 + types := section_body(b, 1) + mut r := SecReader{ data: types, pos: 0 } + count := r.uleb() + mut found_sig := false + for _ in 0 .. count { + assert r.raw(1) == [u8(0x60)] + pc := r.uleb() + mut ps := []u8{} + for _ in 0 .. pc { + ps << r.raw(1)[0] + } + rc := r.uleb() + mut rs := []u8{} + for _ in 0 .. rc { + rs << r.raw(1)[0] + } + if ps == [u8(0x7f), 0x7f] && rs == [u8(0x7f)] { + found_sig = true + } + } + assert found_sig, 'real module must declare (i32,i32)->i32' + // export section must name echo_len_cabi (func) and memory + exps := section_body(b, 7) + mut q := SecReader{ data: exps, pos: 0 } + ecount := q.uleb() + mut names := map[string]bool{} + for _ in 0 .. ecount { + nlen := q.uleb() + nm := q.raw(nlen).bytestr() + kind := q.raw(1)[0] + _ = q.uleb() + if kind == 0 || kind == 2 { + names[nm] = true + } + } + assert 'echo_len_cabi' in names + assert 'memory' in names +} diff --git a/vlib/wasm/tests/component_builder_test.v b/vlib/wasm/tests/component_builder_test.v new file mode 100644 index 00000000000000..3e8cfe24395ea7 --- /dev/null +++ b/vlib/wasm/tests/component_builder_test.v @@ -0,0 +1,61 @@ +module main + +import wasm + +fn test_memory_section_emitted() { + mut m := wasm.Module{} + m.assign_memory('memory', true, 1, none) + mut func := m.new_function('f', [], []) + { + } + m.commit(func, true) + b := m.compile() + // memory section id 5 should be present + mut found := false + mut i := 8 + for i < b.len { + sid := b[i] + i++ + // read uleb size + mut size := u32(0) + mut shift := u32(0) + for { + byte := u32(b[i]) + i++ + size |= (byte & 0x7f) << shift + if byte & 0x80 == 0 { break } + shift += 7 + } + if sid == 5 { found = true } + i += int(size) + } + assert found +} + +fn test_start_section_emitted() { + mut m := wasm.Module{} + mut func := m.new_function('_start', [], []) + { + } + m.commit(func, true) + m.assign_start('_start') + b := m.compile() + mut found := false + mut i := 8 + for i < b.len { + sid := b[i] + i++ + mut size := u32(0) + mut shift := u32(0) + for { + byte := u32(b[i]) + i++ + size |= (byte & 0x7f) << shift + if byte & 0x80 == 0 { break } + shift += 7 + } + if sid == 8 { found = true } + i += int(size) + } + assert found +} \ No newline at end of file diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md new file mode 100644 index 00000000000000..4a7c3497be1cbd --- /dev/null +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -0,0 +1,94 @@ +# WIT Conformance Matrix + +Rows = WIT constructs. Columns = parse / print / codegen / validate. +✅ = supported with a test, ❌ = not supported (reason in Notes). + +| Construct | Parse | Print | Codegen | Validate | +|---|---|---|---|---| +| package (namespace:name@version) | ✅ | ✅ | ✅ | ❌ | +| interface | ✅ | ✅ | ✅ | ✅ | +| world | ✅ | ✅ | ✅ | ✅ | +| record | ✅ | ✅ | ✅ | ✅ | +| variant | ✅ | ✅ | ✅ | ✅ | +| enum | ✅ | ✅ | ✅ | ✅ | +| flags | ✅ | ✅ | ✅ | ✅ | +| resource (basic + constructor + methods) | ✅ | ✅ | ✅ | ✅ | +| resource (static methods) | ✅ | ✅ | ✅ | ✅ | +| type alias | ✅ | ✅ | ✅ | ✅ | +| func | ✅ | ✅ | ✅ | ✅ | +| list | ✅ | ✅ | ✅ | ✅ | +| option | ✅ | ✅ | ✅ | ✅ | +| result | ✅ | ✅ | ✅ | ✅ | +| tuple<...> | ✅ | ✅ | ✅ | ✅ | +| own / borrow | ✅ | ✅ | ✅ | ✅ | +| map | ✅ | ✅ | ✅ | ✅ | +| string (param) | ✅ | ✅ | ✅ | ✅ | +| string (return) | ✅ | ✅ | ✅ | ✅ | +| async / future / stream | ✅ | ✅ | ✅ | ✅ | +| use (incl. `as`) | ✅ | ✅ | ✅ | ✅ | +| include | ✅ | ✅ | ✅ | ✅ | +| include ... with | ✅ | ✅ | ✅ | ✅ | +| @since / @deprecated / @unstable | ✅ | ✅ | ✅ | ✅ | +| @version on package | ✅ | ✅ | ✅ | ✅ | + +## Component lift (core module -> component) + +Function-signature shapes the component wrapper lifts today: + +| Shape | Lifted | +|---|---| +| primitive params/results (i32/i64/f32/f64) | ✅ | +| single string param, `(i32, i32) -> r` core sig | ✅ | +| multiple string params (each expands to `(i32, i32)`) | ✅ | +| string returns, `(ptr, len)` pair via hidden sret slot | ✅ | +| enums (as s32 over core i32) / flags (as u32 over core i32) | ✅ | +| lists / records / variants / resources | ❌ (layout helpers exist, no wrapper yet) | +| options / results / tuples / maps | ❌ (layout helpers exist, no wrapper yet) | + +## Notes + +- **composite layout**: `vlib/wasm/cabi_layout.v` implements canonical-ABI + size/align/offset rules (primitives, strings/lists as `(ptr, len)`, + records/tuples, variants, options, results, enums, flags) and + `vlib/wit/cabi.v` maps WIT types onto them (named-type resolution through + the doc, `(-1, -1)` for resources/handles/unknowns). The core wrapper + (`SSAGen.emit_cabi_wrapper`) does not marshal these shapes yet, so the + component wrapper still refuses them; enums/flags are the exception and + lift as integers (see below). + +- **enum/flags lift**: WIT enums lift as component `s32` over core `i32`, + flags (32 or fewer) as component `u32` over core `i32` + (`wit_func_lift_with_doc`); the V guest already passes both as `i32`, so + no guest remarshal is needed. Flags with more than 32 bits are refused. +- **use codegen**: single-file codegen emits all interfaces into one namespace, + so used names resolve locally; the `// use ...` comment records the source. +- **own/borrow codegen**: handle types map to `voidptr`; `own`/`borrow` of a + non-resource is rejected (`error_not_a_resource`). +- **async codegen**: `future` / `stream` surface types map to `voidptr`; + only the surface (types, imports, exports) is generated, runtime support + is out of scope. +- **map codegen**: emitted as V `map`. +- **include / include ... with**: parsed into `WitInclude` nodes carrying the + path plus `with` substitutions (aliases sorted for stable printing), + re-emitted verbatim, and resolved by validation (unknown targets and + unresolvable renames rejected; external package paths are allowed). + Worlds themselves are not codegened, so includes need no V output. +- **static methods**: parsed (`is_static` on WitFunc), printed with the + `static` keyword, codegened as namespaced functions (`Type_method`); + a static method taking its own resource type is rejected + (`error_static_method_takes_resource`). +- **attributes**: collected canonically (`@name` / `@name(k = v, ...)`, + spacing normalized) and re-emitted; round-trip is AST-identical. + World import/export-level attributes are dropped (no AST field). +- **version checks**: an interface `use` of an item whose `@since` exceeds + the importer's `@since` is rejected (`error_version_incompatible`); + items without their own `@since` inherit their interface's version. + Unknown `use` targets (`error_unknown_interface`) and members + (`error_unresolved_use`) are rejected. Duplicate constructors are + rejected (`error_duplicate_constructor`). +- **validate**: package declarations are checked for a namespace and a + semver `x.y.z` version (`error_invalid_package`, + `error_invalid_package_version`). +- Resource methods have no implicit self parameter in WIT, so no + receiver check applies; constructors are separated by the parser, so + no constructor-as-method check applies. diff --git a/vlib/wasm/wit/testdata/wasi/cli_command.wit b/vlib/wasm/wit/testdata/wasi/cli_command.wit new file mode 100644 index 00000000000000..38ef86b86f9987 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/cli_command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.8; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/vlib/wasm/wit/testdata/wasi/cli_imports.wit b/vlib/wasm/wit/testdata/wasi/cli_imports.wit new file mode 100644 index 00000000000000..cec1be520b365b --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/cli_imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.8; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.8; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.8; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.8; + @since(version = 0.2.0) + include wasi:random/imports@0.2.8; + @since(version = 0.2.0) + include wasi:io/imports@0.2.8; + + @since(version = 0.2.0) + import environment; + @since(version = 0.2.0) + import exit; + @since(version = 0.2.0) + import stdin; + @since(version = 0.2.0) + import stdout; + @since(version = 0.2.0) + import stderr; + @since(version = 0.2.0) + import terminal-input; + @since(version = 0.2.0) + import terminal-output; + @since(version = 0.2.0) + import terminal-stdin; + @since(version = 0.2.0) + import terminal-stdout; + @since(version = 0.2.0) + import terminal-stderr; +} diff --git a/vlib/wasm/wit/testdata/wasi/clocks_mono.wit b/vlib/wasm/wit/testdata/wasi/clocks_mono.wit new file mode 100644 index 00000000000000..e60f366f2f6756 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/clocks_mono.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.8; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.8.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + /// + /// For completeness, this function traps if it's not possible to represent + /// the value of the clock in an `instant`. Consequently, implementations + /// should ensure that the starting time is low enough to avoid the + /// possibility of overflow in practice. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/vlib/wasm/wit/testdata/wasi/clocks_wall.wit b/vlib/wasm/wit/testdata/wasi/clocks_wall.wit new file mode 100644 index 00000000000000..3386c800b7e9f6 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/clocks_wall.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.8; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} diff --git a/vlib/wasm/wit/testdata/wasi/fs_types.wit b/vlib/wasm/wit/testdata/wasi/fs_types.wit new file mode 100644 index 00000000000000..ac68f88af7653c --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/fs_types.wit @@ -0,0 +1,676 @@ +package wasi:filesystem@0.2.8; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:io/streams@0.2.8.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.8.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.2.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.2.0) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.2.0) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.2.0) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.2.0) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.2.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.2.0) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.2.0) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.2.0) + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + @since(version = 0.2.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + @since(version = 0.2.0) + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in POSIX. + @since(version = 0.2.0) + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.2.0) + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.2.0) + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.2.0) + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.2.0) + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + @since(version = 0.2.0) + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.2.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.2.0) + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.2.0) + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.2.0) + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.2.0) + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.2.0) + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.2.0) + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.2.0) + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.2.0) + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.2.0) + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.2.0) + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.2.0) + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + @since(version = 0.2.0) + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + @since(version = 0.2.0) + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/vlib/wasm/wit/testdata/wasi/io_streams.wit b/vlib/wasm/wit/testdata/wasi/io_streams.wit new file mode 100644 index 00000000000000..fbb0268b05d4de --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/io_streams.wit @@ -0,0 +1,258 @@ +package wasi:io@0.2.8; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// Returns success when all of the contents written are successfully + /// flushed to output. If an error occurs at any point before all + /// contents are successfully flushed, that error is returned as soon as + /// possible. If writing and flushing the complete contents causes the + /// stream to become closed, this call should return success, and + /// subsequent calls to check-write or other interfaces should return + /// stream-error::closed. + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// Functionality is equivelant to `blocking-write-and-flush` with + /// contents given as a list of len containing only zeroes. + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/vlib/wasm/wit/testdata/wasi/random.wit b/vlib/wasm/wit/testdata/wasi/random.wit new file mode 100644 index 00000000000000..524e77d4aa2558 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.8; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.2.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.2.0) + get-random-u64: func() -> u64; +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_instance-network.wit b/vlib/wasm/wit/testdata/wasi/sock_instance-network.wit new file mode 100644 index 00000000000000..5f6e6c1cc98308 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_instance-network.wit @@ -0,0 +1,11 @@ + +/// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) +interface instance-network { + @since(version = 0.2.0) + use network.{network}; + + /// Get a handle to the default network. + @since(version = 0.2.0) + instance-network: func() -> network; +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_ip-name-lookup.wit b/vlib/wasm/wit/testdata/wasi/sock_ip-name-lookup.wit new file mode 100644 index 00000000000000..ecdaa84935b6c0 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_ip-name-lookup.wit @@ -0,0 +1,56 @@ +@since(version = 0.2.0) +interface ip-name-lookup { + @since(version = 0.2.0) + use wasi:io/poll@0.2.8.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-address}; + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + resolve-addresses: func(network: borrow, name: string) -> result; + + @since(version = 0.2.0) + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_network.wit b/vlib/wasm/wit/testdata/wasi/sock_network.wit new file mode 100644 index 00000000000000..75a4f7d7fd3092 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_network.wit @@ -0,0 +1,169 @@ +@since(version = 0.2.0) +interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.8.{error}; + + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + /// Attempts to extract a network-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.2.0) + type ipv4-address = tuple; + @since(version = 0.2.0) + type ipv6-address = tuple; + + @since(version = 0.2.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.2.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.2.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.2.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_tcp-create-socket.wit b/vlib/wasm/wit/testdata/wasi/sock_tcp-create-socket.wit new file mode 100644 index 00000000000000..eedbd30768c9ce --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_tcp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface tcp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_tcp.wit b/vlib/wasm/wit/testdata/wasi/sock_tcp.wit new file mode 100644 index 00000000000000..9b5552d2eb2f5d --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_tcp.wit @@ -0,0 +1,387 @@ +@since(version = 0.2.0) +interface tcp { + @since(version = 0.2.0) + use wasi:io/streams@0.2.8.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.8.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.8.{duration}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + @since(version = 0.2.0) + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connected` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) + keep-alive-enabled: func() -> result; + @since(version = 0.2.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-interval: func() -> result; + @since(version = 0.2.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-count: func() -> result; + @since(version = 0.2.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + hop-limit: func() -> result; + @since(version = 0.2.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for more information. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent; shutting down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_udp-create-socket.wit b/vlib/wasm/wit/testdata/wasi/sock_udp-create-socket.wit new file mode 100644 index 00000000000000..e8eeacbfefb462 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_udp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface udp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/vlib/wasm/wit/testdata/wasi/sock_udp.wit b/vlib/wasm/wit/testdata/wasi/sock_udp.wit new file mode 100644 index 00000000000000..0000a157e26957 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/sock_udp.wit @@ -0,0 +1,288 @@ +@since(version = 0.2.0) +interface udp { + @since(version = 0.2.0) + use wasi:io/poll@0.2.8.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + @since(version = 0.2.0) + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + @since(version = 0.2.0) + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + /// A UDP socket handle. + @since(version = 0.2.0) + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + unicast-hop-limit: func() -> result; + @since(version = 0.2.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/vlib/wit/README.md b/vlib/wit/README.md new file mode 100644 index 00000000000000..18849570da367c --- /dev/null +++ b/vlib/wit/README.md @@ -0,0 +1,24 @@ +# wit + +Pure V implementation of a WIT (WebAssembly Interface Types) parser and V binding generator. + +## Usage + +```v +import wit + +fn main() { + src := os.read_file('example.wit')! + doc := wit.parse(src)! + println(doc.to_v()) +} +``` + +## Status + +Experimental. Supports a useful subset of WIT: packages, interfaces, worlds, +records, variants, enums, flags, resources, type aliases, functions, and common +generic types (`list`, `option`, `result`, `tuple`, `own`, `borrow`). + +Component model wrapping is available via `wit.wrap_component` and +`wasm.wrap_core_module` (experimental). \ No newline at end of file diff --git a/vlib/wit/cabi.v b/vlib/wit/cabi.v new file mode 100644 index 00000000000000..ef051e61726e2c --- /dev/null +++ b/vlib/wit/cabi.v @@ -0,0 +1,156 @@ +module wit + +import wasm + +// wit_cabi_size_align computes the canonical-ABI (size, align) of a WIT +// type, resolving named types (records, variants, enums, flags, aliases) +// through doc. Returns (-1, -1) when the type is unresolvable or involves a +// resource/handle, which has no fixed guest layout. Recursion deeper than 8 +// levels is refused the same way. Primitives follow the Canonical ABI +// element-size table; strings and lists are (ptr, len) pairs; options, +// results, variants, records, and tuples follow the union/struct rules in +// wasm.cabi_*_layout. +pub fn wit_cabi_size_align(t WitType, doc WitDoc) (int, int) { + return wit_cabi_size_align_inner(t, doc, 0) +} + +fn wit_cabi_size_align_inner(t WitType, doc WitDoc, depth int) (int, int) { + if depth > 8 { + return -1, -1 + } + match t.kind { + .prim { + if t.prim == 'string' { + return wasm.cabi_string_layout() + } + return wasm.cabi_primitive_size_align(t.prim) + } + .list { + return wasm.cabi_list_layout() + } + .option { + if t.items.len == 0 { + return -1, -1 + } + ps, pa := wit_cabi_size_align_inner(t.items[0], doc, depth + 1) + if ps < 0 { + return -1, -1 + } + return wasm.cabi_option_layout(ps, pa) + } + .result { + mut oks := 0 + mut oka := 1 + mut ers := 0 + mut era := 1 + if t.items.len > 0 { + oks, oka = wit_cabi_size_align_inner(t.items[0], doc, depth + 1) + if oks < 0 { + return -1, -1 + } + } + if t.items.len > 1 { + ers, era = wit_cabi_size_align_inner(t.items[1], doc, depth + 1) + if ers < 0 { + return -1, -1 + } + } + return wasm.cabi_result_layout(oks, oka, ers, era) + } + .tuple { + mut fields := []wasm.CabiField{cap: t.items.len} + for it in t.items { + s, a := wit_cabi_size_align_inner(it, doc, depth + 1) + if s < 0 { + return -1, -1 + } + fields << wasm.CabiField{ name: '', size: s, align: a } + } + _, size, align := wasm.cabi_tuple_layout(fields) + return size, align + } + .named { + return wit_named_cabi_size_align(t.name, doc, depth) + } + } +} + +fn wit_named_cabi_size_align(name string, doc WitDoc, depth int) (int, int) { + if name == '_' || name == '' { + return 0, 1 + } + if name.starts_with('borrow<') || name.starts_with('own<') { + return -1, -1 + } + if name == 'pollable' { + return 4, 4 + } + // Search all interfaces for a matching definition. + for iface in doc.interfaces { + for it in iface.items { + match it.kind { + .record { + if r := it.record { + if r.name == name { + mut fields := []wasm.CabiField{cap: r.fields.len} + for f in r.fields { + s, a := wit_cabi_size_align_inner(f.typ, doc, depth + 1) + if s < 0 { + return -1, -1 + } + fields << wasm.CabiField{ name: f.name, size: s, align: a } + } + _, size, align := wasm.cabi_record_layout(fields) + return size, align + } + } + } + .enum_ { + if e := it.enum_ { + if e.name == name { + return wasm.cabi_enum_layout(e.cases.len) + } + } + } + .flags { + if f := it.flags { + if f.name == name { + return wasm.cabi_flags_layout(f.flags.len) + } + } + } + .variant { + if v := it.variant { + if v.name == name { + mut cases := []wasm.CabiField{cap: v.cases.len} + for c in v.cases { + if pt := c.typ { + s, a := wit_cabi_size_align_inner(pt, doc, depth + 1) + if s < 0 { + return -1, -1 + } + cases << wasm.CabiField{ name: c.name, size: s, align: a } + } else { + cases << wasm.CabiField{ name: c.name, size: 0, align: 1 } + } + } + _, size, align := wasm.cabi_variant_layout(v.cases.len, cases) + return size, align + } + } + } + .type_alias { + if td := it.type_def { + if td.name == name { + return wit_cabi_size_align_inner(td.typ, doc, depth + 1) + } + } + } + else {} + } + } + } + // Unknown names (futures/streams mapped to voidptr in codegen, externals) + // have no canonical guest layout. + return -1, -1 +} diff --git a/vlib/wit/cabi_test.v b/vlib/wit/cabi_test.v new file mode 100644 index 00000000000000..223bf713e0b51c --- /dev/null +++ b/vlib/wit/cabi_test.v @@ -0,0 +1,124 @@ +module wit + +fn test_wit_cabi_layout_primitives() { + doc := WitDoc{} + sz, al := wit_cabi_size_align(prim_type('u32'), doc) + assert sz == 4 && al == 4 + ssz, sal := wit_cabi_size_align(prim_type('string'), doc) + assert ssz == 8 && sal == 4 + lsz, lal := wit_cabi_size_align(WitType{ kind: .list, items: [prim_type('u8')] }, doc) + assert lsz == 8 && lal == 4 + osz, oal := wit_cabi_size_align(WitType{ kind: .option, items: [prim_type('u16')] }, doc) + assert osz == 4 && oal == 2 + rsz, ral := wit_cabi_size_align(WitType{ kind: .result, items: [prim_type('u8'), prim_type('u32')] }, doc) + assert rsz == 8 && ral == 4 + tsz, tal := wit_cabi_size_align(WitType{ kind: .tuple, items: [prim_type('u32'), prim_type('u8')] }, doc) + assert tsz == 8 && tal == 4 +} + +fn test_wit_cabi_layout_named() { + mut doc := WitDoc{} + doc.interfaces << WitInterface{ + name: 'test' + items: [ + WitItem{ + kind: .record + record: WitRecord{ + name: 'point' + fields: [WitField{ name: 'x', typ: prim_type('u32') }, WitField{ + name: 'y' + typ: prim_type('u8') + }] + } + }, + WitItem{ + kind: .enum_ + enum_: WitEnum{ name: 'color', cases: ['red', 'green', 'blue'] } + }, + WitItem{ + kind: .flags + flags: WitFlags{ name: 'perms', flags: ['r', 'w', 'x'] } + }, + WitItem{ + kind: .variant + variant: WitVariant{ + name: 'v' + cases: [WitVariantCase{ name: 'a', typ: prim_type('u8') }, WitVariantCase{ + name: 'b' + typ: prim_type('u32') + }, WitVariantCase{ name: 'c' }] + } + }, + ] + } + psz, pal := wit_cabi_size_align(named_type('point'), doc) + assert psz == 8 && pal == 4 + esz, eal := wit_cabi_size_align(named_type('color'), doc) + assert esz == 1 && eal == 1 + fsz, fal := wit_cabi_size_align(named_type('perms'), doc) + assert fsz == 1 && fal == 1 + vsz, val := wit_cabi_size_align(named_type('v'), doc) + assert vsz == 8 && val == 4 + // option wraps the record: discriminant + 8-byte payload. + osz, oal := wit_cabi_size_align(WitType{ kind: .option, items: [named_type('point')] }, doc) + assert osz == 12 && oal == 4 + // unknown names and resources have no layout. + bsz, _ := wit_cabi_size_align(named_type('missing'), doc) + assert bsz == -1 + hsz, _ := wit_cabi_size_align(named_type('own'), doc) + assert hsz == -1 +} + +fn test_wit_func_lift_with_doc_enums_flags() { + mut doc := WitDoc{} + doc.interfaces << WitInterface{ + name: 'test' + items: [ + WitItem{ + kind: .enum_ + enum_: WitEnum{ name: 'color', cases: ['red', 'green', 'blue'] } + }, + WitItem{ + kind: .flags + flags: WitFlags{ name: 'perms', flags: ['r', 'w', 'x'] } + }, + ] + } + // enum param lifts as component s32 over core i32. + ef := WitFunc{ + name: 'take-color' + params: [WitParam{ name: 'c', typ: named_type('color') }] + results: [prim_type('u32')] + } + if spec := wit_func_lift_with_doc(ef, doc) { + assert spec.core_params == [u8(0x7f)] + assert spec.comp_params == [u8(0x7a)] + assert spec.comp_labels == ['c'] + } else { + assert false, 'enum param must lift' + } + // flags result lifts as component u32 over core i32. + ff := WitFunc{ + name: 'get-perms' + results: [named_type('perms')] + } + if spec := wit_func_lift_with_doc(ff, doc) { + assert spec.core_results == [u8(0x7f)] + assert spec.comp_results == [u8(0x79)] + } else { + assert false, 'flags result must lift' + } + // composites are still refused: no wrapper allocation path yet. + for t in [WitType{ kind: .list, items: [prim_type('u8')] }, WitType{ + kind: .option + items: [prim_type('u8')] + }, WitType{ kind: .tuple, items: [prim_type('u8'), prim_type('u8')] }, named_type('missing')] { + nf := WitFunc{ + name: 'take-it' + params: [WitParam{ name: 'x', typ: t }] + } + if _ := wit_func_lift_with_doc(nf, doc) { + assert false, 'composite ${t} must not lift yet' + } + } +} diff --git a/vlib/wit/component.v b/vlib/wit/component.v new file mode 100644 index 00000000000000..87269d24fde0ce --- /dev/null +++ b/vlib/wit/component.v @@ -0,0 +1,273 @@ +module wit + +import wasm + +// wit_prim_codes maps a WIT primitive to (core wasm valtype, component +// valtype), or (-1, -1) when unrepresentable. +fn wit_prim_codes(p string) (int, int) { + match p { + 'bool' { return 0x7f, 0x7f } + 'u8' { return 0x7f, 0x7d } + 'u16' { return 0x7f, 0x7b } + 'u32' { return 0x7f, 0x79 } + 'u64' { return 0x7e, 0x77 } + 's8' { return 0x7f, 0x7e } + 's16' { return 0x7f, 0x7c } + 's32' { return 0x7f, 0x7a } + 's64' { return 0x7e, 0x78 } + 'f32' { return 0x7d, 0x76 } + 'f64' { return 0x7c, 0x75 } + 'char' { return 0x7f, 0x74 } + else { return -1, -1 } + } +} + +// wit_func_lift describes how one WIT function maps onto a core export, or +// none when the shape is not liftable. The doc-free form lifts only +// string/primitive shapes; use wit_func_lift_with_doc for enum/flags-aware +// lifting (enums lift as s32, flags as u32). +fn wit_func_lift(f WitFunc) ?wasm.CabiStringExport { + mut core_params := []u8{} + mut comp_params := []u8{} + mut comp_labels := []string{} + mut has_string := false + for p in f.params { + if p.typ.kind != .prim { + return none + } + if p.typ.prim == 'string' { + core_params << u8(0x7f) + core_params << u8(0x7f) + comp_params << u8(0x73) + comp_labels << p.name + has_string = true + continue + } + cc, cv := wit_prim_codes(p.typ.prim) + if cc < 0 { + return none + } + core_params << u8(cc) + comp_params << u8(cv) + comp_labels << p.name + } + mut core_results := []u8{} + mut comp_results := []u8{} + if f.results.len == 0 { + } else if f.results.len == 1 { + r := f.results[0] + if r.kind != .prim { + return none + } + if r.prim == 'string' { + core_results = [u8(0x7f), u8(0x7f)] + comp_results = [u8(0x73)] + has_string = true + } else { + cc, cv := wit_prim_codes(r.prim) + if cc < 0 { + return none + } + core_results = [u8(cc)] + comp_results = [u8(cv)] + } + } else { + return none + } + if !has_string { + return none + } + return wasm.CabiStringExport{ + comp_params: comp_params + comp_labels: comp_labels + comp_results: comp_results + core_params: core_params + core_results: core_results + } +} + +// wit_is_enum reports whether name resolves to a WIT enum in doc. +fn wit_is_enum(name string, doc WitDoc) bool { + for iface in doc.interfaces { + for it in iface.items { + if it.kind == .enum_ { + if e := it.enum_ { + if e.name == name { + return true + } + } + } + } + } + return false +} + +// wit_is_flags reports whether name resolves to a WIT flags set in doc. +fn wit_is_flags(name string, doc WitDoc) bool { + for iface in doc.interfaces { + for it in iface.items { + if it.kind == .flags { + if f := it.flags { + if f.name == name { + return true + } + } + } + } + } + return false +} + +// wit_func_lift_with_doc lifts string/primitive shapes like wit_func_lift and +// additionally lifts enum params/results as component s32 over core i32 and +// flags params/results (32 or fewer flags) as component u32 over core i32. +// Composite shapes (lists, records, variants, options/results, tuples, maps, +// resources) are still refused: their canonical layout helpers now exist +// (wasm.cabi_*_layout, wit_cabi_size_align) but the core wrapper that would +// marshal them has no allocation path yet. +fn wit_func_lift_with_doc(f WitFunc, doc WitDoc) ?wasm.CabiStringExport { + mut core_params := []u8{} + mut comp_params := []u8{} + mut comp_labels := []string{} + mut has_lift := false + for p in f.params { + if p.typ.kind == .prim { + if p.typ.prim == 'string' { + core_params << u8(0x7f) + core_params << u8(0x7f) + comp_params << u8(0x73) + comp_labels << p.name + has_lift = true + continue + } + cc, cv := wit_prim_codes(p.typ.prim) + if cc < 0 { + return none + } + core_params << u8(cc) + comp_params << u8(cv) + comp_labels << p.name + continue + } + if p.typ.kind == .named { + if wit_is_enum(p.typ.name, doc) { + core_params << u8(0x7f) + comp_params << u8(0x7a) + comp_labels << p.name + has_lift = true + continue + } + if wit_is_flags(p.typ.name, doc) { + core_params << u8(0x7f) + comp_params << u8(0x79) + comp_labels << p.name + has_lift = true + continue + } + } + return none + } + mut core_results := []u8{} + mut comp_results := []u8{} + if f.results.len == 0 { + } else if f.results.len == 1 { + r := f.results[0] + if r.kind == .prim { + if r.prim == 'string' { + core_results = [u8(0x7f), u8(0x7f)] + comp_results = [u8(0x73)] + has_lift = true + } else { + cc, cv := wit_prim_codes(r.prim) + if cc < 0 { + return none + } + core_results = [u8(cc)] + comp_results = [u8(cv)] + } + } else if r.kind == .named { + if wit_is_enum(r.name, doc) { + core_results = [u8(0x7f)] + comp_results = [u8(0x7a)] + has_lift = true + } else if wit_is_flags(r.name, doc) { + core_results = [u8(0x7f)] + comp_results = [u8(0x79)] + has_lift = true + } else { + return none + } + } else { + return none + } + } else { + return none + } + if !has_lift { + return none + } + return wasm.CabiStringExport{ + comp_params: comp_params + comp_labels: comp_labels + comp_results: comp_results + core_params: core_params + core_results: core_results + } +} + +pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { + // Pick the world: the named one, or the first with exports. + mut widx := -1 + for i, w in doc.worlds { + if world != '' && w.name == world { + widx = i + break + } + if world == '' && w.exports.len > 0 { + widx = i + break + } + } + if widx < 0 { + return wasm.wrap_core_module(core) + } + w := doc.worlds[widx] + mut string_exports := map[string]wasm.CabiStringExport{} + // direct func exports + for e in w.exports { + if f := e.func { + if mut spec := wit_func_lift_with_doc(f, doc) { + spec.wit_name = e.name + string_exports[e.name.replace('-', '_')] = spec + } else if mut spec := wit_func_lift(f) { + spec.wit_name = e.name + string_exports[e.name.replace('-', '_')] = spec + } + } + // interface exports: lift matching funcs by their own names + if e.interface != '' { + for iface in doc.interfaces { + if iface.name != e.interface && iface.name != e.interface.all_after_last('/') { + continue + } + for it in iface.items { + if it.kind == .func { + if f := it.func { + if mut spec := wit_func_lift_with_doc(f, doc) { + spec.wit_name = f.name + string_exports[f.name.replace('-', '_')] = spec + } else if mut spec := wit_func_lift(f) { + spec.wit_name = f.name + string_exports[f.name.replace('-', '_')] = spec + } + } + } + } + } + } + } + if string_exports.len == 0 { + return wasm.wrap_core_module(core) + } + return wasm.wrap_core_module_strings(core, string_exports) +} diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v new file mode 100644 index 00000000000000..4f5ef35e0194ae --- /dev/null +++ b/vlib/wit/gen_v.v @@ -0,0 +1,243 @@ +module wit + +pub fn (d WitDoc) to_v() string { + // collect defined type names first + mut defined := map[string]bool{} + for iface in d.interfaces { + for it in iface.items { + match it.kind { + .record { + if r := it.record { defined[r.name] = true } + } + .enum_ { + if e := it.enum_ { defined[e.name] = true } + } + .variant { + if v := it.variant { defined[v.name] = true } + } + .flags { + if f := it.flags { defined[f.name] = true } + } + .resource { + if r := it.resource { defined[r.name] = true } + } + .type_alias { + if t := it.type_def { defined[t.name] = true } + } + else {} + } + } + } + mut out := []string{} + if d.package.name != '' { + out << '// package: ${d.package.namespace}:${d.package.name}' + if d.package.version != '' { out << '// version: ${d.package.version}' } + } + for iface in d.interfaces { + out << '' + out << '// interface ${iface.name}' + for it in iface.items { + match it.kind { + .record { + if r := it.record { + out << 'pub struct ${to_v_type_name(r.name)} {' + for f in r.fields { + out << '\t${to_v_ident(f.name)} ${wit_type_to_v(f.typ, defined)}' + } + out << '}' + } + } + .enum_ { + if e := it.enum_ { + out << 'pub enum ${to_v_type_name(e.name)} {' + for c in e.cases { + out << '\t${to_v_ident(c)}' + } + out << '}' + } + } + .flags { + if f := it.flags { + out << 'pub type ${to_v_type_name(f.name)} = int' + for i, fl in f.flags { + out << 'pub const ${to_v_ident(f.name)}_${to_v_ident(fl)} = ${1 << i}' + } + } + } + .variant { + if v := it.variant { + for c in v.cases { + if ct := c.typ { + out << 'pub struct ${to_v_type_name(v.name)}_${to_v_type_name(c.name)} {' + out << '\tvalue ${wit_type_to_v(ct, defined)}' + out << '}' + } else { + out << 'pub struct ${to_v_type_name(v.name)}_${to_v_type_name(c.name)} {}' + } + } + mut cases := []string{} + for c in v.cases { + cases << '${to_v_type_name(v.name)}_${to_v_type_name(c.name)}' + } + out << 'pub type ${to_v_type_name(v.name)} = ${cases.join(' | ')}' + } + } + .type_alias { + if t := it.type_def { + out << 'pub type ${to_v_type_name(t.name)} = ${wit_type_to_v(t.typ, defined)}' + } + } + .resource { + if r := it.resource { + out << 'pub struct ${to_v_type_name(r.name)} {}' + for m in r.methods { + mut ps := []string{} + for p in m.params { + ps << '${to_v_ident(p.name)} ${wit_type_to_v(p.typ, defined)}' + } + mut rs := '' + if m.results.len == 1 { + rs = wit_type_to_v(m.results[0], defined) + } else if m.results.len > 1 { + mut ts := []string{} + for r2 in m.results { ts << wit_type_to_v(r2, defined) } + rs = '(${ts.join(', ')})' + } + if m.is_static { + // WIT static methods have no self; emit a plain + // namespaced function. + sig := if rs != '' { + '${ps.join(', ')}) ${rs} {' + } else { + '${ps.join(', ')}) {' + } + out << 'pub fn ${to_v_ident(r.name)}_${to_v_ident(m.name)}(' + sig + } else if rs != '' { + out << 'pub fn (mut self ${to_v_type_name(r.name)}) ${to_v_ident(m.name)}(${ps.join(', ')}) ${rs} {' + } else { + out << 'pub fn (mut self ${to_v_type_name(r.name)}) ${to_v_ident(m.name)}(${ps.join(', ')}) {' + } + out << '\tpanic("TODO: implement")' + out << '}' + } + } + } + .use { + if u := it.use_ { + out << '// use ${u.path} { ${u.items.join(', ')} }' + } + } + .func { + if f := it.func { + mut ps := []string{} + for p in f.params { + ps << '${to_v_ident(p.name)} ${wit_type_to_v(p.typ, defined)}' + } + mut rs := '' + if f.results.len == 1 { + rs = wit_type_to_v(f.results[0], defined) + } else if f.results.len > 1 { + mut ts := []string{} + for r in f.results { ts << wit_type_to_v(r, defined) } + rs = '(${ts.join(', ')})' + } + if rs != '' { + out << 'pub fn ${to_v_ident(f.name)}(${ps.join(', ')}) ${rs} {' + } else { + out << 'pub fn ${to_v_ident(f.name)}(${ps.join(', ')}) {' + } + out << '\tpanic("TODO: implement")' + out << '}' + } + } + else {} + } + } + } + return out.join('\n') +} + +fn to_v_ident(s string) string { + mut r := s.replace('-', '_') + r = r.trim_left('%') + if r.len == 0 || (r[0] >= `0` && r[0] <= `9`) { + r = 'arg_' + r + } + return r +} + +fn to_v_type_name(s string) string { + mut r := s.replace('-', '_') + r = r.trim_left('%') + if r.len == 0 { return r } + r = r[0].ascii_str().to_upper() + r[1..] + return r +} + +fn wit_type_to_v(t WitType, defined map[string]bool) string { + match t.kind { + .prim { + return match t.prim { + 'bool' { 'bool' } + 'u8' { 'u8' } + 'u16' { 'u16' } + 'u32' { 'u32' } + 'u64' { 'u64' } + 's8' { 'i8' } + 's16' { 'i16' } + 's32' { 'int' } + 's64' { 'i64' } + 'f32' { 'f32' } + 'f64' { 'f64' } + 'char' { 'rune' } + 'string' { 'string' } + else { '/*${t.prim}*/' } + } + } + .named { + n := t.name + if n == '_' { return 'void' } + if n.starts_with('borrow<') || n.starts_with('own<') { return 'voidptr' } + if n == 'pollable' { return 'int' } + // Async surface types (future, stream) and other externals + // map to voidptr: only the surface (types, imports, exports) is + // generated; runtime support is out of scope. Known limitation: + // async has surface types only, no runtime (no task/waitable + // state, no pollable dispatch); see CONFORMANCE.md lift rows. + if n !in defined { return 'voidptr' } + return to_v_type_name(n) + } + .list { + if t.items.len > 0 { return '[]${wit_type_to_v(t.items[0], defined)}' } + return '[]u8' + } + .option { + if t.items.len > 0 { return '?${wit_type_to_v(t.items[0], defined)}' } + return '?voidptr' + } + .result { + if t.items.len == 0 { return '!' } + if t.items[0].kind == .named && t.items[0].name == '_' { return '!' } + okstr := wit_type_to_v(t.items[0], defined) + // V cannot express result, E> as !?T; collapse to ?T. + if okstr.starts_with('?') { return okstr } + return '!${okstr}' + } + .tuple { + // V has no anonymous tuple types usable as fields; homogeneous + // tuples map to arrays, heterogeneous ones to voidptr. + mut ts := []string{} + for it in t.items { ts << wit_type_to_v(it, defined) } + if ts.len == 0 { return 'voidptr' } + mut same := true + for x in ts[1..] { + if x != ts[0] { + same = false + break + } + } + if same { return '[]${ts[0]}' } + return 'voidptr' + } + } +} diff --git a/vlib/wit/grammar_test.v b/vlib/wit/grammar_test.v new file mode 100644 index 00000000000000..ed894a03cba602 --- /dev/null +++ b/vlib/wit/grammar_test.v @@ -0,0 +1,177 @@ +module wit + +import os + +fn grammar_roundtrip(src string) string { + doc := parse(src) or { panic(err) } + printed := doc.str() + doc2 := parse(printed) or { panic('reparse failed: ${err}\n${printed}') } + p2 := doc2.str() + assert p2 == printed, 'print must be stable' + return printed +} + +fn grammar_codegen_compiles(name string, src string) string { + doc := parse(src) or { panic(err) } + gen := doc.to_v() + assert gen.len > 0 + path := os.join_path(os.vtmp_dir(), 'wit_grammar_${name}.v') + os.write_file(path, gen) or { panic(err) } + res := os.exec(['v', '-check', path]) + assert res.exit_code == 0, res.output + return gen +} + +fn test_grammar_resource_roundtrip() { + src := 'package p:q; + +interface i { + resource file { + constructor(path: string); + read: func() -> string; + make: static func() -> u32; + } +} +' + printed := grammar_roundtrip(src) + assert printed.contains('constructor(path: string);') + assert printed.contains('static') + gen := grammar_codegen_compiles('resource', src) + assert gen.contains('pub struct File') +} + +fn test_grammar_async_surface() { + src := 'package p:q; + +interface i { + a: func() -> future; + b: func() -> stream; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('future') + gen := grammar_codegen_compiles('async', src) + assert gen.contains('pub fn a') + assert gen.contains('pub fn b') +} + +fn test_grammar_map() { + src := 'package p:q; + +interface i { + m: func() -> map; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('map') + gen := grammar_codegen_compiles('map', src) + assert gen.contains('pub fn m') +} + +fn test_grammar_use() { + src := 'package p:q; + +interface base { + x: func() -> u32; +} + +interface derived { + use base.{x}; + y: func() -> u32; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('use base.{x};') + gen := grammar_codegen_compiles('use', src) + // single-file codegen: both interfaces land in one namespace + assert gen.contains('pub fn x') + assert gen.contains('pub fn y') +} + +fn test_grammar_use_as() { + src := 'package p:q; + +interface base { + x: func() -> u32; +} + +interface derived { + use base.{x as y}; + z: func() -> u32; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('use base.{x as y};') + gen := grammar_codegen_compiles('use_as', src) + assert gen.contains('pub fn z') +} + +fn test_grammar_world_roundtrip() { + src := 'package example:demo@0.1.0; + +world app { + import log: func(msg: string); + export run: func() -> u32; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('import log: func(msg: string);') + assert printed.contains('export run: func() -> u32;') + gen := grammar_codegen_compiles('world', src) + assert gen.len >= 0 +} + +fn test_grammar_include_roundtrip() { + src := 'package p:q; + +world base { + import x: func() -> u32; + export y: func() -> u32; +} + +world derived { + include base; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('include base;') + gen := grammar_codegen_compiles('include', src) + assert gen.len >= 0 + validate(parse(src) or { panic(err) }) or { panic(err) } +} + +fn test_grammar_include_with_roundtrip() { + src := 'package p:q; + +world base { + import x: func() -> u32; +} + +world derived { + include base with { x as renamed }; +} +' + printed := grammar_roundtrip(src) + assert printed.contains('include base with { x as renamed };') + gen := grammar_codegen_compiles('include_with', src) + assert gen.len >= 0 + validate(parse(src) or { panic(err) }) or { panic(err) } +} + +fn test_grammar_attrs_roundtrip() { + src := 'package p:q; + +@since(version = 0.1.0) +interface i { + @deprecated + old: func() -> u32; +} +' + doc := parse(src) or { panic(err) } + printed := doc.str() + assert printed.contains('@since(version = 0.1.0)') + assert printed.contains('@deprecated') + grammar_roundtrip(src) + gen := grammar_codegen_compiles('attrs', src) + assert gen.contains('pub fn old') +} diff --git a/vlib/wit/lexer.v b/vlib/wit/lexer.v new file mode 100644 index 00000000000000..dc057b00887f61 --- /dev/null +++ b/vlib/wit/lexer.v @@ -0,0 +1,114 @@ +module wit + +pub enum TokKind { + eof + ident + punct + string +} + +pub struct Token { +pub: + kind TokKind + val string + pos int +} + +pub struct Lexer { + src string + mut idx int +} + +pub fn new_lexer(src string) Lexer { + return Lexer{ src: src, idx: 0 } +} + +pub fn (mut l Lexer) tokenize() ![]Token { + mut toks := []Token{} + for l.idx < l.src.len { + c := l.src[l.idx] + // whitespace + if c == ` ` || c == `\t` || c == `\n` || c == `\r` { + l.idx++ + continue + } + // line comment + if c == `/` && l.idx + 1 < l.src.len && l.src[l.idx + 1] == `/` { + for l.idx < l.src.len && l.src[l.idx] != `\n` { l.idx++ } + continue + } + // block comment + if c == `/` && l.idx + 1 < l.src.len && l.src[l.idx + 1] == `*` { + l.idx += 2 + for l.idx + 1 < l.src.len && !(l.src[l.idx] == `*` && l.src[l.idx + 1] == `/`) { + l.idx++ + } + l.idx += 2 + continue + } + // string + if c == `"` { + start := l.idx + l.idx++ + mut s := '' + for l.idx < l.src.len && l.src[l.idx] != `"` { + if l.src[l.idx] == `\\` { + l.idx++ + if l.idx < l.src.len { + s += l.src[l.idx..l.idx + 1] + l.idx++ + } + continue + } + s += l.src[l.idx..l.idx + 1] + l.idx++ + } + if l.idx >= l.src.len { return error('unterminated string at ${start}') } + l.idx++ // closing + toks << Token{ kind: .string, val: s, pos: start } + continue + } + // punctuation / multi + if c == `-` && l.idx + 1 < l.src.len && l.src[l.idx + 1] == `>` { + toks << Token{ kind: .punct, val: '->', pos: l.idx } + l.idx += 2 + continue + } + if c == `{` || c == `}` || c == `(` || c == `)` || c == `[` || c == `]` + || c == `,` || c == `:` || c == `;` || c == `=` || c == `<` || c == `>` + || c == `.` || c == `/` || c == `*` || c == `@` { + toks << Token{ kind: .punct, val: rune(c).str(), pos: l.idx } + l.idx++ + continue + } + // ident (letters, digits, _, -) + if is_ident_start(c) { + start := l.idx + for l.idx < l.src.len { + ch := l.src[l.idx] + if ch == `-` && l.idx + 1 < l.src.len && l.src[l.idx + 1] == `>` { + break + } + if is_ident_char(ch) { + l.idx++ + } else { + break + } + } + toks << Token{ kind: .ident, val: l.src[start..l.idx], pos: start } + continue + } + return error('unexpected char ${c} at ${l.idx}') + } + toks << Token{ kind: .eof, val: '', pos: l.src.len } + return toks +} + +fn is_ident_start(c u8) bool { + return (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` + || (c >= `0` && c <= `9`) || c == `%` +} + +fn is_ident_char(c u8) bool { + return is_ident_start(c) || (c >= `0` && c <= `9`) || c == `-` +} diff --git a/vlib/wit/parser.v b/vlib/wit/parser.v new file mode 100644 index 00000000000000..4a6c8cc43150e3 --- /dev/null +++ b/vlib/wit/parser.v @@ -0,0 +1,653 @@ +module wit + +pub struct Parser { +pub: + toks []Token +mut: + idx int +} + +pub fn new_parser(toks []Token) Parser { + return Parser{ toks: toks, idx: 0 } +} + +pub fn parse(src string) !WitDoc { + mut l := new_lexer(src) + toks := l.tokenize()! + mut p := new_parser(toks) + return p.parse_doc() +} + +fn (mut p Parser) parse_doc() !WitDoc { + mut doc := WitDoc{} + p.skip_attributes() + // package + if p.peek_ident() == 'package' { + doc.package = p.parse_package()! + } + for p.peek_kind() != .eof { + attrs := p.collect_attributes() + if p.peek_ident() == 'interface' { + mut iface := p.parse_interface()! + iface.attrs = attrs + doc.interfaces << iface + } else if p.peek_ident() == 'world' { + mut w := p.parse_world(mut doc)! + w.attrs = attrs + doc.worlds << w + } else { + return error('expected interface or world at ${p.pos()}') + } + } + return doc +} + +fn (p Parser) pos() int { + if p.idx < p.toks.len { return p.toks[p.idx].pos } + return -1 +} + +fn (mut p Parser) peek() Token { + if p.idx < p.toks.len { return p.toks[p.idx] } + return Token{ kind: .eof } +} + +fn (mut p Parser) peek_kind() TokKind { + return p.peek().kind +} + +fn (mut p Parser) peek_ident() string { + t := p.peek() + if t.kind == .ident { return t.val } + return '' +} + +fn (mut p Parser) advance() { + if p.idx < p.toks.len { p.idx++ } +} + +fn (mut p Parser) expect_ident_val(v string) ! { + t := p.peek() + if t.kind != .ident || t.val != v { + return error('expected identifier ${v} at ${t.pos}') + } + p.advance() +} + +fn (mut p Parser) expect_ident() !string { + t := p.peek() + if t.kind != .ident { + return error('expected identifier at ${t.pos}') + } + p.advance() + return t.val +} + +fn (mut p Parser) expect_punct(v string) ! { + t := p.peek() + if t.kind != .punct || t.val != v { + return error('expected ${v} at ${t.pos}') + } + p.advance() +} + +fn (mut p Parser) parse_package() !WitPackage { + p.expect_ident_val('package')! + mut pkg := WitPackage{} + // namespace:name[/subpath] + pkg.namespace = p.expect_ident()! + p.expect_punct(':')! + pkg.name = p.expect_ident()! + // optional subpath + for p.peek().kind == .punct && p.peek().val == '/' { + p.advance() + sub := p.expect_ident()! + pkg.name += '/' + sub + } + // optional version + if p.peek().kind == .punct && p.peek().val == '@' { + p.advance() + // consume version tokens until ; + mut ver := '' + for p.peek().kind != .punct || p.peek().val != ';' { + if p.peek().kind == .eof { return error('unterminated package') } + ver += p.peek().val + p.advance() + } + pkg.version = ver + } + p.expect_punct(';')! + return pkg +} + +fn (mut p Parser) parse_interface() !WitInterface { + p.expect_ident_val('interface')! + name := p.expect_ident()! + mut iface := WitInterface{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + if p.peek().kind == .eof { return error('unterminated interface') } + item := p.parse_item()! + iface.items << item + } + p.expect_punct('}')! + return iface +} + +fn (mut p Parser) parse_item() !WitItem { + attrs := p.collect_attributes() + // use + if p.peek_ident() == 'use' { + u := p.parse_use()! + return WitItem{ kind: .use, use_: u, attrs: attrs } + } + // type alias + if p.peek_ident() == 'type' { + td := p.parse_type_def()! + return WitItem{ kind: .type_alias, type_def: td, attrs: attrs } + } + // record + if p.peek_ident() == 'record' { + r := p.parse_record()! + return WitItem{ kind: .record, record: r, attrs: attrs } + } + // variant + if p.peek_ident() == 'variant' { + v := p.parse_variant()! + return WitItem{ kind: .variant, variant: v, attrs: attrs } + } + // enum + if p.peek_ident() == 'enum' { + e := p.parse_enum()! + return WitItem{ kind: .enum_, enum_: e, attrs: attrs } + } + // flags + if p.peek_ident() == 'flags' { + f := p.parse_flags()! + return WitItem{ kind: .flags, flags: f, attrs: attrs } + } + // resource + if p.peek_ident() == 'resource' { + r := p.parse_resource()! + return WitItem{ kind: .resource, resource: r, attrs: attrs } + } + // func item: name: func(...) or name: static func(...) + if p.peek().kind == .ident && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].val == ':' { + mut f := p.parse_func_item()! + f.attrs = attrs + return WitItem{ kind: .func, func: f, attrs: attrs } + } + return error('unexpected item at ${p.pos()}') +} + +fn (mut p Parser) parse_use() !WitUse { + p.expect_ident_val('use')! + // path may contain / and : + mut path := p.expect_ident()! + for p.peek().kind == .punct && (p.peek().val == '/' || p.peek().val == ':' || p.peek().val == '@') { + // careful: ':' ends path? use foo.{bar}; path is before '{' + if p.peek().val == ':' && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].val == '{' { + break + } + tok := p.peek().val + p.advance() + if tok == '@' { + // version may contain dots + mut ver := '' + for p.peek().kind == .ident || (p.peek().kind == .punct && p.peek().val == '.' && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].kind == .ident) { + ver += p.peek().val + p.advance() + } + path += '@' + ver + } else if tok == ':' { + // maybe namespace:name, keep + path += ':' + path += p.expect_ident()! + } else { + path += '/' + p.expect_ident()! + } + } + p.expect_punct('.')! + p.expect_punct('{')! + mut items := []string{} + for !(p.peek().kind == .punct && p.peek().val == '}') { + name := p.expect_ident()! + items << name + // optional alias + if p.peek().kind == .ident && p.peek().val == 'as' { + p.advance() + alias := p.expect_ident()! + items[items.len - 1] = name + ' as ' + alias + } + if p.peek().kind == .punct && p.peek().val == ',' { p.advance() } + } + p.expect_punct('}')! + p.expect_punct(';')! + return WitUse{ path: path, items: items } +} + +fn (mut p Parser) parse_type_def() !WitTypeDef { + p.expect_ident_val('type')! + name := p.expect_ident()! + p.expect_punct('=')! + t := p.parse_type()! + p.expect_punct(';')! + return WitTypeDef{ name: name, typ: t } +} + +fn (mut p Parser) parse_record() !WitRecord { + p.expect_ident_val('record')! + name := p.expect_ident()! + mut r := WitRecord{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + fname := p.expect_ident()! + p.expect_punct(':')! + ft := p.parse_type()! + r.fields << WitField{ name: fname, typ: ft } + if p.peek().kind == .punct && p.peek().val == ',' { p.advance() } + // allow trailing + if p.peek().kind == .punct && p.peek().val == ';' { p.advance() } + } + p.expect_punct('}')! + return r +} + +fn (mut p Parser) parse_variant() !WitVariant { + p.expect_ident_val('variant')! + name := p.expect_ident()! + mut v := WitVariant{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + cname := p.expect_ident()! + mut ct := ?WitType(none) + if p.peek().kind == .punct && p.peek().val == '(' { + p.advance() + t := p.parse_type()! + ct = t + p.expect_punct(')')! + } + v.cases << WitVariantCase{ name: cname, typ: ct } + if p.peek().kind == .punct && (p.peek().val == ',' || p.peek().val == ';') { p.advance() } + } + p.expect_punct('}')! + return v +} + +fn (mut p Parser) parse_enum() !WitEnum { + p.expect_ident_val('enum')! + name := p.expect_ident()! + mut e := WitEnum{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + cname := p.expect_ident()! + e.cases << cname + if p.peek().kind == .punct && (p.peek().val == ',' || p.peek().val == ';') { p.advance() } + } + p.expect_punct('}')! + return e +} + +fn (mut p Parser) parse_flags() !WitFlags { + p.expect_ident_val('flags')! + name := p.expect_ident()! + mut f := WitFlags{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + fname := p.expect_ident()! + f.flags << fname + if p.peek().kind == .punct && (p.peek().val == ',' || p.peek().val == ';') { p.advance() } + } + p.expect_punct('}')! + return f +} + +fn (mut p Parser) parse_resource() !WitResource { + p.expect_ident_val('resource')! + name := p.expect_ident()! + mut r := WitResource{ name: name } + if p.peek().kind == .punct && p.peek().val == ';' { + p.advance() + return r + } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + mattrs := p.collect_attributes() + if p.peek_ident() == 'constructor' { + p.advance() + p.expect_punct('(')! + params := p.parse_param_list()! + p.expect_punct(')')! + f := WitFunc{ name: 'constructor', params: params, results: [], attrs: mattrs } + r.methods << f + if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { + p.advance() + } + continue + } + // method: name: [static] func(...) + if p.peek().kind == .ident && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].val == ':' { + mut f := p.parse_func_item()! + f.attrs = mattrs + r.methods << f + continue + } + return error('unexpected in resource at ${p.pos()}') + } + p.expect_punct('}')! + return r +} + +fn (mut p Parser) parse_func_item() !WitFunc { + name := p.expect_ident()! + p.expect_punct(':')! + // optional static + mut is_static := false + if p.peek_ident() == 'static' { + p.advance() + is_static = true + } + if p.peek_ident() == 'func' { p.advance() } + p.expect_punct('(')! + params := p.parse_param_list()! + p.expect_punct(')')! + mut results := []WitType{} + if p.peek().kind == .punct && p.peek().val == '->' { + p.advance() + results = p.parse_result_list()! + } + if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { p.advance() } + return WitFunc{ name: name, params: params, results: results, is_static: is_static } +} + +fn (mut p Parser) parse_param_list() ![]WitParam { + mut params := []WitParam{} + if p.peek().kind == .punct && p.peek().val == ')' { return params } + for { + pname := p.expect_ident()! + p.expect_punct(':')! + pt := p.parse_type()! + params << WitParam{ name: pname, typ: pt } + if p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + if p.peek().kind == .punct && p.peek().val == ')' { break } + continue + } + break + } + return params +} + +fn (mut p Parser) parse_result_list() ![]WitType { + // single type or tuple in parens + if p.peek().kind == .punct && p.peek().val == '(' { + p.advance() + mut items := []WitType{} + if !(p.peek().kind == .punct && p.peek().val == ')') { + items << p.parse_type()! + for p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + items << p.parse_type()! + } + } + p.expect_punct(')')! + return items + } + t := p.parse_type()! + return [t] +} + +fn (mut p Parser) parse_type() !WitType { + if p.peek().kind == .punct && p.peek().val == '(' { + p.advance() + mut items := []WitType{} + if !(p.peek().kind == .punct && p.peek().val == ')') { + items << p.parse_type()! + for p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + items << p.parse_type()! + } + } + p.expect_punct(')')! + return WitType{ kind: .tuple, items: items } + } + if p.peek().kind != .ident { + return error('expected type at ${p.pos()}') + } + name := p.peek().val + p.advance() + // generic + if p.peek().kind == .punct && p.peek().val == '<' { + p.advance() + if name == 'list' { + t := p.parse_type()! + p.expect_punct('>')! + return WitType{ kind: .list, items: [t] } + } + if name == 'option' { + t := p.parse_type()! + p.expect_punct('>')! + return WitType{ kind: .option, items: [t] } + } + if name == 'result' { + okt := p.parse_type()! + mut errt := ?WitType(none) + if p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + et := p.parse_type()! + errt = et + } + p.expect_punct('>')! + mut its := [okt] + if e := errt { its << e } + return WitType{ kind: .result, items: its } + } + if name == 'tuple' { + mut items := []WitType{} + items << p.parse_type()! + for p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + items << p.parse_type()! + } + p.expect_punct('>')! + return WitType{ kind: .tuple, items: items } + } + // own/borrow/map/other -> named, consuming all comma-separated args + mut args := []string{} + args << p.parse_type()!.str() + for p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + args << p.parse_type()!.str() + } + p.expect_punct('>')! + return WitType{ kind: .named, name: '${name}<${args.join(', ')}>' } + } + if is_prim_name(name) { + return prim_type(name) + } + return named_type(name) +} + +fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { + p.expect_ident_val('world')! + name := p.expect_ident()! + mut w := WitWorld{ name: name } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + if p.peek().kind == .eof { return error('unterminated world') } + p.skip_attributes() + kind := p.peek_ident() + if kind == 'import' || kind == 'export' { + is_export := kind == 'export' + p.advance() + iname := p.expect_ident()! + mut iface := '' + mut f := ?WitFunc(none) + if p.peek().kind == .punct && p.peek().val == ':' { + p.advance() + if p.peek_ident() == 'interface' { + p.advance() + inline := p.parse_inline_interface(mut doc, '${name}_${iname}')! + iface = inline.name + } else if p.peek_ident() == 'func' { + p.advance() + p.expect_punct('(')! + params := p.parse_param_list()! + p.expect_punct(')')! + mut results := []WitType{} + if p.peek().kind == .punct && p.peek().val == '->' { + p.advance() + results = p.parse_result_list()! + } + f = WitFunc{ name: iname, params: params, results: results } + if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { + p.advance() + } + } else { + return error('expected interface or func after : at ${p.pos()}') + } + } else { + iface = iname + if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { + p.advance() + } + } + if is_export { + w.exports << WitWorldExport{ name: iname, interface: iface, func: f } + } else { + w.imports << WitWorldImport{ name: iname, interface: iface, func: f } + } + continue + } + if kind == 'include' { + p.advance() + inc := p.parse_include()! + w.includes << inc + continue + } + return error('unexpected in world at ${p.pos()}') + } + p.expect_punct('}')! + return w +} + +fn (mut p Parser) skip_attributes() { + p.collect_attributes() +} + +fn join_attr_parts(parts []string) string { + mut s := parts.join(' ') + s = s.replace('( ', '(') + s = s.replace(' )', ')') + s = s.replace(' . ', '.') + s = s.replace(' , ', ', ') + return s +} + +// collect_attributes consumes @... attribute annotations and returns them in +// canonical spacing (identical AST on reparse). +fn (mut p Parser) collect_attributes() []string { + mut out := []string{} + for p.peek().kind == .punct && p.peek().val == '@' { + p.advance() + mut name := '' + if p.peek().kind == .ident { + name = p.peek().val + p.advance() + } + if p.peek().kind == .punct && p.peek().val == '(' { + p.advance() + mut parts := []string{} + mut depth := 1 + for p.idx < p.toks.len { + if p.peek().kind == .punct && p.peek().val == '(' { + depth++ + parts << '(' + p.advance() + continue + } + if p.peek().kind == .punct && p.peek().val == ')' { + depth-- + p.advance() + if depth <= 0 { break } + parts << ')' + continue + } + if p.peek().kind == .string { + parts << '"' + p.peek().val + '"' + } else { + parts << p.peek().val + } + p.advance() + } + out << '@' + name + '(' + join_attr_parts(parts) + ')' + } else { + out << '@' + name + } + } + return out +} + +// parse_include parses `include path [with { a as b, ... }] ;` where path is +// a world reference like `base` or `wasi:clocks/imports@0.2.8`. +fn (mut p Parser) parse_include() !WitInclude { + mut inc := WitInclude{} + mut path := p.expect_ident()! + for p.peek().kind == .punct + && (p.peek().val == '/' || p.peek().val == ':' || p.peek().val == '@') { + sep := p.peek().val + p.advance() + if sep == '@' { + mut ver := '' + for p.peek().kind == .ident + || (p.peek().kind == .punct && p.peek().val == '.' + && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].kind == .ident) { + ver += p.peek().val + p.advance() + } + path += '@' + ver + } else if sep == ':' { + path += ':' + path += p.expect_ident()! + } else { + path += '/' + p.expect_ident()! + } + } + inc.path = path + if p.peek_ident() == 'with' { + p.advance() + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + if p.peek().kind == .eof { + return error('unterminated include with at ${p.pos()}') + } + old_name := p.expect_ident()! + mut new_name := old_name + if p.peek_ident() == 'as' { + p.advance() + new_name = p.expect_ident()! + } + inc.with_renames[new_name] = old_name + if p.peek().kind == .punct && p.peek().val == ',' { + p.advance() + } + } + p.expect_punct('}')! + } + if p.peek().kind == .punct && p.peek().val == ';' { + p.advance() + } + return inc +} + +fn (mut p Parser) parse_inline_interface(mut doc WitDoc, name string) !WitInterface { + p.expect_punct('{')! + mut iface := WitInterface{ name: name } + for !(p.peek().kind == .punct && p.peek().val == '}') { + item := p.parse_item()! + iface.items << item + } + p.expect_punct('}')! + doc.interfaces << iface + return iface +} diff --git a/vlib/wit/preview2_test.v b/vlib/wit/preview2_test.v new file mode 100644 index 00000000000000..e12d2dfdcf2899 --- /dev/null +++ b/vlib/wit/preview2_test.v @@ -0,0 +1,83 @@ +module wit + +import os + +fn preview2_case(name string, wit_file string, must_contain []string) { + src := os.read_file(wit_file) or { panic(err) } + doc := parse(src) or { panic(err) } + assert doc.interfaces.len > 0, 'expected interfaces in ${wit_file}' + gen := doc.to_v() + assert gen.len > 0 + path := os.join_path(os.vtmp_dir(), 'wit_preview2_${name}.v') + os.write_file(path, gen) or { panic(err) } + res := os.exec(['v', '-check', path]) + assert res.exit_code == 0, res.output + for s in must_contain { + assert gen.contains(s), 'generated ${name} bindings must contain ${s}' + } +} + +fn test_preview2_io() { + preview2_case('io', 'vlib/wasm/wit/testdata/wasi/io_streams.wit', [ + 'pub struct Input_stream', + '(mut self Input_stream) read', + ]) +} + +fn test_preview2_clocks() { + preview2_case('clocks', 'vlib/wasm/wit/testdata/wasi/clocks_wall.wit', [ + 'pub struct Datetime', + 'pub fn now', + ]) +} + +fn test_preview2_random() { + preview2_case('random', 'vlib/wasm/wit/testdata/wasi/random.wit', [ + 'pub fn get_random_bytes', + ]) +} + +fn test_preview2_cli() { + src := os.read_file('vlib/wasm/wit/testdata/wasi/cli_command.wit') or { panic(err) } + doc := parse(src) or { panic(err) } + assert doc.worlds.len == 1 + assert doc.worlds[0].name == 'command' + gen := doc.to_v() + path := os.join_path(os.vtmp_dir(), 'wit_preview2_cli.v') + os.write_file(path, gen) or { panic(err) } + res := os.exec(['v', '-check', path]) + assert res.exit_code == 0, res.output +} + +fn test_preview2_filesystem() { + preview2_case('filesystem', 'vlib/wasm/wit/testdata/wasi/fs_types.wit', [ + 'pub struct Descriptor', + '(mut self Descriptor) read_via_stream', + ]) +} + +fn test_preview2_sockets() { + preview2_case('sockets-network', 'vlib/wasm/wit/testdata/wasi/sock_network.wit', [ + 'pub struct Network', + 'pub type Ipv4_address', + ]) + preview2_case('sockets-instance-network', 'vlib/wasm/wit/testdata/wasi/sock_instance-network.wit', [ + 'pub fn instance_network', + ]) + preview2_case('sockets-ip-name-lookup', 'vlib/wasm/wit/testdata/wasi/sock_ip-name-lookup.wit', [ + 'pub fn resolve_addresses', + ]) + preview2_case('sockets-tcp', 'vlib/wasm/wit/testdata/wasi/sock_tcp.wit', [ + 'pub struct Tcp_socket', + '(mut self Tcp_socket) start_bind', + ]) + preview2_case('sockets-tcp-create', 'vlib/wasm/wit/testdata/wasi/sock_tcp-create-socket.wit', [ + 'pub fn create_tcp_socket', + ]) + preview2_case('sockets-udp', 'vlib/wasm/wit/testdata/wasi/sock_udp.wit', [ + 'pub struct Udp_socket', + ]) + preview2_case('sockets-udp-create', 'vlib/wasm/wit/testdata/wasi/sock_udp-create-socket.wit', [ + 'pub fn create_udp_socket', + ]) +} diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v new file mode 100644 index 00000000000000..2e0bef855345a1 --- /dev/null +++ b/vlib/wit/printer.v @@ -0,0 +1,137 @@ +module wit + +fn wit_func_sig(f WitFunc) string { + mut ps := []string{} + for p in f.params { + ps << '${p.name}: ${p.typ.str()}' + } + if f.results.len == 0 { + return 'func(${ps.join(', ')})' + } else if f.results.len == 1 { + return 'func(${ps.join(', ')}) -> ${f.results[0].str()}' + } + mut rs := []string{} + for r in f.results { + rs << r.str() + } + return 'func(${ps.join(', ')}) -> (${rs.join(', ')})' +} + +pub fn (d WitDoc) str() string { + mut out := []string{} + if d.package.name != '' { + out << 'package ${d.package.namespace}:${d.package.name}' + if d.package.version != '' { out << '@${d.package.version}' } + out[out.len - 1] += ';' + } + for iface in d.interfaces { + out << '' + for a in iface.attrs { + out << a + } + out << 'interface ${iface.name} {' + for it in iface.items { + for a in it.attrs { + out << '\t${a}' + } + if it.kind == .use { + if u := it.use_ { + out << '\tuse ${u.path}.{${u.items.join(', ')}};' + } + continue + } + match it.kind { + .record { + if r := it.record { + out << '\trecord ${r.name} {' + for f in r.fields { out << '\t\t${f.name}: ${f.typ.str()},' } + out << '\t}' + } + } + .resource { + if r := it.resource { + out << '\tresource ${r.name} {' + for m in r.methods { + for a in m.attrs { + out << '\t\t${a}' + } + mut ps := []string{} + for p in m.params { ps << '${p.name}: ${p.typ.str()}' } + if m.name == 'constructor' { + out << ('\t\tconstructor(' + ps.join(', ') + ');') + continue + } + prefix := if m.is_static { 'static ' } else { '' } + if m.results.len == 0 { + out << '\t\t${m.name}: ${prefix}func(${ps.join(', ')});' + } else if m.results.len == 1 { + out << '\t\t${m.name}: ${prefix}func(${ps.join(', ')}) -> ${m.results[0].str()};' + } else { + mut rs := []string{} + for rr in m.results { rs << rr.str() } + out << '\t\t${m.name}: ${prefix}func(${ps.join(', ')}) -> (${rs.join(', ')});' + } + } + out << '\t}' + } + } + .func { + if f := it.func { + mut ps := []string{} + for p in f.params { ps << '${p.name}: ${p.typ.str()}' } + if f.results.len == 0 { + out << '\t${f.name}: func(${ps.join(', ')});' + } else if f.results.len == 1 { + out << '\t${f.name}: func(${ps.join(', ')}) -> ${f.results[0].str()};' + } else { + mut rs := []string{} + for r in f.results { rs << r.str() } + out << '\t${f.name}: func(${ps.join(', ')}) -> (${rs.join(', ')});' + } + } + } + else {} + } + } + out << '}' + } + for w in d.worlds { + out << '' + for a in w.attrs { + out << a + } + out << 'world ${w.name} {' + for inc in w.includes { + if inc.with_renames.len == 0 { + out << '\tinclude ${inc.path};' + } else { + mut parts := []string{} + for new_name, old_name in inc.with_renames { + if new_name == old_name { + parts << new_name + } else { + parts << '${old_name} as ${new_name}' + } + } + parts.sort() + out << '\tinclude ${inc.path} with { ${parts.join(', ')} };' + } + } + for imp in w.imports { + if f := imp.func { + out << '\timport ${imp.name}: ${wit_func_sig(f)};' + } else if imp.interface != '' { + out << '\timport ${imp.name};' + } + } + for exp in w.exports { + if f := exp.func { + out << '\texport ${exp.name}: ${wit_func_sig(f)};' + } else if exp.interface != '' { + out << '\texport ${exp.name};' + } + } + out << '}' + } + return out.join('\n') +} diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v new file mode 100644 index 00000000000000..a5d32cab20d5ce --- /dev/null +++ b/vlib/wit/validate.v @@ -0,0 +1,456 @@ +module wit + +pub struct WitError { +pub: + code string + message string + line int +} + +fn wit_since_of(attrs []string) (int, int, int) { + for a in attrs { + prefix := '@since(version = ' + if a.starts_with(prefix) && a.ends_with(')') { + inner := a[prefix.len..a.len - 1] + parts := inner.split('.') + if parts.len == 3 { + return parts[0].int(), parts[1].int(), parts[2].int() + } + } + } + return 0, 0, 0 +} + +fn wit_version_newer(a0 int, a1 int, a2 int, b0 int, b1 int, b2 int) bool { + if a0 != b0 { + return a0 > b0 + } + if a1 != b1 { + return a1 > b1 + } + return a2 > b2 +} + +fn wit_find_interface(doc WitDoc, path string) int { + mut base := path + if base.contains('@') { + base = base.all_before('@') + } + for i, iface in doc.interfaces { + if iface.name == path || iface.name == base || iface.name == base.all_after_last('/') { + return i + } + } + return -1 +} + +fn wit_use_member(entry string) string { + if entry.contains(' as ') { + return entry.all_before(' as ').trim_space() + } + return entry.trim_space() +} + +fn wit_item_version(items []WitItem, name string, d0 int, d1 int, d2 int) []int { + for it in items { + mut iname := '' + mut iattrs := []string{} + match it.kind { + .func { + if f := it.func { + iname = f.name + iattrs = f.attrs + } + } + .record { + if r := it.record { + iname = r.name + } + } + .variant { + if v := it.variant { + iname = v.name + } + } + .enum_ { + if e := it.enum_ { + iname = e.name + } + } + .flags { + if f := it.flags { + iname = f.name + } + } + .resource { + if r := it.resource { + iname = r.name + } + } + .type_alias { + if t := it.type_def { + iname = t.name + } + } + else {} + } + if iname == name { + v0, v1, v2 := wit_since_of(it.attrs) + if v0 != 0 || v1 != 0 || v2 != 0 { + return [v0, v1, v2] + } + a, b, c := wit_since_of(iattrs) + if a != 0 || b != 0 || c != 0 { + return [a, b, c] + } + return [d0, d1, d2] + } + } + return [] +} + +pub fn validate(doc WitDoc) ! { + // Collect defined type names + mut types := map[string]bool{} + for iface in doc.interfaces { + for it in iface.items { + match it.kind { + .record { + if r := it.record { + types[r.name] = true + } + } + .enum_ { + if e := it.enum_ { + types[e.name] = true + } + } + .variant { + if v := it.variant { + types[v.name] = true + } + } + .flags { + if f := it.flags { + types[f.name] = true + } + } + .resource { + if r := it.resource { + types[r.name] = true + } + } + .type_alias { + if t := it.type_def { + types[t.name] = true + } + } + else {} + } + } + } + mut resources := map[string]bool{} + for iface in doc.interfaces { + for it in iface.items { + if it.kind == .resource { + if r := it.resource { + resources[r.name] = true + } + } + } + } + // primitive types + for p in ['bool', 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'f32', 'f64', 'char', + 'string'] { + types[p] = true + } + // check duplicate interface names + mut iface_names := map[string]bool{} + for iface in doc.interfaces { + if iface.name in iface_names { + return error('error_duplicate_name: duplicate interface "${iface.name}"') + } + iface_names[iface.name] = true + } + // check duplicate world names + mut world_names := map[string]bool{} + for w in doc.worlds { + if w.name in world_names { + return error('error_duplicate_name: duplicate world "${w.name}"') + } + world_names[w.name] = true + } + // check package declaration + if doc.package.name != '' { + if doc.package.namespace == '' { + return error('error_invalid_package: package "${doc.package.name}" has no namespace') + } + if doc.package.version != '' { + parts := doc.package.version.split('.') + mut bad_version := parts.len != 3 + if !bad_version { + for p in parts { + if p.len == 0 { + bad_version = true + break + } + for c in p.bytes() { + if c < `0` || c > `9` { + bad_version = true + break + } + } + if bad_version { + break + } + } + } + if bad_version { + return error('error_invalid_package_version: package version "${doc.package.version}" is not semver') + } + } + } + // check world includes resolve to a local world or interface + for w in doc.worlds { + for inc in w.includes { + if inc.path.contains(':') { + continue + } + mut base := inc.path + if base.contains('@') { + base = base.all_before('@') + } + if base.contains('/') { + base = base.all_after_last('/') + } + mut found := false + mut target_world := -1 + for i, ww in doc.worlds { + if ww.name == base { + found = true + target_world = i + break + } + } + if !found { + for iface in doc.interfaces { + if iface.name == base { + found = true + break + } + } + } + if !found { + return error('error_unknown_include: include of unknown "${inc.path}" in world "${w.name}"') + } + if target_world >= 0 { + tw := doc.worlds[target_world] + mut offered := map[string]bool{} + for imp in tw.imports { + offered[imp.name] = true + } + for exp in tw.exports { + offered[exp.name] = true + } + for _, old_name in inc.with_renames { + if old_name !in offered { + return error('error_unresolved_include: "${old_name}" not found in "${inc.path}"') + } + } + } + } + } + // check each interface + for iface in doc.interfaces { + // duplicate item names + mut item_names := map[string]bool{} + for it in iface.items { + mut name := '' + match it.kind { + .func { + if f := it.func { + name = f.name + } + } + .record { + if r := it.record { + name = r.name + } + } + .variant { + if v := it.variant { + name = v.name + } + } + .enum_ { + if e := it.enum_ { + name = e.name + } + } + .flags { + if f := it.flags { + name = f.name + } + } + .resource { + if r := it.resource { + name = r.name + } + } + .type_alias { + if t := it.type_def { + name = t.name + } + } + else {} + } + if name != '' { + if name in item_names { + return error('error_duplicate_name: duplicate name "${name}" in interface "${iface.name}"') + } + item_names[name] = true + } + // check use items: target must exist, members must resolve, versions must hold + if it.kind == .use { + if u := it.use_ { + target := wit_find_interface(doc, u.path) + if target < 0 { + return error('error_unknown_interface: use of unknown interface "${u.path}"') + } + iv0, iv1, iv2 := wit_since_of(iface.attrs) + tv0, tv1, tv2 := wit_since_of(doc.interfaces[target].attrs) + for entry in u.items { + mname := wit_use_member(entry) + ver := wit_item_version(doc.interfaces[target].items, mname, tv0, tv1, tv2) + if ver.len == 0 { + return error('error_unresolved_use: "${mname}" not found in interface "${u.path}"') + } + v0, v1, v2 := ver[0], ver[1], ver[2] + if wit_version_newer(v0, v1, v2, iv0, iv1, iv2) { + return error('error_version_incompatible: use of "${mname}" at ${v0}.${v1}.${v2} exceeds importer "${iface.name}" at ${iv0}.${iv1}.${iv2}') + } + } + } + } + // check types in variant cases + if it.kind == .variant { + if v := it.variant { + for c in v.cases { + if t := c.typ { + check_type(t, types, resources)! + } + } + } + } + // check types in records + if it.kind == .record { + if r := it.record { + for f in r.fields { + check_type(f.typ, types, resources)! + } + } + } + // check types in funcs + if it.kind == .func { + if f := it.func { + for p in f.params { + check_type(p.typ, types, resources)! + } + for r in f.results { + check_type(r, types, resources)! + } + } + } + // check type alias + if it.kind == .type_alias { + if t := it.type_def { + check_type(t.typ, types, resources)! + } + } + // check resource methods + if it.kind == .resource { + if r := it.resource { + mut method_names := map[string]bool{} + mut ctors := 0 + for m in r.methods { + if m.name == 'constructor' { + ctors++ + if ctors > 1 { + return error('error_duplicate_constructor: duplicate constructor in resource "${r.name}"') + } + } + if m.name in method_names { + return error('error_duplicate_name: duplicate method "${m.name}" in resource "${r.name}"') + } + method_names[m.name] = true + if m.is_static { + for p in m.params { + if is_bare_resource_ref(p.typ, r.name) { + return error('error_static_method_takes_resource: static method "${m.name}" must not take resource "${r.name}"') + } + } + } + for p in m.params { + check_type(p.typ, types, resources)! + } + for res in m.results { + check_type(res, types, resources)! + } + } + } + } + } + } +} + +fn is_bare_resource_ref(t WitType, rname string) bool { + if t.kind != .named { + return false + } + return t.name == rname +} + +fn check_type(t WitType, types map[string]bool, resources map[string]bool) ! { + match t.kind { + .prim {} + .named { + n := t.name + // strip generic args for checking base name + base := if n.contains('<') { n.all_before('<') } else { n } + if base == 'future' || base == 'stream' || base == 'map' { + // container surface types: element validation deferred + return + } + if base == 'own' || base == 'borrow' { + inner := n.all_after('<').all_before('>') + if inner !in resources { + return error('error_not_a_resource: ${base} of non-resource "${inner}"') + } + return + } + if base !in types { + return error('error_unknown_type: unknown type "${base}"') + } + } + .list { + if t.items.len > 0 { + check_type(t.items[0], types, resources)! + } + } + .option { + if t.items.len > 0 { + check_type(t.items[0], types, resources)! + } + } + .result { + for it in t.items { + check_type(it, types, resources)! + } + } + .tuple { + for it in t.items { + check_type(it, types, resources)! + } + } + } +} diff --git a/vlib/wit/validation_test.v b/vlib/wit/validation_test.v new file mode 100644 index 00000000000000..23c4356770d962 --- /dev/null +++ b/vlib/wit/validation_test.v @@ -0,0 +1,177 @@ +module wit + +fn expect_validate_error(src string, code string) { + doc := parse(src) or { panic(err) } + validate(doc) or { + assert err.msg().contains(code), 'expected ${code}, got: ${err.msg()}' + return + } + panic('expected ${code}, validation passed') +} + +fn test_validate_version_incompatible() { + expect_validate_error('package p:q; + +@since(version = 0.2.0) +interface a { + x: func() -> u32; +} + +@since(version = 0.1.0) +interface b { + use a.{x}; + y: func() -> u32; +} +', 'error_version_incompatible') +} + +fn test_validate_version_ok() { + src := 'package p:q; + +@since(version = 0.2.0) +interface a { + x: func() -> u32; +} + +@since(version = 0.3.0) +interface b { + use a.{x}; + y: func() -> u32; +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { panic(err) } +} + +fn test_validate_unknown_interface_use() { + expect_validate_error('package p:q; + +interface b { + use nope.{x}; +} +', 'error_unknown_interface') +} + +fn test_validate_unresolved_use() { + expect_validate_error('package p:q; + +interface base { + x: func() -> u32; +} + +interface derived { + use base.{missing}; +} +', 'error_unresolved_use') +} + +fn test_validate_not_a_resource() { + expect_validate_error('package p:q; + +interface i { + take: func(h: own) -> u32; +} +', 'error_not_a_resource') +} + +fn test_validate_duplicate_constructor() { + expect_validate_error('package p:q; + +interface i { + resource r { + constructor(a: u32); + constructor(b: u32); + } +} +', 'error_duplicate_constructor') +} + +fn test_validate_static_takes_resource() { + expect_validate_error('package p:q; + +interface i { + resource r { + make: static func(x: r) -> u32; + } +} +', 'error_static_method_takes_resource') +} + +fn test_validate_containers_ok() { + src := 'package p:q; + +interface i { + resource handle { + constructor(); + } + a: func() -> future; + b: func() -> stream; + m: func() -> map; + t: func(h: own) -> borrow; +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { panic(err) } +} + +fn test_validate_unknown_include() { + expect_validate_error('package p:q; + +world base { + import x: func() -> u32; +} + +world derived { + include missing; +} +', 'error_unknown_include') +} + +fn test_validate_unresolved_include_item() { + expect_validate_error('package p:q; + +world base { + import x: func() -> u32; +} + +world derived { + include base with { nope as renamed }; +} +', 'error_unresolved_include') +} + +fn test_validate_bad_package_version() { + expect_validate_error('package p:q@1.0; + +interface i { + f: func() -> u32; +} +', 'error_invalid_package_version') +} + +fn test_validate_multi_interface_ok() { + src2 := 'package example:demo@0.1.0; + +@since(version = 0.1.0) +interface types { + resource handle { + constructor(); + read: func() -> string; + make: static func() -> u32; + } + take: func(h: own) -> u32; +} + +@since(version = 0.1.0) +interface api { + use types.{take}; + go: func() -> u32; +} + +world app { + export api; +} +' + doc := parse(src2) or { panic(err) } + validate(doc) or { panic(err) } +} diff --git a/vlib/wit/wit.v b/vlib/wit/wit.v new file mode 100644 index 00000000000000..7c40c110874976 --- /dev/null +++ b/vlib/wit/wit.v @@ -0,0 +1,205 @@ +module wit + +// WIT (WebAssembly Interface Types) AST + helpers. +// This module is intentionally a small, dependency-free implementation. + +pub enum WitTypeKind { + prim + list + option + result + tuple + named +} + +pub struct WitType { +pub mut: + kind WitTypeKind + prim string + items []WitType + name string +} + +pub fn prim_type(s string) WitType { + return WitType{ kind: .prim, prim: s } +} + +pub fn named_type(s string) WitType { + return WitType{ kind: .named, name: s } +} + +pub struct WitParam { +pub mut: + name string + typ WitType +} + +pub struct WitField { +pub mut: + name string + typ WitType +} + +pub struct WitFunc { +pub mut: + name string + params []WitParam + results []WitType + attrs []string + is_static bool +} + +pub struct WitRecord { +pub mut: + name string + fields []WitField +} + +pub struct WitVariantCase { +pub mut: + name string + typ ?WitType +} + +pub struct WitVariant { +pub mut: + name string + cases []WitVariantCase +} + +pub struct WitEnum { +pub mut: + name string + cases []string +} + +pub struct WitFlags { +pub mut: + name string + flags []string +} + +pub struct WitResource { +pub mut: + name string + methods []WitFunc +} + +pub struct WitTypeDef { +pub mut: + name string + typ WitType +} + +pub struct WitUse { +pub mut: + path string + items []string +} + +pub enum WitItemKind { + func + record + variant + enum_ + flags + resource + type_alias + use +} + +pub struct WitItem { +pub mut: + kind WitItemKind + func ?WitFunc + record ?WitRecord + variant ?WitVariant + enum_ ?WitEnum + flags ?WitFlags + resource ?WitResource + type_def ?WitTypeDef + use_ ?WitUse + attrs []string +} + +pub struct WitInterface { +pub mut: + name string + items []WitItem + attrs []string +} + +pub struct WitWorldImport { +pub mut: + name string + interface string + func ?WitFunc +} + +pub struct WitWorldExport { +pub mut: + name string + interface string + func ?WitFunc +} + +pub struct WitInclude { +pub mut: + path string + with_renames map[string]string +} + +pub struct WitWorld { +pub mut: + name string + imports []WitWorldImport + exports []WitWorldExport + includes []WitInclude + attrs []string +} + +pub struct WitPackage { +pub mut: + namespace string + name string + version string +} + +pub struct WitDoc { +pub mut: + package WitPackage + interfaces []WitInterface + worlds []WitWorld +} + +// ---- helpers ---- + +pub fn (t WitType) str() string { + match t.kind { + .prim { return t.prim } + .named { return t.name } + .list { + if t.items.len > 0 { return 'list<${t.items[0].str()}>' } + return 'list<_>' + } + .option { + if t.items.len > 0 { return 'option<${t.items[0].str()}>' } + return 'option<_>' + } + .result { + okstr := if t.items.len > 0 { t.items[0].str() } else { '_' } + errstr := if t.items.len > 1 { t.items[1].str() } else { '_' } + return 'result<${okstr}, ${errstr}>' + } + .tuple { + mut parts := []string{} + for it in t.items { parts << it.str() } + return 'tuple<${parts.join(', ')}>' + } + } +} + +pub fn is_prim_name(s string) bool { + return s in ['bool', 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'f32', 'f64', 'char', + 'string'] +} diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v new file mode 100644 index 00000000000000..8f7b8a528014a4 --- /dev/null +++ b/vlib/wit/wit_test.v @@ -0,0 +1,394 @@ +module wit + +import os +import wasm + +fn test_parse_basic() { + src := ' +package example:demo@0.1.0; + +interface types { + record point { + x: f64, + y: f64, + } + enum color { red, green, blue } + variant shape { circle(f64), empty } + flags perms { read, write } + type alias = u32; + + add: func(a: s32, b: s32) -> s32; + greet: func(name: string) -> string; +} + +world app { + import types; + export run: func(); +} +' + doc := parse(src) or { panic(err) } + assert doc.package.namespace == 'example' + assert doc.package.name == 'demo' + assert doc.interfaces.len == 1 + iface := doc.interfaces[0] + assert iface.name == 'types' + assert iface.items.len == 7 + assert doc.worlds.len == 1 + w := doc.worlds[0] + assert w.name == 'app' + assert w.imports.len == 1 + assert w.exports.len == 1 + // round-trip type str + assert prim_type('s32').str() == 's32' + + generated := doc.to_v() + assert generated.contains('pub struct Point') + assert generated.contains('pub fn add') + + doc2 := parse(src) or { panic(err) } + printed := doc2.str() + assert printed.contains('interface types') + assert printed.contains('record point') +} + +fn test_wasi_fs_types_codegen_compiles() { + src := os.read_file('vlib/wasm/wit/testdata/wasi/fs_types.wit') or { panic(err) } + doc := parse(src) or { panic(err) } + generated := doc.to_v() + assert generated.len > 0 + os.write_file('vlib/wasm/wit/testdata/wasi/fs_types_generated.v', generated) or { panic(err) } + res := os.exec(['v', '-check', 'vlib/wasm/wit/testdata/wasi/fs_types_generated.v']) + assert res.exit_code == 0, res.output +} + +fn test_validate_unknown_type() { + src := 'package p:q; + +interface i { + f: func(x: unknown_type) -> u32; +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { + assert err.msg().contains('error_unknown_type') + return + } + panic('expected error') +} + +fn test_validate_duplicate_name() { + src := 'package p:q; + +interface i { + x: func() -> u32; + x: func() -> u32; +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { + assert err.msg().contains('error_duplicate_name') + return + } + panic('expected error') +} + +fn test_validate_valid() { + src := 'package p:q; + +interface i { + f: func(x: u32) -> u32; +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { panic(err) } +} + +fn test_validate_duplicate_method() { + src := 'package p:q; + +interface i { + resource r { + m: func() -> u32; + m: func() -> u32; + } +} +' + doc := parse(src) or { panic(err) } + validate(doc) or { + assert err.msg().contains('error_duplicate_name') + return + } + panic('expected error') +} + +fn test_parse_list_option_result() { + src := ' +package p:q; + +interface i { + f: func(x: list, y: option) -> result; +} +' + doc := parse(src) or { panic(err) } + assert doc.interfaces.len == 1 + f := doc.interfaces[0].items[0].func or { panic('no func') } + assert f.params.len == 2 + assert f.results.len == 1 + assert f.results[0].str() == 'result' +} + +fn test_wrap_component_lifts_string_export() { + // core module with (i32, i32) -> i32 echo_len, cabi_realloc and memory + mut m := wasm.Module{} + mut echo := m.new_function('echo_len', [.i32_t, .i32_t], [.i32_t]) + { + echo.local_get(1) + } + m.commit(echo, true) + mut shout := m.new_function('shout', [.i32_t, .i32_t, .i32_t], [.i32_t]) + { + shout.local_get(2) + } + m.commit(shout, true) + mut hello := m.new_function('hello', [], [.i32_t, .i32_t]) + { + hello.i32_const(0) + hello.i32_const(2) + } + m.commit(hello, true) + mut realloc := m.new_function('cabi_realloc', [.i32_t, .i32_t, .i32_t, .i32_t], [ + .i32_t, + ]) + { + realloc.i32_const(0) + } + m.commit(realloc, true) + m.assign_memory('memory', true, 1, none) + core := m.compile() + doc := parse('package example:echo@0.1.0; + +world echo { + export echo-len: func(s: string) -> u32;\n\texport shout: func(s: string, loud: u8) -> u32;\n\texport hello: func() -> string; +} +') or { panic(err) } + comp := wrap_component(core, doc, '') + assert wasm.is_component(comp) + // walk component sections + mut pos := 8 + mut export_names := []string{} + mut has_string_param := false + mut has_string_result := false + mut lift_has_mem_opt := false + mut lift_has_realloc_opt := false + for pos < comp.len { + sid := comp[pos] + pos++ + mut size := 0 + mut shift := 0 + for { + b := comp[pos] + pos++ + size |= (int(b) & 0x7f) << shift + shift += 7 + if b & 0x80 == 0 { + break + } + } + end := pos + size + if sid == 7 { + // type section: look for a functype with one 0x73 (string) param + mut p := pos + mut cnt := 0 + mut sh := 0 + for { + b := comp[p] + p++ + cnt |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + for _ in 0 .. cnt { + assert comp[p] == 0x40 + p++ + mut pc := 0 + sh = 0 + for { + b := comp[p] + p++ + pc |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + mut ptypes := []u8{} + for _ in 0 .. pc { + mut ln := 0 + sh = 0 + for { + b := comp[p] + p++ + ln |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + p += ln + ptypes << comp[p] + p++ + } + if ptypes == [u8(0x73)] { + has_string_param = true + } + // skip results + if comp[p] == 0x01 { + p += 2 + } else { + p += 1 + if comp[p] == 0x73 { + has_string_result = true + } + p += 1 + } + } + } + if sid == 8 { + // canon section: any lift carrying both memory and realloc opts + mut p := pos + mut cnt := 0 + mut sh := 0 + for { + b := comp[p] + p++ + cnt |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + for _ in 0 .. cnt { + assert comp[p] == 0x00 + p++ + assert comp[p] == 0x00 + p++ + // func idx + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + // opts + mut ocnt := 0 + sh = 0 + for { + b := comp[p] + p++ + ocnt |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + mut has_mem := false + mut has_re := false + for _ in 0 .. ocnt { + code := comp[p] + p++ + if code == 0x03 { + has_mem = true + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + } else if code == 0x04 { + has_re = true + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + } else { + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + } + } + // type idx + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + if has_mem && has_re { + lift_has_mem_opt = true + lift_has_realloc_opt = true + } + } + } + if sid == 11 { + mut p := pos + mut cnt := 0 + mut sh := 0 + for { + b := comp[p] + p++ + cnt |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + for _ in 0 .. cnt { + assert comp[p] == 0x00 + p++ + mut ln := 0 + sh = 0 + for { + b := comp[p] + p++ + ln |= (int(b) & 0x7f) << sh + sh += 7 + if b & 0x80 == 0 { + break + } + } + name := comp[p..p + ln].bytestr() + p += ln + export_names << name + // sort + idx + p++ + for { + b := comp[p] + p++ + if b & 0x80 == 0 { + break + } + } + } + } + pos = end + } + assert 'echo-len' in export_names, 'component must export echo-len, got ${export_names}' + assert has_string_param, 'component type must declare a string param' + assert has_string_result, 'component type must declare a string result' + assert 'shout' in export_names + assert 'hello' in export_names + assert lift_has_mem_opt, 'canon lift must carry (memory)' + assert lift_has_realloc_opt, 'canon lift must carry (realloc)' +}