Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions tools/common/wit/ast/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
subinclude("///go//build_defs:go")

go_library(
name = "ast",
srcs = [
"ast.go",
"parser.go",
],
visibility = ["//tools/..."],
)

go_test(
name = "ast_test",
srcs = [
"ast_test.go",
"parser_test.go",
],
deps = [
":ast",
],
)
152 changes: 152 additions & 0 deletions tools/common/wit/ast/ast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package ast

import (
"fmt"
"strings"
)

// TypeKind distinguishes between primitives, compound types, and user references.
type TypeKind int

const (
KindPrimitive TypeKind = iota
KindList
KindOption
KindResult
KindTuple
KindNamed
)

// TypeRef represents a type reference in WIT.
type TypeRef struct {
Kind TypeKind
Name string // e.g. "s32", "string", or user-defined type name
TypeArgs []*TypeRef // For list<T>, option<T>, result<T, E>, tuple<...>
}

func (t *TypeRef) String() string {
if t == nil {
return "unit"
}
switch t.Kind {
case KindPrimitive, KindNamed:
return t.Name
case KindList:
if len(t.TypeArgs) > 0 {
return fmt.Sprintf("list<%s>", t.TypeArgs[0].String())
}
return "list"
case KindOption:
if len(t.TypeArgs) > 0 {
return fmt.Sprintf("option<%s>", t.TypeArgs[0].String())
}
return "option"
case KindResult:
if len(t.TypeArgs) == 2 {
return fmt.Sprintf("result<%s, %s>", t.TypeArgs[0].String(), t.TypeArgs[1].String())
} else if len(t.TypeArgs) == 1 {
return fmt.Sprintf("result<%s>", t.TypeArgs[0].String())
}
return "result"
case KindTuple:
var inner []string
for _, arg := range t.TypeArgs {
inner = append(inner, arg.String())
}
return fmt.Sprintf("tuple<%s>", strings.Join(inner, ", "))
}
return t.Name
}

// Param represents a parameter in a WIT function.
type Param struct {
Name string
Type *TypeRef
}

// Function represents a function declared in a WIT interface.
type Function struct {
Name string
Doc string
Params []Param
Results *TypeRef // nil means void / unit
}

// Field represents a field in a WIT record.
type Field struct {
Name string
Type *TypeRef
}

// Record represents a record structure declared in a WIT interface.
type Record struct {
Name string
Doc string
Fields []Field
}

// EnumCase represents a single case in an enum.
type EnumCase struct {
Name string
}

// Enum represents an enum declared in a WIT interface.
type Enum struct {
Name string
Doc string
Cases []EnumCase
}

// TypeDef represents a type alias declared in WIT.
type TypeDef struct {
Name string
Type *TypeRef
}

// Resource represents a resource declaration in a WIT interface.
type Resource struct {
Name string
Doc string
Constructor *Function
Methods []Function
Static []Function
}

// Interface represents an interface block in WIT.
type Interface struct {
Name string
Doc string
Functions []Function
Records []Record
Enums []Enum
TypeDefs []TypeDef
Resources []Resource
}

// World represents a world definition in WIT.
type World struct {
Name string
Doc string
Imports []string
Exports []string
}

// Package represents a WIT package containing interfaces and worlds.
type Package struct {
Namespace string
Name string
Version string
Interfaces []Interface
Worlds []World
}

// FullName returns the canonical package identifier (e.g. "namespace:name").
func (p *Package) FullName() string {
if p.Namespace != "" && p.Name != "" {
return fmt.Sprintf("%s:%s", p.Namespace, p.Name)
}
if p.Name != "" {
return p.Name
}
return p.Namespace
}
178 changes: 178 additions & 0 deletions tools/common/wit/ast/ast_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package ast

import (
"os"
"path/filepath"
"testing"
)

