Skip to content

Commit

Permalink
bindgen, internal/go, wit: WIP
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ydnar committed Oct 27, 2023
1 parent 5b73438 commit b7f58e4
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 35 deletions.
69 changes: 57 additions & 12 deletions bindgen/bindgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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()
Expand Down
52 changes: 36 additions & 16 deletions bindgen/options.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
})
}

Expand All @@ -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
})
}

Expand All @@ -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
})
}
7 changes: 2 additions & 5 deletions internal/go/gen/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions internal/go/gen/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions wit/ident.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit b7f58e4

Please sign in to comment.