Skip to content

Commit af5b5d3

Browse files
committed
rlp/rlpgen: RLP encoder code generator
This is a new tool to generate EncodeRLP (and DecodeRLP) method implementations from a struct definition.
1 parent 5e7d29b commit af5b5d3

File tree

14 files changed

+1759
-0
lines changed

14 files changed

+1759
-0
lines changed

rlp/rlpgen/gen.go

Lines changed: 735 additions & 0 deletions
Large diffs are not rendered by default.

rlp/rlpgen/gen_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"go/ast"
7+
"go/importer"
8+
"go/parser"
9+
"go/token"
10+
"go/types"
11+
"io/ioutil"
12+
"os"
13+
"path/filepath"
14+
"testing"
15+
)
16+
17+
// Package RLP is loaded only once and reused for all tests.
18+
var (
19+
testFset = token.NewFileSet()
20+
testImporter = importer.ForCompiler(testFset, "source", nil).(types.ImporterFrom)
21+
testPackageRLP *types.Package
22+
)
23+
24+
func init() {
25+
cwd, err := os.Getwd()
26+
if err != nil {
27+
panic(err)
28+
}
29+
testPackageRLP, err = testImporter.ImportFrom(pathOfPackageRLP, cwd, 0)
30+
if err != nil {
31+
panic(fmt.Errorf("can't load package RLP: %v", err))
32+
}
33+
}
34+
35+
var tests = []string{"uints", "nil", "rawvalue", "optional", "bigint"}
36+
37+
func TestOutput(t *testing.T) {
38+
for _, test := range tests {
39+
test := test
40+
t.Run(test, func(t *testing.T) {
41+
inputFile := filepath.Join("testdata", test+".in.txt")
42+
outputFile := filepath.Join("testdata", test+".out.txt")
43+
bctx, typ, err := loadTestSource(inputFile, "Test")
44+
if err != nil {
45+
t.Fatal("error loading test source:", err)
46+
}
47+
output, err := bctx.generate(typ, true, true)
48+
if err != nil {
49+
t.Fatal("error in generate:", err)
50+
}
51+
52+
// Set this environment variable to regenerate the test outputs.
53+
if os.Getenv("WRITE_TEST_FILES") != "" {
54+
ioutil.WriteFile(outputFile, output, 0644)
55+
}
56+
57+
// Check if output matches.
58+
wantOutput, err := ioutil.ReadFile(outputFile)
59+
if err != nil {
60+
t.Fatal("error loading expected test output:", err)
61+
}
62+
if !bytes.Equal(output, wantOutput) {
63+
t.Fatal("output mismatch:\n", string(output))
64+
}
65+
})
66+
}
67+
}
68+
69+
func loadTestSource(file string, typeName string) (*buildContext, *types.Named, error) {
70+
// Load the test input.
71+
content, err := ioutil.ReadFile(file)
72+
if err != nil {
73+
return nil, nil, err
74+
}
75+
f, err := parser.ParseFile(testFset, file, content, 0)
76+
if err != nil {
77+
return nil, nil, err
78+
}
79+
conf := types.Config{Importer: testImporter}
80+
pkg, err := conf.Check("test", testFset, []*ast.File{f}, nil)
81+
if err != nil {
82+
return nil, nil, err
83+
}
84+
85+
// Find the test struct.
86+
bctx := newBuildContext(testPackageRLP)
87+
typ, err := lookupStructType(pkg.Scope(), typeName)
88+
if err != nil {
89+
return nil, nil, fmt.Errorf("can't find type %s: %v", typeName, err)
90+
}
91+
return bctx, typ, nil
92+
}

