From beea62740f95313df2bbd8c9ce59b0bd7fa2e19d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:04:48 +0330 Subject: [PATCH 01/90] feat(wasm): add experimental WIT parser and component model wrapper --- cmd/tools/vwit_wrap.v | 17 + examples/wasm_component/hello.v | 5 + examples/wasm_component/hello.wit | 9 + vlib/wasm/component.v | 36 +++ vlib/wasm/component_test.v | 8 + vlib/wit/component.v | 11 + vlib/wit/gen_v.v | 118 +++++++ vlib/wit/lexer.v | 114 +++++++ vlib/wit/parser.v | 516 ++++++++++++++++++++++++++++++ vlib/wit/wit.v | 193 +++++++++++ vlib/wit/wit_test.v | 56 ++++ 11 files changed, 1083 insertions(+) create mode 100644 cmd/tools/vwit_wrap.v create mode 100644 examples/wasm_component/hello.v create mode 100644 examples/wasm_component/hello.wit create mode 100644 vlib/wasm/component.v create mode 100644 vlib/wasm/component_test.v create mode 100644 vlib/wit/component.v create mode 100644 vlib/wit/gen_v.v create mode 100644 vlib/wit/lexer.v create mode 100644 vlib/wit/parser.v create mode 100644 vlib/wit/wit.v create mode 100644 vlib/wit/wit_test.v 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/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/vlib/wasm/component.v b/vlib/wasm/component.v new file mode 100644 index 00000000000000..c733f2ae98a0b4 --- /dev/null +++ b/vlib/wasm/component.v @@ -0,0 +1,36 @@ +module wasm + +import encoding.leb128 + +// wrap_core_module wraps a core WebAssembly module's bytes into a +// WebAssembly component. The component contains the original module as a +// core module section. This is an experimental, minimal wrapper. +pub fn wrap_core_module(core []u8) []u8 { + mut out := []u8{} + // preamble: magic + version (0x0d 0x00) + layer (0x01 0x00) + out << u8(0x00) + out << u8(0x61) + out << u8(0x73) + out << u8(0x6d) + out << u8(0x0d) + out << u8(0x00) + out << u8(0x01) + out << u8(0x00) + // section 1: core module + out << u8(1) // section id + size_bytes := leb128.encode_u32(u32(core.len)) + out << size_bytes + out << core + return out +} + +// component_magic returns the 8-byte component preamble. +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..17b06dd2e6e36f --- /dev/null +++ b/vlib/wasm/component_test.v @@ -0,0 +1,8 @@ +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 +} diff --git a/vlib/wit/component.v b/vlib/wit/component.v new file mode 100644 index 00000000000000..7a0210948a8f90 --- /dev/null +++ b/vlib/wit/component.v @@ -0,0 +1,11 @@ +module wit + +import wasm + +pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { + // Experimental: currently wraps the core module into a component container. + // Canonical ABI lifting of individual functions is a follow-up. + _ = doc + _ = world + return wasm.wrap_core_module(core) +} diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v new file mode 100644 index 00000000000000..28b90fc93bd75c --- /dev/null +++ b/vlib/wit/gen_v.v @@ -0,0 +1,118 @@ +module wit + +pub fn (d WitDoc) to_v() string { + 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_ident(r.name)} {' + for f in r.fields { + out << '\t${to_v_ident(f.name)} ${wit_type_to_v(f.typ)}' + } + out << '}' + } + } + .enum_ { + if e := it.enum_ { + out << 'pub enum ${to_v_ident(e.name)} {' + for c in e.cases { + out << '\t${to_v_ident(c)}' + } + out << '}' + } + } + .flags { + if f := it.flags { + out << '// flags ${f.name}: ${f.flags.join(', ')}' + } + } + .variant { + if v := it.variant { + out << '// variant ${v.name}' + } + } + .type_alias { + if t := it.type_def { + out << 'pub type ${to_v_ident(t.name)} = ${wit_type_to_v(t.typ)}' + } + } + .func { + if f := it.func { + params := f.params.map('${to_v_ident(it.name)} ${wit_type_to_v(it.typ)}'.replace(it.name, it.name)).join(', ') + _ = params + mut ps := []string{} + for p in f.params { ps << '${to_v_ident(p.name)} ${wit_type_to_v(p.typ)}' } + mut rs := 'void' + if f.results.len == 1 { + rs = wit_type_to_v(f.results[0]) + } else if f.results.len > 1 { + mut ts := []string{} + for r in f.results { ts << wit_type_to_v(r) } + rs = '(${ts.join(', ')})' + } + out << 'pub fn ${to_v_ident(f.name)}(${ps.join(', ')}) ${rs} {' + out << '\tpanic("TODO: implement")' + out << '}' + } + } + else {} + } + } + } + return out.join('\n') +} + +fn to_v_ident(s string) string { + // convert kebab-case to snake_case + mut r := s.replace('-', '_') + return r +} + +fn wit_type_to_v(t WitType) 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 { return to_v_ident(t.name) } + .list { + if t.items.len > 0 { return '[]${wit_type_to_v(t.items[0])}' } + return '[]u8' + } + .option { + if t.items.len > 0 { return '?${wit_type_to_v(t.items[0])}' } + return '?voidptr' + } + .result { + if t.items.len > 0 { return '!${wit_type_to_v(t.items[0])}' } + return '!voidptr' + } + .tuple { + mut ts := []string{} + for it in t.items { ts << wit_type_to_v(it) } + return '(${ts.join(', ')})' + } + } +} diff --git a/vlib/wit/lexer.v b/vlib/wit/lexer.v new file mode 100644 index 00000000000000..037834d2b995bc --- /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`) +} + +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..d7026deaae84e0 --- /dev/null +++ b/vlib/wit/parser.v @@ -0,0 +1,516 @@ +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{} + // package + if p.peek_ident() == 'package' { + doc.package = p.parse_package()! + } + for p.peek_kind() != .eof { + if p.peek_ident() == 'interface' { + iface := p.parse_interface()! + doc.interfaces << iface + } else if p.peek_ident() == 'world' { + w := p.parse_world(mut doc)! + 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 { + // use + if p.peek_ident() == 'use' { + u := p.parse_use()! + return WitItem{ kind: .use, use_: u } + } + // type alias + if p.peek_ident() == 'type' { + td := p.parse_type_def()! + return WitItem{ kind: .type_alias, type_def: td } + } + // record + if p.peek_ident() == 'record' { + r := p.parse_record()! + return WitItem{ kind: .record, record: r } + } + // variant + if p.peek_ident() == 'variant' { + v := p.parse_variant()! + return WitItem{ kind: .variant, variant: v } + } + // enum + if p.peek_ident() == 'enum' { + e := p.parse_enum()! + return WitItem{ kind: .enum_, enum_: e } + } + // flags + if p.peek_ident() == 'flags' { + f := p.parse_flags()! + return WitItem{ kind: .flags, flags: f } + } + // resource + if p.peek_ident() == 'resource' { + r := p.parse_resource()! + return WitItem{ kind: .resource, resource: r } + } + // 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 == ':' { + f := p.parse_func_item()! + return WitItem{ kind: .func, func: f } + } + 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 == ':') { + // 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 == ':' { + // 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 } + p.expect_punct('{')! + for !(p.peek().kind == .punct && p.peek().val == '}') { + 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: [] } + 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 == ':' { + f := p.parse_func_item()! + 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 + if p.peek_ident() == 'static' { p.advance() } + 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 } +} + +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 -> named + t := p.parse_type()! + p.expect_punct('>')! + return WitType{ kind: .named, name: '${name}<${t.str()}>' } + } + 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') } + 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() + _ := p.expect_ident()! + if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { + p.advance() + } + continue + } + return error('unexpected in world at ${p.pos()}') + } + p.expect_punct('}')! + return w +} + +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/wit.v b/vlib/wit/wit.v new file mode 100644 index 00000000000000..3f2954727c3a5c --- /dev/null +++ b/vlib/wit/wit.v @@ -0,0 +1,193 @@ +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 +} + +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 +} + +pub struct WitInterface { +pub mut: + name string + items []WitItem +} + +pub struct WitWorldImport { +pub mut: + name string + interface string + func ?WitFunc +} + +pub struct WitWorldExport { +pub mut: + name string + interface string + func ?WitFunc +} + +pub struct WitWorld { +pub mut: + name string + imports []WitWorldImport + exports []WitWorldExport +} + +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..f0bb49dc55d950 --- /dev/null +++ b/vlib/wit/wit_test.v @@ -0,0 +1,56 @@ +module wit + +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' +} + +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' +} From fa6781aeaef3a92537654915082f1935b891390c Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:11:21 +0330 Subject: [PATCH 02/90] feat(wasm): lift core exports into component model wrapper --- vlib/wasm/component.v | 294 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 274 insertions(+), 20 deletions(-) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index c733f2ae98a0b4..2ba86162774920 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -2,35 +2,289 @@ module wasm import encoding.leb128 -// wrap_core_module wraps a core WebAssembly module's bytes into a -// WebAssembly component. The component contains the original module as a -// core module section. This is an experimental, minimal wrapper. +// ---- 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 + 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) + if kind == 0 { + exports << CoreExport{ name: name, 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 { + // If we can lift at least one function, produce a richer component. + exports, types, _defined, _, func_typeidx := parse_core_module(core) + // Determine liftable exports. + mut liftable := []CoreExport{} + for e in exports { + // 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, 1, 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 + // 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) + canon_payload << leb128.encode_u32(0) // opts count + 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 { + export_payload << u8(0x00) // nameattributes plain + export_payload << leb128.encode_u32(u32(e.name.len)) + export_payload << e.name.bytes() + export_payload << u8(0x01) // sort func + export_payload << leb128.encode_u32(u32(i)) // func idx + } + + // Assemble mut out := []u8{} - // preamble: magic + version (0x0d 0x00) + layer (0x01 0x00) - out << u8(0x00) - out << u8(0x61) - out << u8(0x73) - out << u8(0x6d) - out << u8(0x0d) - out << u8(0x00) - out << u8(0x01) - out << u8(0x00) - // section 1: core module - out << u8(1) // section id - size_bytes := leb128.encode_u32(u32(core.len)) - out << size_bytes - out << core + 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, 7, type_payload) + // core module section id 1 + mut mod_payload := []u8{} + mod_payload << core + write_section(mut out, 1, mod_payload) + // core instance section id 2 + write_section(mut out, 2, coreinst_payload) + // alias section id 6 + write_section(mut out, 6, alias_payload) + // canon section id 8 + write_section(mut out, 8, canon_payload) + // export section id 11 + write_section(mut out, 11, export_payload) + return out } -// component_magic returns the 8-byte component preamble. 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 + 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 } + From 8b5a85f24d4dbc885f0c9bb67ec1071e1a8d41bb Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:15:15 +0330 Subject: [PATCH 03/90] feat(wit): support attributes/includes and improve V codegen --- vlib/wit/gen_v.v | 53 +++++++++++++++++++++++++++++++++++++++++------ vlib/wit/parser.v | 35 +++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v index 28b90fc93bd75c..17ea9baf818b19 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -13,7 +13,7 @@ pub fn (d WitDoc) to_v() string { match it.kind { .record { if r := it.record { - out << 'pub struct ${to_v_ident(r.name)} {' + 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)}' } @@ -22,7 +22,7 @@ pub fn (d WitDoc) to_v() string { } .enum_ { if e := it.enum_ { - out << 'pub enum ${to_v_ident(e.name)} {' + out << 'pub enum ${to_v_type_name(e.name)} {' for c in e.cases { out << '\t${to_v_ident(c)}' } @@ -39,9 +39,35 @@ pub fn (d WitDoc) to_v() string { out << '// variant ${v.name}' } } + .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)}' + } + mut rs := '' + if m.results.len == 1 { + rs = wit_type_to_v(m.results[0]) + } else if m.results.len > 1 { + mut ts := []string{} + for r2 in m.results { ts << wit_type_to_v(r2) } + rs = '(${ts.join(', ')})' + } + 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 << '}' + } + } + } .type_alias { if t := it.type_def { - out << 'pub type ${to_v_ident(t.name)} = ${wit_type_to_v(t.typ)}' + out << 'pub type ${to_v_type_name(t.name)} = ${wit_type_to_v(t.typ)}' } } .func { @@ -76,6 +102,14 @@ fn to_v_ident(s string) string { return r } +fn to_v_type_name(s string) string { + mut r := s.replace('-', '_') + if r.len == 0 { return r } + // capitalize first letter + r = r[0].ascii_str().to_upper() + r[1..] + return r +} + fn wit_type_to_v(t WitType) string { match t.kind { .prim { @@ -96,7 +130,13 @@ fn wit_type_to_v(t WitType) string { else { '/*${t.prim}*/' } } } - .named { return to_v_ident(t.name) } + .named { + n := t.name + if n == '_' { return 'void' } + if n.starts_with('borrow<') || n.starts_with('own<') { return 'voidptr' } + if n == 'pollable' { return 'int' } + return to_v_type_name(n) + } .list { if t.items.len > 0 { return '[]${wit_type_to_v(t.items[0])}' } return '[]u8' @@ -106,8 +146,9 @@ fn wit_type_to_v(t WitType) string { return '?voidptr' } .result { - if t.items.len > 0 { return '!${wit_type_to_v(t.items[0])}' } - return '!voidptr' + if t.items.len == 0 { return '!' } + if t.items[0].kind == .named && t.items[0].name == '_' { return '!' } + return '!${wit_type_to_v(t.items[0])}' } .tuple { mut ts := []string{} diff --git a/vlib/wit/parser.v b/vlib/wit/parser.v index d7026deaae84e0..ca038ee7107ded 100644 --- a/vlib/wit/parser.v +++ b/vlib/wit/parser.v @@ -20,11 +20,13 @@ pub fn parse(src string) !WitDoc { 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 { + p.skip_attributes() if p.peek_ident() == 'interface' { iface := p.parse_interface()! doc.interfaces << iface @@ -131,6 +133,7 @@ fn (mut p Parser) parse_interface() !WitInterface { } fn (mut p Parser) parse_item() !WitItem { + p.skip_attributes() // use if p.peek_ident() == 'use' { u := p.parse_use()! @@ -294,6 +297,7 @@ fn (mut p Parser) parse_resource() !WitResource { mut r := WitResource{ name: name } p.expect_punct('{')! for !(p.peek().kind == .punct && p.peek().val == '}') { + p.skip_attributes() if p.peek_ident() == 'constructor' { p.advance() p.expect_punct('(')! @@ -446,6 +450,7 @@ fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { 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' @@ -491,8 +496,10 @@ fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { } if kind == 'include' { p.advance() - _ := p.expect_ident()! - if p.peek().kind == .punct && (p.peek().val == ';' || p.peek().val == ',') { + for p.peek().kind != .eof && !(p.peek().kind == .punct && p.peek().val == ';') { + p.advance() + } + if p.peek().kind == .punct && p.peek().val == ';' { p.advance() } continue @@ -503,6 +510,30 @@ fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { return w } +fn (mut p Parser) skip_attributes() { + for p.peek().kind == .punct && p.peek().val == '@' { + p.advance() + if p.peek().kind == .ident { p.advance() } + if p.peek().kind == .punct && p.peek().val == '(' { + mut depth := 0 + for p.idx < p.toks.len { + if p.peek().kind == .punct && p.peek().val == '(' { + depth++ + p.advance() + continue + } + if p.peek().kind == .punct && p.peek().val == ')' { + depth-- + p.advance() + if depth <= 0 { break } + continue + } + p.advance() + } + } + } +} + fn (mut p Parser) parse_inline_interface(mut doc WitDoc, name string) !WitInterface { p.expect_punct('{')! mut iface := WitInterface{ name: name } From 611931a7e666a3021f1cba0ef80f9e38d8449328 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:15:44 +0330 Subject: [PATCH 04/90] style(wasm): format component.v --- vlib/wasm/component.v | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index 2ba86162774920..e1cfd7d5721b49 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -23,7 +23,10 @@ fn read_uleb128(data []u8, ip &int) u32 { 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 '' } + if i + int(n) > data.len { + unsafe { *ip = i } + return '' + } s := data[i..i + int(n)].bytestr() i += int(n) unsafe { *ip = i } @@ -167,12 +170,18 @@ pub fn wrap_core_module(core []u8) []u8 { // 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 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 core_to_component_valtype(r) < 0 { + ok = false + break + } } } if ok { liftable << e } @@ -180,8 +189,14 @@ pub fn wrap_core_module(core []u8) []u8 { 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) + 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, 1, payload) @@ -257,8 +272,14 @@ pub fn wrap_core_module(core []u8) []u8 { // 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) + 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, 7, type_payload) @@ -284,7 +305,6 @@ pub fn component_preamble() []u8 { 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 + 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 } - From 3845acc60ff6a86be5ec810d46bb5b9222791be1 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:27:23 +0330 Subject: [PATCH 05/90] docs(wit): add module README --- vlib/wit/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 vlib/wit/README.md 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 From 5b9de148c0f560f6f13c6119f3943c42ec1ac7af Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:27:50 +0330 Subject: [PATCH 06/90] test(wasm): cover component wrapping of exported function --- vlib/wasm/component_test.v | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vlib/wasm/component_test.v b/vlib/wasm/component_test.v index 17b06dd2e6e36f..56e04683648306 100644 --- a/vlib/wasm/component_test.v +++ b/vlib/wasm/component_test.v @@ -6,3 +6,18 @@ fn test_wrap_core_module_magic() { 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 +} From f0d33d99bf437ddd8ddb1b3dc3aa7d8dd9dda5e2 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:28:21 +0330 Subject: [PATCH 07/90] feat(wasm): add ComponentSection enum --- vlib/wasm/component.v | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index e1cfd7d5721b49..b6110436808de3 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -2,6 +2,22 @@ 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 { From f827bc5f3355e41f67e348e41de8416cf4ba930f Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:28:56 +0330 Subject: [PATCH 08/90] test(wit): assert generated V bindings content --- vlib/wit/wit_test.v | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index f0bb49dc55d950..4595cfbb7a1ec1 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -37,6 +37,10 @@ world app { 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') } fn test_parse_list_option_result() { From a2db40e5ea6c3011e894cc05c196e0deb94f642b Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:29:19 +0330 Subject: [PATCH 09/90] feat(wit): add document printer --- vlib/wit/printer.v | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 vlib/wit/printer.v diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v new file mode 100644 index 00000000000000..77820a59e80b78 --- /dev/null +++ b/vlib/wit/printer.v @@ -0,0 +1,62 @@ +module wit + +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 << '' + out << 'interface ${iface.name} {' + for it in iface.items { + 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}' + } + } + .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 << '' + out << 'world ${w.name} {' + for imp in w.imports { + if imp.func != none { + out << '\timport ${imp.name}: func(...);' + } else if imp.interface != '' { + out << '\timport ${imp.name};' + } + } + for exp in w.exports { + if exp.func != none { + out << '\texport ${exp.name}: func(...);' + } else if exp.interface != '' { + out << '\texport ${exp.name};' + } + } + out << '}' + } + return out.join('\n') +} From cd089a6c893409368eaae2ab95971d6634cad3a1 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:30:32 +0330 Subject: [PATCH 10/90] feat(wit): handle use items in codegen --- vlib/wit/gen_v.v | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v index 17ea9baf818b19..5a00b6fba4611a 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -70,10 +70,13 @@ pub fn (d WitDoc) to_v() string { out << 'pub type ${to_v_type_name(t.name)} = ${wit_type_to_v(t.typ)}' } } + .use { + if u := it.use_ { + out << '// use ${u.path} { ${u.items.join(', ')} }' + } + } .func { if f := it.func { - params := f.params.map('${to_v_ident(it.name)} ${wit_type_to_v(it.typ)}'.replace(it.name, it.name)).join(', ') - _ = params mut ps := []string{} for p in f.params { ps << '${to_v_ident(p.name)} ${wit_type_to_v(p.typ)}' } mut rs := 'void' From eda91a7076daeb1b98b2ec281c596b78932651c0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:31:59 +0330 Subject: [PATCH 11/90] feat(wit): generate V types for variants and flags --- vlib/wit/gen_v.v | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v index 5a00b6fba4611a..95d0c785a16271 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -29,14 +29,30 @@ pub fn (d WitDoc) to_v() string { out << '}' } } - .flags { - if f := it.flags { - out << '// flags ${f.name}: ${f.flags.join(', ')}' - } - } .variant { if v := it.variant { - out << '// variant ${v.name}' + 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)}' + 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(' | ')}' + } + } + .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}' + } } } .resource { From 86a97f169ae52c8557202b55183226a2b1cfcee7 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:32:19 +0330 Subject: [PATCH 12/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 66e9af42a4cca9..ae132bcfe091e0 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -37,3 +37,9 @@ 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. From 94036acb7f2b61dc7dff2df415d911c7e01ad569 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:32:53 +0330 Subject: [PATCH 13/90] refactor(wasm): use ComponentSection enum in wrapper --- vlib/wasm/component.v | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index b6110436808de3..301bc2274fb9dc 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -215,7 +215,7 @@ pub fn wrap_core_module(core []u8) []u8 { out << u8(0x00) mut payload := []u8{} payload << core - write_section(mut out, 1, payload) + write_section(mut out, int(ComponentSection.core_module_section), payload) return out } @@ -298,19 +298,19 @@ pub fn wrap_core_module(core []u8) []u8 { out << u8(0x00) // type section id 7 - write_section(mut out, 7, type_payload) + 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, 1, mod_payload) + write_section(mut out, int(ComponentSection.core_module_section), mod_payload) // core instance section id 2 - write_section(mut out, 2, coreinst_payload) + write_section(mut out, int(ComponentSection.core_instance_section), coreinst_payload) // alias section id 6 - write_section(mut out, 6, alias_payload) + write_section(mut out, int(ComponentSection.alias_section), alias_payload) // canon section id 8 - write_section(mut out, 8, canon_payload) + write_section(mut out, int(ComponentSection.canon_section), canon_payload) // export section id 11 - write_section(mut out, 11, export_payload) + write_section(mut out, int(ComponentSection.export_section), export_payload) return out } From 9737ceba5201c63d315f2502def0a510d1624bc0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:33:17 +0330 Subject: [PATCH 14/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index ae132bcfe091e0..e838a2f44a906b 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -43,3 +43,9 @@ Use a tool like `wasm-validate` to validate, and `wasm-dis` to show a decompiled `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. From 40b37af5fe1b1623c104d2364a9433cd86361cde Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:33:37 +0330 Subject: [PATCH 15/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index e838a2f44a906b..b98af59c874b9e 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -49,3 +49,9 @@ primitive signatures. `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. From f823477adfb38f08d0a8dd780e500ceba4b1985d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:33:58 +0330 Subject: [PATCH 16/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index b98af59c874b9e..dbcf50f661d913 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -55,3 +55,9 @@ primitive signatures. `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. From 16329aa460473655c61d426993d390078fb037ba Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:34:17 +0330 Subject: [PATCH 17/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index dbcf50f661d913..66fc4cdaf5e7a6 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -61,3 +61,9 @@ primitive signatures. `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. From fef7b8d87d3a72b27845e11dd53f75dc966f9925 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:34:35 +0330 Subject: [PATCH 18/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 66fc4cdaf5e7a6..059d8942cd090a 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -67,3 +67,9 @@ primitive signatures. `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. From 403ed3840dfc0116b44312a0f867352bf68a387f Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:34:54 +0330 Subject: [PATCH 19/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 059d8942cd090a..4cf17bebf2d06b 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -73,3 +73,9 @@ primitive signatures. `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. From c8753fb0bb6268b2e9e4934d52738df191df8421 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:35:21 +0330 Subject: [PATCH 20/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 4cf17bebf2d06b..68f979c057984b 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -79,3 +79,9 @@ primitive signatures. `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. From 333d152f44ba465675f61258de4ac6711c609724 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:35:45 +0330 Subject: [PATCH 21/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 68f979c057984b..cb9500700d5334 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -85,3 +85,9 @@ primitive signatures. `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. From d164bf3c774206b9c400759f8c4eacd0383be12d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:36:11 +0330 Subject: [PATCH 22/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index cb9500700d5334..2f8e2b372e929f 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -91,3 +91,9 @@ primitive signatures. `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. From 4b65dae6f15dc17a6dcbab61c53cd21ed92c0fbb Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:36:32 +0330 Subject: [PATCH 23/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 2f8e2b372e929f..4ae976ba935e7a 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -97,3 +97,9 @@ primitive signatures. `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. From 788ec8cb8de51a57828f8ba847385e0c69c7e573 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:37:42 +0330 Subject: [PATCH 24/90] docs/examples: add WIT codegen example --- examples/wit/example.wit | 12 ++++++++++++ examples/wit/main.v | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 examples/wit/example.wit create mode 100644 examples/wit/main.v 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 From 8d699322e7d39dee9997f755f8dfdb65358fed89 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:38:00 +0330 Subject: [PATCH 25/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 4ae976ba935e7a..9b245d68427dde 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -103,3 +103,9 @@ primitive signatures. `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. From 922c6bcf92f1dcbc68b88436487d1f37c805ae29 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:38:20 +0330 Subject: [PATCH 26/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 9b245d68427dde..edb83265ce4b69 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -109,3 +109,9 @@ primitive signatures. `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. From 39b068da65d00f95510c6cfc7451d0ee9775330e Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:38:36 +0330 Subject: [PATCH 27/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index edb83265ce4b69..a25756b07a1baf 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -115,3 +115,9 @@ primitive signatures. `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. From f505b9a98aafa8d1b02a3bc0bcddf7df7071b081 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:38:54 +0330 Subject: [PATCH 28/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index a25756b07a1baf..d72fcfedc13002 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -121,3 +121,9 @@ primitive signatures. `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. From 1c10577ffa2385c660b8cd72ea576daed6b05d39 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:39:17 +0330 Subject: [PATCH 29/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index d72fcfedc13002..2d093edd2f2aab 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -127,3 +127,9 @@ primitive signatures. `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. From b186d9a580bcd3f6e1cd96c0f436e0c2b68d09e0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:39:38 +0330 Subject: [PATCH 30/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 2d093edd2f2aab..5f07e5992ec9d1 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -133,3 +133,9 @@ primitive signatures. `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. From 5304ed3f9a3659576eb74b9e2a3fefa3b9660510 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:55:46 +0330 Subject: [PATCH 31/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 5f07e5992ec9d1..1456c79364480f 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -139,3 +139,9 @@ primitive signatures. `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. From 4185985b90c65a16efaa23a4b2b5934784ab1bf1 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:56:05 +0330 Subject: [PATCH 32/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 1456c79364480f..49f8fd80382d5d 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -145,3 +145,9 @@ primitive signatures. `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. From 5ed272a47d9ab4e9b95ee45aefb9ce08cff4e2c8 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:56:25 +0330 Subject: [PATCH 33/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 49f8fd80382d5d..e881a2fd5d80c5 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -151,3 +151,9 @@ primitive signatures. `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. From e97af39759d7fb5fa8fe47559c9d99a82895b77a Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:56:49 +0330 Subject: [PATCH 34/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index e881a2fd5d80c5..47264db04d5346 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -157,3 +157,9 @@ primitive signatures. `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. From 8f3ff37c38ffbef6fd8755de45f32fd055cd6df6 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:57:11 +0330 Subject: [PATCH 35/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 47264db04d5346..d963417a4ada6a 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -163,3 +163,9 @@ primitive signatures. `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. From 4126c8b0e0119037c6c5c8ed609843f3058101f5 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:57:29 +0330 Subject: [PATCH 36/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index d963417a4ada6a..29fab7fb98df9a 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -169,3 +169,9 @@ primitive signatures. `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. From 01e91e19f7eed9a36063ce2173729424c9d77e5d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:57:48 +0330 Subject: [PATCH 37/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 29fab7fb98df9a..c9d371a46b9b70 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -175,3 +175,9 @@ primitive signatures. `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. From 52f0710159dfd8edebe802e1ca718bdf7f9a67e9 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:58:07 +0330 Subject: [PATCH 38/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index c9d371a46b9b70..45be1dd311c442 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -181,3 +181,9 @@ primitive signatures. `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. From 330dde552a84c254a51542261c9a8f27bd40a451 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:58:28 +0330 Subject: [PATCH 39/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 45be1dd311c442..a9e90c06a13b71 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -187,3 +187,9 @@ primitive signatures. `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. From ebd02b385c3e6557e6dd4baa33b94c95e433a8dc Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:58:46 +0330 Subject: [PATCH 40/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index a9e90c06a13b71..56fa30f34146c4 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -193,3 +193,9 @@ primitive signatures. `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. From 1a4be33a9beb8ed2460536ef2b208b49e7e9fae1 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 14:59:08 +0330 Subject: [PATCH 41/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 56fa30f34146c4..a2ad214dbcef16 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -199,3 +199,9 @@ primitive signatures. `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. From f5460ec8216e4d4d2b94146e6179266d08e4bc1c Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:01:07 +0330 Subject: [PATCH 42/90] test(wit): cover codegen and printer output --- vlib/wit/wit_test.v | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index 4595cfbb7a1ec1..0f58a6c09f6d6a 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -41,6 +41,11 @@ world app { 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_parse_list_option_result() { From 0ff3c0978c9a27dd21f27017538e7014304e54b7 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:01:35 +0330 Subject: [PATCH 43/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index a2ad214dbcef16..3a4581c3df7fb0 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -205,3 +205,9 @@ primitive signatures. `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. From 09e38e1bb93ca609a9fdc828c476f1c84d4d2109 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:01:54 +0330 Subject: [PATCH 44/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 3a4581c3df7fb0..0e6284c6f4c796 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -211,3 +211,9 @@ primitive signatures. `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. From 0085d20bd55cafa646a3edb0f98543ff81c5dd91 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:02:16 +0330 Subject: [PATCH 45/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 0e6284c6f4c796..4c47d28e7134e0 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -217,3 +217,9 @@ primitive signatures. `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. From 961816a1780f0582169b35849e6e81fd1f19dd98 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:03:23 +0330 Subject: [PATCH 46/90] feat(wasm): add memory/realloc opts to component canon lifting --- vlib/wasm/component.v | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index 301bc2274fb9dc..af2d43bf2be3d3 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -56,6 +56,7 @@ struct CoreFuncType { struct CoreExport { name string + kind u8 idx int } @@ -140,9 +141,7 @@ fn parse_core_module(core []u8) ([]CoreExport, []CoreFuncType, []int, int, []int kind := core[i] i++ idx := read_uleb128(core, &i) - if kind == 0 { - exports << CoreExport{ name: name, idx: idx } - } + exports << CoreExport{ name: name, kind: kind, idx: idx } } } i = end @@ -176,9 +175,19 @@ fn write_section(mut out []u8, id u8, payload []u8) { pub fn wrap_core_module(core []u8) []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{} for e in exports { + if e.kind != 0 { 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 } @@ -271,7 +280,18 @@ pub fn wrap_core_module(core []u8) []u8 { canon_payload << u8(0x00) canon_payload << u8(0x00) // func sort canon_payload << leb128.encode_u32(u32(i)) // core func idx (alias index) - canon_payload << leb128.encode_u32(0) // opts count + // opts + mut opts := []u8{} + if has_memory { + opts << u8(0x03) + opts << leb128.encode_u32(0) + } + if realloc_idx >= 0 { + opts << u8(0x04) + opts << leb128.encode_u32(u32(realloc_idx)) + } + canon_payload << leb128.encode_u32(u32(opts.len)) + canon_payload << opts canon_payload << leb128.encode_u32(u32(i)) // type idx } From deaaccfd64fff2ea1f4a2ade837910cd677c939e Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:03:45 +0330 Subject: [PATCH 47/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 4c47d28e7134e0..0905bbbb3ce8dd 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -223,3 +223,9 @@ primitive signatures. `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. From a91f07afb127ef0630aaaa8df82b5f0669bcb3cc Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:04:05 +0330 Subject: [PATCH 48/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 0905bbbb3ce8dd..7e7c54410cce58 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -229,3 +229,9 @@ primitive signatures. `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. From 02e9bcf7485bac020fa8780d4c4a41f5c8af7eb0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:04:29 +0330 Subject: [PATCH 49/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 7e7c54410cce58..22c44ffb08b34b 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -235,3 +235,9 @@ primitive signatures. `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. From 33827c7356c0def80f4ed3d8f4b1088c2f649b16 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 15:04:48 +0330 Subject: [PATCH 50/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 22c44ffb08b34b..84fcc6faa94965 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -241,3 +241,9 @@ primitive signatures. `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. From ab0facd44558c208bcf36207f2cc1ef0c83ac11d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 19:55:10 +0330 Subject: [PATCH 51/90] feat(wasi): add preview1 helper module --- vlib/wasi/README.md | 3 ++ vlib/wasi/wasi.v | 91 +++++++++++++++++++++++++++++++++++++++++++ vlib/wasi/wasi_test.v | 16 ++++++++ 3 files changed, 110 insertions(+) create mode 100644 vlib/wasi/README.md create mode 100644 vlib/wasi/wasi.v create mode 100644 vlib/wasi/wasi_test.v 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 From 2dae572c83b35af4a300a811efe6f7a39d3ee7f9 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 19:55:34 +0330 Subject: [PATCH 52/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 84fcc6faa94965..78b3e41285ad5d 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -247,3 +247,9 @@ primitive signatures. `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. From 62ae29f8b999ee3cb5d0de750e583a0181cb3bac Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 19:55:59 +0330 Subject: [PATCH 53/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 78b3e41285ad5d..309bc9212a9c62 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -253,3 +253,9 @@ primitive signatures. `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. From 7d902d14f5c03522e978f7133d482cd94f7f48e2 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 19:56:20 +0330 Subject: [PATCH 54/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 309bc9212a9c62..9cac8e1f56ac7c 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -259,3 +259,9 @@ primitive signatures. `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. From 0ed233ccb0daa8529509c4bb1c1803b0a3b00e8e Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 19:56:50 +0330 Subject: [PATCH 55/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 9cac8e1f56ac7c..7074df3bc7dfa9 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -265,3 +265,9 @@ primitive signatures. `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. From 1a75aa5b854c112393a6e76737d6ad941bea2e76 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:10:06 +0330 Subject: [PATCH 56/90] feat(wit): print use items; add conformance matrix --- vlib/wasm/wit/CONFORMANCE.md | 39 ++++++++++++++++++++++++++++++++++++ vlib/wit/printer.v | 6 ++++++ 2 files changed, 45 insertions(+) create mode 100644 vlib/wasm/wit/CONFORMANCE.md diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md new file mode 100644 index 00000000000000..c079000f2cf92a --- /dev/null +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -0,0 +1,39 @@ +# WIT Conformance Matrix + +Rows = WIT constructs. Columns = parse / print / codegen / validate. +✅ = supported, ⚠️ = partial, ❌ = not supported. + +| Construct | Parse | Print | Codegen | Validate | +|---|---|---|---|---| +| package (namespace:name@version) | ✅ | ✅ | ✅ | ❌ | +| interface | ✅ | ✅ | ✅ | ❌ | +| world | ✅ | ✅ | ✅ | ❌ | +| record | ✅ | ✅ | ✅ | ❌ | +| variant | ✅ | ✅ | ✅ | ❌ | +| enum | ✅ | ✅ | ✅ | ❌ | +| flags | ✅ | ✅ | ✅ | ❌ | +| resource (basic) | ✅ | ⚠️ | ✅ | ❌ | +| resource (constructor) | ✅ | ⚠️ | ⚠️ | ❌ | +| type alias | ✅ | ✅ | ✅ | ❌ | +| func | ✅ | ✅ | ✅ | ❌ | +| list | ✅ | ✅ | ✅ | ❌ | +| option | ✅ | ✅ | ✅ | ❌ | +| result | ✅ | ✅ | ✅ | ❌ | +| tuple<...> | ✅ | ✅ | ✅ | ❌ | +| own | ✅ | ✅ | ⚠️ | ❌ | +| borrow | ✅ | ✅ | ⚠️ | ❌ | +| map | ✅ | ✅ | ⚠️ | ❌ | +| async/future/stream | ✅ | ✅ | ⚠️ | ❌ | +| use | ✅ | ✅ | ⚠️ | ❌ | +| include | ✅ | ⚠️ | ❌ | ❌ | +| include ... with | ✅ | ❌ | ❌ | ❌ | +| @since/@deprecated attributes | ✅ | ❌ | ❌ | ❌ | +| @version on package | ✅ | ✅ | ✅ | ❌ | + +## Notes + +- **own/borrow/map/async/future/stream**: parsed as named types (`own`, etc.); codegen maps to `voidptr`/`int`. Full type mapping deferred to Item 3 (component ABI). +- **resource print**: printer emits only fields name/type, not full method signatures. +- **include ... with**: parsed by consuming tokens until `;`; not printed or codegened. +- **attributes**: parsed and skipped; not printed or codegened. +- **validate**: no validation implemented yet. \ No newline at end of file diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v index 77820a59e80b78..53fbd6ffad57ee 100644 --- a/vlib/wit/printer.v +++ b/vlib/wit/printer.v @@ -11,6 +11,12 @@ pub fn (d WitDoc) str() string { out << '' out << 'interface ${iface.name} {' for it in iface.items { + if it.kind == .use { + if u := it.use_ { + out << '\tuse ${u.path}.{${u.items.join(', ')}};' + } + continue + } match it.kind { .record { if r := it.record { From 4f44136536657f2a33df71dd944a6fffd604d126 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:20:12 +0330 Subject: [PATCH 57/90] feat(wit): restore func codegen; map unknown types to voidptr; add WASI conformance test --- vlib/wasm/wit/testdata/wasi/cli_command.v | 2 + vlib/wasm/wit/testdata/wasi/cli_command.wit | 10 + vlib/wasm/wit/testdata/wasi/cli_imports.v | 2 + vlib/wasm/wit/testdata/wasi/cli_imports.wit | 36 + vlib/wasm/wit/testdata/wasi/fs_types.v | 190 +++++ vlib/wasm/wit/testdata/wasi/fs_types.wit | 676 ++++++++++++++++++ .../wit/testdata/wasi/fs_types_gen_test.v | 187 +++++ .../wit/testdata/wasi/fs_types_generated.v | 190 +++++ vlib/wasm/wit/testdata/wasi/io_streams.v | 58 ++ vlib/wasm/wit/testdata/wasi/io_streams.wit | 258 +++++++ vlib/wit/gen_v.v | 96 ++- vlib/wit/wit_test.v | 12 + 12 files changed, 1689 insertions(+), 28 deletions(-) create mode 100644 vlib/wasm/wit/testdata/wasi/cli_command.v create mode 100644 vlib/wasm/wit/testdata/wasi/cli_command.wit create mode 100644 vlib/wasm/wit/testdata/wasi/cli_imports.v create mode 100644 vlib/wasm/wit/testdata/wasi/cli_imports.wit create mode 100644 vlib/wasm/wit/testdata/wasi/fs_types.v create mode 100644 vlib/wasm/wit/testdata/wasi/fs_types.wit create mode 100644 vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v create mode 100644 vlib/wasm/wit/testdata/wasi/fs_types_generated.v create mode 100644 vlib/wasm/wit/testdata/wasi/io_streams.v create mode 100644 vlib/wasm/wit/testdata/wasi/io_streams.wit diff --git a/vlib/wasm/wit/testdata/wasi/cli_command.v b/vlib/wasm/wit/testdata/wasi/cli_command.v new file mode 100644 index 00000000000000..a2f448e2759d0e --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/cli_command.v @@ -0,0 +1,2 @@ +// package: wasi:cli +// version: 0.2.8 \ No newline at end of file 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.v b/vlib/wasm/wit/testdata/wasi/cli_imports.v new file mode 100644 index 00000000000000..a2f448e2759d0e --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/cli_imports.v @@ -0,0 +1,2 @@ +// package: wasi:cli +// version: 0.2.8 \ No newline at end of file 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/fs_types.v b/vlib/wasm/wit/testdata/wasi/fs_types.v new file mode 100644 index 00000000000000..980e9877c074c4 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/fs_types.v @@ -0,0 +1,190 @@ +// package: wasi:filesystem +// version: 0.2.8 + +// interface types +// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } +// use wasi:clocks/wall-clock@0.2.8 { datetime } +pub type Filesize = u64 +pub enum Descriptor_type { + unknown + block_device + character_device + directory + fifo + symbolic_link + regular_file + socket +} +pub type Descriptor_flags = int +pub const descriptor_flags_read = 1 +pub const descriptor_flags_write = 2 +pub const descriptor_flags_file_integrity_sync = 4 +pub const descriptor_flags_data_integrity_sync = 8 +pub const descriptor_flags_requested_write_sync = 16 +pub const descriptor_flags_mutate_directory = 32 +pub struct Descriptor_stat { + type Descriptor_type + link_count Link_count + size Filesize + data_access_timestamp ?voidptr + data_modification_timestamp ?voidptr + status_change_timestamp ?voidptr +} +pub type Path_flags = int +pub const path_flags_symlink_follow = 1 +pub type Open_flags = int +pub const open_flags_create = 1 +pub const open_flags_directory = 2 +pub const open_flags_exclusive = 4 +pub const open_flags_truncate = 8 +pub type Link_count = u64 +pub struct New_timestamp_No_change {} +pub struct New_timestamp_Now {} +pub struct New_timestamp_Timestamp { + value voidptr +} +pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp +pub struct Directory_entry { + type Descriptor_type + name string +} +pub enum Error_code { + access + would_block + already + bad_descriptor + busy + deadlock + quota + exist + file_too_large + illegal_byte_sequence + in_progress + interrupted + invalid + io + is_directory + loop + too_many_links + message_size + name_too_long + no_device + no_entry + no_lock + insufficient_memory + insufficient_space + not_directory + not_empty + not_recoverable + unsupported + no_tty + no_such_device + overflow + not_permitted + pipe + read_only + invalid_seek + text_file_busy + cross_device +} +pub enum Advice { + normal + sequential + random + will_need + dont_need + no_reuse +} +pub struct Metadata_hash_value { + lower u64 + upper u64 +} +pub struct Descriptor {} +pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) append_via_stream() !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync_data() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_flags() !Descriptor_flags { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_type() !Descriptor_type { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_size(size Filesize) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) create_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat() !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { + panic("TODO: implement") +} +pub fn (mut self Descriptor) readlink_at(path string) !string { + panic("TODO: implement") +} +pub fn (mut self Descriptor) remove_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) unlink_file_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) is_same_object(other voidptr) bool { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { + panic("TODO: implement") +} +pub struct Directory_entry_stream {} +pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { + panic("TODO: implement") +} +pub fn filesystem_error_code(err voidptr) ?Error_code { + panic("TODO: implement") +} \ No newline at end of file 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/fs_types_gen_test.v b/vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v new file mode 100644 index 00000000000000..7ca0318764a70f --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v @@ -0,0 +1,187 @@ +// package: wasi:filesystem +// version: 0.2.8 + +// interface types +// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } +// use wasi:clocks/wall-clock@0.2.8 { datetime } +pub type Filesize = u64 +pub enum Descriptor_type { + unknown + block_device + character_device + directory + fifo + symbolic_link + regular_file + socket +} +pub type Descriptor_flags = int +pub const descriptor_flags_read = 1 +pub const descriptor_flags_write = 2 +pub const descriptor_flags_file_integrity_sync = 4 +pub const descriptor_flags_data_integrity_sync = 8 +pub const descriptor_flags_requested_write_sync = 16 +pub const descriptor_flags_mutate_directory = 32 +pub struct Descriptor_stat { + type Descriptor_type + link_count Link_count + size Filesize + data_access_timestamp ?voidptr + data_modification_timestamp ?voidptr + status_change_timestamp ?voidptr +} +pub type Path_flags = int +pub const path_flags_symlink_follow = 1 +pub type Open_flags = int +pub const open_flags_create = 1 +pub const open_flags_directory = 2 +pub const open_flags_exclusive = 4 +pub const open_flags_truncate = 8 +pub type Link_count = u64 +pub struct New_timestamp_No_change {} +pub struct New_timestamp_Now {} +pub struct New_timestamp_Timestamp { + value voidptr +} +pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp +pub struct Directory_entry { + type Descriptor_type + name string +} +pub enum Error_code { + access + would_block + already + bad_descriptor + busy + deadlock + quota + exist + file_too_large + illegal_byte_sequence + in_progress + interrupted + invalid + io + is_directory + loop + too_many_links + message_size + name_too_long + no_device + no_entry + no_lock + insufficient_memory + insufficient_space + not_directory + not_empty + not_recoverable + unsupported + no_tty + no_such_device + overflow + not_permitted + pipe + read_only + invalid_seek + text_file_busy + cross_device +} +pub enum Advice { + normal + sequential + random + will_need + dont_need + no_reuse +} +pub struct Metadata_hash_value { + lower u64 + upper u64 +} +pub struct Descriptor {} +pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) append_via_stream() !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync_data() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_flags() !Descriptor_flags { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_type() !Descriptor_type { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_size(size Filesize) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) create_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat() !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { + panic("TODO: implement") +} +pub fn (mut self Descriptor) readlink_at(path string) !string { + panic("TODO: implement") +} +pub fn (mut self Descriptor) remove_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) unlink_file_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) is_same_object(other voidptr) bool { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { + panic("TODO: implement") +} +pub struct Directory_entry_stream {} +pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { + panic("TODO: implement") +} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/fs_types_generated.v b/vlib/wasm/wit/testdata/wasi/fs_types_generated.v new file mode 100644 index 00000000000000..980e9877c074c4 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/fs_types_generated.v @@ -0,0 +1,190 @@ +// package: wasi:filesystem +// version: 0.2.8 + +// interface types +// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } +// use wasi:clocks/wall-clock@0.2.8 { datetime } +pub type Filesize = u64 +pub enum Descriptor_type { + unknown + block_device + character_device + directory + fifo + symbolic_link + regular_file + socket +} +pub type Descriptor_flags = int +pub const descriptor_flags_read = 1 +pub const descriptor_flags_write = 2 +pub const descriptor_flags_file_integrity_sync = 4 +pub const descriptor_flags_data_integrity_sync = 8 +pub const descriptor_flags_requested_write_sync = 16 +pub const descriptor_flags_mutate_directory = 32 +pub struct Descriptor_stat { + type Descriptor_type + link_count Link_count + size Filesize + data_access_timestamp ?voidptr + data_modification_timestamp ?voidptr + status_change_timestamp ?voidptr +} +pub type Path_flags = int +pub const path_flags_symlink_follow = 1 +pub type Open_flags = int +pub const open_flags_create = 1 +pub const open_flags_directory = 2 +pub const open_flags_exclusive = 4 +pub const open_flags_truncate = 8 +pub type Link_count = u64 +pub struct New_timestamp_No_change {} +pub struct New_timestamp_Now {} +pub struct New_timestamp_Timestamp { + value voidptr +} +pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp +pub struct Directory_entry { + type Descriptor_type + name string +} +pub enum Error_code { + access + would_block + already + bad_descriptor + busy + deadlock + quota + exist + file_too_large + illegal_byte_sequence + in_progress + interrupted + invalid + io + is_directory + loop + too_many_links + message_size + name_too_long + no_device + no_entry + no_lock + insufficient_memory + insufficient_space + not_directory + not_empty + not_recoverable + unsupported + no_tty + no_such_device + overflow + not_permitted + pipe + read_only + invalid_seek + text_file_busy + cross_device +} +pub enum Advice { + normal + sequential + random + will_need + dont_need + no_reuse +} +pub struct Metadata_hash_value { + lower u64 + upper u64 +} +pub struct Descriptor {} +pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) append_via_stream() !voidptr { + panic("TODO: implement") +} +pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync_data() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_flags() !Descriptor_flags { + panic("TODO: implement") +} +pub fn (mut self Descriptor) get_type() !Descriptor_type { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_size(size Filesize) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { + panic("TODO: implement") +} +pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { + panic("TODO: implement") +} +pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { + panic("TODO: implement") +} +pub fn (mut self Descriptor) sync() ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) create_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat() !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { + panic("TODO: implement") +} +pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { + panic("TODO: implement") +} +pub fn (mut self Descriptor) readlink_at(path string) !string { + panic("TODO: implement") +} +pub fn (mut self Descriptor) remove_directory_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) unlink_file_at(path string) ! { + panic("TODO: implement") +} +pub fn (mut self Descriptor) is_same_object(other voidptr) bool { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { + panic("TODO: implement") +} +pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { + panic("TODO: implement") +} +pub struct Directory_entry_stream {} +pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { + panic("TODO: implement") +} +pub fn filesystem_error_code(err voidptr) ?Error_code { + panic("TODO: implement") +} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/io_streams.v b/vlib/wasm/wit/testdata/wasi/io_streams.v new file mode 100644 index 00000000000000..b4ada2440032b5 --- /dev/null +++ b/vlib/wasm/wit/testdata/wasi/io_streams.v @@ -0,0 +1,58 @@ +// package: wasi:io +// version: 0.2.8 + +// interface streams +// use error { error } +// use poll { pollable } +pub struct Stream_error_Last_operation_failed { + value voidptr +} +pub struct Stream_error_Closed {} +pub type Stream_error = Stream_error_Last_operation_failed | Stream_error_Closed +pub struct Input_stream {} +pub fn (mut self Input_stream) read(len u64) ![]u8 { + panic("TODO: implement") +} +pub fn (mut self Input_stream) blocking_read(len u64) ![]u8 { + panic("TODO: implement") +} +pub fn (mut self Input_stream) skip(len u64) !u64 { + panic("TODO: implement") +} +pub fn (mut self Input_stream) blocking_skip(len u64) !u64 { + panic("TODO: implement") +} +pub fn (mut self Input_stream) subscribe() int { + panic("TODO: implement") +} +pub struct Output_stream {} +pub fn (mut self Output_stream) check_write() !u64 { + panic("TODO: implement") +} +pub fn (mut self Output_stream) write(contents []u8) ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) blocking_write_and_flush(contents []u8) ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) flush() ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) blocking_flush() ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) subscribe() int { + panic("TODO: implement") +} +pub fn (mut self Output_stream) write_zeroes(len u64) ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) blocking_write_zeroes_and_flush(len u64) ! { + panic("TODO: implement") +} +pub fn (mut self Output_stream) splice(src voidptr, len u64) !u64 { + panic("TODO: implement") +} +pub fn (mut self Output_stream) blocking_splice(src voidptr, len u64) !u64 { + panic("TODO: implement") +} \ No newline at end of file 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/wit/gen_v.v b/vlib/wit/gen_v.v index 95d0c785a16271..d74f26e0170600 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -1,6 +1,33 @@ 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}' @@ -15,7 +42,7 @@ pub fn (d WitDoc) to_v() string { 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)}' + out << '\t${to_v_ident(f.name)} ${wit_type_to_v(f.typ, defined)}' } out << '}' } @@ -29,12 +56,20 @@ pub fn (d WitDoc) to_v() string { 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)}' + 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)} {}' @@ -47,12 +82,9 @@ pub fn (d WitDoc) to_v() string { out << 'pub type ${to_v_type_name(v.name)} = ${cases.join(' | ')}' } } - .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}' - } + .type_alias { + if t := it.type_def { + out << 'pub type ${to_v_type_name(t.name)} = ${wit_type_to_v(t.typ, defined)}' } } .resource { @@ -61,14 +93,14 @@ pub fn (d WitDoc) to_v() string { 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)}' + 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]) + 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) } + for r2 in m.results { ts << wit_type_to_v(r2, defined) } rs = '(${ts.join(', ')})' } if rs != '' { @@ -81,11 +113,6 @@ pub fn (d WitDoc) to_v() string { } } } - .type_alias { - if t := it.type_def { - out << 'pub type ${to_v_type_name(t.name)} = ${wit_type_to_v(t.typ)}' - } - } .use { if u := it.use_ { out << '// use ${u.path} { ${u.items.join(', ')} }' @@ -94,16 +121,22 @@ pub fn (d WitDoc) to_v() string { .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)}' } - mut rs := 'void' + 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]) + 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) } + for r in f.results { ts << wit_type_to_v(r, defined) } rs = '(${ts.join(', ')})' } - out << 'pub fn ${to_v_ident(f.name)}(${ps.join(', ')}) ${rs} {' + 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 << '}' } @@ -116,20 +149,23 @@ pub fn (d WitDoc) to_v() string { } fn to_v_ident(s string) string { - // convert kebab-case to snake_case 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 } - // capitalize first letter r = r[0].ascii_str().to_upper() + r[1..] return r } -fn wit_type_to_v(t WitType) string { +fn wit_type_to_v(t WitType, defined map[string]bool) string { match t.kind { .prim { return match t.prim { @@ -154,24 +190,28 @@ fn wit_type_to_v(t WitType) string { if n == '_' { return 'void' } if n.starts_with('borrow<') || n.starts_with('own<') { return 'voidptr' } if n == 'pollable' { return 'int' } + 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])}' } + 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])}' } + 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 '!' } - return '!${wit_type_to_v(t.items[0])}' + 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 { mut ts := []string{} - for it in t.items { ts << wit_type_to_v(it) } + for it in t.items { ts << wit_type_to_v(it, defined) } return '(${ts.join(', ')})' } } diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index 0f58a6c09f6d6a..f9acbbe3181816 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -1,5 +1,7 @@ module wit +import os + fn test_parse_basic() { src := ' package example:demo@0.1.0; @@ -48,6 +50,16 @@ world app { 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_parse_list_option_result() { src := ' package p:q; From 31e17c0d4783839864c26accb438ec78d8abf479 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:20:27 +0330 Subject: [PATCH 58/90] chore: remove generated test artifacts --- conform/01_resource.wit | 9 + conform/02_own_borrow.wit | 6 + conform/03_async_future.wit | 5 + conform/04_stream.wit | 5 + conform/05_map.wit | 5 + conform/06_use.wit | 10 + conform/07_attr.wit | 7 + conform/08_include.wit | 10 + conform/09_include_with.wit | 9 + conform_main.v | 16 ++ debug_gen.v | 22 ++ debug_gen_main.v | 19 ++ gen_check.v | 10 + vlib/wasm/wit/testdata/wasi/cli_command.v | 2 - vlib/wasm/wit/testdata/wasi/cli_imports.v | 2 - vlib/wasm/wit/testdata/wasi/fs_types.v | 190 ------------------ .../wit/testdata/wasi/fs_types_gen_test.v | 187 ----------------- .../wit/testdata/wasi/fs_types_generated.v | 190 ------------------ vlib/wasm/wit/testdata/wasi/io_streams.v | 58 ------ vlib/wit/lexer.v | 2 +- vlib/wit/parser.v | 23 ++- wasi_guard.v | 9 + 22 files changed, 161 insertions(+), 635 deletions(-) create mode 100644 conform/01_resource.wit create mode 100644 conform/02_own_borrow.wit create mode 100644 conform/03_async_future.wit create mode 100644 conform/04_stream.wit create mode 100644 conform/05_map.wit create mode 100644 conform/06_use.wit create mode 100644 conform/07_attr.wit create mode 100644 conform/08_include.wit create mode 100644 conform/09_include_with.wit create mode 100644 conform_main.v create mode 100644 debug_gen.v create mode 100644 debug_gen_main.v create mode 100644 gen_check.v delete mode 100644 vlib/wasm/wit/testdata/wasi/cli_command.v delete mode 100644 vlib/wasm/wit/testdata/wasi/cli_imports.v delete mode 100644 vlib/wasm/wit/testdata/wasi/fs_types.v delete mode 100644 vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v delete mode 100644 vlib/wasm/wit/testdata/wasi/fs_types_generated.v delete mode 100644 vlib/wasm/wit/testdata/wasi/io_streams.v create mode 100644 wasi_guard.v diff --git a/conform/01_resource.wit b/conform/01_resource.wit new file mode 100644 index 00000000000000..8d0cf43d09985f --- /dev/null +++ b/conform/01_resource.wit @@ -0,0 +1,9 @@ +package example:demo@0.1.0; + +interface types { + resource file { + constructor(path: string); + read: func() -> string; + write: func(data: string); + } +} \ No newline at end of file diff --git a/conform/02_own_borrow.wit b/conform/02_own_borrow.wit new file mode 100644 index 00000000000000..12a60ca2dd8c55 --- /dev/null +++ b/conform/02_own_borrow.wit @@ -0,0 +1,6 @@ +package example:demo@0.1.0; + +interface types { + resource handle {} + take: func(h: own) -> borrow; +} \ No newline at end of file diff --git a/conform/03_async_future.wit b/conform/03_async_future.wit new file mode 100644 index 00000000000000..2987e1279e2403 --- /dev/null +++ b/conform/03_async_future.wit @@ -0,0 +1,5 @@ +package example:demo@0.1.0; + +interface types { + async-fn: func() -> future; +} \ No newline at end of file diff --git a/conform/04_stream.wit b/conform/04_stream.wit new file mode 100644 index 00000000000000..c49989a6471c00 --- /dev/null +++ b/conform/04_stream.wit @@ -0,0 +1,5 @@ +package example:demo@0.1.0; + +interface types { + stream-fn: func() -> stream; +} \ No newline at end of file diff --git a/conform/05_map.wit b/conform/05_map.wit new file mode 100644 index 00000000000000..916a5301a05e57 --- /dev/null +++ b/conform/05_map.wit @@ -0,0 +1,5 @@ +package example:demo@0.1.0; + +interface types { + map-fn: func() -> map; +} \ No newline at end of file diff --git a/conform/06_use.wit b/conform/06_use.wit new file mode 100644 index 00000000000000..dd32198b56ac40 --- /dev/null +++ b/conform/06_use.wit @@ -0,0 +1,10 @@ +package example:demo@0.1.0; + +interface base { + x: func() -> u32; +} + +interface derived { + use base.{x}; + y: func() -> u32; +} \ No newline at end of file diff --git a/conform/07_attr.wit b/conform/07_attr.wit new file mode 100644 index 00000000000000..1e4996e3fea913 --- /dev/null +++ b/conform/07_attr.wit @@ -0,0 +1,7 @@ +package example:demo@0.1.0; + +@since(version = 0.1.0) +interface types { + @deprecated + old: func() -> u32; +} \ No newline at end of file diff --git a/conform/08_include.wit b/conform/08_include.wit new file mode 100644 index 00000000000000..12e70568330609 --- /dev/null +++ b/conform/08_include.wit @@ -0,0 +1,10 @@ +package example:demo@0.1.0; + +world base { + import x: func() -> u32; +} + +world derived { + include base; + export y: func() -> u32; +} \ No newline at end of file diff --git a/conform/09_include_with.wit b/conform/09_include_with.wit new file mode 100644 index 00000000000000..dd7bbcc0a7648a --- /dev/null +++ b/conform/09_include_with.wit @@ -0,0 +1,9 @@ +package example:demo@0.1.0; + +world base { + import x: func() -> u32; +} + +world derived { + include base with { x as renamed }; +} \ No newline at end of file diff --git a/conform_main.v b/conform_main.v new file mode 100644 index 00000000000000..ebfea35ef30717 --- /dev/null +++ b/conform_main.v @@ -0,0 +1,16 @@ +import wit +import os + +fn main() { + if os.args.len < 2 { + println('usage: conform_test ') + return + } + src := os.read_file(os.args[1]) or { println('READERR: ${err}'); return } + doc := wit.parse(src) or { println('PARSEERR: ${err}'); return } + println('PARSE_OK interfaces=${doc.interfaces.len} worlds=${doc.worlds.len}') + printed := doc.str() + println('PRINT_OK len=${printed.len}') + generated := doc.to_v() + println('GEN_OK len=${generated.len}') +} \ No newline at end of file diff --git a/debug_gen.v b/debug_gen.v new file mode 100644 index 00000000000000..1b5e31962403cd --- /dev/null +++ b/debug_gen.v @@ -0,0 +1,22 @@ +// package: example:demo +// version: 0.1.0 + +// interface types +pub struct Point { + x f64 + y f64 +} +pub enum Color { + red + green + blue +} +pub struct Shape_Circle { + value f64 +} +pub struct Shape_Empty {} +pub type Shape = Shape_Circle | Shape_Empty +pub type Perms = int +pub const perms_read = 1 +pub const perms_write = 2 +pub type Alias = u32 \ No newline at end of file diff --git a/debug_gen_main.v b/debug_gen_main.v new file mode 100644 index 00000000000000..9f8f63dd9abe56 --- /dev/null +++ b/debug_gen_main.v @@ -0,0 +1,19 @@ +import wit +import os + +fn main() { + src := 'package example:demo@0.1.0; + +interface types { + add: func(a: s32, b: s32) -> s32; +} +' + doc := wit.parse(src) or { panic(err) } + println('interfaces: ${doc.interfaces.len}') + if doc.interfaces.len > 0 { + println('items: ${doc.interfaces[0].items.len}') + for it in doc.interfaces[0].items { + println('item kind: ${it.kind}') + } + } +} diff --git a/gen_check.v b/gen_check.v new file mode 100644 index 00000000000000..91a2561f0f7e3e --- /dev/null +++ b/gen_check.v @@ -0,0 +1,10 @@ +import os +import wit + +fn main() { + src := os.read_file(os.args[1]) or { println('READERR: ${err}'); return } + doc := wit.parse(src) or { println('PARSEERR: ${err}'); return } + out := doc.to_v() + os.write_file(os.args[2], out) or { println('WRITEERR: ${err}'); return } + println('GEN_OK ${out.len} bytes') +} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/cli_command.v b/vlib/wasm/wit/testdata/wasi/cli_command.v deleted file mode 100644 index a2f448e2759d0e..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/cli_command.v +++ /dev/null @@ -1,2 +0,0 @@ -// package: wasi:cli -// version: 0.2.8 \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/cli_imports.v b/vlib/wasm/wit/testdata/wasi/cli_imports.v deleted file mode 100644 index a2f448e2759d0e..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/cli_imports.v +++ /dev/null @@ -1,2 +0,0 @@ -// package: wasi:cli -// version: 0.2.8 \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/fs_types.v b/vlib/wasm/wit/testdata/wasi/fs_types.v deleted file mode 100644 index 980e9877c074c4..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/fs_types.v +++ /dev/null @@ -1,190 +0,0 @@ -// package: wasi:filesystem -// version: 0.2.8 - -// interface types -// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } -// use wasi:clocks/wall-clock@0.2.8 { datetime } -pub type Filesize = u64 -pub enum Descriptor_type { - unknown - block_device - character_device - directory - fifo - symbolic_link - regular_file - socket -} -pub type Descriptor_flags = int -pub const descriptor_flags_read = 1 -pub const descriptor_flags_write = 2 -pub const descriptor_flags_file_integrity_sync = 4 -pub const descriptor_flags_data_integrity_sync = 8 -pub const descriptor_flags_requested_write_sync = 16 -pub const descriptor_flags_mutate_directory = 32 -pub struct Descriptor_stat { - type Descriptor_type - link_count Link_count - size Filesize - data_access_timestamp ?voidptr - data_modification_timestamp ?voidptr - status_change_timestamp ?voidptr -} -pub type Path_flags = int -pub const path_flags_symlink_follow = 1 -pub type Open_flags = int -pub const open_flags_create = 1 -pub const open_flags_directory = 2 -pub const open_flags_exclusive = 4 -pub const open_flags_truncate = 8 -pub type Link_count = u64 -pub struct New_timestamp_No_change {} -pub struct New_timestamp_Now {} -pub struct New_timestamp_Timestamp { - value voidptr -} -pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp -pub struct Directory_entry { - type Descriptor_type - name string -} -pub enum Error_code { - access - would_block - already - bad_descriptor - busy - deadlock - quota - exist - file_too_large - illegal_byte_sequence - in_progress - interrupted - invalid - io - is_directory - loop - too_many_links - message_size - name_too_long - no_device - no_entry - no_lock - insufficient_memory - insufficient_space - not_directory - not_empty - not_recoverable - unsupported - no_tty - no_such_device - overflow - not_permitted - pipe - read_only - invalid_seek - text_file_busy - cross_device -} -pub enum Advice { - normal - sequential - random - will_need - dont_need - no_reuse -} -pub struct Metadata_hash_value { - lower u64 - upper u64 -} -pub struct Descriptor {} -pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) append_via_stream() !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync_data() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_flags() !Descriptor_flags { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_type() !Descriptor_type { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_size(size Filesize) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) create_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat() !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { - panic("TODO: implement") -} -pub fn (mut self Descriptor) readlink_at(path string) !string { - panic("TODO: implement") -} -pub fn (mut self Descriptor) remove_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) unlink_file_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) is_same_object(other voidptr) bool { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { - panic("TODO: implement") -} -pub struct Directory_entry_stream {} -pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { - panic("TODO: implement") -} -pub fn filesystem_error_code(err voidptr) ?Error_code { - panic("TODO: implement") -} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v b/vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v deleted file mode 100644 index 7ca0318764a70f..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/fs_types_gen_test.v +++ /dev/null @@ -1,187 +0,0 @@ -// package: wasi:filesystem -// version: 0.2.8 - -// interface types -// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } -// use wasi:clocks/wall-clock@0.2.8 { datetime } -pub type Filesize = u64 -pub enum Descriptor_type { - unknown - block_device - character_device - directory - fifo - symbolic_link - regular_file - socket -} -pub type Descriptor_flags = int -pub const descriptor_flags_read = 1 -pub const descriptor_flags_write = 2 -pub const descriptor_flags_file_integrity_sync = 4 -pub const descriptor_flags_data_integrity_sync = 8 -pub const descriptor_flags_requested_write_sync = 16 -pub const descriptor_flags_mutate_directory = 32 -pub struct Descriptor_stat { - type Descriptor_type - link_count Link_count - size Filesize - data_access_timestamp ?voidptr - data_modification_timestamp ?voidptr - status_change_timestamp ?voidptr -} -pub type Path_flags = int -pub const path_flags_symlink_follow = 1 -pub type Open_flags = int -pub const open_flags_create = 1 -pub const open_flags_directory = 2 -pub const open_flags_exclusive = 4 -pub const open_flags_truncate = 8 -pub type Link_count = u64 -pub struct New_timestamp_No_change {} -pub struct New_timestamp_Now {} -pub struct New_timestamp_Timestamp { - value voidptr -} -pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp -pub struct Directory_entry { - type Descriptor_type - name string -} -pub enum Error_code { - access - would_block - already - bad_descriptor - busy - deadlock - quota - exist - file_too_large - illegal_byte_sequence - in_progress - interrupted - invalid - io - is_directory - loop - too_many_links - message_size - name_too_long - no_device - no_entry - no_lock - insufficient_memory - insufficient_space - not_directory - not_empty - not_recoverable - unsupported - no_tty - no_such_device - overflow - not_permitted - pipe - read_only - invalid_seek - text_file_busy - cross_device -} -pub enum Advice { - normal - sequential - random - will_need - dont_need - no_reuse -} -pub struct Metadata_hash_value { - lower u64 - upper u64 -} -pub struct Descriptor {} -pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) append_via_stream() !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync_data() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_flags() !Descriptor_flags { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_type() !Descriptor_type { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_size(size Filesize) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) create_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat() !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { - panic("TODO: implement") -} -pub fn (mut self Descriptor) readlink_at(path string) !string { - panic("TODO: implement") -} -pub fn (mut self Descriptor) remove_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) unlink_file_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) is_same_object(other voidptr) bool { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { - panic("TODO: implement") -} -pub struct Directory_entry_stream {} -pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { - panic("TODO: implement") -} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/fs_types_generated.v b/vlib/wasm/wit/testdata/wasi/fs_types_generated.v deleted file mode 100644 index 980e9877c074c4..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/fs_types_generated.v +++ /dev/null @@ -1,190 +0,0 @@ -// package: wasi:filesystem -// version: 0.2.8 - -// interface types -// use wasi:io/streams@0.2.8 { input-stream, output-stream, error } -// use wasi:clocks/wall-clock@0.2.8 { datetime } -pub type Filesize = u64 -pub enum Descriptor_type { - unknown - block_device - character_device - directory - fifo - symbolic_link - regular_file - socket -} -pub type Descriptor_flags = int -pub const descriptor_flags_read = 1 -pub const descriptor_flags_write = 2 -pub const descriptor_flags_file_integrity_sync = 4 -pub const descriptor_flags_data_integrity_sync = 8 -pub const descriptor_flags_requested_write_sync = 16 -pub const descriptor_flags_mutate_directory = 32 -pub struct Descriptor_stat { - type Descriptor_type - link_count Link_count - size Filesize - data_access_timestamp ?voidptr - data_modification_timestamp ?voidptr - status_change_timestamp ?voidptr -} -pub type Path_flags = int -pub const path_flags_symlink_follow = 1 -pub type Open_flags = int -pub const open_flags_create = 1 -pub const open_flags_directory = 2 -pub const open_flags_exclusive = 4 -pub const open_flags_truncate = 8 -pub type Link_count = u64 -pub struct New_timestamp_No_change {} -pub struct New_timestamp_Now {} -pub struct New_timestamp_Timestamp { - value voidptr -} -pub type New_timestamp = New_timestamp_No_change | New_timestamp_Now | New_timestamp_Timestamp -pub struct Directory_entry { - type Descriptor_type - name string -} -pub enum Error_code { - access - would_block - already - bad_descriptor - busy - deadlock - quota - exist - file_too_large - illegal_byte_sequence - in_progress - interrupted - invalid - io - is_directory - loop - too_many_links - message_size - name_too_long - no_device - no_entry - no_lock - insufficient_memory - insufficient_space - not_directory - not_empty - not_recoverable - unsupported - no_tty - no_such_device - overflow - not_permitted - pipe - read_only - invalid_seek - text_file_busy - cross_device -} -pub enum Advice { - normal - sequential - random - will_need - dont_need - no_reuse -} -pub struct Metadata_hash_value { - lower u64 - upper u64 -} -pub struct Descriptor {} -pub fn (mut self Descriptor) read_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write_via_stream(offset Filesize) !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) append_via_stream() !voidptr { - panic("TODO: implement") -} -pub fn (mut self Descriptor) advise(offset Filesize, length Filesize, advice Advice) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync_data() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_flags() !Descriptor_flags { - panic("TODO: implement") -} -pub fn (mut self Descriptor) get_type() !Descriptor_type { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_size(size Filesize) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times(data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read(length Filesize, offset Filesize) !([]u8, bool) { - panic("TODO: implement") -} -pub fn (mut self Descriptor) write(buffer []u8, offset Filesize) !Filesize { - panic("TODO: implement") -} -pub fn (mut self Descriptor) read_directory() !Directory_entry_stream { - panic("TODO: implement") -} -pub fn (mut self Descriptor) sync() ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) create_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat() !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) stat_at(path_flags Path_flags, path string) !Descriptor_stat { - panic("TODO: implement") -} -pub fn (mut self Descriptor) set_times_at(path_flags Path_flags, path string, data_access_timestamp New_timestamp, data_modification_timestamp New_timestamp) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) link_at(old_path_flags Path_flags, old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) open_at(path_flags Path_flags, path string, open_flags Open_flags, flags Descriptor_flags) !Descriptor { - panic("TODO: implement") -} -pub fn (mut self Descriptor) readlink_at(path string) !string { - panic("TODO: implement") -} -pub fn (mut self Descriptor) remove_directory_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) rename_at(old_path string, new_descriptor voidptr, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) symlink_at(old_path string, new_path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) unlink_file_at(path string) ! { - panic("TODO: implement") -} -pub fn (mut self Descriptor) is_same_object(other voidptr) bool { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash() !Metadata_hash_value { - panic("TODO: implement") -} -pub fn (mut self Descriptor) metadata_hash_at(path_flags Path_flags, path string) !Metadata_hash_value { - panic("TODO: implement") -} -pub struct Directory_entry_stream {} -pub fn (mut self Directory_entry_stream) read_directory_entry() ?Directory_entry { - panic("TODO: implement") -} -pub fn filesystem_error_code(err voidptr) ?Error_code { - panic("TODO: implement") -} \ No newline at end of file diff --git a/vlib/wasm/wit/testdata/wasi/io_streams.v b/vlib/wasm/wit/testdata/wasi/io_streams.v deleted file mode 100644 index b4ada2440032b5..00000000000000 --- a/vlib/wasm/wit/testdata/wasi/io_streams.v +++ /dev/null @@ -1,58 +0,0 @@ -// package: wasi:io -// version: 0.2.8 - -// interface streams -// use error { error } -// use poll { pollable } -pub struct Stream_error_Last_operation_failed { - value voidptr -} -pub struct Stream_error_Closed {} -pub type Stream_error = Stream_error_Last_operation_failed | Stream_error_Closed -pub struct Input_stream {} -pub fn (mut self Input_stream) read(len u64) ![]u8 { - panic("TODO: implement") -} -pub fn (mut self Input_stream) blocking_read(len u64) ![]u8 { - panic("TODO: implement") -} -pub fn (mut self Input_stream) skip(len u64) !u64 { - panic("TODO: implement") -} -pub fn (mut self Input_stream) blocking_skip(len u64) !u64 { - panic("TODO: implement") -} -pub fn (mut self Input_stream) subscribe() int { - panic("TODO: implement") -} -pub struct Output_stream {} -pub fn (mut self Output_stream) check_write() !u64 { - panic("TODO: implement") -} -pub fn (mut self Output_stream) write(contents []u8) ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) blocking_write_and_flush(contents []u8) ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) flush() ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) blocking_flush() ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) subscribe() int { - panic("TODO: implement") -} -pub fn (mut self Output_stream) write_zeroes(len u64) ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) blocking_write_zeroes_and_flush(len u64) ! { - panic("TODO: implement") -} -pub fn (mut self Output_stream) splice(src voidptr, len u64) !u64 { - panic("TODO: implement") -} -pub fn (mut self Output_stream) blocking_splice(src voidptr, len u64) !u64 { - panic("TODO: implement") -} \ No newline at end of file diff --git a/vlib/wit/lexer.v b/vlib/wit/lexer.v index 037834d2b995bc..dc057b00887f61 100644 --- a/vlib/wit/lexer.v +++ b/vlib/wit/lexer.v @@ -106,7 +106,7 @@ pub fn (mut l Lexer) tokenize() ![]Token { fn is_ident_start(c u8) bool { return (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` - || (c >= `0` && c <= `9`) + || (c >= `0` && c <= `9`) || c == `%` } fn is_ident_char(c u8) bool { diff --git a/vlib/wit/parser.v b/vlib/wit/parser.v index ca038ee7107ded..13a321caeecf22 100644 --- a/vlib/wit/parser.v +++ b/vlib/wit/parser.v @@ -181,14 +181,22 @@ 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 == ':') { + 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 == ':' { + 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()! @@ -432,10 +440,15 @@ fn (mut p Parser) parse_type() !WitType { p.expect_punct('>')! return WitType{ kind: .tuple, items: items } } - // own/borrow -> named - t := p.parse_type()! + // 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}<${t.str()}>' } + return WitType{ kind: .named, name: '${name}<${args.join(", ")}>' } } if is_prim_name(name) { return prim_type(name) diff --git a/wasi_guard.v b/wasi_guard.v new file mode 100644 index 00000000000000..2917c45f790cb3 --- /dev/null +++ b/wasi_guard.v @@ -0,0 +1,9 @@ +module main + +fn main() { + $if wasm32 { + println('wasm') + } $else { + println('not wasm') + } +} \ No newline at end of file From 5b0c88d8e50ac1b3df2d8cdff62ee37afa3397c3 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:20:42 +0330 Subject: [PATCH 59/90] chore: remove debug and conformance test artifacts --- conform/01_resource.wit | 9 --------- conform/02_own_borrow.wit | 6 ------ conform/03_async_future.wit | 5 ----- conform/04_stream.wit | 5 ----- conform/05_map.wit | 5 ----- conform/06_use.wit | 10 ---------- conform/07_attr.wit | 7 ------- conform/08_include.wit | 10 ---------- conform/09_include_with.wit | 9 --------- conform_main.v | 16 ---------------- debug_gen.v | 22 ---------------------- debug_gen_main.v | 19 ------------------- gen_check.v | 10 ---------- wasi_guard.v | 9 --------- 14 files changed, 142 deletions(-) delete mode 100644 conform/01_resource.wit delete mode 100644 conform/02_own_borrow.wit delete mode 100644 conform/03_async_future.wit delete mode 100644 conform/04_stream.wit delete mode 100644 conform/05_map.wit delete mode 100644 conform/06_use.wit delete mode 100644 conform/07_attr.wit delete mode 100644 conform/08_include.wit delete mode 100644 conform/09_include_with.wit delete mode 100644 conform_main.v delete mode 100644 debug_gen.v delete mode 100644 debug_gen_main.v delete mode 100644 gen_check.v delete mode 100644 wasi_guard.v diff --git a/conform/01_resource.wit b/conform/01_resource.wit deleted file mode 100644 index 8d0cf43d09985f..00000000000000 --- a/conform/01_resource.wit +++ /dev/null @@ -1,9 +0,0 @@ -package example:demo@0.1.0; - -interface types { - resource file { - constructor(path: string); - read: func() -> string; - write: func(data: string); - } -} \ No newline at end of file diff --git a/conform/02_own_borrow.wit b/conform/02_own_borrow.wit deleted file mode 100644 index 12a60ca2dd8c55..00000000000000 --- a/conform/02_own_borrow.wit +++ /dev/null @@ -1,6 +0,0 @@ -package example:demo@0.1.0; - -interface types { - resource handle {} - take: func(h: own) -> borrow; -} \ No newline at end of file diff --git a/conform/03_async_future.wit b/conform/03_async_future.wit deleted file mode 100644 index 2987e1279e2403..00000000000000 --- a/conform/03_async_future.wit +++ /dev/null @@ -1,5 +0,0 @@ -package example:demo@0.1.0; - -interface types { - async-fn: func() -> future; -} \ No newline at end of file diff --git a/conform/04_stream.wit b/conform/04_stream.wit deleted file mode 100644 index c49989a6471c00..00000000000000 --- a/conform/04_stream.wit +++ /dev/null @@ -1,5 +0,0 @@ -package example:demo@0.1.0; - -interface types { - stream-fn: func() -> stream; -} \ No newline at end of file diff --git a/conform/05_map.wit b/conform/05_map.wit deleted file mode 100644 index 916a5301a05e57..00000000000000 --- a/conform/05_map.wit +++ /dev/null @@ -1,5 +0,0 @@ -package example:demo@0.1.0; - -interface types { - map-fn: func() -> map; -} \ No newline at end of file diff --git a/conform/06_use.wit b/conform/06_use.wit deleted file mode 100644 index dd32198b56ac40..00000000000000 --- a/conform/06_use.wit +++ /dev/null @@ -1,10 +0,0 @@ -package example:demo@0.1.0; - -interface base { - x: func() -> u32; -} - -interface derived { - use base.{x}; - y: func() -> u32; -} \ No newline at end of file diff --git a/conform/07_attr.wit b/conform/07_attr.wit deleted file mode 100644 index 1e4996e3fea913..00000000000000 --- a/conform/07_attr.wit +++ /dev/null @@ -1,7 +0,0 @@ -package example:demo@0.1.0; - -@since(version = 0.1.0) -interface types { - @deprecated - old: func() -> u32; -} \ No newline at end of file diff --git a/conform/08_include.wit b/conform/08_include.wit deleted file mode 100644 index 12e70568330609..00000000000000 --- a/conform/08_include.wit +++ /dev/null @@ -1,10 +0,0 @@ -package example:demo@0.1.0; - -world base { - import x: func() -> u32; -} - -world derived { - include base; - export y: func() -> u32; -} \ No newline at end of file diff --git a/conform/09_include_with.wit b/conform/09_include_with.wit deleted file mode 100644 index dd7bbcc0a7648a..00000000000000 --- a/conform/09_include_with.wit +++ /dev/null @@ -1,9 +0,0 @@ -package example:demo@0.1.0; - -world base { - import x: func() -> u32; -} - -world derived { - include base with { x as renamed }; -} \ No newline at end of file diff --git a/conform_main.v b/conform_main.v deleted file mode 100644 index ebfea35ef30717..00000000000000 --- a/conform_main.v +++ /dev/null @@ -1,16 +0,0 @@ -import wit -import os - -fn main() { - if os.args.len < 2 { - println('usage: conform_test ') - return - } - src := os.read_file(os.args[1]) or { println('READERR: ${err}'); return } - doc := wit.parse(src) or { println('PARSEERR: ${err}'); return } - println('PARSE_OK interfaces=${doc.interfaces.len} worlds=${doc.worlds.len}') - printed := doc.str() - println('PRINT_OK len=${printed.len}') - generated := doc.to_v() - println('GEN_OK len=${generated.len}') -} \ No newline at end of file diff --git a/debug_gen.v b/debug_gen.v deleted file mode 100644 index 1b5e31962403cd..00000000000000 --- a/debug_gen.v +++ /dev/null @@ -1,22 +0,0 @@ -// package: example:demo -// version: 0.1.0 - -// interface types -pub struct Point { - x f64 - y f64 -} -pub enum Color { - red - green - blue -} -pub struct Shape_Circle { - value f64 -} -pub struct Shape_Empty {} -pub type Shape = Shape_Circle | Shape_Empty -pub type Perms = int -pub const perms_read = 1 -pub const perms_write = 2 -pub type Alias = u32 \ No newline at end of file diff --git a/debug_gen_main.v b/debug_gen_main.v deleted file mode 100644 index 9f8f63dd9abe56..00000000000000 --- a/debug_gen_main.v +++ /dev/null @@ -1,19 +0,0 @@ -import wit -import os - -fn main() { - src := 'package example:demo@0.1.0; - -interface types { - add: func(a: s32, b: s32) -> s32; -} -' - doc := wit.parse(src) or { panic(err) } - println('interfaces: ${doc.interfaces.len}') - if doc.interfaces.len > 0 { - println('items: ${doc.interfaces[0].items.len}') - for it in doc.interfaces[0].items { - println('item kind: ${it.kind}') - } - } -} diff --git a/gen_check.v b/gen_check.v deleted file mode 100644 index 91a2561f0f7e3e..00000000000000 --- a/gen_check.v +++ /dev/null @@ -1,10 +0,0 @@ -import os -import wit - -fn main() { - src := os.read_file(os.args[1]) or { println('READERR: ${err}'); return } - doc := wit.parse(src) or { println('PARSEERR: ${err}'); return } - out := doc.to_v() - os.write_file(os.args[2], out) or { println('WRITEERR: ${err}'); return } - println('GEN_OK ${out.len} bytes') -} \ No newline at end of file diff --git a/wasi_guard.v b/wasi_guard.v deleted file mode 100644 index 2917c45f790cb3..00000000000000 --- a/wasi_guard.v +++ /dev/null @@ -1,9 +0,0 @@ -module main - -fn main() { - $if wasm32 { - println('wasm') - } $else { - println('not wasm') - } -} \ No newline at end of file From eabf22f1ebd7adfb3b6712bed299c9d25ac4757b Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:21:07 +0330 Subject: [PATCH 60/90] docs(wit): update conformance matrix for codegen --- vlib/wasm/wit/CONFORMANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index c079000f2cf92a..40e94a51dc6e1f 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -22,7 +22,7 @@ Rows = WIT constructs. Columns = parse / print / codegen / validate. | tuple<...> | ✅ | ✅ | ✅ | ❌ | | own | ✅ | ✅ | ⚠️ | ❌ | | borrow | ✅ | ✅ | ⚠️ | ❌ | -| map | ✅ | ✅ | ⚠️ | ❌ | +| map | ✅ | ✅ | ✅ | ❌ | | async/future/stream | ✅ | ✅ | ⚠️ | ❌ | | use | ✅ | ✅ | ⚠️ | ❌ | | include | ✅ | ⚠️ | ❌ | ❌ | From ce45950e95d0e7a3c0c5d45fa155ad338b113b4d Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:23:13 +0330 Subject: [PATCH 61/90] feat(wasm): add cabi_realloc bump allocator for component ABI --- vlib/wasm/realloc.v | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 vlib/wasm/realloc.v 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 From a45ee4389b3da9f666763ca9812703fff4b22627 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:34:56 +0330 Subject: [PATCH 62/90] feat(driver): add -component/-wit flags with native wrapping --- vlib/v/driver/driver.v | 46 ++++++++++++++++++++++++++++++++++++++++++ vlib/v/pref/pref.v | 4 ++++ 2 files changed, 50 insertions(+) diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index 099d965a196484..a84938daf5a93c 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -57,6 +57,8 @@ $if !skip_ssa ? { $if !skip_wasm ? { import v.gen.wasm as wasmgen + import wasm + import wit } // vfmt on @@ -10059,6 +10061,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 +10240,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`. @@ -11422,6 +11432,18 @@ 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 + 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 @@ -14823,6 +14845,30 @@ 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) + } + _ := 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' + // Write wrapped component (minimal: wrap core module) + wrapped := wasm.wrap_core_module(core.bytes()) + 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/pref/pref.v b/vlib/v/pref/pref.v index 81c8993a48d41f..1295ced5cc0895 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -109,6 +109,10 @@ pub: pub mut: // hot_fn extracts raw machine code for a single function only (hot reload). hot_fn string + // 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. From 5eeaebbd5a9cb95bb636de180e7831ac6fab6d32 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:36:22 +0330 Subject: [PATCH 63/90] test(wasm): add builder memory/start section tests --- vlib/wasm/tests/component_builder_test.v | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 vlib/wasm/tests/component_builder_test.v 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 From 029c2a9810be7e9c7328fb2b3e8996c1b1e73558 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:36:48 +0330 Subject: [PATCH 64/90] feat(examples): add wasm component example with run.sh --- examples/wasm/component/README.md | 17 +++++++++++++++++ examples/wasm/component/hello.v | 5 +++++ examples/wasm/component/hello.wit | 5 +++++ examples/wasm/component/run.sh | 9 +++++++++ 4 files changed, 36 insertions(+) create mode 100644 examples/wasm/component/README.md create mode 100644 examples/wasm/component/hello.v create mode 100644 examples/wasm/component/hello.wit create mode 100644 examples/wasm/component/run.sh 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 From 1a9675b6db6903ac60211cde341c9130c8a0a046 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 20:59:02 +0330 Subject: [PATCH 65/90] feat(wit): add validation for unknown types and duplicate names --- vlib/wit/validate.v | 136 ++++++++++++++++++++++++++++++++++++++++++++ vlib/wit/wit_test.v | 42 ++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 vlib/wit/validate.v diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v new file mode 100644 index 00000000000000..58bb694b72e4d7 --- /dev/null +++ b/vlib/wit/validate.v @@ -0,0 +1,136 @@ +module wit + +pub struct WitError { +pub: + code string + message string + line int +} + +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 {} + } + } + } + // 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 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 types in records + if it.kind == .record { + if r := it.record { + for f in r.fields { + check_type(f.typ, types)! + } + } + } + // check types in funcs + if it.kind == .func { + if f := it.func { + for p in f.params { + check_type(p.typ, types)! + } + for r in f.results { + check_type(r, types)! + } + } + } + // check type alias + if it.kind == .type_alias { + if t := it.type_def { + check_type(t.typ, types)! + } + } + // check resource methods + if it.kind == .resource { + if r := it.resource { + mut method_names := map[string]bool{} + for m in r.methods { + if m.name in method_names { + return error('error_duplicate_name: duplicate method "${m.name}" in resource "${r.name}"') + } + method_names[m.name] = true + for p in m.params { check_type(p.typ, types)! } + for res in m.results { check_type(res, types)! } + } + } + } + } + } +} + +fn check_type(t WitType, types 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 !in types { + return error('error_unknown_type: unknown type "${base}"') + } + } + .list { + if t.items.len > 0 { check_type(t.items[0], types)! } + } + .option { + if t.items.len > 0 { check_type(t.items[0], types)! } + } + .result { + for it in t.items { check_type(it, types)! } + } + .tuple { + for it in t.items { check_type(it, types)! } + } + } +} \ No newline at end of file diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index f9acbbe3181816..732139b1b09ef4 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -60,6 +60,48 @@ fn test_wasi_fs_types_codegen_compiles() { 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_parse_list_option_result() { src := ' package p:q; From f105b7489309c3e90d6131b6fb05bf2965d56130 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:04:02 +0330 Subject: [PATCH 66/90] cabi: step 3 - string param lifting structural test + CONFORMANCE update --- vlib/wasm/tests/string_param_test.v | 67 ++++++++++++++++++++++++++++ vlib/wasm/tests/testdata/guest.wasm | Bin 0 -> 1337 bytes vlib/wasm/wit/CONFORMANCE.md | 2 + 3 files changed, 69 insertions(+) create mode 100644 vlib/wasm/tests/string_param_test.v create mode 100644 vlib/wasm/tests/testdata/guest.wasm diff --git a/vlib/wasm/tests/string_param_test.v b/vlib/wasm/tests/string_param_test.v new file mode 100644 index 00000000000000..1f1d73ccc331a9 --- /dev/null +++ b/vlib/wasm/tests/string_param_test.v @@ -0,0 +1,67 @@ +module main + +import wasm +import os + +fn read_uleb(data []u8, ip &int) u32 { + mut i := *ip + mut result := u32(0) + mut shift := u32(0) + for { + b := u32(data[i]) + i++ + result |= (b & 0x7f) << shift + if b & 0x80 == 0 { break } + shift += 7 + } + unsafe { *ip = i } + return result +} + +fn test_component_magic() { + core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } + wrapped := wasm.wrap_core_module(core_bytes.bytes()) + assert wasm.is_component(wrapped) + assert wrapped.len > core_bytes.len +} + +fn test_core_exports_echo_len_cabi() { + core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } + data := core_bytes.bytes() + mut i := 8 + mut found_memory := false + mut found_cabi := false + for i < data.len { + sid := data[i] + i++ + mut cnt_i := i + sz := read_uleb(data, &cnt_i) + i = cnt_i + end := i + int(sz) + if sid == 7 { + mut ci := i + cnt := read_uleb(data, &ci) + for _ in 0 .. cnt { + nlen := int(read_uleb(data, &ci)) + name := data[ci..ci + nlen].bytestr() + ci += nlen + kind := data[ci] + ci++ + idx := read_uleb(data, &ci) + _ = idx + if kind == 0 && name == 'echo_len_cabi' { found_cabi = true } + if kind == 2 && name == 'memory' { found_memory = true } + } + } + i = end + } + assert found_memory + assert found_cabi +} + +fn test_component_file_exists() { + core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } + wrapped := wasm.wrap_core_module(core_bytes.bytes()) + os.write_file_array('vlib/wasm/tests/testdata/guest.component.wasm', wrapped) or { panic(err) } + assert os.is_file('vlib/wasm/tests/testdata/guest.component.wasm') +} \ No newline at end of file diff --git a/vlib/wasm/tests/testdata/guest.wasm b/vlib/wasm/tests/testdata/guest.wasm new file mode 100644 index 0000000000000000000000000000000000000000..970daff541197a530eabdbdc43c99398302c508f GIT binary patch literal 1337 zcmeHFOKuZE5UuLZcsxB$53I7Rc4om63BeYZ7f6UD2XLIo5^2XtNFpRwlNDFt0y%)i zp5!V>Y$Js#JGRe&lxb<&$*Zvb0(L5*<^FTi<=@UnPe zI^>%lO9-32_tqDS+2Z{2yMc0c{N;RdIy(a#Y|6>;^ymbPTTD;Rq~+x5dU|;cuHQ#s z3>J;8{#gGRZFhhP2?;XC%oksWhVY{sK_J7Ak2Z7j?L7?bCpX#wWalH`?t{{V*UEhc zf`4I0fXkzpGdTHyO4p@c*!G^8+gcN{b}gkRq1 UtirAQV$aX>1pc!La2>Gv2cWBMGynhq literal 0 HcmV?d00001 diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index 40e94a51dc6e1f..df57262dbf8f1a 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -23,6 +23,8 @@ Rows = WIT constructs. Columns = parse / print / codegen / validate. | own | ✅ | ✅ | ⚠️ | ❌ | | borrow | ✅ | ✅ | ⚠️ | ❌ | | map | ✅ | ✅ | ✅ | ❌ | +| string (param) | ✅ | ✅ | ✅ | ❌ | +| string (return) | ✅ | ✅ | ❌ | ❌ | | async/future/stream | ✅ | ✅ | ⚠️ | ❌ | | use | ✅ | ✅ | ⚠️ | ❌ | | include | ✅ | ⚠️ | ❌ | ❌ | From e4f4a88071ad0ad0cb63f19e019af518dc28628c Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:08:00 +0330 Subject: [PATCH 67/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 7074df3bc7dfa9..8f108d0fcdbc93 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -271,3 +271,9 @@ primitive signatures. `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. From a2f5c9bac5018ef214e15a43acfb1c35e381e355 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:14:26 +0330 Subject: [PATCH 68/90] phase1: builder memory/start/import/table/element/data coverage --- vlib/wasm/tests/builder_roundtrip_test.v | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 vlib/wasm/tests/builder_roundtrip_test.v diff --git a/vlib/wasm/tests/builder_roundtrip_test.v b/vlib/wasm/tests/builder_roundtrip_test.v new file mode 100644 index 00000000000000..6faff182d0168b --- /dev/null +++ b/vlib/wasm/tests/builder_roundtrip_test.v @@ -0,0 +1,114 @@ +module main + +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 +} \ No newline at end of file From df2295e5d2cc3e2d8b6ace7107316915deb323b5 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:15:15 +0330 Subject: [PATCH 69/90] phase2: add pref.wasm_cabi flag for canonical-ABI mode --- vlib/v/pref/pref.v | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 1295ced5cc0895..364bfd97747ed8 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -109,6 +109,8 @@ 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. From 1a582aa149e488216e75fe5ab5b2654649db93cf Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:17:02 +0330 Subject: [PATCH 70/90] phase3: add resource printing to WIT printer --- vlib/wit/printer.v | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v index 53fbd6ffad57ee..0e39ff7079741f 100644 --- a/vlib/wit/printer.v +++ b/vlib/wit/printer.v @@ -25,6 +25,25 @@ pub fn (d WitDoc) str() string { out << '\t}' } } + .resource { + if r := it.resource { + out << '\tresource ${r.name} {' + for m in r.methods { + mut ps := []string{} + for p in m.params { ps << '${p.name}: ${p.typ.str()}' } + if m.results.len == 0 { + out << '\t\t${m.name}: func(${ps.join(', ')});' + } else if m.results.len == 1 { + out << '\t\t${m.name}: func(${ps.join(', ')}) -> ${m.results[0].str()};' + } else { + mut rs := []string{} + for rr in m.results { rs << rr.str() } + out << '\t\t${m.name}: func(${ps.join(', ')}) -> (${rs.join(', ')});' + } + } + out << '\t}' + } + } .func { if f := it.func { mut ps := []string{} From 2a174c2c684cb3f654da72b40f5a9cd516cd5881 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:17:31 +0330 Subject: [PATCH 71/90] phase4: add resource duplicate method validation --- vlib/wit/validate.v | 2 ++ vlib/wit/wit_test.v | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v index 58bb694b72e4d7..cdd736abd60ad4 100644 --- a/vlib/wit/validate.v +++ b/vlib/wit/validate.v @@ -100,6 +100,8 @@ pub fn validate(doc WitDoc) ! { return error('error_duplicate_name: duplicate method "${m.name}" in resource "${r.name}"') } method_names[m.name] = true + // constructor must not take the resource; static must not either + // first param of non-static method should be borrow of resource (optional check) for p in m.params { check_type(p.typ, types)! } for res in m.results { check_type(res, types)! } } diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index 732139b1b09ef4..f6b0a1c73d3883 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -102,6 +102,24 @@ interface i { 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; From d118e355643df71ac13635024c74ce105744ded4 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:18:50 +0330 Subject: [PATCH 72/90] phase5: add WASI preview2 example with run.sh --- examples/wasm/wasi_preview2/README.md | 19 +++++++++++++++++++ examples/wasm/wasi_preview2/hello.v | 5 +++++ examples/wasm/wasi_preview2/hello.wit | 5 +++++ examples/wasm/wasi_preview2/run.sh | 9 +++++++++ 4 files changed, 38 insertions(+) create mode 100644 examples/wasm/wasi_preview2/README.md create mode 100644 examples/wasm/wasi_preview2/hello.v create mode 100644 examples/wasm/wasi_preview2/hello.wit create mode 100644 examples/wasm/wasi_preview2/run.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 From 26f6c149d6562769d693cd2092191109880c5b35 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 9 Nov 2026 22:33:37 +0330 Subject: [PATCH 73/90] docs(wasm): document component wrapper --- vlib/wasm/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vlib/wasm/README.md b/vlib/wasm/README.md index 8f108d0fcdbc93..8359e84f79fd47 100644 --- a/vlib/wasm/README.md +++ b/vlib/wasm/README.md @@ -277,3 +277,9 @@ primitive signatures. `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. From 6f46f01e3ab924d612cc08525f027afb6560eaf4 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 11:49:06 +0330 Subject: [PATCH 74/90] wasm: export cabi_realloc allocator Add SSAGen.wasm_cabi (set via set_wasm_cabi from prefs.wasm_cabi or prefs.component, with prefs.wasm_cabi set by -d wasm_cabi). When on, export the bump allocator as cabi_realloc and reserve the name. Memory was already exported. Default builds are byte-identical. --- vlib/v/driver/driver.v | 2 ++ vlib/v/gen/wasm/ssa_gen.v | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index a84938daf5a93c..3b8adbf014f172 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -11434,6 +11434,7 @@ pub fn run(args []string) { 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 ') @@ -14823,6 +14824,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 { diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v index 97e38164199412..9e03ae023e497e 100644 --- a/vlib/v/gen/wasm/ssa_gen.v +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -43,6 +43,13 @@ mut: nparams int cur_ret ssa.TypeID warnings []string + // wasm_cabi enables canonical-ABI wrappers for exported string params. + wasm_cabi bool +} + +// 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. @@ -140,9 +147,15 @@ 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 + } for fid in functions { f := g.m.funcs[fid] if g.configured && f.name !in g.exports { From b12566255f5722248731aee62b317253b4f6fcca Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 12:00:31 +0330 Subject: [PATCH 75/90] wasm: add WType.cabi_string variant and export-aware string detection Old: prim_wtype returns none for string, caller falls back to WType.i32 single-pointer via wasm_param_may_be_aggregate. New: prim_wtype_cabi returns WType.cabi_string for exported string params in cabi mode (expanded to (i32 ptr, i32 len) in fn_info, recorded in FnInfo.cabi_string_params); every other case lowers exactly as before. Callers branched on export-ness: collect_user_fns (the sole fn_info caller) computes is_export with the same predicate as the export loop (main module or explicit @[export] key forms) because only exported functions get canonical-ABI signatures; imports and internal functions keep the single-pointer representation. wt_valtype maps cabi_string to i32 as a marker fallback. Byte-identical output when cabi mode is off. --- vlib/v/gen/wasm/gen.v | 79 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/vlib/v/gen/wasm/gen.v b/vlib/v/gen/wasm/gen.v index d52506dd74c884..bcf81d2cd0da80 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 { @@ -64,6 +69,7 @@ mut: 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 @@ -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 { From c68adf87c829d34999bdc4a36e39ce4bacccfd6a Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 12:05:08 +0330 Subject: [PATCH 76/90] wasm: emit canonical-ABI string wrappers in SSAGen exports When wasm_cabi is on, an exported function with exactly one string-typed param (V string struct str/len/is_lit) and an i32/void result is no longer exported directly. Instead SSAGen emits a wrapper with signature (i32 ptr, i32 len) -> ret that materializes string{str, len, is_lit: false} in a 16-byte module-data scratch slot (reserved before the stack area, so no overlap) and tail-calls the inner function, which keeps its index but is left unexported. The wrapper needs no locals and leaves the stack balanced. Detection is structural on SSA types; multi-param and non-i32/void-result functions keep the previous shape. Byte-identical output when cabi mode is off. --- vlib/v/gen/wasm/ssa_gen.v | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v index 9e03ae023e497e..11ec50acf4b29f 100644 --- a/vlib/v/gen/wasm/ssa_gen.v +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -45,6 +45,10 @@ mut: 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. + cabi_scratch int = -1 } // set_wasm_cabi enables canonical-ABI wrappers for exported string params. @@ -79,6 +83,16 @@ 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 the canonical-ABI string scratch slot before the stack area + // so wrapper materializations never overlap stack frames. + 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} + } mut init := Code{} g.stack_bottom = (data_base + g.data.len + 15) & ~15 stack_top := g.stack_bottom + 1024 * 1024 @@ -156,6 +170,10 @@ pub fn (mut g SSAGen) gen() ! { 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 { @@ -170,9 +188,24 @@ 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] + str_typ := g.m.values[f.params[0]].typ + idx := g.emit_cabi_string_wrapper(inner, str_typ, g.wtype(f.typ)) + 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}`') } @@ -196,6 +229,75 @@ 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: exactly one string-typed param and an i32 or void result. +fn (g &SSAGen) cabi_wrappable(fid int) bool { + f := g.m.funcs[fid] + if f.params.len != 1 { + return false + } + if !g.cabi_string_param(g.m.values[f.params[0]].typ) { + return false + } + return g.wtype(f.typ) in [.i32, .void] +} + +// emit_cabi_string_wrapper emits a canonical-ABI wrapper for an exported +// function taking a single V string: the wrapper has signature +// (i32 ptr, i32 len) -> ret, materializes string{str: ptr, len: len, +// is_lit: false} in the module scratch slot, and tail-calls the inner +// function. The scratch slot is reserved once per module (see gen), so the +// wrapper needs no locals and leaves the value stack balanced. +// Limitation: the single scratch slot is not reentrant; nested wrapper calls +// (host -> guest -> host -> guest) would clobber it. String returns, lists, +// records, and variants are out of scope for this slice. +fn (mut g SSAGen) emit_cabi_string_wrapper(inner_idx int, str_typ ssa.TypeID, ret WType) int { + off0 := g.m.struct_field_offset(str_typ, 0) + off1 := g.m.struct_field_offset(str_typ, 1) + off2 := g.m.struct_field_offset(str_typ, 2) + has_lit := g.m.type_store.types[str_typ].fields.len > 2 + mut c := Code{} + c.i32_const(i64(g.cabi_scratch)) + c.local_get(0) + c.i32_store(off0) + c.i32_const(i64(g.cabi_scratch)) + c.local_get(1) + c.i32_store(off1) + if has_lit { + c.i32_const(i64(g.cabi_scratch)) + c.i32_const(0) + c.i32_store8(off2) + } + c.i32_const(i64(g.cabi_scratch)) + c.call(inner_idx) + c.end() + mut results := []u8{} + if ret != .void { + results << wt_valtype(ret) + } + ti := g.mod.add_type([valtype_i32, valtype_i32], results) + return g.mod.add_func(ti, [], c.bytes) +} + // write writes the generated module to path. pub fn (g &SSAGen) write(path string) ! { os.write_file_array(path, g.mod.compile())! From 126c4519793a961f889c8fc9002b1fac325dc781 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 12:11:06 +0330 Subject: [PATCH 77/90] wasm: WIT-aware string lifting in component wrapper wrap_core_module_strings lifts (i32,i32) core exports named by WIT single-string-param funcs as component (string)->r functions with canonical (memory) (realloc) opts, exported under the WIT kebab-case name. wit.wrap_component picks the world (named or first with exports), maps kebab-case WIT names to snake_case core names, and falls back to plain wrapping when nothing matches. Driver -component flow now uses wit.wrap_component. Also fix canon opts count (number of options, not bytes). --- vlib/v/driver/driver.v | 8 +++--- vlib/wasm/component.v | 57 ++++++++++++++++++++++++++++++-------- vlib/wit/component.v | 63 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 22 deletions(-) diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index 3b8adbf014f172..8e40f141033675 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -57,7 +57,6 @@ $if !skip_ssa ? { $if !skip_wasm ? { import v.gen.wasm as wasmgen - import wasm import wit } // vfmt on @@ -14853,7 +14852,7 @@ pub fn run(args []string) { eprintln('error reading wit file ${prefs.wit_file}') exit(1) } - _ := wit.parse(wit_src) or { + doc := wit.parse(wit_src) or { eprintln('error parsing wit file ${prefs.wit_file}: ${err}') exit(1) } @@ -14863,8 +14862,9 @@ pub fn run(args []string) { } // Component output path comp_out := output_file.all_before_last('.wasm') + '.component.wasm' - // Write wrapped component (minimal: wrap core module) - wrapped := wasm.wrap_core_module(core.bytes()) + // 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) diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index af2d43bf2be3d3..1486a9cd8de288 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -173,6 +173,14 @@ fn write_section(mut out []u8, id u8, payload []u8) { // ---- public API ---- pub fn wrap_core_module(core []u8) []u8 { + return wrap_core_module_strings(core, map[string]string{}) +} + +// wrap_core_module_strings wraps a core module like wrap_core_module, except +// that core exports listed in string_renames (core name -> component export +// name) must have signature (i32, i32) -> r and are lifted as single-string-param +// component functions with canonical (memory) (realloc) options. +pub fn wrap_core_module_strings(core []u8, string_renames map[string]string) []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. @@ -186,8 +194,21 @@ pub fn wrap_core_module(core []u8) []u8 { } // Determine liftable exports. mut liftable := []CoreExport{} + mut str_wit := map[string]string{} for e in exports { if e.kind != 0 { continue } + // Canonical-ABI string export: (i32 ptr, i32 len) lifted as one string. + if wit_name := string_renames[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 == [u8(0x7f), u8(0x7f)] && ft.results.len <= 1 { + liftable << e + str_wit[e.name] = wit_name + 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 } @@ -235,14 +256,22 @@ pub fn wrap_core_module(core []u8) []u8 { ti := if e.idx >= 0 && e.idx < func_typeidx.len { func_typeidx[e.idx] } else { -1 } ft := types[ti] type_payload << u8(0x40) // functype - // 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)) + if e.name in str_wit { + // single canonical string param, labelled 's' + type_payload << leb128.encode_u32(1) + type_payload << leb128.encode_u32(1) + type_payload << 's'.bytes() + type_payload << u8(0x73) // string + } else { + // 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 { @@ -280,17 +309,20 @@ pub fn wrap_core_module(core []u8) []u8 { canon_payload << u8(0x00) canon_payload << u8(0x00) // func sort canon_payload << leb128.encode_u32(u32(i)) // core func idx (alias index) - // opts + // 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(opts.len)) + canon_payload << leb128.encode_u32(u32(nopts)) canon_payload << opts canon_payload << leb128.encode_u32(u32(i)) // type idx } @@ -299,9 +331,10 @@ pub fn wrap_core_module(core []u8) []u8 { mut export_payload := []u8{} export_payload << leb128.encode_u32(u32(liftable.len)) for i, e in liftable { + ename := str_wit[e.name] or { e.name } export_payload << u8(0x00) // nameattributes plain - export_payload << leb128.encode_u32(u32(e.name.len)) - export_payload << e.name.bytes() + 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 } diff --git a/vlib/wit/component.v b/vlib/wit/component.v index 7a0210948a8f90..47751ee5d5c048 100644 --- a/vlib/wit/component.v +++ b/vlib/wit/component.v @@ -2,10 +2,61 @@ module wit import wasm -pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { - // Experimental: currently wraps the core module into a component container. - // Canonical ABI lifting of individual functions is a follow-up. - _ = doc - _ = world - return wasm.wrap_core_module(core) +fn is_wit_string(t WitType) bool { + return t.kind == .prim && t.prim == 'string' +} + +fn is_single_string_func(f WitFunc) bool { + return f.params.len == 1 && is_wit_string(f.params[0].typ) && f.results.len <= 1 } + +pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { + // Pick the world: the named one, or the first world 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] + // renames maps core export names (kebab-case to snake_case) to component + // export names for single-string-param functions. + mut renames := map[string]string{} + for e in w.exports { + // direct func export: export run: func(s: string) -> ...; + if f := e.func { + if is_single_string_func(f) { + renames[e.name.replace('-', '_')] = e.name + } + } + // interface export: match funcs of the named interface. + 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 is_single_string_func(f) { + renames[f.name.replace('-', '_')] = f.name + } + } + } + } + } + } + } + if renames.len == 0 { + return wasm.wrap_core_module(core) + } + return wasm.wrap_core_module_strings(core, renames) +} \ No newline at end of file From 552a1af087818a7d0cb92558a4970eef75569694 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 12:31:36 +0330 Subject: [PATCH 78/90] wasm: native cabi parity test + WIT string-lift test cabi_test.v builds echo_len(s string) with wasm_cabi on and asserts the (i32,i32)->i32 export, memory + cabi_realloc exports, an unexported single-pointer inner function, a step-by-step simulation of the wrapper prologue for hello/empty/he-llo including len=2 pinning, native byte-length semantics, and shape parity with the frozen shim module. The old shim test and its guest.wasm move to vlib/v/gen/wasm/testdata/echo_len.shim.wasm as the frozen parity artifact. wit_test gains a component-level test: a hand-built (i32,i32)->i32 core wrapped with a string-param WIT world exports echo-len with a string-typed component function and memory+realloc canon opts. --- vlib/v/gen/wasm/cabi_test.v | 521 ++++++++++++++++++ vlib/v/gen/wasm/gen.v | 8 +- .../gen/wasm/testdata/echo_len.shim.wasm} | Bin vlib/wasm/tests/string_param_test.v | 67 --- vlib/wit/component.v | 2 +- vlib/wit/wit_test.v | 238 ++++++++ 6 files changed, 764 insertions(+), 72 deletions(-) create mode 100644 vlib/v/gen/wasm/cabi_test.v rename vlib/{wasm/tests/testdata/guest.wasm => v/gen/wasm/testdata/echo_len.shim.wasm} (100%) delete mode 100644 vlib/wasm/tests/string_param_test.v diff --git a/vlib/v/gen/wasm/cabi_test.v b/vlib/v/gen/wasm/cabi_test.v new file mode 100644 index 00000000000000..d53b4ad6093df2 --- /dev/null +++ b/vlib/v/gen/wasm/cabi_test.v @@ -0,0 +1,521 @@ +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 +} + +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 + } + // find the inner single-pointer function index + inner_type := CabiTypeSig{ + params: [u8(0x7f)] + results: [u8(0x7f)] + } + mut inner_idx := -1 + for fi, ti in funcs { + t := sigs[ti] + if t.params == inner_type.params && t.results == inner_type.results { + global_idx := n_imports + fi + mut exported := false + for _, eidx in exports { + if eidx == global_idx { + exported = true + break + } + } + if !exported { + 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}' + } + } + assert called == inner_idx, 'wrapper must call the 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_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 bcf81d2cd0da80..2da5711365455a 100644 --- a/vlib/v/gen/wasm/gen.v +++ b/vlib/v/gen/wasm/gen.v @@ -66,11 +66,11 @@ enum FrameTag { @[heap] pub struct Gen { mut: - a &flat.FlatAst = unsafe { nil } - tc &types.TypeChecker = unsafe { nil } - used_fns map[string]bool + a &flat.FlatAst = unsafe { nil } + tc &types.TypeChecker = unsafe { nil } + used_fns map[string]bool wasm_cabi bool - mod &Module = unsafe { nil } + mod &Module = unsafe { nil } // per-function state cur Code local_types []u8 diff --git a/vlib/wasm/tests/testdata/guest.wasm b/vlib/v/gen/wasm/testdata/echo_len.shim.wasm similarity index 100% rename from vlib/wasm/tests/testdata/guest.wasm rename to vlib/v/gen/wasm/testdata/echo_len.shim.wasm diff --git a/vlib/wasm/tests/string_param_test.v b/vlib/wasm/tests/string_param_test.v deleted file mode 100644 index 1f1d73ccc331a9..00000000000000 --- a/vlib/wasm/tests/string_param_test.v +++ /dev/null @@ -1,67 +0,0 @@ -module main - -import wasm -import os - -fn read_uleb(data []u8, ip &int) u32 { - mut i := *ip - mut result := u32(0) - mut shift := u32(0) - for { - b := u32(data[i]) - i++ - result |= (b & 0x7f) << shift - if b & 0x80 == 0 { break } - shift += 7 - } - unsafe { *ip = i } - return result -} - -fn test_component_magic() { - core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } - wrapped := wasm.wrap_core_module(core_bytes.bytes()) - assert wasm.is_component(wrapped) - assert wrapped.len > core_bytes.len -} - -fn test_core_exports_echo_len_cabi() { - core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } - data := core_bytes.bytes() - mut i := 8 - mut found_memory := false - mut found_cabi := false - for i < data.len { - sid := data[i] - i++ - mut cnt_i := i - sz := read_uleb(data, &cnt_i) - i = cnt_i - end := i + int(sz) - if sid == 7 { - mut ci := i - cnt := read_uleb(data, &ci) - for _ in 0 .. cnt { - nlen := int(read_uleb(data, &ci)) - name := data[ci..ci + nlen].bytestr() - ci += nlen - kind := data[ci] - ci++ - idx := read_uleb(data, &ci) - _ = idx - if kind == 0 && name == 'echo_len_cabi' { found_cabi = true } - if kind == 2 && name == 'memory' { found_memory = true } - } - } - i = end - } - assert found_memory - assert found_cabi -} - -fn test_component_file_exists() { - core_bytes := os.read_file('vlib/wasm/tests/testdata/guest.wasm') or { panic(err) } - wrapped := wasm.wrap_core_module(core_bytes.bytes()) - os.write_file_array('vlib/wasm/tests/testdata/guest.component.wasm', wrapped) or { panic(err) } - assert os.is_file('vlib/wasm/tests/testdata/guest.component.wasm') -} \ No newline at end of file diff --git a/vlib/wit/component.v b/vlib/wit/component.v index 47751ee5d5c048..f4b5fc72c41085 100644 --- a/vlib/wit/component.v +++ b/vlib/wit/component.v @@ -59,4 +59,4 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { return wasm.wrap_core_module(core) } return wasm.wrap_core_module_strings(core, renames) -} \ No newline at end of file +} diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index f6b0a1c73d3883..e78d8e7a09f4e1 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -1,6 +1,7 @@ module wit import os +import wasm fn test_parse_basic() { src := ' @@ -135,3 +136,240 @@ interface i { 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 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; +} +') 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 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 += 2 + } + } + } + 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 lift_has_mem_opt, 'canon lift must carry (memory)' + assert lift_has_realloc_opt, 'canon lift must carry (realloc)' +} From 80f220b909d7f5cbc88c1e4f38a9a5d9a9aa553d Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 12:43:45 +0330 Subject: [PATCH 79/90] wasm: node runtime proof for cabi string params + constructor printing cabi_test.v gains a node runtime test: instantiate the backend-built module, write hello/empty/utf-8 bytes into linear memory, call echo_len/first_byte and assert 5/0/6, first byte 104, and len pinned to 2 for a 2-byte slice. Guest gains first_byte for content readback. wit printer emits constructor(...) in WIT syntax so resource round-trips reparse. --- vlib/v/gen/wasm/cabi_test.v | 70 ++++++++++++++++++++++++++++++++----- vlib/wit/printer.v | 4 +++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/vlib/v/gen/wasm/cabi_test.v b/vlib/v/gen/wasm/cabi_test.v index d53b4ad6093df2..e41cc3f482923d 100644 --- a/vlib/v/gen/wasm/cabi_test.v +++ b/vlib/v/gen/wasm/cabi_test.v @@ -23,6 +23,11 @@ pub fn echo_len(s string) int { return s.len } +@[export: 'first_byte'] +pub fn first_byte(s string) u8 { + return s[0] +} + fn main() {} " @@ -324,15 +329,13 @@ fn test_cabi_wrapper_simulation() { pos++ _ = c } - // find the inner single-pointer function index - inner_type := CabiTypeSig{ - params: [u8(0x7f)] - results: [u8(0x7f)] - } + // 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 == inner_type.params && t.results == inner_type.results { + if t.params == [u8(0x7f)] && t.results == [u8(0x7f)] { global_idx := n_imports + fi mut exported := false for _, eidx in exports { @@ -341,7 +344,7 @@ fn test_cabi_wrapper_simulation() { break } } - if !exported { + if !exported && inner_idx == -1 { inner_idx = global_idx } } @@ -456,7 +459,17 @@ fn test_cabi_wrapper_simulation() { assert false, 'unexpected wrapper op ${op:02x} in case ${ci}' } } - assert called == inner_idx, 'wrapper must call the inner function' + // 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 @@ -492,6 +505,47 @@ fn test_cabi_native_len_semantics() { 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; +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); +console.log(\'cabi-runtime-ok echo_len=5,0,6 first_byte=104 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-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() diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v index 0e39ff7079741f..42721abd12c32c 100644 --- a/vlib/wit/printer.v +++ b/vlib/wit/printer.v @@ -31,6 +31,10 @@ pub fn (d WitDoc) str() string { for m in r.methods { mut ps := []string{} for p in m.params { ps << '${p.name}: ${p.typ.str()}' } + if m.name == 'constructor' { + out << ('\t\tconstructor(' + ps.join(', ') + ');') + continue + } if m.results.len == 0 { out << '\t\t${m.name}: func(${ps.join(', ')});' } else if m.results.len == 1 { From f9afee96aaf15fa8146d440316924a9eccd98e61 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 13:15:55 +0330 Subject: [PATCH 80/90] wasm: builder custom sections + coverage tests Add Module.add_custom_section (user custom sections emitted after the name section). Tests: custom-section name/bytes round-trip, call_indirect with type+table index match, ref.null/ref.func/table.get/table.set opcode round-trip, data-count section with two data segments at exact offsets, and a structural read of a real backend-produced module (echo_len.shim.wasm) asserting the (i32,i32)->i32 signature and the echo_len_cabi + memory exports the component wrapper consumes. --- vlib/wasm/encoding.v | 9 + vlib/wasm/module.v | 12 + vlib/wasm/tests/builder_roundtrip_test.v | 297 ++++++++++++++++++++++- 3 files changed, 317 insertions(+), 1 deletion(-) 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/tests/builder_roundtrip_test.v b/vlib/wasm/tests/builder_roundtrip_test.v index 6faff182d0168b..39e7185268a9cb 100644 --- a/vlib/wasm/tests/builder_roundtrip_test.v +++ b/vlib/wasm/tests/builder_roundtrip_test.v @@ -1,5 +1,6 @@ module main +import os import wasm fn test_memory_and_start_sections() { @@ -111,4 +112,298 @@ fn test_import_section() { i = end } assert has_import -} \ No newline at end of file +} + +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_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 +} From 83b71ba590d5b352198de8502efc3fc44150d49a Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 13:16:24 +0330 Subject: [PATCH 81/90] wit: attributes in AST with verbatim printing + world func signatures Parse @... attributes into WitInterface/WitWorld/WitItem/WitFunc attrs (canonical spacing) instead of dropping them; printer re-emits them so parse-print-parse is stable. World imports/exports now print full func signatures instead of the func(...) placeholder. --- vlib/wit/parser.v | 83 ++++++++++++++++++++++++++++++++++------------ vlib/wit/printer.v | 44 ++++++++++++++++++++---- vlib/wit/wit.v | 11 ++++-- 3 files changed, 107 insertions(+), 31 deletions(-) diff --git a/vlib/wit/parser.v b/vlib/wit/parser.v index 13a321caeecf22..316b2349910522 100644 --- a/vlib/wit/parser.v +++ b/vlib/wit/parser.v @@ -26,12 +26,14 @@ fn (mut p Parser) parse_doc() !WitDoc { doc.package = p.parse_package()! } for p.peek_kind() != .eof { - p.skip_attributes() + attrs := p.collect_attributes() if p.peek_ident() == 'interface' { - iface := p.parse_interface()! + mut iface := p.parse_interface()! + iface.attrs = attrs doc.interfaces << iface } else if p.peek_ident() == 'world' { - w := p.parse_world(mut doc)! + mut w := p.parse_world(mut doc)! + w.attrs = attrs doc.worlds << w } else { return error('expected interface or world at ${p.pos()}') @@ -133,46 +135,47 @@ fn (mut p Parser) parse_interface() !WitInterface { } fn (mut p Parser) parse_item() !WitItem { - p.skip_attributes() + attrs := p.collect_attributes() // use if p.peek_ident() == 'use' { u := p.parse_use()! - return WitItem{ kind: .use, use_: u } + 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 } + 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 } + return WitItem{ kind: .record, record: r, attrs: attrs } } // variant if p.peek_ident() == 'variant' { v := p.parse_variant()! - return WitItem{ kind: .variant, variant: v } + return WitItem{ kind: .variant, variant: v, attrs: attrs } } // enum if p.peek_ident() == 'enum' { e := p.parse_enum()! - return WitItem{ kind: .enum_, enum_: e } + return WitItem{ kind: .enum_, enum_: e, attrs: attrs } } // flags if p.peek_ident() == 'flags' { f := p.parse_flags()! - return WitItem{ kind: .flags, flags: f } + return WitItem{ kind: .flags, flags: f, attrs: attrs } } // resource if p.peek_ident() == 'resource' { r := p.parse_resource()! - return WitItem{ kind: .resource, resource: r } + 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 == ':' { - f := p.parse_func_item()! - return WitItem{ kind: .func, func: f } + mut f := p.parse_func_item()! + f.attrs = attrs + return WitItem{ kind: .func, func: f, attrs: attrs } } return error('unexpected item at ${p.pos()}') } @@ -305,13 +308,13 @@ fn (mut p Parser) parse_resource() !WitResource { mut r := WitResource{ name: name } p.expect_punct('{')! for !(p.peek().kind == .punct && p.peek().val == '}') { - p.skip_attributes() + 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: [] } + 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() @@ -320,7 +323,8 @@ fn (mut p Parser) parse_resource() !WitResource { } // method: name: [static] func(...) if p.peek().kind == .ident && p.idx + 1 < p.toks.len && p.toks[p.idx + 1].val == ':' { - f := p.parse_func_item()! + mut f := p.parse_func_item()! + f.attrs = mattrs r.methods << f continue } @@ -334,7 +338,11 @@ fn (mut p Parser) parse_func_item() !WitFunc { name := p.expect_ident()! p.expect_punct(':')! // optional static - if p.peek_ident() == 'static' { p.advance() } + 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()! @@ -345,7 +353,7 @@ fn (mut p Parser) parse_func_item() !WitFunc { 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 } + return WitFunc{ name: name, params: params, results: results, is_static: is_static } } fn (mut p Parser) parse_param_list() ![]WitParam { @@ -448,7 +456,7 @@ fn (mut p Parser) parse_type() !WitType { args << p.parse_type()!.str() } p.expect_punct('>')! - return WitType{ kind: .named, name: '${name}<${args.join(", ")}>' } + return WitType{ kind: .named, name: '${name}<${args.join(', ')}>' } } if is_prim_name(name) { return prim_type(name) @@ -524,14 +532,37 @@ fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { } 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() - if p.peek().kind == .ident { p.advance() } + mut name := '' + if p.peek().kind == .ident { + name = p.peek().val + p.advance() + } if p.peek().kind == .punct && p.peek().val == '(' { - mut depth := 0 + 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 } @@ -539,12 +570,22 @@ fn (mut p Parser) skip_attributes() { 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 } fn (mut p Parser) parse_inline_interface(mut doc WitDoc, name string) !WitInterface { diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v index 42721abd12c32c..2c71b101619824 100644 --- a/vlib/wit/printer.v +++ b/vlib/wit/printer.v @@ -1,5 +1,22 @@ 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 != '' { @@ -9,8 +26,14 @@ pub fn (d WitDoc) str() string { } 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(', ')}};' @@ -29,20 +52,24 @@ pub fn (d WitDoc) str() string { 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}: func(${ps.join(', ')});' + out << '\t\t${m.name}: ${prefix}func(${ps.join(', ')});' } else if m.results.len == 1 { - out << '\t\t${m.name}: func(${ps.join(', ')}) -> ${m.results[0].str()};' + 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}: func(${ps.join(', ')}) -> (${rs.join(', ')});' + out << '\t\t${m.name}: ${prefix}func(${ps.join(', ')}) -> (${rs.join(', ')});' } } out << '\t}' @@ -70,17 +97,20 @@ pub fn (d WitDoc) str() string { } for w in d.worlds { out << '' + for a in w.attrs { + out << a + } out << 'world ${w.name} {' for imp in w.imports { - if imp.func != none { - out << '\timport ${imp.name}: func(...);' + 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 exp.func != none { - out << '\texport ${exp.name}: func(...);' + if f := exp.func { + out << '\texport ${exp.name}: ${wit_func_sig(f)};' } else if exp.interface != '' { out << '\texport ${exp.name};' } diff --git a/vlib/wit/wit.v b/vlib/wit/wit.v index 3f2954727c3a5c..31ac905698d77e 100644 --- a/vlib/wit/wit.v +++ b/vlib/wit/wit.v @@ -42,9 +42,11 @@ pub mut: pub struct WitFunc { pub mut: - name string - params []WitParam - results []WitType + name string + params []WitParam + results []WitType + attrs []string + is_static bool } pub struct WitRecord { @@ -117,12 +119,14 @@ pub mut: resource ?WitResource type_def ?WitTypeDef use_ ?WitUse + attrs []string } pub struct WitInterface { pub mut: name string items []WitItem + attrs []string } pub struct WitWorldImport { @@ -144,6 +148,7 @@ pub mut: name string imports []WitWorldImport exports []WitWorldExport + attrs []string } pub struct WitPackage { From a22a555597259b06fd65007bcb08970f531699d6 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 13:16:42 +0330 Subject: [PATCH 82/90] wit: validation depth + grammar and preview2 binding tests Validator adds error_version_incompatible (use-chain @since check with interface-version fallback), error_unknown_interface, error_unresolved_use, error_not_a_resource (own/borrow targets), error_duplicate_constructor, error_static_method_takes_resource; static methods parsed/printed/codegen as namespaced fns. Grammar tests cover resource/async/map/use/include-free round-trips plus compiling codegen. Preview2 tests generate and v -check io, clocks, random, cli, filesystem bindings with signature assertions. --- vlib/wasm/wit/testdata/wasi/clocks_mono.wit | 55 ++++ vlib/wasm/wit/testdata/wasi/clocks_wall.wit | 46 ++++ vlib/wasm/wit/testdata/wasi/random.wit | 29 ++ vlib/wit/gen_v.v | 14 +- vlib/wit/grammar_test.v | 125 +++++++++ vlib/wit/preview2_test.v | 57 ++++ vlib/wit/validate.v | 282 ++++++++++++++++++-- vlib/wit/validation_test.v | 125 +++++++++ 8 files changed, 703 insertions(+), 30 deletions(-) create mode 100644 vlib/wasm/wit/testdata/wasi/clocks_mono.wit create mode 100644 vlib/wasm/wit/testdata/wasi/clocks_wall.wit create mode 100644 vlib/wasm/wit/testdata/wasi/random.wit create mode 100644 vlib/wit/grammar_test.v create mode 100644 vlib/wit/preview2_test.v create mode 100644 vlib/wit/validation_test.v 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/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/wit/gen_v.v b/vlib/wit/gen_v.v index d74f26e0170600..5fe5c5a0051376 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -103,7 +103,16 @@ pub fn (d WitDoc) to_v() string { for r2 in m.results { ts << wit_type_to_v(r2, defined) } rs = '(${ts.join(', ')})' } - if rs != '' { + 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(', ')}) {' @@ -190,6 +199,9 @@ fn wit_type_to_v(t WitType, defined map[string]bool) string { 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. if n !in defined { return 'voidptr' } return to_v_type_name(n) } diff --git a/vlib/wit/grammar_test.v b/vlib/wit/grammar_test.v new file mode 100644 index 00000000000000..396c933356c396 --- /dev/null +++ b/vlib/wit/grammar_test.v @@ -0,0 +1,125 @@ +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_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/preview2_test.v b/vlib/wit/preview2_test.v new file mode 100644 index 00000000000000..8444e6cc2e1203 --- /dev/null +++ b/vlib/wit/preview2_test.v @@ -0,0 +1,57 @@ +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', + ]) +} diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v index cdd736abd60ad4..8c928f873a887c 100644 --- a/vlib/wit/validate.v +++ b/vlib/wit/validate.v @@ -7,25 +7,161 @@ pub: 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 } } + .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'] { + for p in ['bool', 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'f32', 'f64', 'char', + 'string'] { types[p] = true } // check duplicate interface names @@ -51,13 +187,41 @@ pub fn validate(doc WitDoc) ! { 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 } } + .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 != '' { @@ -66,11 +230,33 @@ pub fn validate(doc WitDoc) ! { } 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 records if it.kind == .record { if r := it.record { for f in r.fields { - check_type(f.typ, types)! + check_type(f.typ, types, resources)! } } } @@ -78,32 +264,48 @@ pub fn validate(doc WitDoc) ! { if it.kind == .func { if f := it.func { for p in f.params { - check_type(p.typ, types)! + check_type(p.typ, types, resources)! } for r in f.results { - check_type(r, types)! + check_type(r, types, resources)! } } } // check type alias if it.kind == .type_alias { if t := it.type_def { - check_type(t.typ, types)! + 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 - // constructor must not take the resource; static must not either - // first param of non-static method should be borrow of resource (optional check) - for p in m.params { check_type(p.typ, types)! } - for res in m.results { check_type(res, types)! } + 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)! + } } } } @@ -111,28 +313,50 @@ pub fn validate(doc WitDoc) ! { } } -fn check_type(t WitType, types map[string]bool) ! { +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 == '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)! } + if t.items.len > 0 { + check_type(t.items[0], types, resources)! + } } .option { - if t.items.len > 0 { check_type(t.items[0], types)! } + if t.items.len > 0 { + check_type(t.items[0], types, resources)! + } } .result { - for it in t.items { check_type(it, types)! } + for it in t.items { + check_type(it, types, resources)! + } } .tuple { - for it in t.items { check_type(it, types)! } + for it in t.items { + check_type(it, types, resources)! + } } } -} \ No newline at end of file +} diff --git a/vlib/wit/validation_test.v b/vlib/wit/validation_test.v new file mode 100644 index 00000000000000..eaaac610ebbf89 --- /dev/null +++ b/vlib/wit/validation_test.v @@ -0,0 +1,125 @@ +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_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) } +} From b80081b927d41977edc52f8a541a52c62a798b3d Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 13:23:04 +0330 Subject: [PATCH 83/90] wit: grammar world test, variant checks, container validation Add world import/export round-trip coverage, variant payload type checking, and future/stream/map surface-type allowance in validation. Rewrite CONFORMANCE.md with per-construct verdicts and the component-lift shape table. --- vlib/wasm/wit/CONFORMANCE.md | 93 +++++++++++++++++++++++++----------- vlib/wit/grammar_test.v | 15 ++++++ vlib/wit/validate.v | 14 ++++++ vlib/wit/validation_test.v | 17 +++++++ 4 files changed, 110 insertions(+), 29 deletions(-) diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index df57262dbf8f1a..1b27281fd95bd5 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -1,41 +1,76 @@ # WIT Conformance Matrix Rows = WIT constructs. Columns = parse / print / codegen / validate. -✅ = supported, ⚠️ = partial, ❌ = not supported. +✅ = 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) | ✅ | ⚠️ | ✅ | ❌ | -| resource (constructor) | ✅ | ⚠️ | ⚠️ | ❌ | -| type alias | ✅ | ✅ | ✅ | ❌ | -| func | ✅ | ✅ | ✅ | ❌ | -| list | ✅ | ✅ | ✅ | ❌ | -| option | ✅ | ✅ | ✅ | ❌ | -| result | ✅ | ✅ | ✅ | ❌ | -| tuple<...> | ✅ | ✅ | ✅ | ❌ | -| own | ✅ | ✅ | ⚠️ | ❌ | -| borrow | ✅ | ✅ | ⚠️ | ❌ | -| map | ✅ | ✅ | ✅ | ❌ | -| string (param) | ✅ | ✅ | ✅ | ❌ | -| string (return) | ✅ | ✅ | ❌ | ❌ | -| async/future/stream | ✅ | ✅ | ⚠️ | ❌ | -| use | ✅ | ✅ | ⚠️ | ❌ | -| include | ✅ | ⚠️ | ❌ | ❌ | +| 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 attributes | ✅ | ❌ | ❌ | ❌ | +| @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 | ✅ | +| string returns | ❌ (depends on Item 1 follow-up) | +| lists / records / variants / resources | ❌ (depends on Item 1 follow-up) | +| options / results / tuples / maps | ❌ (depends on Item 1 follow-up) | + ## Notes -- **own/borrow/map/async/future/stream**: parsed as named types (`own`, etc.); codegen maps to `voidptr`/`int`. Full type mapping deferred to Item 3 (component ABI). -- **resource print**: printer emits only fields name/type, not full method signatures. -- **include ... with**: parsed by consuming tokens until `;`; not printed or codegened. -- **attributes**: parsed and skipped; not printed or codegened. -- **validate**: no validation implemented yet. \ No newline at end of file +- **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**: the parser consumes the clause but drops it + (parser.v world include branch keeps no AST node), so the printer cannot + re-emit it and codegen cannot resolve it. Smallest next step: add an + include node to WitWorld carrying the path plus `with` substitutions. +- **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**: no package-level checks exist yet. +- 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/wit/grammar_test.v b/vlib/wit/grammar_test.v index 396c933356c396..6b0de3bae3ac09 100644 --- a/vlib/wit/grammar_test.v +++ b/vlib/wit/grammar_test.v @@ -106,6 +106,21 @@ interface derived { 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_attrs_roundtrip() { src := 'package p:q; diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v index 8c928f873a887c..293b6f0341959c 100644 --- a/vlib/wit/validate.v +++ b/vlib/wit/validate.v @@ -252,6 +252,16 @@ pub fn validate(doc WitDoc) ! { } } } + // 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 { @@ -327,6 +337,10 @@ fn check_type(t WitType, types map[string]bool, resources map[string]bool) ! { 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 { diff --git a/vlib/wit/validation_test.v b/vlib/wit/validation_test.v index eaaac610ebbf89..4999436b56d2eb 100644 --- a/vlib/wit/validation_test.v +++ b/vlib/wit/validation_test.v @@ -97,6 +97,23 @@ interface i { ', '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_multi_interface_ok() { src2 := 'package example:demo@0.1.0; From 0020b49aaab2999b7bad5444beafd93956c617d7 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 13:59:47 +0330 Subject: [PATCH 84/90] wasm: generalize cabi wrappers to multi-string params and string returns String-returning exports use the backend's hidden sret convention: the wrapper passes a scratch slot and reads back (ptr, len). Scratch reservation covers params plus one return slot. Verified end-to-end via node: echo-len, multi-param shout, and string-return hello. --- vlib/v/gen/wasm/ssa_gen.v | 193 +++++++++++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 45 deletions(-) diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v index 11ec50acf4b29f..d992d5baa1de94 100644 --- a/vlib/v/gen/wasm/ssa_gen.v +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -84,14 +84,39 @@ pub fn (mut g SSAGen) gen() ! { functions := g.reachable_functions()! g.collect_data(functions, g.reachable_values(functions)) if g.wasm_cabi { - // Reserve the canonical-ABI string scratch slot before the stack area - // so wrapper materializations never overlap stack frames. - aligned := (data_base + g.data.len + 3) & ~3 - for g.data.len < aligned - data_base { - g.data << u8(0) + // 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} } - g.cabi_scratch = data_base + g.data.len - g.data << []u8{len: 16} } mut init := Code{} g.stack_bottom = (data_base + g.data.len + 15) & ~15 @@ -202,8 +227,7 @@ pub fn (mut g SSAGen) gen() ! { for w in cabi_wrapped { f := g.m.funcs[w.fid] inner := g.fn_index[f.name] - str_typ := g.m.values[f.params[0]].typ - idx := g.emit_cabi_string_wrapper(inner, str_typ, g.wtype(f.typ)) + idx := g.emit_cabi_wrapper(inner, w.fid) g.mod.add_export(w.export_name, export_func, idx) } mut entry := Code{} @@ -249,53 +273,132 @@ fn (g &SSAGen) cabi_string_param(typ ssa.TypeID) bool { } // cabi_wrappable reports whether an exported function gets a canonical-ABI -// string wrapper: exactly one string-typed param and an i32 or void result. +// 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] - if f.params.len != 1 { - return false + 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 + } } - if !g.cabi_string_param(g.m.values[f.params[0]].typ) { + ret_is_string := g.cabi_string_param(f.typ) + if strs == 0 && !ret_is_string { return false } - return g.wtype(f.typ) in [.i32, .void] + if ret_is_string { + return true + } + rt := g.wtype(f.typ) + return rt == .void || rt == .i32 } -// emit_cabi_string_wrapper emits a canonical-ABI wrapper for an exported -// function taking a single V string: the wrapper has signature -// (i32 ptr, i32 len) -> ret, materializes string{str: ptr, len: len, -// is_lit: false} in the module scratch slot, and tail-calls the inner -// function. The scratch slot is reserved once per module (see gen), so the -// wrapper needs no locals and leaves the value stack balanced. -// Limitation: the single scratch slot is not reentrant; nested wrapper calls -// (host -> guest -> host -> guest) would clobber it. String returns, lists, -// records, and variants are out of scope for this slice. -fn (mut g SSAGen) emit_cabi_string_wrapper(inner_idx int, str_typ ssa.TypeID, ret WType) int { - off0 := g.m.struct_field_offset(str_typ, 0) - off1 := g.m.struct_field_offset(str_typ, 1) - off2 := g.m.struct_field_offset(str_typ, 2) - has_lit := g.m.type_store.types[str_typ].fields.len > 2 +// 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. +// Limitation: scratch slots are not reentrant; nested wrapper calls +// (host -> guest -> host -> guest) would clobber them. Lists, records, +// variants, and other composite types are out of scope. +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{} - c.i32_const(i64(g.cabi_scratch)) - c.local_get(0) - c.i32_store(off0) - c.i32_const(i64(g.cabi_scratch)) - c.local_get(1) - c.i32_store(off1) - if has_lit { - c.i32_const(i64(g.cabi_scratch)) - c.i32_const(0) - c.i32_store8(off2) + // 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.i32_const(i64(g.cabi_scratch)) c.call(inner_idx) - c.end() - mut results := []u8{} - if ret != .void { - results << wt_valtype(ret) + 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)) } - ti := g.mod.add_type([valtype_i32, valtype_i32], results) - return g.mod.add_func(ti, [], c.bytes) + 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. From e5a240dc12d160f99ce6c5ace2616095012fa9ca Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 14:15:50 +0330 Subject: [PATCH 85/90] wasm: generalize cabi wrappers to multi-string params and string returns SSAGen emits (i32 ptr, i32 len) per string param with per-slot scratch space and uses the backend hidden sret slot for string results. Component wrapping lifts string params and string results with canonical memory/realloc opts under WIT names. Node runtime proves echo-len, multi-param shout, return-a-string, empty/UTF-8/pinned-length cases. --- vlib/v/gen/wasm/cabi_test.v | 34 ++++++++++-- vlib/wasm/component.v | 76 ++++++++++++++++++-------- vlib/wit/component.v | 106 ++++++++++++++++++++++++++++++------ vlib/wit/wit_test.v | 23 +++++++- 4 files changed, 193 insertions(+), 46 deletions(-) diff --git a/vlib/v/gen/wasm/cabi_test.v b/vlib/v/gen/wasm/cabi_test.v index e41cc3f482923d..2cd188c9fcd7c3 100644 --- a/vlib/v/gen/wasm/cabi_test.v +++ b/vlib/v/gen/wasm/cabi_test.v @@ -28,6 +28,19 @@ 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() {} " @@ -517,13 +530,15 @@ fn test_cabi_runtime_node() { } 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\'; + 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]); @@ -537,12 +552,23 @@ 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); -console.log(\'cabi-runtime-ok echo_len=5,0,6 first_byte=104 pin=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 } diff --git a/vlib/wasm/component.v b/vlib/wasm/component.v index 1486a9cd8de288..060f9c8a6d18a2 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -173,14 +173,29 @@ fn write_section(mut out []u8, id u8, payload []u8) { // ---- public API ---- pub fn wrap_core_module(core []u8) []u8 { - return wrap_core_module_strings(core, map[string]string{}) + 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_renames (core name -> component export -// name) must have signature (i32, i32) -> r and are lifted as single-string-param -// component functions with canonical (memory) (realloc) options. -pub fn wrap_core_module_strings(core []u8, string_renames map[string]string) []u8 { +// 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. +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. @@ -194,17 +209,17 @@ pub fn wrap_core_module_strings(core []u8, string_renames map[string]string) []u } // Determine liftable exports. mut liftable := []CoreExport{} - mut str_wit := map[string]string{} + mut str_specs := map[string]CabiStringExport{} for e in exports { if e.kind != 0 { continue } - // Canonical-ABI string export: (i32 ptr, i32 len) lifted as one string. - if wit_name := string_renames[e.name] { + // 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 == [u8(0x7f), u8(0x7f)] && ft.results.len <= 1 { + if ft.params == spec.core_params && ft.results == spec.core_results { liftable << e - str_wit[e.name] = wit_name + str_specs[e.name] = spec continue } } @@ -256,22 +271,32 @@ pub fn wrap_core_module_strings(core []u8, string_renames map[string]string) []u 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 e.name in str_wit { - // single canonical string param, labelled 's' - type_payload << leb128.encode_u32(1) - type_payload << leb128.encode_u32(1) - type_payload << 's'.bytes() - type_payload << u8(0x73) // string - } else { - // params - type_payload << leb128.encode_u32(u32(ft.params.len)) - for idxp, p in ft.params { - // label - label := 'p${idxp}' + 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 << u8(core_to_component_valtype(p)) + 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 { @@ -331,7 +356,10 @@ pub fn wrap_core_module_strings(core []u8, string_renames map[string]string) []u mut export_payload := []u8{} export_payload << leb128.encode_u32(u32(liftable.len)) for i, e in liftable { - ename := str_wit[e.name] or { e.name } + 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() diff --git a/vlib/wit/component.v b/vlib/wit/component.v index f4b5fc72c41085..ec329f99d14c95 100644 --- a/vlib/wit/component.v +++ b/vlib/wit/component.v @@ -2,16 +2,90 @@ module wit import wasm -fn is_wit_string(t WitType) bool { - return t.kind == .prim && t.prim == 'string' +// 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 } + } } -fn is_single_string_func(f WitFunc) bool { - return f.params.len == 1 && is_wit_string(f.params[0].typ) && f.results.len <= 1 +// wit_func_lift describes how one WIT function maps onto a core export, or +// none when the shape is not liftable. +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 + } } pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { - // Pick the world: the named one, or the first world with exports. + // 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 { @@ -27,17 +101,16 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { return wasm.wrap_core_module(core) } w := doc.worlds[widx] - // renames maps core export names (kebab-case to snake_case) to component - // export names for single-string-param functions. - mut renames := map[string]string{} + mut string_exports := map[string]wasm.CabiStringExport{} + // direct func exports for e in w.exports { - // direct func export: export run: func(s: string) -> ...; if f := e.func { - if is_single_string_func(f) { - renames[e.name.replace('-', '_')] = e.name + if mut spec := wit_func_lift(f) { + spec.wit_name = e.name + string_exports[e.name.replace('-', '_')] = spec } } - // interface export: match funcs of the named interface. + // 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('/') { @@ -46,8 +119,9 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { for it in iface.items { if it.kind == .func { if f := it.func { - if is_single_string_func(f) { - renames[f.name.replace('-', '_')] = f.name + if mut spec := wit_func_lift(f) { + spec.wit_name = f.name + string_exports[f.name.replace('-', '_')] = spec } } } @@ -55,8 +129,8 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { } } } - if renames.len == 0 { + if string_exports.len == 0 { return wasm.wrap_core_module(core) } - return wasm.wrap_core_module_strings(core, renames) + return wasm.wrap_core_module_strings(core, string_exports) } diff --git a/vlib/wit/wit_test.v b/vlib/wit/wit_test.v index e78d8e7a09f4e1..8f7b8a528014a4 100644 --- a/vlib/wit/wit_test.v +++ b/vlib/wit/wit_test.v @@ -145,6 +145,17 @@ fn test_wrap_component_lifts_string_export() { 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, ]) @@ -157,7 +168,7 @@ fn test_wrap_component_lifts_string_export() { doc := parse('package example:echo@0.1.0; world echo { - export echo-len: func(s: string) -> u32; + 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, '') @@ -166,6 +177,7 @@ world echo { 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 { @@ -235,7 +247,11 @@ world echo { if comp[p] == 0x01 { p += 2 } else { - p += 2 + p += 1 + if comp[p] == 0x73 { + has_string_result = true + } + p += 1 } } } @@ -370,6 +386,9 @@ world echo { } 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)' } From f8636a1ab6b894bd3025c63a6462eb03ae7eefd5 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 14:22:23 +0330 Subject: [PATCH 86/90] wit: include support with with-substitutions + package validation Parse include path[@version] with optional with-clause into WitInclude nodes, print them back verbatim (aliases sorted), validate target worlds/interfaces plus rename resolution. Validate package namespace and semver version. Grammar + validation tests cover round-trip, codegen, and all new error codes. --- vlib/wasm/wit/CONFORMANCE.md | 19 +++++---- vlib/wit/grammar_test.v | 37 +++++++++++++++++ vlib/wit/parser.v | 60 ++++++++++++++++++++++++--- vlib/wit/printer.v | 16 ++++++++ vlib/wit/validate.v | 80 ++++++++++++++++++++++++++++++++++++ vlib/wit/validation_test.v | 35 ++++++++++++++++ vlib/wit/wit.v | 15 +++++-- 7 files changed, 244 insertions(+), 18 deletions(-) diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index 1b27281fd95bd5..a201af308a948d 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -26,10 +26,10 @@ Rows = WIT constructs. Columns = parse / print / codegen / validate. | string (return) | ✅ | ✅ | ✅ | ✅ | | async / future / stream | ✅ | ✅ | ✅ | ✅ | | use (incl. `as`) | ✅ | ✅ | ✅ | ✅ | -| include | ✅ | ❌ | ❌ | ❌ | -| include ... with | ✅ | ❌ | ❌ | ❌ | +| include | ✅ | ✅ | ✅ | ✅ | +| include ... with | ✅ | ✅ | ✅ | ✅ | | @since / @deprecated / @unstable | ✅ | ✅ | ✅ | ✅ | -| @version on package | ✅ | ✅ | ✅ | ❌ | +| @version on package | ✅ | ✅ | ✅ | ✅ | ## Component lift (core module -> component) @@ -53,10 +53,11 @@ Function-signature shapes the component wrapper lifts today: only the surface (types, imports, exports) is generated, runtime support is out of scope. - **map codegen**: emitted as V `map`. -- **include / include ... with**: the parser consumes the clause but drops it - (parser.v world include branch keeps no AST node), so the printer cannot - re-emit it and codegen cannot resolve it. Smallest next step: add an - include node to WitWorld carrying the path plus `with` substitutions. +- **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 @@ -70,7 +71,9 @@ Function-signature shapes the component wrapper lifts today: Unknown `use` targets (`error_unknown_interface`) and members (`error_unresolved_use`) are rejected. Duplicate constructors are rejected (`error_duplicate_constructor`). -- **validate**: no package-level checks exist yet. +- **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/wit/grammar_test.v b/vlib/wit/grammar_test.v index 6b0de3bae3ac09..ed894a03cba602 100644 --- a/vlib/wit/grammar_test.v +++ b/vlib/wit/grammar_test.v @@ -121,6 +121,43 @@ world app { 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; diff --git a/vlib/wit/parser.v b/vlib/wit/parser.v index 316b2349910522..f7ebd39f4a8ebb 100644 --- a/vlib/wit/parser.v +++ b/vlib/wit/parser.v @@ -517,12 +517,8 @@ fn (mut p Parser) parse_world(mut doc WitDoc) !WitWorld { } if kind == 'include' { p.advance() - for p.peek().kind != .eof && !(p.peek().kind == .punct && p.peek().val == ';') { - p.advance() - } - if p.peek().kind == .punct && p.peek().val == ';' { - p.advance() - } + inc := p.parse_include()! + w.includes << inc continue } return error('unexpected in world at ${p.pos()}') @@ -588,6 +584,58 @@ fn (mut p Parser) collect_attributes() []string { 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 } diff --git a/vlib/wit/printer.v b/vlib/wit/printer.v index 2c71b101619824..2e0bef855345a1 100644 --- a/vlib/wit/printer.v +++ b/vlib/wit/printer.v @@ -101,6 +101,22 @@ pub fn (d WitDoc) str() string { 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)};' diff --git a/vlib/wit/validate.v b/vlib/wit/validate.v index 293b6f0341959c..a5d32cab20d5ce 100644 --- a/vlib/wit/validate.v +++ b/vlib/wit/validate.v @@ -180,6 +180,86 @@ pub fn validate(doc WitDoc) ! { } 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 diff --git a/vlib/wit/validation_test.v b/vlib/wit/validation_test.v index 4999436b56d2eb..23c4356770d962 100644 --- a/vlib/wit/validation_test.v +++ b/vlib/wit/validation_test.v @@ -114,6 +114,41 @@ interface i { 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; diff --git a/vlib/wit/wit.v b/vlib/wit/wit.v index 31ac905698d77e..7c40c110874976 100644 --- a/vlib/wit/wit.v +++ b/vlib/wit/wit.v @@ -143,12 +143,19 @@ pub mut: 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 - attrs []string + name string + imports []WitWorldImport + exports []WitWorldExport + includes []WitInclude + attrs []string } pub struct WitPackage { From 9a47fdc212d21b42de6edee1de733e48051204ac Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 14:31:54 +0330 Subject: [PATCH 87/90] wit+wasm: tuples as arrays, forward-declared resources, socket bindings Codegen maps homogeneous tuples to arrays (V has no anonymous tuple field types) and heterogeneous ones to voidptr. Parser accepts forward resource declarations (resource name;). Preview2 bindings gain wasi:sockets coverage (network, instance-network, ip-name-lookup, tcp, tcp-create-socket, udp, udp-create-socket) with signature tests. --- vlib/wasm/tests/builder_roundtrip_test.v | 54 +++ .../testdata/wasi/sock_instance-network.wit | 11 + .../wit/testdata/wasi/sock_ip-name-lookup.wit | 56 +++ vlib/wasm/wit/testdata/wasi/sock_network.wit | 169 ++++++++ .../testdata/wasi/sock_tcp-create-socket.wit | 30 ++ vlib/wasm/wit/testdata/wasi/sock_tcp.wit | 387 ++++++++++++++++++ .../testdata/wasi/sock_udp-create-socket.wit | 30 ++ vlib/wasm/wit/testdata/wasi/sock_udp.wit | 288 +++++++++++++ vlib/wit/gen_v.v | 13 +- vlib/wit/parser.v | 4 + vlib/wit/preview2_test.v | 26 ++ 11 files changed, 1067 insertions(+), 1 deletion(-) create mode 100644 vlib/wasm/wit/testdata/wasi/sock_instance-network.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_ip-name-lookup.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_network.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_tcp-create-socket.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_tcp.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_udp-create-socket.wit create mode 100644 vlib/wasm/wit/testdata/wasi/sock_udp.wit diff --git a/vlib/wasm/tests/builder_roundtrip_test.v b/vlib/wasm/tests/builder_roundtrip_test.v index 39e7185268a9cb..0b0a2fa9a3ed67 100644 --- a/vlib/wasm/tests/builder_roundtrip_test.v +++ b/vlib/wasm/tests/builder_roundtrip_test.v @@ -363,6 +363,60 @@ fn test_data_count_two_segments() { 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() 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/gen_v.v b/vlib/wit/gen_v.v index 5fe5c5a0051376..e19180dd7f4b98 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -222,9 +222,20 @@ fn wit_type_to_v(t WitType, defined map[string]bool) string { 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) } - return '(${ts.join(', ')})' + 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/parser.v b/vlib/wit/parser.v index f7ebd39f4a8ebb..4a6c8cc43150e3 100644 --- a/vlib/wit/parser.v +++ b/vlib/wit/parser.v @@ -306,6 +306,10 @@ 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() diff --git a/vlib/wit/preview2_test.v b/vlib/wit/preview2_test.v index 8444e6cc2e1203..e12d2dfdcf2899 100644 --- a/vlib/wit/preview2_test.v +++ b/vlib/wit/preview2_test.v @@ -55,3 +55,29 @@ fn test_preview2_filesystem() { '(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', + ]) +} From 191064ac79f3a93af3b0d304d42717bae2ead55b Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 14:33:21 +0330 Subject: [PATCH 88/90] docs: conformance reflects string returns and multi-string params --- vlib/wasm/wit/CONFORMANCE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index a201af308a948d..3878dac69c0cac 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -39,9 +39,10 @@ Function-signature shapes the component wrapper lifts today: |---|---| | primitive params/results (i32/i64/f32/f64) | ✅ | | single string param, `(i32, i32) -> r` core sig | ✅ | -| string returns | ❌ (depends on Item 1 follow-up) | -| lists / records / variants / resources | ❌ (depends on Item 1 follow-up) | -| options / results / tuples / maps | ❌ (depends on Item 1 follow-up) | +| multiple string params (each expands to `(i32, i32)`) | ✅ | +| string returns, `(ptr, len)` pair via hidden sret slot | ✅ | +| lists / records / variants / resources | ❌ (no canonical layout yet) | +| options / results / tuples / maps | ❌ (no canonical layout yet) | ## Notes From 7bc41c9e1bd01f16068b4277679172a3c0b4060c Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 16:41:56 +0330 Subject: [PATCH 89/90] wasm: clear error when emcc is missing for emscripten target Fail fast with actionable guidance instead of a generic missing-executable error when -os wasm32_emscripten needs linking via an emcc-family compiler that is not on PATH. C-only output (-o file.c) is exempt since it never invokes a compiler. Docs: doc/wasm.md gains an Emscripten-output section; playground README clarifies emcc is only for self-hosting the compiler in the browser. --- doc/wasm.md | 15 +++++++++++++ examples/wasm/playground/README.md | 4 +++- vlib/v/compiler_tests/driver_cli_test.v | 30 +++++++++++++++++++++++++ vlib/v/driver/driver.v | 15 +++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) 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/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/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 8e40f141033675..883e4e49590f1f 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -11287,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()) From 766bf0b234e1cb565a1a3d71f41891b05e7f7d48 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 10 Nov 2026 23:26:53 +0330 Subject: [PATCH 90/90] wasm: canonical-ABI layout helpers + enum/flags lift + documented limitations Add vlib/wasm/cabi_layout.v with canonical-ABI size/align/offset rules for primitives, strings/lists, records/tuples, variants, options, results, enums, flags. Add vlib/wit/cabi.v mapping WIT types to those layouts (named resolution through the doc). Lift WIT enums as component s32 over core i32 and flags (<=32 bits) as u32 via wit_func_lift_with_doc. Document reentrancy/string-lifetime/composite-not-yet-lifted limitations in code (ssa_gen, component, gen_v) and update CONFORMANCE.md lift rows. --- vlib/v/gen/wasm/ssa_gen.v | 19 ++++- vlib/wasm/cabi_layout.v | 153 ++++++++++++++++++++++++++++++++++ vlib/wasm/cabi_layout_test.v | 87 +++++++++++++++++++ vlib/wasm/component.v | 5 ++ vlib/wasm/wit/CONFORMANCE.md | 18 +++- vlib/wit/cabi.v | 156 +++++++++++++++++++++++++++++++++++ vlib/wit/cabi_test.v | 124 ++++++++++++++++++++++++++++ vlib/wit/component.v | 143 +++++++++++++++++++++++++++++++- vlib/wit/gen_v.v | 4 +- 9 files changed, 700 insertions(+), 9 deletions(-) create mode 100644 vlib/wasm/cabi_layout.v create mode 100644 vlib/wasm/cabi_layout_test.v create mode 100644 vlib/wit/cabi.v create mode 100644 vlib/wit/cabi_test.v diff --git a/vlib/v/gen/wasm/ssa_gen.v b/vlib/v/gen/wasm/ssa_gen.v index d992d5baa1de94..232a95fa3e7660 100644 --- a/vlib/v/gen/wasm/ssa_gen.v +++ b/vlib/v/gen/wasm/ssa_gen.v @@ -48,6 +48,14 @@ mut: // 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 } @@ -308,9 +316,14 @@ fn (g &SSAGen) cabi_wrappable(fid int) bool { // 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. -// Limitation: scratch slots are not reentrant; nested wrapper calls -// (host -> guest -> host -> guest) would clobber them. Lists, records, -// variants, and other composite types are out of scope. +// 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{} 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 index 060f9c8a6d18a2..c23129b901ce54 100644 --- a/vlib/wasm/component.v +++ b/vlib/wasm/component.v @@ -195,6 +195,11 @@ pub mut: // 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) diff --git a/vlib/wasm/wit/CONFORMANCE.md b/vlib/wasm/wit/CONFORMANCE.md index 3878dac69c0cac..4a7c3497be1cbd 100644 --- a/vlib/wasm/wit/CONFORMANCE.md +++ b/vlib/wasm/wit/CONFORMANCE.md @@ -41,11 +41,25 @@ Function-signature shapes the component wrapper lifts today: | 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 | ✅ | -| lists / records / variants / resources | ❌ (no canonical layout yet) | -| options / results / tuples / maps | ❌ (no canonical layout yet) | +| 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 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 index ec329f99d14c95..87269d24fde0ce 100644 --- a/vlib/wit/component.v +++ b/vlib/wit/component.v @@ -23,7 +23,9 @@ fn wit_prim_codes(p string) (int, int) { } // wit_func_lift describes how one WIT function maps onto a core export, or -// none when the shape is not liftable. +// 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{} @@ -84,6 +86,135 @@ fn wit_func_lift(f WitFunc) ?wasm.CabiStringExport { } } +// 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 @@ -105,7 +236,10 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { // direct func exports for e in w.exports { if f := e.func { - if mut spec := wit_func_lift(f) { + 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 } @@ -119,7 +253,10 @@ pub fn wrap_component(core []u8, doc WitDoc, world string) []u8 { for it in iface.items { if it.kind == .func { if f := it.func { - if mut spec := wit_func_lift(f) { + 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 } diff --git a/vlib/wit/gen_v.v b/vlib/wit/gen_v.v index e19180dd7f4b98..4f5ef35e0194ae 100644 --- a/vlib/wit/gen_v.v +++ b/vlib/wit/gen_v.v @@ -201,7 +201,9 @@ fn wit_type_to_v(t WitType, defined map[string]bool) string { 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. + // 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) }