func TestParseSimpleWit(t *testing.T) {
input := `
// Package comment
package test:structures@0.1.0;

interface two-sum {
solve: func(nums: list<s32>, target: s32) -> list<s32>;
reset: func();
}

world two-sum-world {
export two-sum;
}
`
pkg, err := ParseContent(input)
if err != nil {
t.Fatalf("ParseContent failed: %v", err)
}

if pkg.Namespace != "test" {
t.Errorf("expected namespace 'test', got %q", pkg.Namespace)
}
if pkg.Name != "structures" {
t.Errorf("expected name 'structures', got %q", pkg.Name)
}
if pkg.Version != "0.1.0" {
t.Errorf("expected version '0.1.0', got %q", pkg.Version)
}

if len(pkg.Interfaces) != 1 {
t.Fatalf("expected 1 interface, got %d", len(pkg.Interfaces))
}
iface := pkg.Interfaces[0]
if iface.Name != "two-sum" {
t.Errorf("expected interface 'two-sum', got %q", iface.Name)
}

if len(iface.Functions) != 2 {
t.Fatalf("expected 2 functions, got %d", len(iface.Functions))
}

fn1 := iface.Functions[0]
if fn1.Name != "solve" {
t.Errorf("expected fn 'solve', got %q", fn1.Name)
}
if len(fn1.Params) != 2 {
t.Fatalf("expected 2 params, got %d", len(fn1.Params))
}
if fn1.Params[0].Name != "nums" || fn1.Params[0].Type.Kind != KindList {
t.Errorf("unexpected param 0: %+v", fn1.Params[0])
}
if fn1.Params[1].Name != "target" || fn1.Params[1].Type.Name != "s32" {
t.Errorf("unexpected param 1: %+v", fn1.Params[1])
}
if fn1.Results == nil || fn1.Results.Kind != KindList {
t.Errorf("unexpected results: %+v", fn1.Results)
}

fn2 := iface.Functions[1]
if fn2.Name != "reset" || fn2.Results != nil {
t.Errorf("unexpected fn2: %+v", fn2)
}

if len(pkg.Worlds) != 1 {
t.Fatalf("expected 1 world, got %d", len(pkg.Worlds))
}
world := pkg.Worlds[0]
if world.Name != "two-sum-world" {
t.Errorf("expected world 'two-sum-world', got %q", world.Name)
}
if len(world.Exports) != 1 || world.Exports[0] != "two-sum" {
t.Errorf("unexpected exports: %v", world.Exports)
}
}

func TestParseRecordsAndEnums(t *testing.T) {
input := `
package test:types;

interface data-types {
/* Multi line comment
about point
*/
record point {
x: f64,
y: f64,
}

enum color {
red,
green,
blue,
}

type point-alias = point;

get-color: func(p: point) -> option<color>;
calc: func(a: s32) -> result<s32, string>;
}
`
pkg, err := ParseContent(input)
if err != nil {
t.Fatalf("ParseContent failed: %v", err)
}

if len(pkg.Interfaces) != 1 {
t.Fatalf("expected 1 interface, got %d", len(pkg.Interfaces))
}
iface := pkg.Interfaces[0]

if len(iface.Records) != 1 {
t.Fatalf("expected 1 record, got %d", len(iface.Records))
}
rec := iface.Records[0]
if rec.Name != "point" || len(rec.Fields) != 2 {
t.Errorf("unexpected record: %+v", rec)
}

if len(iface.Enums) != 1 {
t.Fatalf("expected 1 enum, got %d", len(iface.Enums))
}
enm := iface.Enums[0]
if enm.Name != "color" || len(enm.Cases) != 3 {
t.Errorf("unexpected enum: %+v", enm)
}

if len(iface.TypeDefs) != 1 {
t.Fatalf("expected 1 type def, got %d", len(iface.TypeDefs))
}

if len(iface.Functions) != 2 {
t.Fatalf("expected 2 functions, got %d", len(iface.Functions))
}

fn1 := iface.Functions[0]
if fn1.Results == nil || fn1.Results.Kind != KindOption {
t.Errorf("expected option result, got %+v", fn1.Results)
}

fn2 := iface.Functions[1]
if fn2.Results == nil || fn2.Results.Kind != KindResult {
t.Errorf("expected result result, got %+v", fn2.Results)
}
}

func TestParsePath(t *testing.T) {
tmpDir := t.TempDir()
witFile := filepath.Join(tmpDir, "test.wit")
content := `
package test:structures;

interface disjoint-set {
find: func(x: s32) -> s32;
}
`
if err := os.WriteFile(witFile, []byte(content), 0644); err != nil {
t.Fatal(err)
}

pkg, err := ParsePath(tmpDir)
if err != nil {
t.Fatalf("ParsePath failed: %v", err)
}
if pkg.FullName() != "test:structures" {
t.Errorf("expected test:structures, got %q", pkg.FullName())
}
if len(pkg.Interfaces) != 1 || pkg.Interfaces[0].Name != "disjoint-set" {
t.Errorf("unexpected interfaces: %+v", pkg.Interfaces)
}
}
Loading
Loading