Skip to content

Commit 7ef22df

Browse files
authored
Merge pull request #287 from bytecodealliance/ydnar/issue281
wit/bindgen: fix wasm-tools WIT generation bugs
2 parents 0521583 + bd158ac commit 7ef22df

9 files changed

+181
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1616

1717
### Fixed
1818

19+
- [#281](https://github.com/bytecodealliance/go-modules/issues/281): errors from internal `wasm-tools` calls are no longer silently ignored. This required fixing a number of related issues, including synthetic world packages for Component Model metadata generation, WIT generation, and WIT keyword escaping in WIT package or interface names.
1920
- [#284](https://github.com/bytecodealliance/go-modules/issues/284): do not use `bool` for `variant` or `result` GC shapes. TinyGo returns `result` and `variant` values with `bool` as 0 or 1, which breaks the memory representation of tagged unions (variants).
2021

2122
## [v0.5.0] — 2024-12-14

testdata/issues/issue281.wit

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package issues:issue281@0.1.0;
2+
3+
world imports {
4+
import wasi:http/types@0.2.1;
5+
}
6+
7+
package wasi:http@0.2.1 {
8+
interface types {
9+
type t = u8;
10+
}
11+
}

testdata/issues/issue281.wit.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"worlds": [
3+
{
4+
"name": "imports",
5+
"imports": {
6+
"interface-0": {
7+
"interface": {
8+
"id": 0
9+
}
10+
}
11+
},
12+
"exports": {},
13+
"package": 1
14+
}
15+
],
16+
"interfaces": [
17+
{
18+
"name": "types",
19+
"types": {
20+
"t": 0
21+
},
22+
"functions": {},
23+
"package": 0
24+
}
25+
],
26+
"types": [
27+
{
28+
"name": "t",
29+
"kind": {
30+
"type": "u8"
31+
},
32+
"owner": {
33+
"interface": 0
34+
}
35+
}
36+
],
37+
"packages": [
38+
{
39+
"name": "wasi:[email protected]",
40+
"interfaces": {
41+
"types": 0
42+
},
43+
"worlds": {}
44+
},
45+
{
46+
"name": "issues:[email protected]",
47+
"interfaces": {},
48+
"worlds": {
49+
"imports": 0
50+
}
51+
}
52+
]
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package issues:issue281@0.1.0;
2+
3+
world imports {
4+
import wasi:http/types@0.2.1;
5+
}
6+
7+
package wasi:http@0.2.1 {
8+
interface types {
9+
type t = u8;
10+
}
11+
}

wit/bindgen/generator.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing/fstest"
2121
"time"
2222

23+
"github.com/tetratelabs/wazero/sys"
2324
"go.bytecodealliance.org/cm"
2425
"go.bytecodealliance.org/internal/codec"
2526
"go.bytecodealliance.org/internal/go/gen"
@@ -2364,13 +2365,15 @@ func (g *generator) newPackage(w *wit.World, i *wit.Interface, name string) (*ge
23642365
// Generate wasm file
23652366
res, world := synthesizeWorld(g.res, w, worldName)
23662367
witText := res.WIT(wit.Filter(world, i), "")
2368+
world.Package.Worlds.Delete(worldName) // Undo mutation
23672369
if g.opts.generateWIT {
23682370
witFile := g.witFileFor(owner)
23692371
witFile.WriteString(witText)
23702372
}
23712373
content, err := g.componentEmbed(witText)
23722374
if err != nil {
2373-
// return nil, err
2375+
g.opts.logger.Errorf("WIT:\n%s\n\n", witText)
2376+
return nil, err
23742377
}
23752378
componentType := &wasm.CustomSection{
23762379
Name: "component-type:" + worldName,
@@ -2390,43 +2393,41 @@ func (g *generator) newPackage(w *wit.World, i *wit.Interface, name string) (*ge
23902393
return pkg, nil
23912394
}
23922395

2393-
var replacer = strings.NewReplacer("/", "-", ":", "-", "@", "-v", ".", "")
2396+
var replacer = strings.NewReplacer("/", "-", ":", "-", "@", "-v", ".", "", "%", "")
23942397

23952398
// componentEmbed runs generated WIT through wasm-tools to generate a wasm file with a component-type custom section.
23962399
func (g *generator) componentEmbed(witData string) ([]byte, error) {
23972400
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
23982401
defer cancel()
23992402

24002403
// TODO: --all-features?
2401-
filename := "./component.wit"
2404+
filename := "component.wit"
24022405
args := []string{"component", "embed", "--only-custom", filename}
24032406
fsMap := map[string]fs.FS{
2404-
".": fstest.MapFS{
2407+
"": fstest.MapFS{
24052408
filename: &fstest.MapFile{Data: []byte(witData)},
24062409
},
24072410
}
24082411
stdout := &bytes.Buffer{}
2409-
err := g.wasmTools.Run(ctx, nil, stdout, nil, fsMap, args...)
2412+
stderr := &bytes.Buffer{}
2413+
err := g.wasmTools.Run(ctx, nil, stdout, stderr, fsMap, args...)
24102414
if err != nil {
2415+
if _, ok := err.(*sys.ExitError); ok {
2416+
return nil, fmt.Errorf("wasm-tools: %s", stderr.String())
2417+
}
24112418
return nil, fmt.Errorf("wasm-tools: %w", err)
24122419
}
24132420
return stdout.Bytes(), err
24142421
}
24152422

24162423
func synthesizeWorld(r *wit.Resolve, w *wit.World, name string) (*wit.Resolve, *wit.World) {
2417-
p := &wit.Package{}
2418-
p.Name.Namespace = "go"
2419-
p.Name.Package = "bindgen"
2420-
24212424
w = w.Clone()
24222425
w.Name = name
24232426
w.Docs = wit.Docs{}
2424-
w.Package = p
2425-
p.Worlds.Set(name, w)
2427+
w.Package.Worlds.Set(name, w)
24262428

24272429
r = r.Clone()
24282430
r.Worlds = append(r.Worlds, w)
2429-
r.Packages = append(r.Packages, p)
24302431

24312432
return r, w
24322433
}

wit/bindgen/testdata_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"go.bytecodealliance.org/internal/go/gen"
2222
"go.bytecodealliance.org/internal/relpath"
2323
"go.bytecodealliance.org/wit"
24+
"go.bytecodealliance.org/wit/logging"
2425
)
2526

2627
var writeGoFiles = flag.Bool("write", false, "write generated Go files")
@@ -108,6 +109,7 @@ func validateGeneratedGo(t *testing.T, res *wit.Resolve, origin string) {
108109
GeneratedBy("test"),
109110
PackageRoot(pkgPath),
110111
Versioned(true),
112+
Logger(logging.NewLogger(os.Stderr, logging.LevelWarn)),
111113
)
112114
if err != nil {
113115
t.Error(err)

wit/ident.go

+70-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package wit
22

33
import (
44
"errors"
5+
"strconv"
56
"strings"
67

78
"github.com/coreos/go-semver/semver"
@@ -36,21 +37,30 @@ type Ident struct {
3637
func ParseIdent(s string) (Ident, error) {
3738
var id Ident
3839
name, ver, hasVer := strings.Cut(s, "@")
39-
base, ext, hasExt := strings.Cut(name, "/")
40-
id.Namespace, id.Package, _ = strings.Cut(base, ":")
4140
if hasVer {
4241
var err error
4342
id.Version, err = semver.NewVersion(ver)
4443
if err != nil {
4544
return id, err
4645
}
4746
}
47+
base, ext, hasExt := strings.Cut(name, "/")
48+
ns, pkg, _ := strings.Cut(base, ":")
49+
id.Namespace = trimPercent(ns)
50+
id.Package = trimPercent(pkg)
4851
if hasExt {
49-
id.Extension = ext
52+
id.Extension = trimPercent(ext)
5053
}
5154
return id, id.Validate()
5255
}
5356

57+
func trimPercent(s string) string {
58+
if len(s) > 0 && s[0] == '%' {
59+
return s[1:]
60+
}
61+
return s
62+
}
63+
5464
// Validate validates id, returning any errors.
5565
func (id *Ident) Validate() error {
5666
switch {
@@ -59,6 +69,59 @@ func (id *Ident) Validate() error {
5969
case id.Package == "":
6070
return errors.New("missing package name")
6171
}
72+
if err := validateName(id.Namespace); err != nil {
73+
return err
74+
}
75+
if err := validateName(id.Package); err != nil {
76+
return err
77+
}
78+
return validateName(id.Extension)
79+
}
80+
81+
func validateName(s string) error {
82+
if len(s) == 0 {
83+
return nil
84+
}
85+
var prev rune
86+
for _, c := range s {
87+
switch {
88+
case c >= 'a' && c <= 'z':
89+
switch {
90+
case prev >= 'A' && prev <= 'Z':
91+
return errors.New("invalid character " + strconv.Quote(string(c)))
92+
}
93+
case c >= 'A' && c <= 'Z':
94+
switch {
95+
case prev == 0: // start of string
96+
case prev >= 'A' && prev <= 'Z':
97+
case prev >= '0' && prev <= '9':
98+
case prev == '-':
99+
default:
100+
return errors.New("invalid character " + strconv.Quote(string(c)))
101+
}
102+
case c >= '0' && c <= '9':
103+
switch {
104+
case prev >= 'a' && prev <= 'z':
105+
case prev >= 'A' && prev <= 'Z':
106+
case prev >= '0' && prev <= '9':
107+
default:
108+
return errors.New("invalid character " + strconv.Quote(string(c)))
109+
}
110+
case c == '-':
111+
switch prev {
112+
case 0: // start of string
113+
return errors.New("invalid leading -")
114+
case '-':
115+
return errors.New("invalid double --")
116+
}
117+
default:
118+
return errors.New("invalid character " + strconv.Quote(string(c)))
119+
}
120+
prev = c
121+
}
122+
if prev == '-' {
123+
return errors.New("invalid trailing -")
124+
}
62125
return nil
63126
}
64127

@@ -68,15 +131,15 @@ func (id *Ident) String() string {
68131
return id.UnversionedString()
69132
}
70133
if id.Extension == "" {
71-
return id.Namespace + ":" + id.Package + "@" + id.Version.String()
134+
return escape(id.Namespace) + ":" + escape(id.Package) + "@" + id.Version.String()
72135
}
73-
return id.Namespace + ":" + id.Package + "/" + id.Extension + "@" + id.Version.String()
136+
return escape(id.Namespace) + ":" + escape(id.Package) + "/" + escape(id.Extension) + "@" + id.Version.String()
74137
}
75138

76139
// UnversionedString returns a string representation of an [Ident] without version information.
77140
func (id *Ident) UnversionedString() string {
78141
if id.Extension == "" {
79-
return id.Namespace + ":" + id.Package
142+
return escape(id.Namespace) + ":" + escape(id.Package)
80143
}
81-
return id.Namespace + ":" + id.Package + "/" + id.Extension
144+
return escape(id.Namespace) + ":" + escape(id.Package) + "/" + escape(id.Extension)
82145
}

wit/ident_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ func TestIdent(t *testing.T) {
1818
{"wasi:io/streams", Ident{Namespace: "wasi", Package: "io", Extension: "streams"}, false},
1919
{"wasi:io/[email protected]", Ident{Namespace: "wasi", Package: "io", Extension: "streams", Version: semver.New("0.2.0")}, false},
2020

21+
// Escaping
22+
{"%use:%own", Ident{Namespace: "use", Package: "own"}, false},
23+
{"%use:%[email protected]", Ident{Namespace: "use", Package: "own", Version: semver.New("0.2.0")}, false},
24+
{"%use:%own/%type", Ident{Namespace: "use", Package: "own", Extension: "type"}, false},
25+
{"%use:%own/%[email protected]", Ident{Namespace: "use", Package: "own", Extension: "type", Version: semver.New("0.2.0")}, false},
26+
27+
// Mixed-case
28+
{"ABC:def-GHI", Ident{Namespace: "ABC", Package: "def-GHI"}, false},
29+
{"ABC1:def2-GHI3", Ident{Namespace: "ABC1", Package: "def2-GHI3"}, false},
30+
2131
// Errors
2232
{"", Ident{}, true},
2333
{":", Ident{}, true},
@@ -28,6 +38,15 @@ func TestIdent(t *testing.T) {
2838
{"wasi:/", Ident{}, true},
2939
{"wasi:clocks@", Ident{}, true},
3040
{"wasi:clocks/wall-clock@", Ident{}, true},
41+
{"foo%:bar%baz", Ident{Namespace: "foo%", Package: "bar%baz"}, true},
42+
{"-foo:bar", Ident{Namespace: "-foo", Package: "bar"}, true},
43+
{"foo-:bar", Ident{Namespace: "foo-", Package: "bar"}, true},
44+
{"foo--foo:bar", Ident{Namespace: "foo--foo", Package: "bar"}, true},
45+
{"aBc:bar", Ident{Namespace: "aBc", Package: "bar"}, true},
46+
{"1:2", Ident{Namespace: "1", Package: "2"}, true},
47+
{"1a:2b", Ident{Namespace: "1a", Package: "2b"}, true},
48+
{"foo-1:bar", Ident{Namespace: "foo-1", Package: "bar"}, true},
49+
{"foo:bar-1", Ident{Namespace: "foo", Package: "bar-2"}, true},
3150
}
3251
for _, tt := range tests {
3352
t.Run(tt.s, func(t *testing.T) {

wit/wit.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ func relativeName(o TypeOwner, p *Package) string {
578578
return ""
579579
}
580580
qualifiedName := op.Name
581-
qualifiedName.Package += "/" + name
581+
qualifiedName.Package += "/" + escape(name)
582582
return qualifiedName.String()
583583
}
584584

0 commit comments

Comments
 (0)