Skip to content

Commit

Permalink
refactor(gnovm): move go type checking to own file (#3491)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehowl authored Jan 14, 2025
1 parent 7761c3b commit 6909efa
Show file tree
Hide file tree
Showing 4 changed files with 538 additions and 525 deletions.
173 changes: 0 additions & 173 deletions gnovm/pkg/gnolang/go2gno.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,16 @@ package gnolang
*/

import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
"os"
"path"
"reflect"
"slices"
"strconv"
"strings"

"github.com/davecgh/go-spew/spew"
"github.com/gnolang/gno/gnovm"
"github.com/gnolang/gno/tm2/pkg/errors"
"go.uber.org/multierr"
)

func MustReadFile(path string) *FileNode {
Expand Down Expand Up @@ -483,171 +475,6 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) {
}
}

//----------------------------------------
// type checking (using go/types)
// XXX move to gotypecheck.go.

// MemPackageGetter implements the GetMemPackage() method. It is a subset of
// [Store], separated for ease of testing.
type MemPackageGetter interface {
GetMemPackage(path string) *gnovm.MemPackage
}

// TypeCheckMemPackage performs type validation and checking on the given
// mempkg. To retrieve dependencies, it uses getter.
//
// The syntax checking is performed entirely using Go's go/types package.
//
// If format is true, the code will be automatically updated with the
// formatted source code.
func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool) error {
return typeCheckMemPackage(mempkg, getter, false, format)
}

// TypeCheckMemPackageTest performs the same type checks as [TypeCheckMemPackage],
// but allows re-declarations.
//
// Note: like TypeCheckMemPackage, this function ignores tests and filetests.
func TypeCheckMemPackageTest(mempkg *gnovm.MemPackage, getter MemPackageGetter) error {
return typeCheckMemPackage(mempkg, getter, true, false)
}

func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool) error {
var errs error
imp := &gnoImporter{
getter: getter,
cache: map[string]gnoImporterResult{},
cfg: &types.Config{
Error: func(err error) {
errs = multierr.Append(errs, err)
},
},
allowRedefinitions: testing,
}
imp.cfg.Importer = imp

_, err := imp.parseCheckMemPackage(mempkg, format)
// prefer to return errs instead of err:
// err will generally contain only the first error encountered.
if errs != nil {
return errs
}
return err
}

type gnoImporterResult struct {
pkg *types.Package
err error
}

type gnoImporter struct {
getter MemPackageGetter
cache map[string]gnoImporterResult
cfg *types.Config

// allow symbol redefinitions? (test standard libraries)
allowRedefinitions bool
}

// Unused, but satisfies the Importer interface.
func (g *gnoImporter) Import(path string) (*types.Package, error) {
return g.ImportFrom(path, "", 0)
}

type importNotFoundError string

func (e importNotFoundError) Error() string { return "import not found: " + string(e) }

// ImportFrom returns the imported package for the given import
// path when imported by a package file located in dir.
func (g *gnoImporter) ImportFrom(path, _ string, _ types.ImportMode) (*types.Package, error) {
if pkg, ok := g.cache[path]; ok {
return pkg.pkg, pkg.err
}
mpkg := g.getter.GetMemPackage(path)
if mpkg == nil {
err := importNotFoundError(path)
g.cache[path] = gnoImporterResult{err: err}
return nil, err
}
fmt := false
result, err := g.parseCheckMemPackage(mpkg, fmt)
g.cache[path] = gnoImporterResult{pkg: result, err: err}
return result, err
}

func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*types.Package, error) {
// This map is used to allow for function re-definitions, which are allowed
// in Gno (testing context) but not in Go.
// This map links each function identifier with a closure to remove its
// associated declaration.
var delFunc map[string]func()
if g.allowRedefinitions {
delFunc = make(map[string]func())
}

fset := token.NewFileSet()
files := make([]*ast.File, 0, len(mpkg.Files))
var errs error
for _, file := range mpkg.Files {
// Ignore non-gno files.
// TODO: support filetest type checking. (should probably handle as each its
// own separate pkg, which should also be typechecked)
if !strings.HasSuffix(file.Name, ".gno") ||
strings.HasSuffix(file.Name, "_test.gno") ||
strings.HasSuffix(file.Name, "_filetest.gno") {
continue
}

const parseOpts = parser.ParseComments | parser.DeclarationErrors | parser.SkipObjectResolution
f, err := parser.ParseFile(fset, path.Join(mpkg.Path, file.Name), file.Body, parseOpts)
if err != nil {
errs = multierr.Append(errs, err)
continue
}

if delFunc != nil {
deleteOldIdents(delFunc, f)
}

// enforce formatting
if fmt {
var buf bytes.Buffer
err = format.Node(&buf, fset, f)
if err != nil {
errs = multierr.Append(errs, err)
continue
}
file.Body = buf.String()
}

files = append(files, f)
}
if errs != nil {
return nil, errs
}

return g.cfg.Check(mpkg.Path, fset, files, nil)
}

func deleteOldIdents(idents map[string]func(), f *ast.File) {
for _, decl := range f.Decls {
fd, ok := decl.(*ast.FuncDecl)
if !ok || fd.Recv != nil { // ignore methods
continue
}
if del := idents[fd.Name.Name]; del != nil {
del()
}
decl := decl
idents[fd.Name.Name] = func() {
// NOTE: cannot use the index as a file may contain multiple decls to be removed,
// so removing one would make all "later" indexes wrong.
f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d })
}
}
}

//----------------------------------------
// utility methods

Expand Down
Loading

0 comments on commit 6909efa

Please sign in to comment.