Skip to content

Commit 1c96d70

Browse files
committed
Synchronize and finish plugging gas metering in
1 parent 62e8849 commit 1c96d70

File tree

3 files changed

+193
-234
lines changed

3 files changed

+193
-234
lines changed

gnovm/pkg/gnolang/gotypecheck.go

+98-38
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"github.com/gnolang/gno/gnovm"
15+
storetypes "github.com/gnolang/gno/tm2/pkg/store/types"
1516
"go.uber.org/multierr"
1617
)
1718

@@ -23,26 +24,40 @@ type MemPackageGetter interface {
2324
GetMemPackage(path string) *gnovm.MemPackage
2425
}
2526

27+
const DEFAULT_MAX_GAS_UGNOT = 1_000_000 // 1Gnot aka 1e6 ugnots
28+
2629
// TypeCheckMemPackage performs type validation and checking on the given
2730
// mempkg. To retrieve dependencies, it uses getter.
2831
//
2932
// The syntax checking is performed entirely using Go's go/types package.
3033
//
3134
// If format is true, the code will be automatically updated with the
3235
// formatted source code.
36+
//
37+
// By default is uses a gas meter of `DEFAULT_MAX_GAS_UGNOT`.
3338
func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool) error {
34-
return typeCheckMemPackage(mempkg, getter, false, format)
39+
return typeCheckMemPackage(mempkg, getter, false, format, newDefaultGasMeter())
40+
}
41+
42+
// TypeCheckMemPackageWithGasMeter is like TypeCheckMemPackage, except
43+
// that it allows passing in the gas meter to use.
44+
func TypeCheckMemPackageWithGasMeter(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool, gasMeter storetypes.GasMeter) error {
45+
return typeCheckMemPackage(mempkg, getter, false, format, gasMeter)
3546
}
3647

3748
// TypeCheckMemPackageTest performs the same type checks as [TypeCheckMemPackage],
3849
// but allows re-declarations.
3950
//
4051
// Note: like TypeCheckMemPackage, this function ignores tests and filetests.
4152
func TypeCheckMemPackageTest(mempkg *gnovm.MemPackage, getter MemPackageGetter) error {
42-
return typeCheckMemPackage(mempkg, getter, true, false)
53+
return typeCheckMemPackage(mempkg, getter, true, false, newDefaultGasMeter())
4354
}
4455

45-
func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool) error {
56+
func newDefaultGasMeter() storetypes.GasMeter {
57+
return storetypes.NewGasMeter(DEFAULT_MAX_GAS_UGNOT)
58+
}
59+
60+
func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool, gasMeter storetypes.GasMeter) error {
4661
var errs error
4762
imp := &gnoImporter{
4863
getter: getter,
@@ -53,6 +68,7 @@ func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, test
5368
},
5469
},
5570
allowRedefinitions: testing,
71+
gasMeter: gasMeter,
5672
}
5773
imp.cfg.Importer = imp
5874

@@ -77,6 +93,7 @@ type gnoImporter struct {
7793

7894
// allow symbol redefinitions? (test standard libraries)
7995
allowRedefinitions bool
96+
gasMeter storetypes.GasMeter
8097
}
8198

8299
// Unused, but satisfies the Importer interface.
@@ -136,7 +153,7 @@ func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*t
136153
continue
137154
}
138155

139-
// m.chargeGasForTypecheck(f)
156+
chargeGasForTypecheck(g.gasMeter, f)
140157

141158
if delFunc != nil {
142159
deleteOldIdents(delFunc, f)
@@ -162,64 +179,107 @@ func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*t
162179
return g.cfg.Check(mpkg.Path, fset, files, nil)
163180
}
164181