rlp/rlpgen/main.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2021 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"bytes"
21+
"errors"
22+
"flag"
23+
"fmt"
24+
"go/types"
25+
"io/ioutil"
26+
"os"
27+
28+
"golang.org/x/tools/go/packages"
29+
)
30+
31+
const pathOfPackageRLP = "github.com/ethereum/go-ethereum/rlp"
32+
33+
func main() {
34+
var (
35+
pkgdir = flag.String("dir", ".", "input package")
36+
output = flag.String("out", "-", "output file (default is stdout)")
37+
genEncoder = flag.Bool("encoder", true, "generate EncodeRLP?")
38+
genDecoder = flag.Bool("decoder", false, "generate DecodeRLP?")
39+
typename = flag.String("type", "", "type to generate methods for")
40+
)
41+
flag.Parse()
42+
43+
cfg := Config{
44+
Dir: *pkgdir,
45+
Type: *typename,
46+
GenerateEncoder: *genEncoder,
47+
GenerateDecoder: *genDecoder,
48+
}
49+
code, err := cfg.process()
50+
if err != nil {
51+
fatal(err)
52+
}
53+
if *output == "-" {
54+
os.Stdout.Write(code)
55+
} else if err := ioutil.WriteFile(*output, code, 0644); err != nil {
56+
fatal(err)
57+
}
58+
}
59+
60+
func fatal(args ...interface{}) {
61+
fmt.Fprintln(os.Stderr, args...)
62+
os.Exit(1)
63+
}
64+
65+
type Config struct {
66+
Dir string // input package directory
67+
Type string
68+
69+
GenerateEncoder bool
70+
GenerateDecoder bool
71+
}
72+
73+
// process generates the Go code.
74+
func (cfg *Config) process() (code []byte, err error) {
75+
// Load packages.
76+
pcfg := &packages.Config{
77+
Mode: packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
78+
Dir: cfg.Dir,
79+
BuildFlags: []string{"-tags", "norlpgen"},
80+
}
81+
ps, err := packages.Load(pcfg, pathOfPackageRLP, ".")
82+
if err != nil {
83+
return nil, err
84+
}
85+
if len(ps) == 0 {
86+
return nil, fmt.Errorf("no Go package found in %s", cfg.Dir)
87+
}
88+
packages.PrintErrors(ps)
89+
90+
// Find the packages that were loaded.
91+
var (
92+
pkg *types.Package
93+
packageRLP *types.Package
94+
)
95+
for _, p := range ps {
96+
if len(p.Errors) > 0 {
97+
return nil, fmt.Errorf("package %s has errors", p.PkgPath)
98+
}
99+
if p.PkgPath == pathOfPackageRLP {
100+
packageRLP = p.Types
101+
} else {
102+
pkg = p.Types
103+
}
104+
}
105+
bctx := newBuildContext(packageRLP)
106+
107+
// Find the type and generate.
108+
typ, err := lookupStructType(pkg.Scope(), cfg.Type)
109+
if err != nil {
110+
return nil, fmt.Errorf("can't find %s in %s: %v", typ, pkg, err)
111+
}
112+
code, err = bctx.generate(typ, cfg.GenerateEncoder, cfg.GenerateDecoder)
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
// Add build comments.
118+
// This is done here to avoid processing these lines with gofmt.
119+
var header bytes.Buffer
120+
fmt.Fprint(&header, "// Code generated by rlpgen. DO NOT EDIT.\n\n")
121+
fmt.Fprint(&header, "//go:build !norlpgen\n")
122+
fmt.Fprint(&header, "// +build !norlpgen\n\n")
123+
return append(header.Bytes(), code...), nil
124+
}
125+
126+
func lookupStructType(scope *types.Scope, name string) (*types.Named, error) {
127+
typ, err := lookupType(scope, name)
128+
if err != nil {
129+
return nil, err
130+
}
131+
_, ok := typ.Underlying().(*types.Struct)
132+
if !ok {
133+
return nil, errors.New("not a struct type")
134+
}
135+
return typ, nil
136+
}
137+
138+
func lookupType(scope *types.Scope, name string) (*types.Named, error) {
139+
obj := scope.Lookup(name)
140+
if obj == nil {
141+
return nil, errors.New("no such identifier")
142+
}
143+
typ, ok := obj.(*types.TypeName)
144+
if !ok {
145+
return nil, errors.New("not a type")
146+
}
147+
return typ.Type().(*types.Named), nil
148+
}

rlp/rlpgen/testdata/bigint.in.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// -*- mode: go -*-
2+
3+
package test
4+
5+
import "math/big"
6+
7+
type Test struct {
8+
Int *big.Int
9+
IntNoPtr big.Int
10+
}

rlp/rlpgen/testdata/bigint.out.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package test
2+
3+
import "github.com/ethereum/go-ethereum/rlp"
4+
import "io"
5+
6+
func (obj *Test) EncodeRLP(_w io.Writer) error {
7+
w := rlp.NewEncoderBuffer(_w)
8+
_tmp0 := w.List()
9+
if obj.Int == nil {
10+
w.Write(rlp.EmptyString)
11+
} else {
12+
if obj.Int.Sign() == -1 {
13+
return rlp.ErrNegativeBigInt
14+
}
15+
w.WriteBigInt(obj.Int)
16+
}
17+
if obj.IntNoPtr.Sign() == -1 {
18+
return rlp.ErrNegativeBigInt
19+
}
20+
w.WriteBigInt(&obj.IntNoPtr)
21+
w.ListEnd(_tmp0)
22+
return w.Flush()
23+
}
24+
25+
func (obj *Test) DecodeRLP(dec *rlp.Stream) error {
26+
var _tmp0 Test
27+
{
28+
if _, err := dec.List(); err != nil {
29+
return err
30+
}
31+
// Int:
32+
_tmp1, err := dec.BigInt()
33+
if err != nil {
34+
return err
35+
}
36+
_tmp0.Int = _tmp1
37+
// IntNoPtr:
38+
_tmp2, err := dec.BigInt()
39+
if err != nil {
40+
return err
41+
}
42+
_tmp0.IntNoPtr = (*_tmp2)
43+
if err := dec.ListEnd(); err != nil {
44+
return err
45+
}
46+
}
47+
*obj = _tmp0
48+
return nil
49+
}

rlp/rlpgen/testdata/nil.in.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// -*- mode: go -*-
2+
3+
package test
4+
5+
type Aux struct{
6+
A uint32
7+
}
8+
9+
type Test struct{
10+
Uint8 *byte `rlp:"nil"`
11+
Uint8List *byte `rlp:"nilList"`
12+
13+
Uint32 *uint32 `rlp:"nil"`
14+
Uint32List *uint32 `rlp:"nilList"`
15+
16+
Uint64 *uint64 `rlp:"nil"`
17+
Uint64List *uint64 `rlp:"nilList"`
18+
19+
String *string `rlp:"nil"`
20+
StringList *string `rlp:"nilList"`
21+
22+
ByteArray *[3]byte `rlp:"nil"`
23+
ByteArrayList *[3]byte `rlp:"nilList"`
24+
25+
ByteSlice *[]byte `rlp:"nil"`
26+
ByteSliceList *[]byte `rlp:"nilList"`
27+
28+
Struct *Aux `rlp:"nil"`
29+
StructString *Aux `rlp:"nilString"`
30+
}

0 commit comments

Comments
 (0)