From b7f58e4533199257d53b166d353a73cf1577c9de Mon Sep 17 00:00:00 2001 From: Randy Reddig Date: Fri, 27 Oct 2023 14:50:08 -0700 Subject: [PATCH] bindgen, internal/go, wit: WIP Generating valid Go names from WIT names: 1. Scan for an explicit mapping, e.g. wasi:clocks/wall-clock -> wasi/clocks/wallclock 2. Perform string conversion, e.g. : -> /, strip - 3. (optional) validate short package name as valid Go identifier 4. Check against list of Go reserved words 5. For decl names, convert to Go-style names (TitleCase), check against existing decl --- bindgen/bindgen.go | 69 ++++++++++++++++++++++++++++++------- bindgen/options.go | 52 +++++++++++++++++++--------- internal/go/gen/gen.go | 7 ++-- internal/go/gen/gen_test.go | 6 ++-- wit/ident.go | 23 +++++++++++++ 5 files changed, 122 insertions(+), 35 deletions(-) diff --git a/bindgen/bindgen.go b/bindgen/bindgen.go index 57e60f1f..2109c53e 100644 --- a/bindgen/bindgen.go +++ b/bindgen/bindgen.go @@ -5,7 +5,9 @@ package bindgen import ( "fmt" + "strings" + "github.com/ydnar/wasm-tools-go/internal/codec" "github.com/ydnar/wasm-tools-go/internal/go/gen" "github.com/ydnar/wasm-tools-go/wit" ) @@ -15,35 +17,78 @@ const HeaderPattern = `// Code generated by %s. DO NOT EDIT.` // Go generates one or more Go packages from [wit.Resolve] res. // It returns any error that occurs during code generation. func Go(res *wit.Resolve, opts ...Option) ([]*gen.Package, error) { - var state genState - state.opts.apply(opts...) - state.res = res + g, err := newGenerator(res, opts...) + if err != nil { + return nil, err + } // By default, each WIT interface and world maps to a single Go package. // Options might override the Go package, including combining multiple // WIT interfaces and/or worlds into a single Go package. - for _, w := range state.res.Worlds { + for _, w := range g.res.Worlds { id := worldIdent(w) fmt.Printf("%s → %s\n", id, id) } - return state.finish() + return g.finish() +} + +type generator struct { + opts options + res *wit.Resolve + packages map[string]*gen.Package + packageMap map[wit.Ident]*gen.Package } -type genState struct { - opts options - res *wit.Resolve - packages map[wit.Ident]*gen.Package +func newGenerator(res *wit.Resolve, opts ...Option) (*generator, error) { + state := &generator{} + err := state.opts.apply(opts...) + if err != nil { + return nil, err + } + state.res = res + return state, nil } -func (state *genState) finish() ([]*gen.Package, error) { +func (g *generator) finish() ([]*gen.Package, error) { var packages []*gen.Package - for _, pkg := range state.packages { - packages = append(packages, pkg) + for _, path := range codec.SortedKeys(g.packages) { + packages = append(packages, g.packages[path]) } return packages, nil } +func (g *generator) goPackageIdent(id wit.Ident) gen.Ident { + // Remove Name field, which doesn’t affect package path. + id = id.InterfaceIdent() + + // Check existing Go packages first. + if pkg, ok := g.packageMap[id]; ok { + return pkg.Ident + } + + // Check ident mappings. + if gid, ok := g.opts.idents[id]; ok { + return gid + } + var base string + if gid, ok := g.opts.idents[id.PackageIdent()]; ok { + base = gid.Path + } else { + base = strings.ReplaceAll(id.Package, ":", "/") + } + + // Concatenate + if id.Interface != "" { + return gen.Ident{ + // TODO: strip '-' characters + Path: base + "/" + id.Interface, + } + } + + return gen.Ident{Path: base} +} + func worldIdent(w *wit.World) wit.Ident { var id wit.Ident id.Package = w.Package.Name.ShortString() diff --git a/bindgen/options.go b/bindgen/options.go index 31b48fa3..fae3fd56 100644 --- a/bindgen/options.go +++ b/bindgen/options.go @@ -1,14 +1,19 @@ package bindgen +import ( + "github.com/ydnar/wasm-tools-go/internal/go/gen" + "github.com/ydnar/wasm-tools-go/wit" +) + // Option represents a single configuration option for this package. type Option interface { - applyOption(*options) + applyOption(*options) error } -type optionFunc func(*options) +type optionFunc func(*options) error -func (f optionFunc) applyOption(opts *options) { - f(opts) +func (f optionFunc) applyOption(opts *options) error { + return f(opts) } type options struct { @@ -21,20 +26,25 @@ type options struct { // "wasi:clocks/wall-clock" -> "wasi/clocks/wall#wallclock" (for a Go short package name of wallclock) // "wasi:clocks/wall-clock.datetime" -> "wasi/clocks/wall#DateTime" // "wasi:clocks/wall-clock.now" -> "wasi/clocks/wall#Now" - idents map[string]string + idents map[wit.Ident]gen.Ident } -func (opts *options) apply(o ...Option) { +func (opts *options) apply(o ...Option) error { for _, o := range o { - o.applyOption(opts) + err := o.applyOption(opts) + if err != nil { + return err + } } + return nil } // GeneratedBy returns an [Option] that specifies the name of the program or package // that will appear in the "Code generated by ..." header on generated files. func GeneratedBy(name string) Option { - return optionFunc(func(opts *options) { + return optionFunc(func(opts *options) error { opts.generator = name + return nil }) } @@ -50,11 +60,20 @@ func GeneratedBy(name string) Option { // // [WIT]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md func MapIdent(from, to string) Option { - return optionFunc(func(opts *options) { + return optionFunc(func(opts *options) error { if opts.idents == nil { - opts.idents = make(map[string]string) + opts.idents = make(map[wit.Ident]gen.Ident) + } + fromIdent, err := wit.ParseIdent(from) + if err != nil { + return err } - opts.idents[from] = to + toIdent, err := gen.ParseIdent(to) + if err != nil { + return err + } + opts.idents[fromIdent] = toIdent + return nil }) } @@ -63,12 +82,13 @@ func MapIdent(from, to string) Option { // // [WIT]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md func MapIdents(idents map[string]string) Option { - return optionFunc(func(opts *options) { - if opts.idents == nil { - opts.idents = make(map[string]string, len(idents)) - } + return optionFunc(func(opts *options) error { for from, to := range idents { - opts.idents[from] = to + err := MapIdent(from, to).applyOption(opts) + if err != nil { + return err + } } + return nil }) } diff --git a/internal/go/gen/gen.go b/internal/go/gen/gen.go index c14cae83..ef9caea2 100644 --- a/internal/go/gen/gen.go +++ b/internal/go/gen/gen.go @@ -11,11 +11,8 @@ import ( // Package represents a Go package, containing zero or more files // of generated code, along with zero or more declarations. type Package struct { - // Path is the Go package path, e.g. "encoding/json" - Path string - - // Name is the short Go package name, e.g. "json" - Name string + // Ident is the package path and name. + Ident // Files is the list of Go source files in this package. Files map[string]*File diff --git a/internal/go/gen/gen_test.go b/internal/go/gen/gen_test.go index f59fc6eb..06e6c273 100644 --- a/internal/go/gen/gen_test.go +++ b/internal/go/gen/gen_test.go @@ -9,8 +9,10 @@ func TestFileString(t *testing.T) { f := &File{ Build: "wasm || wasm32 || tinygo.wasm", Package: &Package{ - Path: "wasm/wasi/clocks/wallclock", - Name: "wallclock", + Ident: Ident{ + Path: "wasm/wasi/clocks/wallclock", + Name: "wallclock", + }, }, Imports: map[string]string{ "encoding/json": "json", diff --git a/wit/ident.go b/wit/ident.go index 9b9b7b6b..b8393c5a 100644 --- a/wit/ident.go +++ b/wit/ident.go @@ -28,6 +28,29 @@ func ParseIdent(s string) (Ident, error) { return id, err } +// PackageIdent returns the package-only Ident for id. +// For example, "wasi:clocks/wall-clock.DateTime" returns "wasi:clocks". +func (id Ident) PackageIdent() Ident { + return Ident{ + Package: id.Package, + } +} + +// InterfaceIdent returns the interface-only Ident for id. +// For example, "wasi:clocks/wall-clock.DateTime" returns "wasi:clocks/wall-clock". +func (id Ident) InterfaceIdent() Ident { + return Ident{ + Package: id.Package, + Interface: id.Interface, + } +} + +// WorldIdent returns the world-only Ident for id. +// For example, "wasi:clocks/imports.DateTime" returns "wasi:clocks/imports". +func (id Ident) WorldIdent() Ident { + return id.InterfaceIdent() +} + // String returns the canonical string representation of id. It implements the [fmt.Stringer] interface. // // The canonical string representation of an [Ident] is "$package[/$interface[.$name]]". Examples: