diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index fc4b49893..f1d84e211 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -211,6 +211,7 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) { f = c.newFunc(decl) } + f.rng.Start = uint16(c.prog.Len()) c.scope = f ast.Inspect(decl, c.scope.analyzeVoidCalls) // @OPTIMIZE @@ -260,6 +261,8 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) { emit.Opcode(c.prog.BinWriter, opcode.DROP) emit.Opcode(c.prog.BinWriter, opcode.RET) } + + f.rng.End = uint16(c.prog.Len() - 1) } func (c *codegen) Visit(node ast.Node) ast.Visitor { diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go new file mode 100644 index 000000000..178b47e3f --- /dev/null +++ b/pkg/compiler/debug.go @@ -0,0 +1,242 @@ +package compiler + +import ( + "encoding/json" + "errors" + "fmt" + "go/ast" + "go/types" + "strconv" + "strings" +) + +// DebugInfo represents smart-contract debug information. +type DebugInfo struct { + EntryPoint string `json:"entrypoint"` + Documents []string `json:"documents"` + Methods []MethodDebugInfo `json:"methods"` + Events []EventDebugInfo `json:"events"` +} + +// MethodDebugInfo represents smart-contract's method debug information. +type MethodDebugInfo struct { + ID string `json:"id"` + // Name is the name of the method together with the namespace it belongs to. + Name DebugMethodName `json:"name"` + // Range is the range of smart-contract's opcodes corresponding to the method. + Range DebugRange `json:"range"` + // Parameters is a list of method's parameters. + Parameters []DebugParam `json:"params"` + // ReturnType is method's return type. + ReturnType string `json:"return-type"` + Variables []string `json:"variables"` + // SeqPoints is a map between source lines and byte-code instruction offsets. + SeqPoints []DebugSeqPoint `json:"sequence-points"` +} + +// DebugMethodName is a combination of a namespace and name. +type DebugMethodName struct { + Namespace string + Name string +} + +// EventDebugInfo represents smart-contract's event debug information. +type EventDebugInfo struct { + ID string `json:"id"` + // Name is a human-readable event name in a format "{namespace}-{name}". + Name string `json:"name"` + Parameters []DebugParam `json:"parameters"` +} + +// DebugSeqPoint represents break-point for debugger. +type DebugSeqPoint struct { + // Opcode is an opcode's address. + Opcode int + // Document is an index of file where sequence point occurs. + Document int + // StartLine is the first line of the break-pointed statement. + StartLine int + // StartCol is the first column of the break-pointed statement. + StartCol int + // EndLine is the last line of the break-pointed statement. + EndLine int + // EndCol is the last column of the break-pointed statement. + EndCol int +} + +// DebugRange represents method's section in bytecode. +type DebugRange struct { + Start uint16 + End uint16 +} + +// DebugParam represents variables's name and type. +type DebugParam struct { + Name string + Type string +} + +func (c *codegen) emitDebugInfo() *DebugInfo { + d := &DebugInfo{ + EntryPoint: mainIdent, + Events: []EventDebugInfo{}, + } + for name, scope := range c.funcs { + m := c.methodInfoFromScope(name, scope) + if m.Range.Start == m.Range.End { + continue + } + d.Methods = append(d.Methods, *m) + } + return d +} + +func (c *codegen) methodInfoFromScope(name string, scope *funcScope) *MethodDebugInfo { + ps := scope.decl.Type.Params + params := make([]DebugParam, 0, ps.NumFields()) + for i := range params { + for j := range ps.List[i].Names { + params = append(params, DebugParam{ + Name: ps.List[i].Names[j].Name, + Type: c.scTypeFromExpr(ps.List[i].Type), + }) + } + } + return &MethodDebugInfo{ + ID: name, + Name: DebugMethodName{Name: name}, + Range: scope.rng, + Parameters: params, + ReturnType: c.scReturnTypeFromScope(scope), + } +} + +func (c *codegen) scReturnTypeFromScope(scope *funcScope) string { + results := scope.decl.Type.Results + switch results.NumFields() { + case 0: + return "Void" + case 1: + return c.scTypeFromExpr(results.List[0].Type) + default: + // multiple return values are not supported in debugger + return "Any" + } +} + +func (c *codegen) scTypeFromExpr(typ ast.Expr) string { + switch t := c.typeInfo.Types[typ].Type.(type) { + case *types.Basic: + info := t.Info() + switch { + case info&types.IsInteger != 0: + return "Integer" + case info&types.IsBoolean != 0: + return "Boolean" + case info&types.IsString != 0: + return "String" + default: + return "Any" + } + case *types.Map: + return "Map" + case *types.Struct: + return "Struct" + case *types.Slice: + if isByteArrayType(t) { + return "ByteArray" + } + return "Array" + default: + return "Any" + } +} + +// MarshalJSON implements json.Marshaler interface. +func (d *DebugRange) MarshalJSON() ([]byte, error) { + return []byte(`"` + strconv.FormatUint(uint64(d.Start), 10) + `-` + + strconv.FormatUint(uint64(d.End), 10) + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (d *DebugRange) UnmarshalJSON(data []byte) error { + startS, endS, err := parsePairJSON(data, "-") + if err != nil { + return err + } + start, err := strconv.ParseUint(startS, 10, 16) + if err != nil { + return err + } + end, err := strconv.ParseUint(endS, 10, 16) + if err != nil { + return err + } + + d.Start = uint16(start) + d.End = uint16(end) + + return nil +} + +// MarshalJSON implements json.Marshaler interface. +func (d *DebugParam) MarshalJSON() ([]byte, error) { + return []byte(`"` + d.Name + `,` + d.Type + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (d *DebugParam) UnmarshalJSON(data []byte) error { + startS, endS, err := parsePairJSON(data, ",") + if err != nil { + return err + } + + d.Name = startS + d.Type = endS + + return nil +} + +// MarshalJSON implements json.Marshaler interface. +func (d *DebugMethodName) MarshalJSON() ([]byte, error) { + return []byte(`"` + d.Namespace + `,` + d.Name + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (d *DebugMethodName) UnmarshalJSON(data []byte) error { + startS, endS, err := parsePairJSON(data, ",") + if err != nil { + return err + } + + d.Namespace = startS + d.Name = endS + + return nil +} + +// MarshalJSON implements json.Marshaler interface. +func (d *DebugSeqPoint) MarshalJSON() ([]byte, error) { + s := fmt.Sprintf("%d[%d]%d:%d-%d:%d", d.Opcode, d.Document, + d.StartLine, d.StartCol, d.EndLine, d.EndCol) + return []byte(`"` + s + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (d *DebugSeqPoint) UnmarshalJSON(data []byte) error { + _, err := fmt.Sscanf(string(data), `"%d[%d]%d:%d-%d:%d"`, + &d.Opcode, &d.Document, &d.StartLine, &d.StartCol, &d.EndLine, &d.EndCol) + return err +} + +func parsePairJSON(data []byte, sep string) (string, string, error) { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return "", "", err + } + ss := strings.SplitN(s, sep, 2) + if len(ss) != 2 { + return "", "", errors.New("invalid range format") + } + return ss[0], ss[1], nil +} diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go new file mode 100644 index 000000000..f98493077 --- /dev/null +++ b/pkg/compiler/debug_test.go @@ -0,0 +1,43 @@ +package compiler + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" +) + +func TestDebugInfo_MarshalJSON(t *testing.T) { + d := &DebugInfo{ + EntryPoint: "main", + Documents: []string{"/path/to/file"}, + Methods: []MethodDebugInfo{ + { + ID: "id1", + Name: DebugMethodName{ + Namespace: "default", + Name: "method1", + }, + Range: DebugRange{Start: 10, End: 20}, + Parameters: []DebugParam{ + {"param1", "Integer"}, + {"ok", "Boolean"}, + }, + ReturnType: "ByteArray", + Variables: []string{}, + SeqPoints: []DebugSeqPoint{ + { + Opcode: 123, + Document: 1, + StartLine: 2, + StartCol: 3, + EndLine: 4, + EndCol: 5, + }, + }, + }, + }, + Events: []EventDebugInfo{}, + } + + testserdes.MarshalUnmarshalJSON(t, d, new(DebugInfo)) +} diff --git a/pkg/compiler/func_scope.go b/pkg/compiler/func_scope.go index f0af3d231..a43a0ec20 100644 --- a/pkg/compiler/func_scope.go +++ b/pkg/compiler/func_scope.go @@ -21,6 +21,9 @@ type funcScope struct { // Program label of the scope label uint16 + // Range of opcodes corresponding to the function. + rng DebugRange + // Local variables locals map[string]int