165-
func (m *Machine) chargeGasForTypecheck(f *ast.File) {
166-
ast.Walk(&astTraversingGasCharger{m}, f)
182+
func deleteOldIdents(idents map[string]func(), f *ast.File) {
183+
for _, decl := range f.Decls {
184+
fd, ok := decl.(*ast.FuncDecl)
185+
if !ok || fd.Recv != nil { // ignore methods
186+
continue
187+
}
188+
if del := idents[fd.Name.Name]; del != nil {
189+
del()
190+
}
191+
decl := decl
192+
idents[fd.Name.Name] = func() {
193+
// NOTE: cannot use the index as a file may contain multiple decls to be removed,
194+
// so removing one would make all "later" indexes wrong.
195+
f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d })
196+
}
197+
}
198+
}
199+
200+
func chargeGasForTypecheck(gasMeter storetypes.GasMeter, f *ast.File) {
201+
ast.Walk(&astTraversingGasCharger{gasMeter}, f)
167202
}
168203

169204
// astTraversingGasCharger is an ast.Visitor helper that statically traverses an AST
170205
// charging gas for the respective typechecking operations so as to bear a cost
171206
// and not let typechecking be abused
172207
type astTraversingGasCharger struct {
173-
m *Machine
208+
m storetypes.GasMeter
174209
}
175210

176211
var _ ast.Visitor = (*astTraversingGasCharger)(nil)
177212

213+
func (atgc *astTraversingGasCharger) consumeGas(amount storetypes.Gas) {
214+
atgc.m.ConsumeGas(amount, "typeCheck")
215+
}
216+
217+
const _BASIC_TYPECHECK_GAS_CHARGE = 5 // Arbitrary value.
218+
178219
func (wmv *astTraversingGasCharger) Visit(n ast.Node) ast.Visitor {
179-
switch n := n.(type) {
220+
switch n.(type) {
180221
case *ast.ImportSpec:
222+
// No need to charge gas for imports.
181223
return nil
182224

225+
case *ast.UnaryExpr:
226+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 2)
227+
183228
case *ast.BinaryExpr:
184-
// TODO: Charge more gas for the operands that aren't constant.
185-
opCode := word2UnaryOp(Word(n.Op))
186-
wmv.m.incrCPU(opCode.cpuCycles())
187-
wmv.m.incrCPU(OpCPUBinary1)
188-
return wmv
229+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
189230

190-
case *ast.UnaryExpr:
191-
opCode := word2BinaryOp(Word(n.Op))
192-
wmv.m.incrCPU(opCode.cpuCycles())
193-
// TODO: Charge gas for the expression itself too?
231+
case *ast.BasicLit:
232+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 2)
233+
234+
case *ast.CompositeLit:
235+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
194236

195237
case *ast.CallExpr:
196-
wmv.m.incrCPU(OpCall.cpuCycles())
238+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 4)
197239

198240
case *ast.ForStmt:
199-
wmv.m.incrCPU(OpCPUForLoop)
241+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 5)
200242

201243
case *ast.RangeStmt:
202-
wmv.m.incrCPU(OpRangeIter)
244+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 6)
203245
// TODO: Alternate on the different type of range statements.
204-
}
205246

206-
return wmv
207-
}
247+
case *ast.FuncDecl:
248+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 6)
208249

209-
func deleteOldIdents(idents map[string]func(), f *ast.File) {
210-
for _, decl := range f.Decls {
211-
fd, ok := decl.(*ast.FuncDecl)
212-
if !ok || fd.Recv != nil { // ignore methods
213-
continue
214-
}
215-
if del := idents[fd.Name.Name]; del != nil {
216-
del()
217-
}
218-
decl := decl
219-
idents[fd.Name.Name] = func() {
220-
// NOTE: cannot use the index as a file may contain multiple decls to be removed,
221-
// so removing one would make all "later" indexes wrong.
222-
f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d })
223-
}
250+
case *ast.SwitchStmt:
251+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 4)
252+
253+
case *ast.IfStmt:
254+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 5)
255+
256+
case *ast.CaseClause:
257+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
258+
259+
case *ast.BranchStmt:
260+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
261+
262+
case *ast.AssignStmt:
263+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 2)
264+
265+
case *ast.Ident:
266+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 1)
267+
268+
case *ast.SelectorExpr:
269+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 5)
270+
271+
case *ast.ParenExpr:
272+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
273+
274+
case *ast.ReturnStmt, *ast.DeferStmt:
275+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 2)
276+
277+
case nil:
278+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE / 2)
279+
280+
default:
281+
wmv.consumeGas(_BASIC_TYPECHECK_GAS_CHARGE * 3)
224282
}
283+
284+
return wmv
225285
}

