-
Notifications
You must be signed in to change notification settings - Fork 387
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Manfred Touron <[email protected]>
- Loading branch information
Showing
11 changed files
with
905 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"go/ast" | ||
"go/parser" | ||
"go/printer" | ||
"go/token" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/gnolang/gno/gnovm/pkg/gnoffee" | ||
) | ||
|
||
var writeFlag bool | ||
|
||
func init() { | ||
flag.BoolVar(&writeFlag, "w", false, "write result to gnoffee.gen.go file instead of stdout") | ||
} | ||
|
||
func main() { | ||
flag.Parse() | ||
args := flag.Args() | ||
|
||
if len(args) < 1 { | ||
fmt.Fprintln(os.Stderr, "Usage: gnoffee [-w] <package-path or file.gnoffee or '-'>") | ||
return | ||
} | ||
|
||
err := doMain(args[0]) | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "%v\n", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func doMain(arg string) error { | ||
fset, pkg, err := processPackageOrFileOrStdin(arg) | ||
if err != nil { | ||
return fmt.Errorf("parse error: %w", err) | ||
} | ||
|
||
newFile, err := gnoffee.Stage2(pkg) | ||
if err != nil { | ||
return fmt.Errorf("processing the AST: %w", err) | ||
} | ||
|
||
// combine existing files into newFile to generate a unique file for the whole package. | ||
for _, file := range pkg { | ||
newFile.Decls = append(newFile.Decls, file.Decls...) | ||
} | ||
|
||
if writeFlag { | ||
filename := "gnoffee.gen.go" | ||
f, err := os.Create(filename) | ||
if err != nil { | ||
return fmt.Errorf("creating file %q: %w", filename, err) | ||
} | ||
defer f.Close() | ||
|
||
err = printer.Fprint(f, fset, newFile) | ||
if err != nil { | ||
return fmt.Errorf("writing to file %q: %w", filename, err) | ||
} | ||
} else { | ||
_ = printer.Fprint(os.Stdout, fset, newFile) | ||
} | ||
return nil | ||
} | ||
|
||
func processPackageOrFileOrStdin(arg string) (*token.FileSet, map[string]*ast.File, error) { | ||
var fset = token.NewFileSet() | ||
var pkg = map[string]*ast.File{} | ||
|
||
processFile := func(data []byte, filename string) error { | ||
source := string(data) | ||
source = gnoffee.Stage1(source) | ||
|
||
parsedFile, err := parser.ParseFile(fset, filename, source, parser.ParseComments) | ||
if err != nil { | ||
return fmt.Errorf("parsing file %q: %v", filename, err) | ||
} | ||
pkg[filename] = parsedFile | ||
return nil | ||
} | ||
|
||
// process arg | ||
if arg == "-" { | ||
// Read from stdin and process | ||
data, err := ioutil.ReadAll(os.Stdin) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("reading from stdin: %w", err) | ||
} | ||
if err := processFile(data, "stdin.gnoffee"); err != nil { | ||
return nil, nil, err | ||
} | ||
} else { | ||
// If it's a directory, gather all .go and .gnoffee files and process accordingly | ||
if info, err := os.Stat(arg); err == nil && info.IsDir() { | ||
err := filepath.Walk(arg, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
ext := filepath.Ext(path) | ||
if ext == ".gnoffee" { | ||
data, err := ioutil.ReadFile(path) | ||
if err != nil { | ||
return fmt.Errorf("reading file %q: %v", path, err) | ||
} | ||
if err := processFile(data, path); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
} else { | ||
data, err := ioutil.ReadFile(arg) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("reading file %q: %w", arg, err) | ||
} | ||
if err := processFile(data, arg); err != nil { | ||
return nil, nil, err | ||
} | ||
} | ||
} | ||
return fset, pkg, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package main | ||
|
||
import ( | ||
"os/exec" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/jaekwon/testify/require" | ||
"github.com/rogpeppe/go-internal/testscript" | ||
) | ||
|
||
func TestTest(t *testing.T) { | ||
testscript.Run(t, setupTestScript(t, "testdata")) | ||
} | ||
|
||
func setupTestScript(t *testing.T, txtarDir string) testscript.Params { | ||
t.Helper() | ||
// Get root location of github.com/gnolang/gno | ||
goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() | ||
require.NoError(t, err) | ||
rootDir := filepath.Dir(string(goModPath)) | ||
// Build a fresh gno binary in a temp directory | ||
gnoffeeBin := filepath.Join(t.TempDir(), "gnoffee") | ||
err = exec.Command("go", "build", "-o", gnoffeeBin, filepath.Join(rootDir, "gnovm", "cmd", "gnoffee")).Run() | ||
require.NoError(t, err) | ||
// Define script params | ||
return testscript.Params{ | ||
Setup: func(env *testscript.Env) error { | ||
return nil | ||
}, | ||
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ | ||
// add a custom "gnoffee" command so txtar files can easily execute "gno" | ||
// without knowing where is the binary or how it is executed. | ||
"gnoffee": func(ts *testscript.TestScript, neg bool, args []string) { | ||
err := ts.Exec(gnoffeeBin, args...) | ||
if err != nil { | ||
ts.Logf("[%v]\n", err) | ||
if !neg { | ||
ts.Fatalf("unexpected gnoffee command failure") | ||
} | ||
} else { | ||
if neg { | ||
ts.Fatalf("unexpected gnoffee command success") | ||
} | ||
} | ||
}, | ||
}, | ||
Dir: txtarDir, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Test with a valid sample.gnoffee | ||
|
||
gnoffee -w . | ||
|
||
! stderr .+ | ||
! stdout .+ | ||
|
||
cmp gen.golden gnoffee.gen.go | ||
|
||
-- sample.gnoffee -- | ||
package sample | ||
|
||
type foo struct{} | ||
|
||
export baz as Bar | ||
|
||
var baz = foo{} | ||
|
||
func (f *foo) Hello() string { | ||
return "Hello from foo!" | ||
} | ||
|
||
func (f *foo) Bye() { | ||
println("Goodbye from foo!") | ||
} | ||
|
||
type Bar interface { | ||
Hello() string | ||
Bye() | ||
} | ||
|
||
-- gen.golden -- | ||
package sample | ||
|
||
// This function was generated by gnoffee due to the export directive. | ||
func Hello() string { | ||
return baz.Hello() | ||
} | ||
|
||
// This function was generated by gnoffee due to the export directive. | ||
func Bye() { | ||
baz.Bye() | ||
} | ||
|
||
type foo struct{} | ||
|
||
var baz = foo{} | ||
|
||
func (f *foo) Hello() string { | ||
return "Hello from foo!" | ||
} | ||
|
||
func (f *foo) Bye() { | ||
println("Goodbye from foo!") | ||
} | ||
|
||
type Bar interface { | ||
Hello() string | ||
Bye() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Package gnoffee provides a transpiler that extends the Go language | ||
// with additional, custom keywords. These keywords offer enhanced | ||
// functionality, aiming to make Go programming even more efficient | ||
// and expressive. | ||
// | ||
// Current supported keywords and transformations: | ||
// - `export <structName> as <interfaceName>`: | ||
// This allows for the automatic generation of top-level functions | ||
// in the package that call methods on a specific instance of the struct. | ||
// It's a way to "expose" or "proxy" methods of a struct via free functions. | ||
// | ||
// How Gnoffee Works: | ||
// Gnoffee operates in multiple stages. The first stage transforms | ||
// gnoffee-specific keywords into their comment directive equivalents, | ||
// paving the way for the second stage to handle the transpiling logic. | ||
// | ||
// The Package Path: | ||
// Gnoffee is currently housed under the gnovm namespace, with the | ||
// package path being: github.com/gnolang/gno/gnovm/pkg/gnoffee. | ||
// | ||
// However, it's important to note that while gnoffee resides in the gnovm | ||
// namespace, it operates independently from the gnovm. There's potential | ||
// for gnoffee to be relocated in the future based on its evolving role | ||
// and development trajectory. | ||
// | ||
// Future Changes: | ||
// As the Go and Gno ecosystems and requirements evolve, gnoffee might see the | ||
// introduction of new keywords or alterations to its current functionality. | ||
// Always refer to the package documentation for the most up-to-date details. | ||
package gnoffee |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package gnoffee | ||
|
||
import ( | ||
"bytes" | ||
"go/ast" | ||
"go/format" | ||
"go/parser" | ||
"go/token" | ||
"testing" | ||
) | ||
|
||
func TestPackage(t *testing.T) { | ||
inputCode := ` | ||
package sample | ||
export foo as Bar | ||
type foo struct{} | ||
func (f *foo) Hello() string { | ||
return "Hello from foo!" | ||
} | ||
func (f *foo) Bye() { | ||
println("Goodbye from foo!") | ||
} | ||
type Bar interface { | ||
Hello() string | ||
Bye() | ||
} | ||
` | ||
expectedOutput := ` | ||
package sample | ||
// This function was generated by gnoffee due to the export directive. | ||
func Hello() string { | ||
return foo.Hello() | ||
} | ||
// This function was generated by gnoffee due to the export directive. | ||
func Bye() { | ||
foo.Bye() | ||
} | ||
` | ||
|
||
// Stage 1 | ||
inputCode = Stage1(inputCode) | ||
|
||
// Stage 2 | ||
fset := token.NewFileSet() | ||
file, err := parser.ParseFile(fset, "sample.go", inputCode, parser.ParseComments) | ||
if err != nil { | ||
t.Fatalf("Failed to parse input: %v", err) | ||
} | ||
|
||
files := map[string]*ast.File{ | ||
"sample.go": file, | ||
} | ||
|
||
generatedFile, err := Stage2(files) | ||
if err != nil { | ||
t.Fatalf("Error during Stage2 generation: %v", err) | ||
} | ||
|
||
var buf bytes.Buffer | ||
if err := format.Node(&buf, fset, generatedFile); err != nil { | ||
t.Fatalf("Failed to format generated output: %v", err) | ||
} | ||
|
||
generatedCode := normalizeGoCode(buf.String()) | ||
expected := normalizeGoCode(expectedOutput) | ||
if generatedCode != expected { | ||
t.Errorf("Generated code does not match expected output.\nExpected:\n\n%v\n\nGot:\n\n%v", expected, generatedCode) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package gnoffee | ||
|
||
import ( | ||
"regexp" | ||
) | ||
|
||
// Stage1 converts the gnoffee-specific keywords into their comment directive equivalents. | ||
func Stage1(src string) string { | ||
// Handling the 'export' keyword | ||
exportRegex := regexp.MustCompile(`(?m)^export\s+`) | ||
src = exportRegex.ReplaceAllString(src, "//gnoffee:export ") | ||
|
||
// Handling the 'invar' keyword | ||
invarRegex := regexp.MustCompile(`(?m)^invar\s+([\w\d_]+)\s+(.+)`) | ||
src = invarRegex.ReplaceAllString(src, "//gnoffee:invar $1\nvar $1 $2") | ||
|
||
return src | ||
} |
Oops, something went wrong.