From d7ecc72b4144bf869948db47b8fa4c17d07eeee0 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Tue, 25 Feb 2025 20:15:02 -0500 Subject: [PATCH] lang: ast, gapi, interfaces, parser: Print line numbers on error This adds an initial implementation of printing line numbers on type unification errors. It also attempts to print a visual position indicator for most scenarios. This patch was started by Felix Frank and finished by James Shubin. Co-authored-by: Felix Frank --- lang/ast/structs.go | 362 ++++++++++++++---- lang/ast/util.go | 155 ++++++++ lang/gapi/gapi.go | 1 + lang/interfaces/ast.go | 55 +++ lang/interfaces/unification.go | 9 +- lang/interpolate/interpolate.go | 1 + lang/interpolate/interpolate_test.go | 12 +- lang/interpret_test.go | 28 ++ .../polydoubleincludewithtype.txtar | 2 +- .../TestAstFunc2/fortytwoerror.txtar | 2 +- .../TestAstFunc2/printfunificationerr0.txtar | 2 +- lang/interpret_test/TestAstFunc2/res1.txtar | 2 +- .../unify-interpolate-edge1-fail.txtar | 2 +- .../unify-interpolate-edge2-fail.txtar | 2 +- .../unify-interpolate-res-fail.txtar | 2 +- lang/lang.go | 4 + lang/parser/lexparse_test.go | 48 ++- lang/parser/parser.y | 181 +++++---- lang/unification/fastsolver/fastsolver.go | 11 +- lang/unification/solvers/unification_test.go | 5 + 20 files changed, 714 insertions(+), 172 deletions(-) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 97e2e5d4..563f52ad 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -180,6 +180,9 @@ var ( // StmtBind is a representation of an assignment, which binds a variable to an // expression. type StmtBind struct { + Textarea + data *interfaces.Data + Ident string Value interfaces.Expr Type *types.Type @@ -205,6 +208,9 @@ func (obj *StmtBind) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtBind) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Ident == "" { return fmt.Errorf("bind ident is empty") } @@ -221,9 +227,11 @@ func (obj *StmtBind) Interpolate() (interfaces.Stmt, error) { return nil, err } return &StmtBind{ - Ident: obj.Ident, - Value: interpolated, - Type: obj.Type, + Textarea: obj.Textarea, + data: obj.data, + Ident: obj.Ident, + Value: interpolated, + Type: obj.Type, }, nil } @@ -242,9 +250,11 @@ func (obj *StmtBind) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtBind{ - Ident: obj.Ident, - Value: value, - Type: obj.Type, + Textarea: obj.Textarea, + data: obj.data, + Ident: obj.Ident, + Value: value, + Type: obj.Type, }, nil } @@ -319,6 +329,7 @@ func (obj *StmtBind) TypeCheck() ([]*interfaces.UnificationInvariant, error) { } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Value, Expect: typExpr, // obj.Type Actual: typ, @@ -359,6 +370,7 @@ func (obj *StmtBind) Output(map[interfaces.Func]types.Value) (*interfaces.Output // TODO: Consider expanding Name to have this return a list of Res's in the // Output function if it is a map[name]struct{}, or even a map[[]name]struct{}. type StmtRes struct { + Textarea data *interfaces.Data Kind string // kind of resource, eg: pkg, file, svc, etc... @@ -393,6 +405,9 @@ func (obj *StmtRes) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtRes) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Kind == "" { return fmt.Errorf("res kind is empty") } @@ -401,7 +416,6 @@ func (obj *StmtRes) Init(data *interfaces.Data) error { return fmt.Errorf("kind must not contain underscores") } - obj.data = data if err := obj.Name.Init(data); err != nil { return err } @@ -457,6 +471,7 @@ func (obj *StmtRes) Interpolate() (interfaces.Stmt, error) { } return &StmtRes{ + Textarea: obj.Textarea, data: obj.data, Kind: obj.Kind, Name: name, @@ -497,6 +512,7 @@ func (obj *StmtRes) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtRes{ + Textarea: obj.Textarea, data: obj.data, Kind: obj.Kind, Name: name, @@ -628,6 +644,7 @@ func (obj *StmtRes) TypeCheck() ([]*interfaces.UnificationInvariant, error) { } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Name, Expect: typExpr, // the name Actual: typ, @@ -1201,6 +1218,9 @@ type StmtResContents interface { // StmtResField represents a single field in the parsed resource representation. // This does not satisfy the Stmt interface. type StmtResField struct { + Textarea + data *interfaces.Data + Field string Value interfaces.Expr valuePtr interfaces.Func // ptr for table lookup @@ -1234,6 +1254,9 @@ func (obj *StmtResField) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtResField) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Field == "" { return fmt.Errorf("res field name is empty") } @@ -1264,6 +1287,8 @@ func (obj *StmtResField) Interpolate() (StmtResContents, error) { } } return &StmtResField{ + Textarea: obj.Textarea, + data: obj.data, Field: obj.Field, Value: interpolated, Condition: condition, @@ -1297,6 +1322,8 @@ func (obj *StmtResField) Copy() (StmtResContents, error) { return obj, nil } return &StmtResField{ + Textarea: obj.Textarea, + data: obj.data, Field: obj.Field, Value: value, Condition: condition, @@ -1388,6 +1415,7 @@ func (obj *StmtResField) TypeCheck(kind string) ([]*interfaces.UnificationInvari // XXX: Is this needed? invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Condition, Expect: types.TypeBool, Actual: typ, @@ -1428,6 +1456,7 @@ func (obj *StmtResField) TypeCheck(kind string) ([]*interfaces.UnificationInvari // regular scenario invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Value, Expect: typExpr, Actual: typ, @@ -1474,6 +1503,9 @@ func (obj *StmtResField) Graph() (*pgraph.Graph, error) { // StmtResEdge represents a single edge property in the parsed resource // representation. This does not satisfy the Stmt interface. type StmtResEdge struct { + Textarea + data *interfaces.Data + Property string // TODO: iota constant instead? EdgeHalf *StmtEdgeHalf Condition interfaces.Expr // the value will be used if nil or true @@ -1506,6 +1538,9 @@ func (obj *StmtResEdge) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtResEdge) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Property == "" { return fmt.Errorf("res edge property is empty") } @@ -1539,6 +1574,8 @@ func (obj *StmtResEdge) Interpolate() (StmtResContents, error) { } } return &StmtResEdge{ + Textarea: obj.Textarea, + data: obj.data, Property: obj.Property, EdgeHalf: interpolated, Condition: condition, @@ -1571,6 +1608,8 @@ func (obj *StmtResEdge) Copy() (StmtResContents, error) { return obj, nil } return &StmtResEdge{ + Textarea: obj.Textarea, + data: obj.data, Property: obj.Property, EdgeHalf: edgeHalf, Condition: condition, @@ -1663,6 +1702,7 @@ func (obj *StmtResEdge) TypeCheck(kind string) ([]*interfaces.UnificationInvaria // XXX: Is this needed? invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Condition, Expect: types.TypeBool, Actual: typ, @@ -1713,6 +1753,9 @@ func (obj *StmtResEdge) Graph() (*pgraph.Graph, error) { // correspond to the particular meta parameter specified. This does not satisfy // the Stmt interface. type StmtResMeta struct { + Textarea + data *interfaces.Data + Property string // TODO: iota constant instead? MetaExpr interfaces.Expr metaExprPtr interfaces.Func // ptr for table lookup @@ -1746,6 +1789,9 @@ func (obj *StmtResMeta) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtResMeta) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Property == "" { return fmt.Errorf("res meta property is empty") } @@ -1799,6 +1845,8 @@ func (obj *StmtResMeta) Interpolate() (StmtResContents, error) { } } return &StmtResMeta{ + Textarea: obj.Textarea, + data: obj.data, Property: obj.Property, MetaExpr: interpolated, Condition: condition, @@ -1831,6 +1879,8 @@ func (obj *StmtResMeta) Copy() (StmtResContents, error) { return obj, nil } return &StmtResMeta{ + Textarea: obj.Textarea, + data: obj.data, Property: obj.Property, MetaExpr: metaExpr, Condition: condition, @@ -1924,6 +1974,7 @@ func (obj *StmtResMeta) TypeCheck(kind string) ([]*interfaces.UnificationInvaria // XXX: Is this needed? invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Condition, Expect: types.TypeBool, Actual: typ, @@ -2002,6 +2053,7 @@ func (obj *StmtResMeta) TypeCheck(kind string) ([]*interfaces.UnificationInvaria } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.MetaExpr, Expect: typExpr, Actual: typ, @@ -2055,6 +2107,9 @@ func (obj *StmtResMeta) Graph() (*pgraph.Graph, error) { // names are compatible and listed. In this case of Send/Recv, only lists of // length two are legal. type StmtEdge struct { + Textarea + data *interfaces.Data + EdgeHalfList []*StmtEdgeHalf // represents a chain of edges // TODO: should notify be an Expr? @@ -2083,6 +2138,9 @@ func (obj *StmtEdge) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtEdge) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + for _, x := range obj.EdgeHalfList { if err := x.Init(data); err != nil { return err @@ -2109,6 +2167,8 @@ func (obj *StmtEdge) Interpolate() (interfaces.Stmt, error) { } return &StmtEdge{ + Textarea: obj.Textarea, + data: obj.data, EdgeHalfList: edgeHalfList, Notify: obj.Notify, }, nil @@ -2133,6 +2193,8 @@ func (obj *StmtEdge) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtEdge{ + Textarea: obj.Textarea, + data: obj.data, EdgeHalfList: edgeHalfList, Notify: obj.Notify, }, nil @@ -2394,6 +2456,9 @@ func (obj *StmtEdge) Output(table map[interfaces.Func]types.Value) (*interfaces. // is assumed that a list of strings should be expected. More mechanisms to // determine if the value is static may be added over time. type StmtEdgeHalf struct { + Textarea + data *interfaces.Data + Kind string // kind of resource, eg: pkg, file, svc, etc... Name interfaces.Expr // unique name for the res of this kind namePtr interfaces.Func // ptr for table lookup @@ -2421,6 +2486,8 @@ func (obj *StmtEdgeHalf) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtEdgeHalf) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) if obj.Kind == "" { return fmt.Errorf("edge half kind is empty") } @@ -2443,6 +2510,7 @@ func (obj *StmtEdgeHalf) Interpolate() (*StmtEdgeHalf, error) { } return &StmtEdgeHalf{ + Textarea: obj.Textarea, Kind: obj.Kind, Name: name, SendRecv: obj.SendRecv, @@ -2464,6 +2532,7 @@ func (obj *StmtEdgeHalf) Copy() (*StmtEdgeHalf, error) { return obj, nil } return &StmtEdgeHalf{ + Textarea: obj.Textarea, Kind: obj.Kind, Name: name, SendRecv: obj.SendRecv, @@ -2523,6 +2592,7 @@ func (obj *StmtEdgeHalf) TypeCheck() ([]*interfaces.UnificationInvariant, error) } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Name, Expect: typExpr, // the name Actual: typ, @@ -2557,6 +2627,9 @@ func (obj *StmtEdgeHalf) Graph() (*pgraph.Graph, error) { // optional, it is the else branch, although this struct allows either to be // optional, even if it is not commonly used. type StmtIf struct { + Textarea + data *interfaces.Data + Condition interfaces.Expr conditionPtr interfaces.Func // ptr for table lookup ThenBranch interfaces.Stmt // optional, but usually present @@ -2604,6 +2677,9 @@ func (obj *StmtIf) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtIf) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if err := obj.Condition.Init(data); err != nil { return err } @@ -2643,6 +2719,8 @@ func (obj *StmtIf) Interpolate() (interfaces.Stmt, error) { } } return &StmtIf{ + Textarea: obj.Textarea, + data: obj.data, Condition: condition, ThenBranch: thenBranch, ElseBranch: elseBranch, @@ -2685,6 +2763,8 @@ func (obj *StmtIf) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtIf{ + Textarea: obj.Textarea, + data: obj.data, Condition: condition, ThenBranch: thenBranch, ElseBranch: elseBranch, @@ -2807,6 +2887,7 @@ func (obj *StmtIf) TypeCheck() ([]*interfaces.UnificationInvariant, error) { typExpr := types.TypeBool // default invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Condition, Expect: typExpr, // the condition Actual: typ, @@ -2915,6 +2996,7 @@ func (obj *StmtIf) Output(table map[interfaces.Func]types.Value) (*interfaces.Ou // the bind statement's are correctly applied in this scope, and irrespective of // their order of definition. type StmtProg struct { + Textarea data *interfaces.Data scope *interfaces.Scope // store for use by imports @@ -3038,6 +3120,7 @@ func (obj *StmtProg) Interpolate() (interfaces.Stmt, error) { body = append(body, interpolated) } return &StmtProg{ + Textarea: obj.Textarea, data: obj.data, scope: obj.scope, importProgs: obj.importProgs, // TODO: do we even need this here? @@ -3065,6 +3148,7 @@ func (obj *StmtProg) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtProg{ + Textarea: obj.Textarea, data: obj.data, scope: obj.scope, importProgs: obj.importProgs, // TODO: do we even need this here? @@ -3646,6 +3730,7 @@ func (obj *StmtProg) importScopeWithParsedInputs(input *inputs.ParsedInput, scop LexParser: obj.data.LexParser, Downloader: obj.data.Downloader, StrInterpolater: obj.data.StrInterpolater, + SourceFinder: obj.data.SourceFinder, //World: obj.data.World, // TODO: do we need this? //Prefix: obj.Prefix, // TODO: add a path on? @@ -4326,6 +4411,9 @@ func (obj *StmtProg) IsModuleUnsafe() error { // TODO: rename this function? // the supplied function in the current scope and irrespective of the order of // definition. type StmtFunc struct { + Textarea + data *interfaces.Data + Name string Func interfaces.Expr Type *types.Type @@ -4351,11 +4439,13 @@ func (obj *StmtFunc) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtFunc) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Name == "" { return fmt.Errorf("func name is empty") } - //obj.data = data // TODO: ??? if err := obj.Func.Init(data); err != nil { return err } @@ -4373,9 +4463,11 @@ func (obj *StmtFunc) Interpolate() (interfaces.Stmt, error) { } return &StmtFunc{ - Name: obj.Name, - Func: interpolated, - Type: obj.Type, + Textarea: obj.Textarea, + data: obj.data, + Name: obj.Name, + Func: interpolated, + Type: obj.Type, }, nil } @@ -4394,9 +4486,11 @@ func (obj *StmtFunc) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtFunc{ - Name: obj.Name, - Func: fn, - Type: obj.Type, + Textarea: obj.Textarea, + data: obj.data, + Name: obj.Name, + Func: fn, + Type: obj.Type, }, nil } @@ -4487,6 +4581,7 @@ func (obj *StmtFunc) TypeCheck() ([]*interfaces.UnificationInvariant, error) { } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.Func, Expect: typExpr, // obj.Type Actual: typ, @@ -4530,6 +4625,8 @@ func (obj *StmtFunc) Output(map[interfaces.Func]types.Value) (*interfaces.Output // TODO: We don't currently support defining polymorphic classes (eg: different // signatures for the same class name) but it might be something to consider. type StmtClass struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later Name string @@ -4557,6 +4654,9 @@ func (obj *StmtClass) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtClass) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Name == "" { return fmt.Errorf("class name is empty") } @@ -4579,10 +4679,12 @@ func (obj *StmtClass) Interpolate() (interfaces.Stmt, error) { } return &StmtClass{ - scope: obj.scope, - Name: obj.Name, - Args: args, // ensure this has length == 0 instead of nil - Body: interpolated, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + Name: obj.Name, + Args: args, // ensure this has length == 0 instead of nil + Body: interpolated, }, nil } @@ -4606,10 +4708,12 @@ func (obj *StmtClass) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtClass{ - scope: obj.scope, - Name: obj.Name, - Args: args, // ensure this has length == 0 instead of nil - Body: body, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + Name: obj.Name, + Args: args, // ensure this has length == 0 instead of nil + Body: body, }, nil } @@ -4731,6 +4835,9 @@ func (obj *StmtClass) Output(table map[interfaces.Func]types.Value) (*interfaces // to call a class except that it produces output instead of a value. Most of // the interesting logic for classes happens here or in StmtProg. type StmtInclude struct { + Textarea + data *interfaces.Data + class *StmtClass // copy of class that we're using orig *StmtInclude // original pointer to this @@ -4770,6 +4877,9 @@ func (obj *StmtInclude) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *StmtInclude) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if obj.Name == "" { return fmt.Errorf("include name is empty") } @@ -4800,6 +4910,8 @@ func (obj *StmtInclude) Interpolate() (interfaces.Stmt, error) { orig = obj.orig } return &StmtInclude{ + Textarea: obj.Textarea, + data: obj.data, //class: obj.class, // TODO: is this necessary? orig: orig, Name: obj.Name, @@ -4834,6 +4946,8 @@ func (obj *StmtInclude) Copy() (interfaces.Stmt, error) { return obj, nil } return &StmtInclude{ + Textarea: obj.Textarea, + data: obj.data, //class: obj.class, // TODO: is this necessary? orig: orig, Name: obj.Name, @@ -5051,6 +5165,7 @@ func (obj *StmtInclude) TypeCheck() ([]*interfaces.UnificationInvariant, error) // add invariants between the args and the class if typExpr := obj.class.Args[i].Type; typExpr != nil { invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: x, Expect: typExpr, // type of arg Actual: typ, @@ -5100,6 +5215,9 @@ func (obj *StmtInclude) Output(table map[interfaces.Func]types.Value) (*interfac // file. As with any statement, it produces output, but that output is empty. To // benefit from its inclusion, reference the scope definitions you want. type StmtImport struct { + Textarea + data *interfaces.Data + Name string Alias string } @@ -5118,7 +5236,9 @@ func (obj *StmtImport) Apply(fn func(interfaces.Node) error) error { return fn(o // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *StmtImport) Init(*interfaces.Data) error { +func (obj *StmtImport) Init(data *interfaces.Data) error { + obj.Textarea.Setup(data) + if obj.Name == "" { return fmt.Errorf("import name is empty") } @@ -5130,8 +5250,10 @@ func (obj *StmtImport) Init(*interfaces.Data) error { // on any child elements and builds the new node with those new node contents. func (obj *StmtImport) Interpolate() (interfaces.Stmt, error) { return &StmtImport{ - Name: obj.Name, - Alias: obj.Alias, + Textarea: obj.Textarea, + data: obj.data, + Name: obj.Name, + Alias: obj.Alias, }, nil } @@ -5202,6 +5324,8 @@ func (obj *StmtImport) Output(map[interfaces.Func]types.Value) (*interfaces.Outp // formatting) but so that they can exist anywhere in the code. Currently these // are dropped by the lexer. type StmtComment struct { + Textarea + Value string } @@ -5219,7 +5343,9 @@ func (obj *StmtComment) Apply(fn func(interfaces.Node) error) error { return fn( // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *StmtComment) Init(*interfaces.Data) error { +func (obj *StmtComment) Init(data *interfaces.Data) error { + obj.Textarea.Setup(data) + return nil } @@ -5280,6 +5406,8 @@ func (obj *StmtComment) Output(map[interfaces.Func]types.Value) (*interfaces.Out // ExprBool is a representation of a boolean. type ExprBool struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later V bool @@ -5297,7 +5425,11 @@ func (obj *ExprBool) Apply(fn func(interfaces.Node) error) error { return fn(obj // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *ExprBool) Init(*interfaces.Data) error { return nil } +func (obj *ExprBool) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + return nil +} // Interpolate returns a new node (aka a copy) once it has been expanded. This // generally increases the size of the AST when it is used. It calls Interpolate @@ -5305,8 +5437,10 @@ func (obj *ExprBool) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprBool) Interpolate() (interfaces.Expr, error) { return &ExprBool{ - scope: obj.scope, - V: obj.V, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + V: obj.V, }, nil } @@ -5359,6 +5493,7 @@ func (obj *ExprBool) Infer() (*types.Type, []*interfaces.UnificationInvariant, e // This adds the obj ptr, so it's seen as an expr that we need to solve. return types.TypeBool, []*interfaces.UnificationInvariant{ { + Node: obj, Expr: obj, Expect: types.TypeBool, Actual: types.TypeBool, @@ -5424,6 +5559,7 @@ func (obj *ExprBool) Value() (types.Value, error) { // ExprStr is a representation of a string. type ExprStr struct { + Textarea data *interfaces.Data scope *interfaces.Scope // store for referencing this later @@ -5444,6 +5580,7 @@ func (obj *ExprStr) Apply(fn func(interfaces.Node) error) error { return fn(obj) // validate. func (obj *ExprStr) Init(data *interfaces.Data) error { obj.data = data + obj.Textarea.Setup(data) return nil } @@ -5455,6 +5592,7 @@ func (obj *ExprStr) Init(data *interfaces.Data) error { // a function which returns a string as its root. Otherwise it returns itself. func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { pos := &interfaces.Pos{ + // XXX: populate this? // column/line number, starting at 1 //Column: -1, // TODO //Line: -1, // TODO @@ -5474,6 +5612,7 @@ func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { LexParser: obj.data.LexParser, Downloader: obj.data.Downloader, StrInterpolater: obj.data.StrInterpolater, + SourceFinder: obj.data.SourceFinder, //World: obj.data.World, // TODO: do we need this? Prefix: obj.data.Prefix, @@ -5489,9 +5628,10 @@ func (obj *ExprStr) Interpolate() (interfaces.Expr, error) { } if result == nil { return &ExprStr{ - data: obj.data, - scope: obj.scope, - V: obj.V, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + V: obj.V, }, nil } // we got something, overwrite the existing static str @@ -5556,6 +5696,7 @@ func (obj *ExprStr) Infer() (*types.Type, []*interfaces.UnificationInvariant, er // This adds the obj ptr, so it's seen as an expr that we need to solve. return types.TypeStr, []*interfaces.UnificationInvariant{ { + Node: obj, Expr: obj, Expect: types.TypeStr, Actual: types.TypeStr, @@ -5620,6 +5761,8 @@ func (obj *ExprStr) Value() (types.Value, error) { // ExprInt is a representation of an int. type ExprInt struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later V int64 @@ -5637,7 +5780,11 @@ func (obj *ExprInt) Apply(fn func(interfaces.Node) error) error { return fn(obj) // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *ExprInt) Init(*interfaces.Data) error { return nil } +func (obj *ExprInt) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + return nil +} // Interpolate returns a new node (aka a copy) once it has been expanded. This // generally increases the size of the AST when it is used. It calls Interpolate @@ -5645,8 +5792,10 @@ func (obj *ExprInt) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprInt) Interpolate() (interfaces.Expr, error) { return &ExprInt{ - scope: obj.scope, - V: obj.V, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + V: obj.V, }, nil } @@ -5699,6 +5848,7 @@ func (obj *ExprInt) Infer() (*types.Type, []*interfaces.UnificationInvariant, er // This adds the obj ptr, so it's seen as an expr that we need to solve. return types.TypeInt, []*interfaces.UnificationInvariant{ { + Node: obj, Expr: obj, Expect: types.TypeInt, Actual: types.TypeInt, @@ -5763,6 +5913,8 @@ func (obj *ExprInt) Value() (types.Value, error) { // ExprFloat is a representation of a float. type ExprFloat struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later V float64 @@ -5782,7 +5934,11 @@ func (obj *ExprFloat) Apply(fn func(interfaces.Node) error) error { return fn(ob // Init initializes this branch of the AST, and returns an error if it fails to // validate. -func (obj *ExprFloat) Init(*interfaces.Data) error { return nil } +func (obj *ExprFloat) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + return nil +} // Interpolate returns a new node (aka a copy) once it has been expanded. This // generally increases the size of the AST when it is used. It calls Interpolate @@ -5790,8 +5946,10 @@ func (obj *ExprFloat) Init(*interfaces.Data) error { return nil } // Here it simply returns itself, as no interpolation is possible. func (obj *ExprFloat) Interpolate() (interfaces.Expr, error) { return &ExprFloat{ - scope: obj.scope, - V: obj.V, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + V: obj.V, }, nil } @@ -5844,6 +6002,7 @@ func (obj *ExprFloat) Infer() (*types.Type, []*interfaces.UnificationInvariant, // This adds the obj ptr, so it's seen as an expr that we need to solve. return types.TypeFloat, []*interfaces.UnificationInvariant{ { + Node: obj, Expr: obj, Expect: types.TypeFloat, Actual: types.TypeFloat, @@ -5908,6 +6067,8 @@ func (obj *ExprFloat) Value() (types.Value, error) { // ExprList is a representation of a list. type ExprList struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -5941,6 +6102,9 @@ func (obj *ExprList) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprList) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + for _, x := range obj.Elements { if err := x.Init(data); err != nil { return err @@ -5962,6 +6126,8 @@ func (obj *ExprList) Interpolate() (interfaces.Expr, error) { elements = append(elements, interpolated) } return &ExprList{ + Textarea: obj.Textarea, + data: obj.data, scope: obj.scope, typ: obj.typ, Elements: elements, @@ -5987,6 +6153,8 @@ func (obj *ExprList) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprList{ + Textarea: obj.Textarea, + data: obj.data, scope: obj.scope, typ: obj.typ, Elements: elements, @@ -6136,6 +6304,7 @@ func (obj *ExprList) Infer() (*types.Type, []*interfaces.UnificationInvariant, e } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, @@ -6262,6 +6431,8 @@ func (obj *ExprList) Value() (types.Value, error) { // ExprMap is a representation of a (dictionary) map. type ExprMap struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -6297,6 +6468,9 @@ func (obj *ExprMap) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprMap) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + // XXX: Can we check that there aren't any duplicate keys? Can we Cmp? for _, x := range obj.KVs { if err := x.Key.Init(data); err != nil { @@ -6330,9 +6504,11 @@ func (obj *ExprMap) Interpolate() (interfaces.Expr, error) { kvs = append(kvs, kv) } return &ExprMap{ - scope: obj.scope, - typ: obj.typ, - KVs: kvs, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + KVs: kvs, }, nil } @@ -6373,9 +6549,11 @@ func (obj *ExprMap) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprMap{ - scope: obj.scope, - typ: obj.typ, - KVs: kvs, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + KVs: kvs, }, nil } @@ -6577,6 +6755,7 @@ func (obj *ExprMap) Infer() (*types.Type, []*interfaces.UnificationInvariant, er } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, @@ -6741,12 +6920,16 @@ func (obj *ExprMap) Value() (types.Value, error) { // ExprMapKV represents a key and value pair in a (dictionary) map. This does // not satisfy the Expr interface. type ExprMapKV struct { + Textarea + Key interfaces.Expr // keys can be strings, int's, etc... Val interfaces.Expr } // ExprStruct is a representation of a struct. type ExprStruct struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -6779,6 +6962,9 @@ func (obj *ExprStruct) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprStruct) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + fields := make(map[string]struct{}) for _, x := range obj.Fields { // Validate field names and ensure no duplicates! @@ -6811,9 +6997,11 @@ func (obj *ExprStruct) Interpolate() (interfaces.Expr, error) { fields = append(fields, field) } return &ExprStruct{ - scope: obj.scope, - typ: obj.typ, - Fields: fields, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + Fields: fields, }, nil } @@ -6842,9 +7030,11 @@ func (obj *ExprStruct) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprStruct{ - scope: obj.scope, - typ: obj.typ, - Fields: fields, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + Fields: fields, }, nil } @@ -7001,6 +7191,7 @@ func (obj *ExprStruct) Infer() (*types.Type, []*interfaces.UnificationInvariant, } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, @@ -7133,6 +7324,8 @@ func (obj *ExprStruct) Value() (types.Value, error) { // ExprStructField represents a name value pair in a struct field. This does not // satisfy the Expr interface. type ExprStructField struct { + Textarea + Name string Value interfaces.Expr } @@ -7147,6 +7340,7 @@ type ExprStructField struct { // 4. A pure built-in function (set Values to a singleton) // 5. A pure polymorphic built-in function (set Values to a list) type ExprFunc struct { + Textarea data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -7310,6 +7504,7 @@ func (obj *ExprFunc) Interpolate() (interfaces.Expr, error) { } return &ExprFunc{ + Textarea: obj.Textarea, data: obj.data, scope: obj.scope, typ: obj.typ, @@ -7405,6 +7600,7 @@ func (obj *ExprFunc) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprFunc{ + Textarea: obj.Textarea, data: obj.data, scope: obj.scope, // TODO: copy? typ: obj.typ, @@ -7749,6 +7945,7 @@ func (obj *ExprFunc) Infer() (*types.Type, []*interfaces.UnificationInvariant, e } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, @@ -7912,6 +8109,7 @@ func (obj *ExprFunc) Value() (types.Value, error) { // declaration or implementation of a new function value. This struct has an // analogous symmetry with ExprVar. type ExprCall struct { + Textarea data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -7971,6 +8169,7 @@ func (obj *ExprCall) Apply(fn func(interfaces.Node) error) error { // validate. func (obj *ExprCall) Init(data *interfaces.Data) error { obj.data = data + obj.Textarea.Setup(data) if obj.Name == "" && obj.Anon == nil { return fmt.Errorf("missing call name") @@ -8016,9 +8215,10 @@ func (obj *ExprCall) Interpolate() (interfaces.Expr, error) { } return &ExprCall{ - data: obj.data, - scope: obj.scope, - typ: obj.typ, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, // XXX: Copy copies this, do we want to here as well? (or maybe // we want to do it here, but not in Copy?) expr: obj.expr, @@ -8088,16 +8288,17 @@ func (obj *ExprCall) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprCall{ - data: obj.data, - scope: obj.scope, - typ: obj.typ, - expr: expr, // it seems that we need to copy this for it to work - orig: orig, - V: obj.V, - Name: obj.Name, - Args: args, - Var: obj.Var, - Anon: anon, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + expr: expr, // it seems that we need to copy this for it to work + orig: orig, + V: obj.V, + Name: obj.Name, + Args: args, + Var: obj.Var, + Anon: anon, }, nil } @@ -8537,6 +8738,7 @@ func (obj *ExprCall) Infer() (*types.Type, []*interfaces.UnificationInvariant, e } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, @@ -8607,6 +8809,7 @@ func (obj *ExprCall) Infer() (*types.Type, []*interfaces.UnificationInvariant, e } invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj.expr, // this should NOT be obj Expect: typFunc, // TODO: are these two reversed here? Actual: typFn, @@ -8743,6 +8946,7 @@ func (obj *ExprCall) Value() (types.Value, error) { // ExprVar is a representation of a variable lookup. It returns the expression // that that variable refers to. type ExprVar struct { + Textarea data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -8775,10 +8979,11 @@ func (obj *ExprVar) Init(data *interfaces.Data) error { // support variable, variables or anything crazy like that. func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { return &ExprVar{ - data: obj.data, - scope: obj.scope, - typ: obj.typ, - Name: obj.Name, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + Name: obj.Name, }, nil } @@ -8789,10 +8994,11 @@ func (obj *ExprVar) Interpolate() (interfaces.Expr, error) { // and they won't be able to have different values. func (obj *ExprVar) Copy() (interfaces.Expr, error) { return &ExprVar{ - data: obj.data, - scope: obj.scope, - typ: obj.typ, - Name: obj.Name, + Textarea: obj.Textarea, + data: obj.data, + scope: obj.scope, + typ: obj.typ, + Name: obj.Name, }, nil } @@ -8926,6 +9132,7 @@ func (obj *ExprVar) Infer() (*types.Type, []*interfaces.UnificationInvariant, er // This adds the obj ptr, so it's seen as an expr that we need to solve. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typ, Actual: typ, @@ -9153,6 +9360,7 @@ func (obj *ExprParam) Infer() (*types.Type, []*interfaces.UnificationInvariant, // This adds the obj ptr, so it's seen as an expr that we need to solve. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typ, Actual: typ, @@ -9460,6 +9668,7 @@ func (obj *ExprTopLevel) Infer() (*types.Type, []*interfaces.UnificationInvarian // This adds the obj ptr, so it's seen as an expr that we need to solve. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typ, Actual: typ, @@ -9654,6 +9863,7 @@ func (obj *ExprSingleton) Infer() (*types.Type, []*interfaces.UnificationInvaria // This adds the obj ptr, so it's seen as an expr that we need // to solve. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typ, Actual: typ, @@ -9717,6 +9927,8 @@ func (obj *ExprSingleton) Value() (types.Value, error) { // returns a value. As a result, it has a type. This is different from a StmtIf, // which does not need to have both branches, and which does not return a value. type ExprIf struct { + Textarea + data *interfaces.Data scope *interfaces.Scope // store for referencing this later typ *types.Type @@ -9754,6 +9966,9 @@ func (obj *ExprIf) Apply(fn func(interfaces.Node) error) error { // Init initializes this branch of the AST, and returns an error if it fails to // validate. func (obj *ExprIf) Init(data *interfaces.Data) error { + obj.data = data + obj.Textarea.Setup(data) + if err := obj.Condition.Init(data); err != nil { return err } @@ -9785,6 +10000,8 @@ func (obj *ExprIf) Interpolate() (interfaces.Expr, error) { return nil, errwrap.Wrapf(err, "could not interpolate ElseBranch") } return &ExprIf{ + Textarea: obj.Textarea, + data: obj.data, scope: obj.scope, typ: obj.typ, Condition: condition, @@ -9823,6 +10040,8 @@ func (obj *ExprIf) Copy() (interfaces.Expr, error) { return obj, nil } return &ExprIf{ + Textarea: obj.Textarea, + data: obj.data, scope: obj.scope, typ: obj.typ, Condition: condition, @@ -10021,6 +10240,7 @@ func (obj *ExprIf) Infer() (*types.Type, []*interfaces.UnificationInvariant, err } // This must be added even if redundant, so that we collect the obj ptr. invar := &interfaces.UnificationInvariant{ + Node: obj, Expr: obj, Expect: typExpr, // This is the type that we return. Actual: typType, diff --git a/lang/ast/util.go b/lang/ast/util.go index d382afa4..2f9f3341 100644 --- a/lang/ast/util.go +++ b/lang/ast/util.go @@ -31,6 +31,7 @@ package ast import ( "fmt" + "os" "sort" "strings" "sync" @@ -40,6 +41,7 @@ import ( "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -393,3 +395,156 @@ func lambdaScopeFeedback(scope *interfaces.Scope, logf func(format string, v ... logf("$%s(...)", name) } } + +// Textarea stores the coordinates of a statement or expression in the form of a +// starting line/column and ending line/column. +type Textarea struct { + // debug represents if we're running in debug mode or not. + debug bool + + // logf is a logger which should be used. + logf func(format string, v ...interface{}) + + // sf is the SourceFinder function implementation that maps a filename + // to the source. + sf interfaces.SourceFinderFunc + + // path is the full path/filename where this text area exists. + path string + + // This data is zero-based. (Eg: first line of file is 0) + startLine int // first + startColumn int // left + endLine int // last + endColumn int // right + + isSet bool + + // Bug5819 works around issue https://github.com/golang/go/issues/5819 + Bug5819 interface{} // XXX: workaround +} + +// Setup is used during AST initialization in order to store in each AST node +// the name of the source file from which it was generated. +func (obj *Textarea) Setup(data *interfaces.Data) { + obj.debug = data.Debug + obj.logf = data.Logf + obj.sf = data.SourceFinder + obj.path = data.AbsFilename() +} + +// IsSet returns if the position was already set with Locate already. +func (obj *Textarea) IsSet() bool { + return obj.isSet +} + +// Locate is used by the parser to store the token positions in AST nodes. The +// path will be filled during AST node initialization usually, because the +// parser does not know the name of the file it is processing. +func (obj *Textarea) Locate(line int, col int, endline int, endcol int) { + obj.startLine = line + obj.startColumn = col + obj.endLine = endline + obj.endColumn = endcol + obj.isSet = true +} + +// Pos returns the starting line/column of an AST node. +func (obj *Textarea) Pos() (int, int) { + return obj.startLine, obj.startColumn +} + +// End returns the end line/column of an AST node. +func (obj *Textarea) End() (int, int) { + return obj.endLine, obj.endColumn +} + +// Path returns the name of the source file that holds the code for an AST node. +func (obj *Textarea) Path() string { + return obj.path +} + +// Filename returns the printable filename that we'd like to display. It tries +// to return a relative version if possible. +func (obj *Textarea) Filename() string { + if obj.path == "" { + return "" // TODO: should this be ? + } + + wd, _ := os.Getwd() // ignore error since "" would just pass through + wd += "/" // it's a dir + if s, err := util.RemoveBasePath(obj.path, wd); err == nil { + return s + } + + return obj.path +} + +// Byline gives a succinct representation of the Textarea, but is useful only in +// debugging. In order to generate pretty error messages, see HighlightText. +func (obj *Textarea) Byline() string { + // We convert to 1-based for user display. + return fmt.Sprintf("%s @ %d:%d-%d:%d", obj.Filename(), obj.startLine+1, obj.startColumn+1, obj.endLine+1, obj.endColumn+1) +} + +// HighlightText generates a generic description that just visually indicates +// part of the line described by a Textarea. If the coordinates that are passed +// span multiple lines, don't show those lines, but just a description of the +// area. If it can't generate a valid snippet, then it returns the empty string. +func (obj *Textarea) HighlightText() string { + b, err := obj.sf(obj.path) // source finder! + if err != nil { + return "" + } + contents := string(b) + + result := &strings.Builder{} + + result.WriteString(obj.Byline()) + + lines := strings.Split(contents, "\n") + if len(lines) < obj.endLine-1 { + // XXX: out of bounds? + return "" + } + + result.WriteString("\n--\n") + + if obj.startLine == obj.endLine { + line := lines[obj.startLine] + "\n" + text := strings.TrimLeft(line, " \t") + indent := strings.TrimSuffix(line, text) + offset := len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + // TODO: add on the width of the second element as well + result.WriteString(strings.Repeat("^", obj.endColumn-obj.startColumn+1)) + result.WriteString("\n") + + return result.String() + } + + line := lines[obj.startLine] + "\n" + text := strings.TrimLeft(line, " \t") + indent := strings.TrimSuffix(line, text) + offset := len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + result.WriteString("^ from here ...\n") + + line = lines[obj.endLine] + "\n" + text = strings.TrimLeft(line, " \t") + indent = strings.TrimSuffix(line, text) + offset = len(indent) + + result.WriteString(line) + result.WriteString(indent) + result.WriteString(strings.Repeat(" ", obj.startColumn-offset)) + result.WriteString("^ ... to here\n") + + return result.String() +} diff --git a/lang/gapi/gapi.go b/lang/gapi/gapi.go index 64328fd2..c517b9f9 100644 --- a/lang/gapi/gapi.go +++ b/lang/gapi/gapi.go @@ -214,6 +214,7 @@ func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) { LexParser: parser.LexParse, Downloader: downloader, StrInterpolater: interpolate.StrInterpolate, + SourceFinder: os.ReadFile, //Local: obj.Local, // TODO: do we need this? //World: obj.World, // TODO: do we need this? diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index 86d9a550..636fcb43 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -230,6 +230,15 @@ type Data struct { // cycles. StrInterpolater func(string, *Pos, *Data) (Expr, error) + // SourceFinder is a function that returns the contents of a source file + // when requested by filename. This data is used to annotate error + // messages with some context from the source, and as a result is + // optional. This function is passed in this way so that the different + // consumers of this can use different methods to find the source. The + // three main users are: (1) normal GAPI CLI, before the bundle is + // created, (2) the main bundled execution, and (3) the tests. + SourceFinder SourceFinderFunc + //World engine.World // TODO: do we need this? // Prefix provides a unique path prefix that we can namespace in. It is @@ -244,6 +253,16 @@ type Data struct { Logf func(format string, v ...interface{}) } +// AbsFilename returns the absolute filename path to the code this Data struct +// is running. This is used to pull out a filename for error messages. +func (obj *Data) AbsFilename() string { + // TODO: is this correct? Do we want to check if Metadata is nil? + if obj == nil || obj.Metadata == nil { // for tests + return "" + } + return obj.Base + obj.Metadata.Main +} + // Scope represents a mapping between a variables identifier and the // corresponding expression it is bound to. Local scopes in this language exist // and are formed by nesting within if statements. Child scopes can shadow @@ -424,3 +443,39 @@ func EmptyOutput() *Output { Edges: []*Edge{}, } } + +// PositionableNode is the interface implemented by AST nodes that store their +// code position. It is implemented by node types that embed Textarea. +type PositionableNode interface { + // IsSet returns if the position was already set with Locate already. + IsSet() bool + + // Locate sets the position in zero-based (start line, start column, end + // line, end column) format. + Locate(int, int, int, int) + + // Pos returns the zero-based start line and then start column position. + Pos() (int, int) + + // End returns the zero-based end line and then end column position. + End() (int, int) + + // String returns a friendly representation of the positions. + String() string +} + +// TextDisplayer is a graph node that is aware of its position in the source +// code, and can emit a textual representation of that part of the source. +type TextDisplayer interface { + // Byline returns a simple version of the error location. + Byline() string + + // HighlightText returns a textual representation of this definition + // for this node in source. + HighlightText() string +} + +// SourceFinderFunc is the function signature used to return the contents of a +// source file when requested by filename. This data is used to annotate error +// messages with some context from the source, and as a result is optional. +type SourceFinderFunc = func(string) ([]byte, error) diff --git a/lang/interfaces/unification.go b/lang/interfaces/unification.go index 0985d895..b41e779d 100644 --- a/lang/interfaces/unification.go +++ b/lang/interfaces/unification.go @@ -39,8 +39,12 @@ import ( // solution. Those two types are symmetrical in that it doesn't matter which is // used where, it only affects how we print out error messages. type UnificationInvariant struct { // formerly the SamInvariant - // Expr is the expression we are determining the type for. This improves - // our error messages. + // Node is the AST node holding the expression. This improves our error + // messages. + Node Node + + // Expr is the expression we are determining the type for. This is what + // we are unifying. This improves our error messages. Expr Expr // Expect is one of the two types to unify. @@ -65,6 +69,7 @@ func GenericCheck(obj Expr, typ *types.Type) ([]*UnificationInvariant, error) { invar := &UnificationInvariant{ Expr: obj, + Node: obj, Expect: typ, // sam says not backwards Actual: actual, } diff --git a/lang/interpolate/interpolate.go b/lang/interpolate/interpolate.go index 187eacb3..16409006 100644 --- a/lang/interpolate/interpolate.go +++ b/lang/interpolate/interpolate.go @@ -155,6 +155,7 @@ func HilInterpolate(str string, pos *interfaces.Pos, data *interfaces.Data) (int LexParser: data.LexParser, Downloader: data.Downloader, StrInterpolater: data.StrInterpolater, + SourceFinder: data.SourceFinder, //World: data.World, // TODO: do we need this? Prefix: data.Prefix, diff --git a/lang/interpolate/interpolate_test.go b/lang/interpolate/interpolate_test.go index b0d08bae..e3633b44 100644 --- a/lang/interpolate/interpolate_test.go +++ b/lang/interpolate/interpolate_test.go @@ -34,6 +34,7 @@ package interpolate import ( "fmt" "reflect" + "regexp" "strings" "testing" @@ -228,6 +229,9 @@ func TestInterpolate0(t *testing.T) { data := &interfaces.Data{ // TODO: add missing fields here if/when needed StrInterpolater: StrInterpolate, + SourceFinder: func(string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") + }, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { @@ -271,7 +275,7 @@ func TestInterpolate0(t *testing.T) { StripPackageNames: true, HidePrivateFields: true, HideZeroValues: true, - //FieldExclusions: regexp.MustCompile(`^(data)$`), + FieldExclusions: regexp.MustCompile(`^(Textarea)$`), //FieldFilter func(reflect.StructField, reflect.Value) bool //HomePackage string //Separator string @@ -480,6 +484,9 @@ func TestInterpolateBasicStmt(t *testing.T) { data := &interfaces.Data{ // TODO: add missing fields here if/when needed StrInterpolater: StrInterpolate, + SourceFinder: func(string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") + }, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { @@ -787,6 +794,9 @@ func TestInterpolateBasicExpr(t *testing.T) { data := &interfaces.Data{ // TODO: add missing fields here if/when needed StrInterpolater: StrInterpolate, + SourceFinder: func(string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") + }, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { diff --git a/lang/interpret_test.go b/lang/interpret_test.go index 790b5e7e..965e8478 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -181,11 +181,19 @@ func TestAstFunc1(t *testing.T) { t.Logf("comment: %s\n", comment) } + sources := map[string][]byte{} + sourceFinder := func(path string) ([]byte, error) { + if b, exists := sources[path]; exists { + return b, nil + } + return nil, os.ErrNotExist + } // copy files out into the test temp directory var testOutput []byte var testConfig []byte found := false for _, file := range archive.Files { + sources["/"+file.Name] = file.Data // store! if file.Name == "OUTPUT" { testOutput = file.Data found = true @@ -247,6 +255,7 @@ func TestAstFunc1(t *testing.T) { if strings.HasPrefix(expstr, magicError) { errStr = strings.TrimPrefix(expstr, magicError) expstr = errStr + t.Logf("errStr has length %d", len(errStr)) if strings.HasPrefix(expstr, magicErrorLexParse) { errStr = strings.TrimPrefix(expstr, magicErrorLexParse) @@ -398,6 +407,7 @@ func TestAstFunc1(t *testing.T) { LexParser: parser.LexParse, StrInterpolater: interpolate.StrInterpolate, + SourceFinder: sourceFinder, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { @@ -675,11 +685,19 @@ func TestAstFunc2(t *testing.T) { t.Logf("comment: %s\n", comment) } + sources := map[string][]byte{} + sourceFinder := func(path string) ([]byte, error) { + if b, exists := sources[path]; exists { + return b, nil + } + return nil, os.ErrNotExist + } // copy files out into the test temp directory var testOutput []byte var testConfig []byte found := false for _, file := range archive.Files { + sources["/"+file.Name] = file.Data // store! if file.Name == "OUTPUT" { testOutput = file.Data found = true @@ -939,6 +957,7 @@ func TestAstFunc2(t *testing.T) { LexParser: parser.LexParse, StrInterpolater: interpolate.StrInterpolate, + SourceFinder: sourceFinder, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { @@ -1495,11 +1514,19 @@ func TestAstFunc3(t *testing.T) { t.Logf("comment: %s\n", comment) } + sources := map[string][]byte{} + sourceFinder := func(path string) ([]byte, error) { + if b, exists := sources[path]; exists { + return b, nil + } + return nil, os.ErrNotExist + } // copy files out into the test temp directory var testOutput []byte var testConfig []byte found := false for _, file := range archive.Files { + sources["/"+file.Name] = file.Data // store! if file.Name == "OUTPUT" { testOutput = file.Data found = true @@ -1759,6 +1786,7 @@ func TestAstFunc3(t *testing.T) { LexParser: parser.LexParse, StrInterpolater: interpolate.StrInterpolate, + SourceFinder: sourceFinder, Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { diff --git a/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar b/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar index c4978b63..5a70008c 100644 --- a/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar +++ b/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar @@ -12,4 +12,4 @@ class c1($a, $b []str) { } } -- OUTPUT -- -# err: errUnify: unify error with: str("hello"): type error: list != str +# err: errUnify: type error: list != str: /main.mcl @ 4:1-4:25 diff --git a/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar b/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar index 5ddf0d72..29bcb97b 100644 --- a/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar +++ b/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar @@ -15,4 +15,4 @@ test ["x",] { float32 => $b, } -- OUTPUT -- -# err: errUnify: unify error with: topLevel(func() { }): type error: int != float +# err: errUnify: type error: int != float: /main.mcl @ 10:6-10:22 diff --git a/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar b/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar index ba82486b..61f4bd55 100644 --- a/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar +++ b/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar @@ -2,4 +2,4 @@ import "fmt" test fmt.printf("%d%d", 42) {} # should not pass, missing second int -- OUTPUT -- -# err: errUnify: unify error with: call:fmt.printf(str("%d%d"), int(42)): type error: str != list +# err: errUnify: type error: str != list: /main.mcl @ 2:1-2:30 diff --git a/lang/interpret_test/TestAstFunc2/res1.txtar b/lang/interpret_test/TestAstFunc2/res1.txtar index 4dc3d623..e88c6b52 100644 --- a/lang/interpret_test/TestAstFunc2/res1.txtar +++ b/lang/interpret_test/TestAstFunc2/res1.txtar @@ -3,4 +3,4 @@ test "t1" { stringptr => 42, # int, not str } -- OUTPUT -- -# err: errUnify: unify error with: int(42): type error: str != int +# err: errUnify: type error: str != int: /main.mcl @ 2:2-2:17 diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar index c18d5599..16151c64 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar @@ -7,4 +7,4 @@ test "test" {} Test["${name}"] -> Test["test"] # must fail -- OUTPUT -- -# err: errUnify: unify error with: var(name): type error: str != list +# err: errUnify: type error: str != list: /main.mcl @ 6:1-6:15 diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar index 3d3109c5..06e0c499 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar @@ -7,4 +7,4 @@ test "test" {} Test["test"] -> Test["${name}"] # must fail -- OUTPUT -- -# err: errUnify: unify error with: var(name): type error: str != list +# err: errUnify: type error: str != list: /main.mcl @ 6:17-6:31 diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar index 6e18badc..fc59296d 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar @@ -5,4 +5,4 @@ $name = ["a", "bb", "ccc",] test "${name}" {} # must fail -- OUTPUT -- -# err: errUnify: unify error with: var(name): type error: str != list +# err: errUnify: type error: str != list: /main.mcl @ 4:1-4:17 diff --git a/lang/lang.go b/lang/lang.go index c3bd82a4..ec3c4d69 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -189,6 +189,10 @@ func (obj *Lang) Init(ctx context.Context) error { LexParser: parser.LexParse, Downloader: nil, // XXX: is this used here? StrInterpolater: interpolate.StrInterpolate, + SourceFinder: func(string) ([]byte, error) { + // We're running a bundle as part of a deploy. + return nil, fmt.Errorf("not implemented") // XXX: read from the fs? + }, //Local: obj.Local, // TODO: do we need this? //World: obj.World, // TODO: do we need this? diff --git a/lang/parser/lexparse_test.go b/lang/parser/lexparse_test.go index 94f9956a..10c1bada 100644 --- a/lang/parser/lexparse_test.go +++ b/lang/parser/lexparse_test.go @@ -35,6 +35,7 @@ import ( "fmt" "io" "reflect" + "regexp" "strings" "testing" @@ -45,8 +46,9 @@ import ( langUtil "github.com/purpleidea/mgmt/lang/util" "github.com/purpleidea/mgmt/util" - "github.com/davecgh/go-spew/spew" + godiff "github.com/kylelemons/godebug/diff" "github.com/kylelemons/godebug/pretty" + "github.com/sanity-io/litter" ) func TestLexParse0(t *testing.T) { @@ -2221,6 +2223,36 @@ func TestLexParse0(t *testing.T) { } // double check because DeepEqual is different since the func exists + lo := &litter.Options{ + //Compact: false, + StripPackageNames: true, + HidePrivateFields: true, + HideZeroValues: true, + FieldExclusions: regexp.MustCompile(`^(Textarea)$`), + //FieldFilter func(reflect.StructField, reflect.Value) bool + //HomePackage string + //Separator string + DisablePointerReplacement: true, + } + + // The litter package adds pointer comments everywhere, + // which make it not diff correctly. Clean them here! + pattern := regexp.MustCompile(`\ \/\/\ p[0-9]+$`) // the p0, p1 comments... + clean := func(s string) string { + lines := []string{} + for _, line := range strings.Split(s, "\n") { + s := pattern.ReplaceAllLiteralString(line, "") + lines = append(lines, s) + } + return strings.Join(lines, "\n") + } + + lo1 := clean(lo.Sdump(exp)) + lo2 := clean(lo.Sdump(xast)) + if lo1 == lo2 { // simple diff + return + } + // more details, for tricky cases: diffable := &pretty.Config{ Diffable: true, @@ -2228,18 +2260,22 @@ func TestLexParse0(t *testing.T) { //PrintStringers: false, // always false! //PrintTextMarshalers: false, SkipZeroFields: true, + //Formatter: map[reflect.Type]interface{}{ + // reflect.TypeOf(ast.Textarea{}): func(x ast.Textarea) string { + // return "" + // }, + //}, } diff := diffable.Compare(exp, xast) if diff == "" { // bonus return } + diff = godiff.Diff(lo1, lo2) // for printing + t.Errorf("test #%d: AST did not match expected", index) // TODO: consider making our own recursive print function - t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(xast)) - t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(exp)) - - t.Logf("test #%d: actual: \n\n%s\n", index, diffable.Sprint(xast)) - t.Logf("test #%d: expected: \n\n%s", index, diffable.Sprint(exp)) + t.Logf("test #%d: actual: \n\n%s\n", index, lo1) + t.Logf("test #%d: expected: \n\n%s", index, lo2) t.Logf("test #%d: diff:\n%s", index, diff) }) } diff --git a/lang/parser/parser.y b/lang/parser/parser.y index 9d2aa362..d568ecd5 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -58,12 +58,12 @@ func init() { //err error // TODO: if we ever match ERROR in the parser - bool bool - str string - int int64 // this is the .int as seen in lexer.nex - float float64 + bool bool + str string + int int64 // this is the .int as seen in lexer.nex + float float64 - typ *types.Type + typ *types.Type stmts []interfaces.Stmt stmt interfaces.Stmt @@ -166,54 +166,55 @@ prog: $$.stmt = &ast.StmtProg{ Body: stmts, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } } ; stmt: COMMENT { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtComment{ Value: $1.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | bind { - posLast(yylex, yyDollar) // our pos $$.stmt = $1.stmt + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | panic { - posLast(yylex, yyDollar) // our pos $$.stmt = $1.stmt + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | resource { - posLast(yylex, yyDollar) // our pos $$.stmt = $1.stmt + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | edge { - posLast(yylex, yyDollar) // our pos $$.stmt = $1.stmt + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | IF expr OPEN_CURLY prog CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtIf{ Condition: $2.expr, ThenBranch: $4.stmt, //ElseBranch: nil, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } | IF expr OPEN_CURLY prog CLOSE_CURLY ELSE OPEN_CURLY prog CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtIf{ Condition: $2.expr, ThenBranch: $4.stmt, ElseBranch: $8.stmt, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // this is the named version, iow, a user-defined function (statement) // `func name() { }` @@ -221,7 +222,6 @@ stmt: // `func name(, ) { }` | FUNC_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY expr CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtFunc{ Name: $2.str, Func: &ast.ExprFunc{ @@ -230,11 +230,11 @@ stmt: Body: $7.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `func name(...) { }` | FUNC_IDENTIFIER IDENTIFIER OPEN_PAREN args CLOSE_PAREN type OPEN_CURLY expr CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos fn := &ast.ExprFunc{ Args: $4.args, Return: $6.typ, // return type is known @@ -271,191 +271,192 @@ stmt: Func: fn, Type: typ, // sam says add the type here instead... } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `class name { }` | CLASS_IDENTIFIER colon_identifier OPEN_CURLY prog CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtClass{ Name: $2.str, Args: nil, Body: $4.stmt, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `class name() { }` // `class name(, ) { }` | CLASS_IDENTIFIER colon_identifier OPEN_PAREN args CLOSE_PAREN OPEN_CURLY prog CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtClass{ Name: $2.str, Args: $4.args, Body: $7.stmt, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `include name` | INCLUDE_IDENTIFIER dotted_identifier { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtInclude{ Name: $2.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `include name(...)` | INCLUDE_IDENTIFIER dotted_identifier OPEN_PAREN call_args CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtInclude{ Name: $2.str, Args: $4.exprs, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `include name as foo` // TODO: should we support: `include name as *` | INCLUDE_IDENTIFIER dotted_identifier AS_IDENTIFIER IDENTIFIER { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtInclude{ Name: $2.str, Alias: $4.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `include name(...) as foo` // TODO: should we support: `include name(...) as *` | INCLUDE_IDENTIFIER dotted_identifier OPEN_PAREN call_args CLOSE_PAREN AS_IDENTIFIER IDENTIFIER { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtInclude{ Name: $2.str, Args: $4.exprs, Alias: $7.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `import "name"` | IMPORT_IDENTIFIER STRING { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtImport{ Name: $2.str, //Alias: "", } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `import "name" as alias` | IMPORT_IDENTIFIER STRING AS_IDENTIFIER IDENTIFIER { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtImport{ Name: $2.str, Alias: $4.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `import "name" as *` | IMPORT_IDENTIFIER STRING AS_IDENTIFIER MULTIPLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtImport{ Name: $2.str, Alias: $4.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } /* // resource bind | rbind { - posLast(yylex, yyDollar) // our pos $$.stmt = $1.stmt + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } */ ; expr: BOOL { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprBool{ V: $1.bool, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | STRING { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprStr{ V: $1.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | INTEGER { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprInt{ V: $1.int, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | FLOAT { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprFloat{ V: $1.float, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | list { - posLast(yylex, yyDollar) // our pos // TODO: list could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | map { - posLast(yylex, yyDollar) // our pos // TODO: map could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | struct { - posLast(yylex, yyDollar) // our pos // TODO: struct could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | call { - posLast(yylex, yyDollar) // our pos // TODO: call could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | var { - posLast(yylex, yyDollar) // our pos // TODO: var could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | func { - posLast(yylex, yyDollar) // our pos // TODO: var could be squashed in here directly... $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | IF expr OPEN_CURLY expr CLOSE_CURLY ELSE OPEN_CURLY expr CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprIf{ Condition: $2.expr, ThenBranch: $4.expr, ElseBranch: $8.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // parenthesis wrap an expression for precedence | OPEN_PAREN expr CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos $$.expr = $2.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; list: // `[42, 0, -13]` OPEN_BRACK list_elements CLOSE_BRACK { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprList{ Elements: $2.exprs, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; list_elements: @@ -473,18 +474,18 @@ list_elements: list_element: expr COMMA { - posLast(yylex, yyDollar) // our pos $$.expr = $1.expr + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; map: // `{"hello" => "there", "world" => "big",}` OPEN_CURLY map_kvs CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprMap{ KVs: $2.mapKVs, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; map_kvs: @@ -513,7 +514,6 @@ struct: // `struct{answer => 0, truth => false, hello => "world",}` STRUCT_IDENTIFIER OPEN_CURLY struct_fields CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprStruct{ Fields: $3.structFields, } @@ -546,18 +546,17 @@ call: // iter.map(...) dotted_identifier OPEN_PAREN call_args CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: $1.str, Args: $3.exprs, //Var: false, // default } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // calling a function that's stored in a variable (a lambda) // `$foo(4, "hey")` # call function value | dotted_var_identifier OPEN_PAREN call_args CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: $1.str, Args: $3.exprs, @@ -565,34 +564,34 @@ call: // prefix to the Name, but I felt this was more elegant. Var: true, // lambda } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // calling an inline function | func OPEN_PAREN call_args CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: "", // anonymous! Args: $3.exprs, Anon: $1.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr PLUS expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ - &ast.ExprStr{ // operator first - V: $2.str, // for PLUS this is a `+` character + &ast.ExprStr{ // operator first + V: $2.str, // for PLUS this is a `+` character }, $1.expr, $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr MINUS expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -603,10 +602,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr MULTIPLY expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -617,10 +616,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr DIVIDE expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -631,10 +630,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr EQ expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -645,10 +644,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr NEQ expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -659,10 +658,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr LT expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -673,10 +672,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr GT expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -687,10 +686,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr LTE expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -701,10 +700,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr GTE expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -715,10 +714,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr AND expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -729,10 +728,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr OR expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -743,10 +742,10 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | NOT expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: operators.OperatorFuncName, Args: []interfaces.Expr{ @@ -756,13 +755,13 @@ call: $2.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // lookup an index in a list or a key in a map // lookup($foo, $key) // `$foo[$key]` // no default specifier | expr OPEN_BRACK expr CLOSE_BRACK { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: funcs.LookupFuncName, Args: []interfaces.Expr{ @@ -771,13 +770,13 @@ call: //$6.expr, // the default }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // lookup an index in a list or a key in a map with a default // lookup_default($foo, $key, $default) // `$foo[$key] || "default"` | expr OPEN_BRACK expr CLOSE_BRACK DEFAULT expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: funcs.LookupDefaultFuncName, Args: []interfaces.Expr{ @@ -786,13 +785,13 @@ call: $6.expr, // the default }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // lookup a field in a struct // _struct_lookup($foo, "field") // $foo->field | expr ARROW IDENTIFIER { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: funcs.StructLookupFuncName, Args: []interfaces.Expr{ @@ -803,13 +802,13 @@ call: //$5.expr, // the default }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // lookup a field in a struct with a default // _struct_lookup_optional($foo, "field", "default") // $foo->field || "default" | expr ARROW IDENTIFIER DEFAULT expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: funcs.StructLookupOptionalFuncName, Args: []interfaces.Expr{ @@ -820,10 +819,10 @@ call: $5.expr, // the default }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } | expr IN expr { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ Name: funcs.ContainsFuncName, Args: []interfaces.Expr{ @@ -831,6 +830,7 @@ call: $3.expr, }, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; // list order gets us the position of the arg, but named params would work too! @@ -856,10 +856,10 @@ call_args: var: dotted_var_identifier { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprVar{ Name: $1.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; func: @@ -869,17 +869,16 @@ func: // `func(, ) { }` FUNC_IDENTIFIER OPEN_PAREN args CLOSE_PAREN OPEN_CURLY expr CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprFunc{ Args: $3.args, //Return: nil, Body: $6.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } // `func(...) { }` | FUNC_IDENTIFIER OPEN_PAREN args CLOSE_PAREN type OPEN_CURLY expr CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprFunc{ Args: $3.args, Return: $5.typ, // return type is known @@ -909,6 +908,7 @@ func: yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err)) } } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; args: @@ -949,17 +949,16 @@ bind: // `$s = "hey"` var_identifier EQUALS expr { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtBind{ Ident: $1.str, Value: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // `$x bool = true` // `$x int = if true { 42 } else { 13 }` | var_identifier type EQUALS expr { - posLast(yylex, yyDollar) // our pos var expr interfaces.Expr = $4.expr // XXX: We still need to do this for now it seems... if err := expr.SetType($2.typ); err != nil { @@ -971,6 +970,7 @@ bind: Value: expr, Type: $2.typ, // sam says add the type here instead... } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } ; panic: @@ -981,7 +981,6 @@ panic: //} PANIC_IDENTIFIER OPEN_PAREN call_args CLOSE_PAREN { - posLast(yylex, yyDollar) // our pos call := &ast.ExprCall{ Name: $1.str, // the function name Args: $3.exprs, @@ -1000,6 +999,7 @@ panic: ThenBranch: res, //ElseBranch: nil, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } ; /* TODO: do we want to include this? @@ -1007,7 +1007,6 @@ panic: rbind: var_identifier EQUALS resource { - posLast(yylex, yyDollar) // our pos // XXX: this kind of bind is different than the others, because // it can only really be used for send->recv stuff, eg: // foo.SomeString -> bar.SomeOtherString @@ -1015,6 +1014,7 @@ rbind: Ident: $1.str, Value: $3.stmt, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.expr) } ; */ @@ -1022,12 +1022,12 @@ resource: // `file "/tmp/hello" { ... }` or `aws:ec2 "/tmp/hello" { ... }` colon_identifier expr OPEN_CURLY resource_body CLOSE_CURLY { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtRes{ Kind: $1.str, Name: $2.expr, Contents: $4.resContents, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } ; resource_body: @@ -1080,53 +1080,52 @@ resource_body: resource_field: IDENTIFIER ROCKET expr COMMA { - posLast(yylex, yyDollar) // our pos $$.resField = &ast.StmtResField{ Field: $1.str, Value: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resField) } ; conditional_resource_field: // content => $present ?: "hello", IDENTIFIER ROCKET expr ELVIS expr COMMA { - posLast(yylex, yyDollar) // our pos $$.resField = &ast.StmtResField{ Field: $1.str, Value: $5.expr, Condition: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resField) } ; resource_edge: // Before => Test["t1"], CAPITALIZED_IDENTIFIER ROCKET edge_half COMMA { - posLast(yylex, yyDollar) // our pos $$.resEdge = &ast.StmtResEdge{ Property: $1.str, EdgeHalf: $3.edgeHalf, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resEdge) } ; conditional_resource_edge: // Before => $present ?: Test["t1"], CAPITALIZED_IDENTIFIER ROCKET expr ELVIS edge_half COMMA { - posLast(yylex, yyDollar) // our pos $$.resEdge = &ast.StmtResEdge{ Property: $1.str, EdgeHalf: $5.edgeHalf, Condition: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resEdge) } ; resource_meta: // Meta:noop => true, CAPITALIZED_IDENTIFIER COLON IDENTIFIER ROCKET expr COMMA { - posLast(yylex, yyDollar) // our pos if strings.ToLower($1.str) != strings.ToLower(ast.MetaField) { // this will ultimately cause a parser error to occur... yylex.Error(fmt.Sprintf("%s: %s", ErrParseResFieldInvalid, $1.str)) @@ -1135,6 +1134,7 @@ resource_meta: Property: $3.str, MetaExpr: $5.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resMeta) } ; conditional_resource_meta: @@ -1151,13 +1151,13 @@ conditional_resource_meta: MetaExpr: $7.expr, Condition: $5.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resMeta) } ; resource_meta_struct: // Meta => struct{meta => true, retry => 3,}, CAPITALIZED_IDENTIFIER ROCKET expr COMMA { - posLast(yylex, yyDollar) // our pos if strings.ToLower($1.str) != strings.ToLower(ast.MetaField) { // this will ultimately cause a parser error to occur... yylex.Error(fmt.Sprintf("%s: %s", ErrParseResFieldInvalid, $1.str)) @@ -1166,13 +1166,13 @@ resource_meta_struct: Property: $1.str, MetaExpr: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resMeta) } ; conditional_resource_meta_struct: // Meta => $present ?: struct{poll => 60, sema => ["foo:1", "bar:3",],}, CAPITALIZED_IDENTIFIER ROCKET expr ELVIS expr COMMA { - posLast(yylex, yyDollar) // our pos if strings.ToLower($1.str) != strings.ToLower(ast.MetaField) { // this will ultimately cause a parser error to occur... yylex.Error(fmt.Sprintf("%s: %s", ErrParseResFieldInvalid, $1.str)) @@ -1182,6 +1182,7 @@ conditional_resource_meta_struct: MetaExpr: $5.expr, Condition: $3.expr, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.resMeta) } ; edge: @@ -1190,16 +1191,15 @@ edge: // Test["t1"] -> Test["t2"] -> Test["t3"] # chain or pair edge_half_list { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtEdge{ EdgeHalfList: $1.edgeHalfList, //Notify: false, // unused here } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } // Test["t1"].foo_send -> Test["t2"].blah_recv # send/recv | edge_half_sendrecv ARROW edge_half_sendrecv { - posLast(yylex, yyDollar) // our pos $$.stmt = &ast.StmtEdge{ EdgeHalfList: []*ast.StmtEdgeHalf{ $1.edgeHalf, @@ -1207,6 +1207,7 @@ edge: }, //Notify: false, // unused here, it is implied (i think) } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.stmt) } ; edge_half_list: @@ -1225,24 +1226,24 @@ edge_half: // eg: Test["t1"] capitalized_res_identifier OPEN_BRACK expr CLOSE_BRACK { - posLast(yylex, yyDollar) // our pos $$.edgeHalf = &ast.StmtEdgeHalf{ Kind: $1.str, Name: $3.expr, //SendRecv: "", // unused } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.edgeHalf) } ; edge_half_sendrecv: // eg: Test["t1"].foo_send capitalized_res_identifier OPEN_BRACK expr CLOSE_BRACK DOT IDENTIFIER { - posLast(yylex, yyDollar) // our pos $$.edgeHalf = &ast.StmtEdgeHalf{ Kind: $1.str, Name: $3.expr, SendRecv: $6.str, } + locate(yylex, $1, yyDollar[len(yyDollar)-1], $$.edgeHalf) } ; type: @@ -1496,7 +1497,21 @@ func cast(y yyLexer) *lexParseAST { return x.(*lexParseAST) } -// posLast pulls out the "last token" and does a pos with that. This is a hack! +// locate should be called after creating AST nodes from lexer tokens to store +// the positions of the involved tokens in the AST node. +func locate(y yyLexer, first yySymType, last yySymType, node interface{}) { + pos(y, last) + // Only run Locate on nodes that look like they have not received + // locations yet otherwise the parser will come back and overwrite them + // with invalid ending positions. + if pn, ok := node.(interfaces.PositionableNode); !ok { + return + } else if !pn.IsSet() { + pn.Locate(first.row, first.col, last.row, last.col) + } +} + +// posLast runs pos on the last token of the current stmt/expr. func posLast(y yyLexer, dollars []yySymType) { // pick the last token in the set matched by the parser pos(y, dollars[len(dollars)-1]) // our pos diff --git a/lang/unification/fastsolver/fastsolver.go b/lang/unification/fastsolver/fastsolver.go index 1775e922..fd6818e9 100644 --- a/lang/unification/fastsolver/fastsolver.go +++ b/lang/unification/fastsolver/fastsolver.go @@ -134,8 +134,15 @@ func (obj *FastInvariantSolver) Solve(ctx context.Context, data *unification.Dat if err := unificationUtil.Unify(x.Expect, x.Actual); err != nil { // Storing the Expr with this invariant is so that we // can generate this more helpful error message here. - // TODO: Improve this error message! - return nil, errwrap.Wrapf(err, "unify error with: %s", x.Expr) + displayer, ok := x.Node.(interfaces.TextDisplayer) + if !ok { + obj.Logf("not displayable: %v\n", x.Node) + return nil, errwrap.Wrapf(err, "unify error with: %s", x.Expr) + } + if highlight := displayer.HighlightText(); highlight != "" { + obj.Logf("%s: %s", err.Error(), highlight) + } + return nil, fmt.Errorf("%s: %s", err.Error(), displayer.Byline()) } if obj.Debug { e1, e2 := unificationUtil.Extract(x.Expect), unificationUtil.Extract(x.Actual) diff --git a/lang/unification/solvers/unification_test.go b/lang/unification/solvers/unification_test.go index b4181d2e..68e6bd60 100644 --- a/lang/unification/solvers/unification_test.go +++ b/lang/unification/solvers/unification_test.go @@ -859,6 +859,11 @@ func TestUnification1(t *testing.T) { data := &interfaces.Data{ // TODO: add missing fields here if/when needed + Metadata: &interfaces.Metadata{}, + SourceFinder: func(string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") + }, + Debug: testing.Verbose(), // set via the -test.v flag to `go test` Logf: func(format string, v ...interface{}) { t.Logf(fmt.Sprintf("test #%d", index)+": ast: "+format, v...)