gnovm/pkg/gnolang/gotypecheck_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package gnolang
22

33
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"runtime"
48
"testing"
59

610
"github.com/gnolang/gno/gnovm"
11+
storetypes "github.com/gnolang/gno/tm2/pkg/store/types"
12+
713
"github.com/stretchr/testify/assert"
814
"github.com/stretchr/testify/require"
915
"go.uber.org/multierr"
@@ -357,3 +363,92 @@ func Hello(name string) string {
357363
assert.NotEqual(t, input, pkg.Files[0].Body)
358364
assert.Equal(t, expected, pkg.Files[0].Body)
359365
}
366+
367+
func TestTypecheckMemPackageGasMetering(t *testing.T) {
368+
t.Parallel()
369+
370+
tests := []struct {
371+
name string
372+
src string
373+
wantErr string
374+
}{
375+
{
376+
"basic expression + print",
377+
`
378+
package main
379+
func main() {
380+
println("abcdefgh")
381+
var _ = 1+2+3+4+5+6+7+8+9+10
382+
}`,
383+
"",
384+
},
385+
{
386+
name: "this entire file",
387+
src: func() string {
388+
_, thisFile, _, _ := runtime.Caller(1)
389+
blob, err := os.ReadFile(thisFile)
390+
require.NoError(t, err)
391+
return string(blob)
392+
}(),
393+
wantErr: "could not import", // Not out of gas.
394+
},
395+
{
396+
name: "deeply nested expression",
397+
src: func() string {
398+
buf := new(bytes.Buffer)
399+
N := 1000
400+
buf.WriteString("package main\nfunc main() {\n\tconst x = 1")
401+
for i := 0; i < 3; i++ {
402+
for j := 0; j < N; j++ {
403+
buf.WriteString("+\n(1")
404+
}
405+
for j := 0; j < N; j++ {
406+
buf.WriteByte(')')
407+
}
408+
}
409+
buf.WriteString("\nprintln(x)\n}")
410+
return buf.String()
411+
}(),
412+
wantErr: "out of gas",
413+
},
414+
}
415+
416+
for _, tt := range tests {
417+
t.Run(tt.name, func(t *testing.T) {
418+
pkg := &gnovm.MemPackage{
419+
Name: "hello",
420+
Path: "gno.land/p/demo/hello",
421+
Files: []*gnovm.MemFile{
422+
{
423+
Name: "hello.gno",
424+
Body: tt.src,
425+
},
426+
},
427+
}
428+
429+
mpkgGetter := mockPackageGetter{}
430+
format := false
431+
gasMeter := storetypes.NewGasMeter(100_000)
432+
var err error
433+
434+
defer func() {
435+
r := recover()
436+
if r != nil && err == nil {
437+
err = fmt.Errorf("%v", r)
438+
}
439+
440+
if tt.wantErr != "" {
441+
assert.Error(t, err)
442+
assert.Contains(t, err.Error(), tt.wantErr)
443+
return
444+
}
445+
446+
assert.NoError(t, err)
447+
assert.Equal(t, tt.src, pkg.Files[0].Body) // unchanged
448+
assert.True(t, gasMeter.Remaining() > 0)
449+
}()
450+
451+
err = TypeCheckMemPackageWithGasMeter(pkg, mpkgGetter, format, gasMeter)
452+
})
453+
}
454+
}

0 commit comments

Comments
 (0)