diff --git a/asm/compiler.go b/asm/compiler.go index a44960b..3624300 100644 --- a/asm/compiler.go +++ b/asm/compiler.go @@ -126,7 +126,12 @@ func (c *Compiler) errorAt(inst ast.Statement, err error) { if err == nil { panic("BUG: errorAt(st, nil)") } - c.errors.add(&astError{inst: inst, err: err}) + c.errors.add(&statementError{inst: inst, err: err}) +} + +// warnf pushes a warning to the error list. +func (c *Compiler) warnf(inst ast.Statement, format string, args ...any) { + c.errors.add(&simpleWarning{pos: inst.Position(), str: fmt.Sprintf(format, args...)}) } func (c *Compiler) compileSource(filename string, input []byte) []byte { @@ -181,6 +186,15 @@ func (c *Compiler) compileDocument(doc *ast.Document) (output []byte) { break } + if c.errors.hasError() { + return nil // no output if source has errors + } + + // Run analysis. Note this is also disabled if there are errors because there could + // be lots of useless warnings otherwise. + c.checkLabelsUsed(doc, e) + + // Create the bytecode. return c.generateOutput(prog) } @@ -274,11 +288,6 @@ func (c *Compiler) parseIncludeFile(file string, inst *ast.IncludeSt, depth int) // generateOutput creates the bytecode. This is also where instruction names get resolved. func (c *Compiler) generateOutput(prog *compilerProg) []byte { - if c.errors.hasError() { - // Refuse to output if source had errors. - return nil - } - var output []byte for _, inst := range prog.iterInstructions() { if len(output) != inst.pc { diff --git a/asm/compiler_analysis.go b/asm/compiler_analysis.go new file mode 100644 index 0000000..89be267 --- /dev/null +++ b/asm/compiler_analysis.go @@ -0,0 +1,42 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package asm + +import ( + "github.com/fjl/geas/internal/ast" +) + +// checkLabelsUsed warns about label definitions that were not hit by the evaluator. +func (c *Compiler) checkLabelsUsed(doc *ast.Document, e *evaluator) { + stack := []*ast.Document{doc} + for len(stack) > 0 { + top := stack[len(stack)-1] + for _, st := range top.Statements { + switch st := st.(type) { + case *ast.LabelDefSt: + if !e.isLabelUsed(st) { + c.warnf(st, "label %s unused in program", st) + } + case *ast.IncludeSt: + if incdoc := c.includes[st]; incdoc != nil { + stack = append(stack, incdoc) + } + } + } + stack = stack[1:] + } +} diff --git a/asm/error.go b/asm/error.go index d0cb85f..e94de5a 100644 --- a/asm/error.go +++ b/asm/error.go @@ -114,24 +114,38 @@ func (e compilerError) Error() string { } } -// astError is an error related to an assembler instruction. -type astError struct { +// statementError is an error related to an assembler instruction. +type statementError struct { inst ast.Statement err error } -func (e *astError) Position() ast.Position { +func (e *statementError) Position() ast.Position { return e.inst.Position() } -func (e *astError) Unwrap() error { +func (e *statementError) Unwrap() error { return e.err } -func (e *astError) Error() string { +func (e *statementError) Error() string { return fmt.Sprintf("%v: %s", e.inst.Position(), e.err.Error()) } +// simpleWarning is a warning issued by the compiler. +type simpleWarning struct { + pos ast.Position + str string +} + +func (e *simpleWarning) Error() string { + return fmt.Sprintf("%v: warning: %s", e.pos, e.str) +} + +func (e *simpleWarning) IsWarning() bool { + return true +} + // unassignedLabelError signals use of a label that doesn't have a valid PC. type unassignedLabelError struct { lref *ast.LabelRefExpr diff --git a/asm/evaluator.go b/asm/evaluator.go index ed7ea0c..2cb491e 100644 --- a/asm/evaluator.go +++ b/asm/evaluator.go @@ -28,9 +28,10 @@ import ( // evaluator is for evaluating expressions. type evaluator struct { - inStack map[*ast.ExpressionMacroDef]struct{} - labelPC map[evalLabelKey]int - globals *globalScope + inStack map[*ast.ExpressionMacroDef]struct{} + labelPC map[evalLabelKey]int + usedLabels map[*ast.LabelDefSt]struct{} + globals *globalScope } type evalLabelKey struct { @@ -46,9 +47,10 @@ type evalEnvironment struct { func newEvaluator(gs *globalScope) *evaluator { return &evaluator{ - inStack: make(map[*ast.ExpressionMacroDef]struct{}), - labelPC: make(map[evalLabelKey]int), - globals: gs, + inStack: make(map[*ast.ExpressionMacroDef]struct{}), + labelPC: make(map[evalLabelKey]int), + usedLabels: make(map[*ast.LabelDefSt]struct{}), + globals: gs, } } @@ -95,9 +97,17 @@ func (e *evaluator) lookupLabel(doc *ast.Document, lref *ast.LabelRefExpr) (pc i if li.Dotted != lref.Dotted { return 0, false, fmt.Errorf("undefined label %v (but %v exists)", lref, li) } + // mark label used (for unused label analysis) + e.usedLabels[li] = struct{}{} return pc, pcValid, nil } +// isLabelUsed reports whether the given label definition was used during expression evaluation. +func (e *evaluator) isLabelUsed(li *ast.LabelDefSt) bool { + _, ok := e.usedLabels[li] + return ok +} + func (e *evaluator) eval(expr ast.Expr, env *evalEnvironment) (*big.Int, error) { switch expr := expr.(type) { case *ast.LiteralExpr: diff --git a/asm/global.go b/asm/global.go index 2ff765b..9e07eb7 100644 --- a/asm/global.go +++ b/asm/global.go @@ -78,7 +78,7 @@ func (gs *globalScope) registerLabel(def *ast.LabelDefSt, doc *ast.Document) { func (gs *globalScope) registerInstrMacro(name string, def globalDef[*ast.InstructionMacroDef]) error { firstDef, found := gs.instrMacro[name] if found { - return &astError{ + return &statementError{ inst: def.def, err: fmt.Errorf("macro %%%s already defined%s", name, firstDef.doc.CreationString()), } @@ -91,7 +91,7 @@ func (gs *globalScope) registerInstrMacro(name string, def globalDef[*ast.Instru func (gs *globalScope) registerExprMacro(name string, def globalDef[*ast.ExpressionMacroDef]) error { firstDef, found := gs.exprMacro[name] if found { - return &astError{ + return &statementError{ inst: def.def, err: fmt.Errorf("macro %s already defined%s", name, firstDef.doc.CreationString()), } diff --git a/asm/testdata/compiler-tests.yaml b/asm/testdata/compiler-tests.yaml index a5eb4be..7a2d901 100644 --- a/asm/testdata/compiler-tests.yaml +++ b/asm/testdata/compiler-tests.yaml @@ -499,6 +499,8 @@ instr-macro-inner-label: theLabel: stop output: bytecode: "6005 56 6002 5b 6001 5b 00" + warnings: + - ':9: warning: label @theLabel unused in program' instr-macro-outer-label: input: @@ -686,34 +688,37 @@ macro-call: input: code: | #define myMacro(a, b) = (100 + $a) / $b - start: PUSH myMacro(4, 2) + ADD + PUSH myMacro(4, 2) output: - bytecode: "5b 6034" + bytecode: "01 6034" macro-ref: input: code: | #define myMacro = 100 - start: + ADD PUSH myMacro output: - bytecode: "5b 60 64" + bytecode: "01 60 64" macro-ref-call-empty: input: code: | #define myMacro() = 100 - start: PUSH myMacro + ADD + PUSH myMacro output: - bytecode: "5b 6064" + bytecode: "01 6064" macro-ref-call-empty-2: input: code: | #define myMacro = 100 - start: PUSH myMacro() + ADD + PUSH myMacro() output: - bytecode: 5b 6064 + bytecode: 01 6064 push-expression: input: @@ -822,3 +827,35 @@ pragma-unknown: output: errors: - ':1: unknown #pragma something' + +unused-label-warning: + input: + code: | + top: + push 1 + push 2 + add + jump @top + end: + output: + bytecode: "5b60016002016000565b" + warnings: + - ':6: warning: label @end unused in program' + +unused-label-in-include: + input: + code: | + top: + push 1 + push 2 + add + #include "label.evm" + jump @top + files: + label.evm: | + push 3 + label: + output: + bytecode: "5b600160020160035b600056" + warnings: + - 'label.evm:2: warning: label @label unused in program'