From 1c0a98a0cc3acda19b68c71356b8f1d37ecf1fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20G=C3=A9lineau?= Date: Mon, 4 Dec 2023 21:56:42 -0500 Subject: [PATCH] lang: ast: ExprBind is now monomorphic This adds ExprTopLevel and ExprSingleton and ensures that ExprBind is now monomorphic. This corrects a previous design bug where it was not monomorphic and would thus cause spawning of many more copies than necessary. In most cases this was only harmful to memory and performance, and not behaviour, since these functions were pure, and we didn't have a test for this. This also adds a bunch more tests. Most notably, the graph shape tests generally produce smaller graphs now. Lastly, a lambda cannot have two different types when used at two different call sites. It is rare that this would be used, and when it would make sense, there are easy workarounds to accomplish equivalent goals. This was mostly authored by Sam, James helped with some cleanup and debugging. Co-authored-by: James Shubin --- lang/ast/scopegraph.go | 22 + lang/ast/structs.go | 496 +++++++++++++++--- lang/ast/util.go | 13 +- .../TestAstFunc1/doubleclass.txtar | 4 - .../TestAstFunc1/doubleinclude.txtar | 1 - .../TestAstFunc1/efficient-lambda.txtar | 1 - .../TestAstFunc1/lambda-chained.txtar | 5 - .../TestAstFunc1/simple-lambda2.txtar | 1 - .../TestAstFunc1/slow_unification0.txtar | 36 -- .../TestAstFunc1/static-function0.txtar | 2 - .../TestAstFunc1/static-function1.txtar | 2 - .../TestAstFunc2/clear-env-on-var.txtar | 2 +- .../TestAstFunc2/deploy-readfile2.txtar | 49 ++ .../TestAstFunc2/deploy-readfile3.txtar | 26 + .../TestAstFunc2/deploy-readfile4.txtar | 17 + .../interpret_test/TestAstFunc2/lookup3.txtar | 2 +- .../TestAstFunc2/printfempty.txtar | 2 +- .../TestAstFunc2/printfunificationerr0.txtar | 2 +- .../TestAstFunc2/test-monomorphic-bind.txtar | 13 + .../test-monomorphic-class-arg.txtar | 15 + .../test-monomorphic-func-arg.txtar | 13 + .../test-monomorphic-lambda-arg.txtar | 13 + ...nce.txtar => test-one-instance-bind.txtar} | 0 .../test-one-instance-class-arg.txtar | 22 + .../test-one-instance-func-arg.txtar | 19 + .../test-one-instance-lambda-arg.txtar | 19 + .../TestAstFunc2/test-polymorphic-class.txtar | 10 + .../TestAstFunc2/test-polymorphic-func.txtar | 10 + 28 files changed, 677 insertions(+), 140 deletions(-) create mode 100644 lang/interpret_test/TestAstFunc2/deploy-readfile2.txtar create mode 100644 lang/interpret_test/TestAstFunc2/deploy-readfile3.txtar create mode 100644 lang/interpret_test/TestAstFunc2/deploy-readfile4.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar rename lang/interpret_test/TestAstFunc2/{test-one-instance.txtar => test-one-instance-bind.txtar} (100%) create mode 100644 lang/interpret_test/TestAstFunc2/test-one-instance-class-arg.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-one-instance-func-arg.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-one-instance-lambda-arg.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-polymorphic-class.txtar create mode 100644 lang/interpret_test/TestAstFunc2/test-polymorphic-func.txtar diff --git a/lang/ast/scopegraph.go b/lang/ast/scopegraph.go index 374633f8..489422fb 100644 --- a/lang/ast/scopegraph.go +++ b/lang/ast/scopegraph.go @@ -195,6 +195,28 @@ func (obj *ExprPoly) ScopeGraph(g *pgraph.Graph) { g.AddEdge(obj, obj.Definition, &pgraph.SimpleEdge{Name: "def"}) } +// ScopeGraph adds nodes and vertices to the supplied graph. +func (obj *ExprTopLevel) ScopeGraph(g *pgraph.Graph) { + g.AddVertex(obj) + definition, ok := obj.Definition.(interfaces.ScopeGrapher) + if !ok { + panic("can't graph scope") // programming error + } + definition.ScopeGraph(g) + g.AddEdge(obj, obj.Definition, &pgraph.SimpleEdge{Name: "def"}) +} + +// ScopeGraph adds nodes and vertices to the supplied graph. +func (obj *ExprSingleton) ScopeGraph(g *pgraph.Graph) { + g.AddVertex(obj) + definition, ok := obj.Definition.(interfaces.ScopeGrapher) + if !ok { + panic("can't graph scope") // programming error + } + definition.ScopeGraph(g) + g.AddEdge(obj, obj.Definition, &pgraph.SimpleEdge{Name: "def"}) +} + // ScopeGraph adds nodes and vertices to the supplied graph. func (obj *ExprIf) ScopeGraph(g *pgraph.Graph) { g.AddVertex(obj) diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 171e1507..f42b341b 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -231,29 +231,18 @@ func (obj *StmtBind) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap return graph, cons, nil } -// SetScope sets the scope of the child expression bound to it. If a variable -// uses the value which this StmtBind binds, they will make a copy and call -// SetScope on the copy. +// SetScope stores the scope for later use in this resource and its children, +// which it propagates this downwards to. func (obj *StmtBind) SetScope(scope *interfaces.Scope) error { - return nil + emptyContext := map[string]interfaces.Expr{} + return obj.Value.SetScope(scope, emptyContext) } // Unify returns the list of invariants that this node produces. It recursively // calls Unify on any children elements that exist in the AST, and returns the // collection to the caller. func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { - // Invariants from an ExprFunc come in from the copy of it in ExprCall. - // We could exclude *all* recursion here, however when multiple ExprVar - // expressions use a bound variable from here, they'd end up calling it - // multiple times so it's better to do it here even if it's not elegant - // symmetrically. - // FIXME: There must be a way to keep this symmetrical, isn't there? - // FIXME: Keep it symmetrical and inefficient for now... - //if _, ok := obj.Value.(*ExprFunc); !ok { - // return obj.Value.Unify() - //} - - return []interfaces.Invariant{}, nil + return obj.Value.Unify() } // Graph returns the reactive function graph which is expressed by this node. It @@ -265,11 +254,9 @@ func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { // the graph. It is not logically done in the ExprVar since that could exist // multiple times for the single binding operation done here. func (obj *StmtBind) Graph() (*pgraph.Graph, error) { - // It seems that adding this to the graph will end up including an - // expression in the case of an ExprFunc lambda, since we copy it and - // build a new ExprFunc when it's used by ExprCall. - //return obj.Value.Graph(nil) // nope! - return pgraph.NewGraph("stmtbind") // empty graph + emptyContext := map[string]interfaces.Func{} + g, _, err := obj.Value.Graph(emptyContext) + return g, err } // Output for the bind statement produces no output. Any values of interest come @@ -3570,8 +3557,10 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { binds[bind.Ident] = struct{}{} // mark as found in scope // add to scope, (overwriting, aka shadowing is ok) - newScope.Variables[bind.Ident] = &ExprPoly{ // XXX: is this ExprPoly approach optimal? - Definition: bind.Value, + newScope.Variables[bind.Ident] = &ExprTopLevel{ + Definition: &ExprSingleton{ + Definition: bind.Value, + }, CapturedScope: newScope, } if obj.data.Debug { // TODO: is this message ever useful? @@ -3610,8 +3599,10 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { fn := fnList[0].Func // local reference to avoid changing it in the loop... // add to scope, (overwriting, aka shadowing is ok) newScope.Functions[name] = &ExprPoly{ // XXX: is this ExprPoly approach optimal? - Definition: fn, // store the *ExprFunc - CapturedScope: newScope, + Definition: &ExprTopLevel{ + Definition: fn, // store the *ExprFunc + CapturedScope: newScope, + }, } continue } @@ -3807,9 +3798,9 @@ func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { if _, ok := x.(*StmtFunc); ok { // TODO: is this correct? continue } - if _, ok := x.(*StmtBind); ok { // TODO: is this correct? - continue - } + //if _, ok := x.(*StmtBind); ok { // TODO: is this correct? + // continue + //} invars, err := x.Unify() if err != nil { @@ -4516,7 +4507,13 @@ func (obj *StmtInclude) SetScope(scope *interfaces.Scope) error { newScope := obj.class.scope.Copy() // Add our args `include foo(42, "bar", true)` into the class scope. for i, arg := range obj.class.Args { // copy - newScope.Variables[arg.Name] = obj.Args[i] + newScope.Variables[arg.Name] = &ExprTopLevel{ + Definition: &ExprSingleton{ + Definition: obj.Args[i], + }, + CapturedScope: newScope, + } + } // recursion detection @@ -7647,13 +7644,8 @@ func (obj *ExprCall) SetScope(scope *interfaces.Scope, sctx map[string]interface } // This call now has the only reference to monomorphicTarget, so - // it is our responsibility to scope-check it. We must use the - // scope which was captured at the definition site, not the - // scope argument we received as input, as that is the scope - // which is available at the call site. - definitionScope := polymorphicTarget.CapturedScope.Copy() - - if err := monomorphicTarget.SetScope(definitionScope, map[string]interfaces.Expr{}); err != nil { + // it is our responsibility to scope-check it. + if err := monomorphicTarget.SetScope(scope, sctx); err != nil { return errwrap.Wrapf(err, "scope-checking the function definition `%s`", prefixedName) } @@ -7807,7 +7799,7 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { } invar := &interfaces.CallFuncArgsValueInvariant{ Expr: obj, - Func: obj.expr, + Func: trueCallee(obj.expr), Args: argsCopy, } invariants = append(invariants, invar) @@ -8355,8 +8347,7 @@ func (obj *ExprVar) SetScope(scope *interfaces.Scope, sctx map[string]interfaces if monomorphicTarget, exists := sctx[obj.Name]; exists { // This ExprVar refers to a parameter bound by an enclosing - // lambda definition. We do _not_ copy the definition, because - // it is already monomorphic. + // lambda definition. obj.scope.Variables[obj.Name] = monomorphicTarget // There is no need to scope-check the target, it's just a @@ -8369,36 +8360,10 @@ func (obj *ExprVar) SetScope(scope *interfaces.Scope, sctx map[string]interfaces return fmt.Errorf("variable %s not in scope", obj.Name) } - if polymorphicTarget, isPolymorphic := target.(*ExprPoly); isPolymorphic { - // This ExprVar refers to a polymorphic expression. Those - // expressions can be instantiated at different types in - // different parts of the program, so the definition we found - // has a "polymorphic" type. - // - // This particular ExprVar is one of the parts of the program - // which uses the polymorphic expression at a single, - // "monomorphic" type. We make a copy of the definition, and - // later each copy will be type-checked separately. - monomorphicTarget, err := polymorphicTarget.Definition.Copy() - if err != nil { - return errwrap.Wrapf(err, "copying the ExprPoly definition to which an ExprVar refers") - } - obj.scope.Variables[obj.Name] = monomorphicTarget - - // This ExprVar now has the only reference to monomorphicTarget, - // so it is our responsibility to scope-check it. We must use - // the scope which was captured at the definition site, not the - // scope argument we received as input, as that is the scope - // which is available at the use site. - definitionScope := polymorphicTarget.CapturedScope.Copy() - - return monomorphicTarget.SetScope(definitionScope, map[string]interfaces.Expr{}) - } - - // This ExprVar refers to a monomorphic expression which has already been - // scope-checked, so we don't need to scope-check it again. obj.scope.Variables[obj.Name] = target + // This ExprVar refers to a top-level definition which has already been + // scope-checked, so we don't need to scope-check it again. return nil } @@ -8504,12 +8469,8 @@ func (obj *ExprVar) Graph(env map[string]interfaces.Func) (*pgraph.Graph, interf return graph, targetFunc, nil } - // The variable points to a top-level expression. The parameters which are - // visible at this use site must not be visible at the definition site, so - // we pass an empty environment. - emptyEnv := map[string]interfaces.Func{} - graph, varFunc, err := targetExpr.Graph(emptyEnv) - return graph, varFunc, err + // The variable points to a top-level expression. + return targetExpr.Graph(env) } // SetValue here is a no-op, because algorithmically when this is called from @@ -8692,8 +8653,7 @@ func (obj *ExprParam) Value() (types.Value, error) { // call SetScope on the copy. We must be careful to use the scope captured at // the definition site, not the scope which is available at the call site. type ExprPoly struct { - Definition interfaces.Expr // The definition. - CapturedScope *interfaces.Scope // The scope at the definition site. + Definition interfaces.Expr // The definition. } // String returns a short representation of this expression. @@ -8729,8 +8689,7 @@ func (obj *ExprPoly) Interpolate() (interfaces.Expr, error) { } return &ExprPoly{ - Definition: definition, - CapturedScope: obj.CapturedScope, + Definition: definition, }, nil } @@ -8749,10 +8708,7 @@ func (obj *ExprPoly) Ordering(produces map[string]interfaces.Node) (*pgraph.Grap // SetScope stores the scope for use in this resource. func (obj *ExprPoly) SetScope(scope *interfaces.Scope, sctx map[string]interfaces.Expr) error { - // Don't recur into the definition yet; instead, capture the scope for later, - // so that ExprVar can call SetScope on the definition at each use site. - obj.CapturedScope = scope - return nil + panic("ExprPoly.SetScope(): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling SetScope") } // SetType is used to set the type of this expression once it is known. This @@ -8802,6 +8758,369 @@ func (obj *ExprPoly) Value() (types.Value, error) { return nil, fmt.Errorf("no value for ExprPoly") } +// ExprTopLevel is intended to wrap top-level definitions. It captures the +// variables which are in scope at the the top-level, so that when use sites +// call ExprTopLevel.SetScope() with the variables which are in scope at the use +// site, ExprTopLevel can automatically correct this by using the variables +// which are in scope at the definition site. +type ExprTopLevel struct { + Definition interfaces.Expr // The definition. + CapturedScope *interfaces.Scope // The scope at the definition site. +} + +// String returns a short representation of this expression. +func (obj *ExprTopLevel) String() string { + return fmt.Sprintf("topLevel(%s)", obj.Definition.String()) +} + +// Apply is a general purpose iterator method that operates on any AST node. It +// is not used as the primary AST traversal function because it is less readable +// and easy to reason about than manually implementing traversal for each node. +// Nevertheless, it is a useful facility for operations that might only apply to +// a select number of node types, since they won't need extra noop iterators... +func (obj *ExprTopLevel) Apply(fn func(interfaces.Node) error) error { + if err := obj.Definition.Apply(fn); err != nil { + return err + } + return fn(obj) +} + +// Init initializes this branch of the AST, and returns an error if it fails to +// validate. +func (obj *ExprTopLevel) Init(data *interfaces.Data) error { + return obj.Definition.Init(data) +} + +// 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 +// on any child elements and builds the new node with those new node contents. +func (obj *ExprTopLevel) Interpolate() (interfaces.Expr, error) { + definition, err := obj.Definition.Interpolate() + if err != nil { + return nil, err + } + + return &ExprTopLevel{ + Definition: definition, + CapturedScope: obj.CapturedScope, + }, nil +} + +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprTopLevel) Copy() (interfaces.Expr, error) { + definition, err := obj.Definition.Copy() + if err != nil { + return nil, err + } + + return &ExprTopLevel{ + Definition: definition, + CapturedScope: obj.CapturedScope, + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprTopLevel) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, err + } + graph.AddVertex(obj) + + // Additional constraint: We know the Definition has to be satisfied before + // this ExprTopLevel expression itself can be used, since ExprTopLevel + // delegates to the Definition. + edge := &pgraph.SimpleEdge{Name: "exprtoplevel"} + graph.AddEdge(obj.Definition, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Definition.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprtopleveldefinition"} + graph.AddEdge(n, k, edge) + } + + return graph, cons, nil +} + +// SetScope stores the scope for use in this resource. +func (obj *ExprTopLevel) SetScope(scope *interfaces.Scope, sctx map[string]interfaces.Expr) error { + // Use the scope captured at the definition site. The parameters from + // functions enclosing the use site are not visible at the top-level either, + // so we must clear sctx. + return obj.Definition.SetScope(obj.CapturedScope, make(map[string]interfaces.Expr)) +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprTopLevel) SetType(typ *types.Type) error { + return obj.Definition.SetType(typ) +} + +// Type returns the type of this expression. +func (obj *ExprTopLevel) Type() (*types.Type, error) { + return obj.Definition.Type() +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprTopLevel) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + var invar interfaces.Invariant + + invars, err := obj.Definition.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + invar = &interfaces.EqualityInvariant{ + Expr1: obj, + Expr2: obj.Definition, + } + invariants = append(invariants, invar) + + // We don't want this to have it's SetType run in the unified solution. + invar = &interfaces.SkipInvariant{ + Expr: obj, + } + invariants = append(invariants, invar) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. +func (obj *ExprTopLevel) Graph(env map[string]interfaces.Func) (*pgraph.Graph, interfaces.Func, error) { + return obj.Definition.Graph(env) +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child fields (the dest lookup expr) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprTopLevel) SetValue(value types.Value) error { + return obj.Definition.SetValue(value) +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +func (obj *ExprTopLevel) Value() (types.Value, error) { + return obj.Definition.Value() +} + +// ExprSingleton is intended to wrap top-level variable definitions. It ensures +// that a single Func is created even if multiple use sites call +// ExprSingleton.Graph(). +type ExprSingleton struct { + Definition interfaces.Expr + + singletonGraph *pgraph.Graph + singletonExpr interfaces.Func +} + +// String returns a short representation of this expression. +func (obj *ExprSingleton) String() string { + return fmt.Sprintf("singleton(%s)", obj.Definition.String()) +} + +// Apply is a general purpose iterator method that operates on any AST node. It +// is not used as the primary AST traversal function because it is less readable +// and easy to reason about than manually implementing traversal for each node. +// Nevertheless, it is a useful facility for operations that might only apply to +// a select number of node types, since they won't need extra noop iterators... +func (obj *ExprSingleton) Apply(fn func(interfaces.Node) error) error { + if err := obj.Definition.Apply(fn); err != nil { + return err + } + return fn(obj) +} + +// Init initializes this branch of the AST, and returns an error if it fails to +// validate. +func (obj *ExprSingleton) Init(data *interfaces.Data) error { + return obj.Definition.Init(data) +} + +// 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 +// on any child elements and builds the new node with those new node contents. +func (obj *ExprSingleton) Interpolate() (interfaces.Expr, error) { + definition, err := obj.Definition.Interpolate() + if err != nil { + return nil, err + } + + return &ExprSingleton{ + Definition: definition, + singletonGraph: nil, // each copy should have its own Graph + singletonExpr: nil, // each copy should have its own Func + }, nil +} + +// Copy returns a light copy of this struct. Anything static will not be copied. +func (obj *ExprSingleton) Copy() (interfaces.Expr, error) { + definition, err := obj.Definition.Copy() + if err != nil { + return nil, err + } + + return &ExprSingleton{ + Definition: definition, + singletonGraph: nil, // each copy should have its own Graph + singletonExpr: nil, // each copy should have its own Func + }, nil +} + +// Ordering returns a graph of the scope ordering that represents the data flow. +// This can be used in SetScope so that it knows the correct order to run it in. +func (obj *ExprSingleton) Ordering(produces map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) { + graph, err := pgraph.NewGraph("ordering") + if err != nil { + return nil, nil, err + } + graph.AddVertex(obj) + + // Additional constraint: We know the Definition has to be satisfied before + // this ExprSingleton expression itself can be used, since ExprSingleton + // delegates to the Definition. + edge := &pgraph.SimpleEdge{Name: "exprsingleton"} + graph.AddEdge(obj.Definition, obj, edge) // prod -> cons + + cons := make(map[interfaces.Node]string) + + g, c, err := obj.Definition.Ordering(produces) + if err != nil { + return nil, nil, err + } + graph.AddGraph(g) // add in the child graph + + for k, v := range c { // c is consumes + x, exists := cons[k] + if exists && v != x { + return nil, nil, fmt.Errorf("consumed value is different, got `%+v`, expected `%+v`", x, v) + } + cons[k] = v // add to map + + n, exists := produces[v] + if !exists { + continue + } + edge := &pgraph.SimpleEdge{Name: "exprsingletondefinition"} + graph.AddEdge(n, k, edge) + } + + return graph, cons, nil +} + +// SetScope stores the scope for use in this resource. +func (obj *ExprSingleton) SetScope(scope *interfaces.Scope, sctx map[string]interfaces.Expr) error { + return obj.Definition.SetScope(scope, sctx) +} + +// SetType is used to set the type of this expression once it is known. This +// usually happens during type unification, but it can also happen during +// parsing if a type is specified explicitly. Since types are static and don't +// change on expressions, if you attempt to set a different type than what has +// previously been set (when not initially known) this will error. +func (obj *ExprSingleton) SetType(typ *types.Type) error { + return obj.Definition.SetType(typ) +} + +// Type returns the type of this expression. +func (obj *ExprSingleton) Type() (*types.Type, error) { + return obj.Definition.Type() +} + +// Unify returns the list of invariants that this node produces. It recursively +// calls Unify on any children elements that exist in the AST, and returns the +// collection to the caller. +func (obj *ExprSingleton) Unify() ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + var invar interfaces.Invariant + + invars, err := obj.Definition.Unify() + if err != nil { + return nil, err + } + invariants = append(invariants, invars...) + + invar = &interfaces.EqualityInvariant{ + Expr1: obj, + Expr2: obj.Definition, + } + invariants = append(invariants, invar) + + // We don't want this to have it's SetType run in the unified solution. + invar = &interfaces.SkipInvariant{ + Expr: obj, + } + invariants = append(invariants, invar) + + return invariants, nil +} + +// Graph returns the reactive function graph which is expressed by this node. It +// includes any vertices produced by this node, and the appropriate edges to any +// vertices that are produced by its children. Nodes which fulfill the Expr +// interface directly produce vertices (and possible children) where as nodes +// that fulfill the Stmt interface do not produces vertices, where as their +// children might. +func (obj *ExprSingleton) Graph(env map[string]interfaces.Func) (*pgraph.Graph, interfaces.Func, error) { + if obj.singletonExpr == nil { + g, f, err := obj.Definition.Graph(env) + if err != nil { + return nil, nil, err + } + obj.singletonGraph = g + obj.singletonExpr = f + return g, f, nil + } + + return obj.singletonGraph, obj.singletonExpr, nil +} + +// SetValue here is a no-op, because algorithmically when this is called from +// the func engine, the child fields (the dest lookup expr) will have had this +// done to them first, and as such when we try and retrieve the set value from +// this expression by calling `Value`, it will build it from scratch! +func (obj *ExprSingleton) SetValue(value types.Value) error { + return obj.Definition.SetValue(value) +} + +// Value returns the value of this expression in our type system. This will +// usually only be valid once the engine has run and values have been produced. +// This might get called speculatively (early) during unification to learn more. +func (obj *ExprSingleton) Value() (types.Value, error) { + return obj.Definition.Value() +} + // ExprIf represents an if expression which *must* have both branches, and which // 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. @@ -9250,3 +9569,18 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) { return nil, fmt.Errorf("unexpected: %+v", node) } } + +// trueCallee is a helper function because ExprTopLevel and ExprSingleton are +// sometimes added around builtins. This makes it difficult for the type checker +// to check if a particular builtin is the callee or not. This function removes +// the ExprTopLevel and ExprSingleton wrappers, if they exist. +func trueCallee(apparentCallee interfaces.Expr) interfaces.Expr { + switch x := apparentCallee.(type) { + case *ExprTopLevel: + return trueCallee(x.Definition) + case *ExprSingleton: + return trueCallee(x.Definition) + default: + return apparentCallee + } +} diff --git a/lang/ast/util.go b/lang/ast/util.go index 438c4481..f35956f0 100644 --- a/lang/ast/util.go +++ b/lang/ast/util.go @@ -77,8 +77,10 @@ func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr { exprPolys := make(map[string]interfaces.Expr) for name, expr := range exprs { exprPolys[name] = &ExprPoly{ - Definition: expr, - CapturedScope: interfaces.EmptyScope(), + Definition: &ExprTopLevel{ + Definition: expr, + CapturedScope: interfaces.EmptyScope(), + }, } } @@ -97,7 +99,12 @@ func VarPrefixToVariablesScope(prefix string) map[string]interfaces.Expr { if err != nil { panic(fmt.Sprintf("could not build expr: %+v", err)) } - exprs[name] = expr + exprs[name] = &ExprTopLevel{ + Definition: &ExprSingleton{ + Definition: expr, + }, + CapturedScope: interfaces.EmptyScope(), + } } return exprs } diff --git a/lang/interpret_test/TestAstFunc1/doubleclass.txtar b/lang/interpret_test/TestAstFunc1/doubleclass.txtar index 96b7398e..22983e34 100644 --- a/lang/interpret_test/TestAstFunc1/doubleclass.txtar +++ b/lang/interpret_test/TestAstFunc1/doubleclass.txtar @@ -59,7 +59,3 @@ Vertex: const Vertex: const Vertex: const Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const diff --git a/lang/interpret_test/TestAstFunc1/doubleinclude.txtar b/lang/interpret_test/TestAstFunc1/doubleinclude.txtar index 0be2e1b4..2036f7e1 100644 --- a/lang/interpret_test/TestAstFunc1/doubleinclude.txtar +++ b/lang/interpret_test/TestAstFunc1/doubleinclude.txtar @@ -11,4 +11,3 @@ $foo = "hey" Vertex: const Vertex: const Vertex: const -Vertex: const diff --git a/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar b/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar index e6249fc9..513007e0 100644 --- a/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar +++ b/lang/interpret_test/TestAstFunc1/efficient-lambda.txtar @@ -13,7 +13,6 @@ test $out2 {} # hellob Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Vertex: FuncValue -Vertex: FuncValue Vertex: call Vertex: call Vertex: const diff --git a/lang/interpret_test/TestAstFunc1/lambda-chained.txtar b/lang/interpret_test/TestAstFunc1/lambda-chained.txtar index e4b299d2..65578e3c 100644 --- a/lang/interpret_test/TestAstFunc1/lambda-chained.txtar +++ b/lang/interpret_test/TestAstFunc1/lambda-chained.txtar @@ -14,12 +14,7 @@ test $out2 {} -- OUTPUT -- Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: call Vertex: call Vertex: call Vertex: const -Vertex: const diff --git a/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar b/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar index c5c3b052..a522b189 100644 --- a/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar +++ b/lang/interpret_test/TestAstFunc1/simple-lambda2.txtar @@ -16,7 +16,6 @@ Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Vertex: FuncValue Vertex: FuncValue -Vertex: FuncValue Vertex: call Vertex: call Vertex: call diff --git a/lang/interpret_test/TestAstFunc1/slow_unification0.txtar b/lang/interpret_test/TestAstFunc1/slow_unification0.txtar index 43d1a1f4..274d77fd 100644 --- a/lang/interpret_test/TestAstFunc1/slow_unification0.txtar +++ b/lang/interpret_test/TestAstFunc1/slow_unification0.txtar @@ -59,18 +59,6 @@ Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Edge: FuncValue -> call # fn -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue Vertex: FuncValue Vertex: FuncValue Vertex: FuncValue @@ -85,30 +73,6 @@ Vertex: call Vertex: call Vertex: call Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: call -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const -Vertex: const Vertex: const Vertex: const Vertex: const diff --git a/lang/interpret_test/TestAstFunc1/static-function0.txtar b/lang/interpret_test/TestAstFunc1/static-function0.txtar index 9e824c8e..b93e310d 100644 --- a/lang/interpret_test/TestAstFunc1/static-function0.txtar +++ b/lang/interpret_test/TestAstFunc1/static-function0.txtar @@ -20,8 +20,6 @@ Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue Vertex: call Vertex: call Vertex: call diff --git a/lang/interpret_test/TestAstFunc1/static-function1.txtar b/lang/interpret_test/TestAstFunc1/static-function1.txtar index da23a96a..91a91f34 100644 --- a/lang/interpret_test/TestAstFunc1/static-function1.txtar +++ b/lang/interpret_test/TestAstFunc1/static-function1.txtar @@ -22,8 +22,6 @@ Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Edge: FuncValue -> call # fn Vertex: FuncValue -Vertex: FuncValue -Vertex: FuncValue Vertex: call Vertex: call Vertex: call diff --git a/lang/interpret_test/TestAstFunc2/clear-env-on-var.txtar b/lang/interpret_test/TestAstFunc2/clear-env-on-var.txtar index 1e9ec3ac..a51eea0d 100644 --- a/lang/interpret_test/TestAstFunc2/clear-env-on-var.txtar +++ b/lang/interpret_test/TestAstFunc2/clear-env-on-var.txtar @@ -6,4 +6,4 @@ $f = func($x) { test $f("foo") {} -- OUTPUT -- -# err: errSetScope: scope-checking the function definition `$f`: failed to set scope on function body: variable x not in scope +# err: errSetScope: variable x not in scope diff --git a/lang/interpret_test/TestAstFunc2/deploy-readfile2.txtar b/lang/interpret_test/TestAstFunc2/deploy-readfile2.txtar new file mode 100644 index 00000000..173e5df4 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/deploy-readfile2.txtar @@ -0,0 +1,49 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "golang/strings" +import "deploy" +import "second.mcl" + +#$f1 = "/metadata.yaml" # works +#$f1 = "/main.mcl" # works +$f1 = "/files/file1" + +$f2 = "/files/file2" + +# the abspath method shouldn't be used often, it's here for testing... +if $f1 != deploy.abspath($f1) { # should be the same, since we're in the same dir + test "f1 error" {} +} +if $f2 != $second.f2 { + test "f2 error" {} +} + +# the readfileabs method shouldn't be used often, it's here for testing... +$x1 = deploy.readfileabs($f1) +$x2 = deploy.readfileabs($f2) + +if $x1 != deploy.readfile($f1) { + test "x1 error" {} +} +if $x2 != $second.x2 { + test "x2 error" {} +} + +# hide the newlines from our output +test strings.trim_space($x1) {} +test strings.trim_space($x2) {} +-- second.mcl -- +import "deploy" + +# relative paths for us +$f = "/files/file2" # real file is here as well +$f2 = deploy.abspath($f) +$x2 = deploy.readfile($f) +-- files/file1 -- +This is file1 in the files/ folder. +-- files/file2 -- +This is file2 in the files/ folder. +-- OUTPUT -- +Vertex: test[This is file1 in the files/ folder.] +Vertex: test[This is file2 in the files/ folder.] diff --git a/lang/interpret_test/TestAstFunc2/deploy-readfile3.txtar b/lang/interpret_test/TestAstFunc2/deploy-readfile3.txtar new file mode 100644 index 00000000..9698cffd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/deploy-readfile3.txtar @@ -0,0 +1,26 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "golang/strings" +import "deploy" + +$f1 = "/files/file1" + +# the abspath method shouldn't be used often, it's here for testing... +if $f1 != deploy.abspath($f1) { # should be the same, since we're in the same dir + test "f1 error" {} +} + +# the readfileabs method shouldn't be used often, it's here for testing... +$x1 = deploy.readfileabs($f1) + +if $x1 != deploy.readfile($f1) { + test "x1 error" {} +} + +# hide the newlines from our output +test strings.trim_space($x1) {} +-- files/file1 -- +This is file1 in the files/ folder. +-- OUTPUT -- +Vertex: test[This is file1 in the files/ folder.] diff --git a/lang/interpret_test/TestAstFunc2/deploy-readfile4.txtar b/lang/interpret_test/TestAstFunc2/deploy-readfile4.txtar new file mode 100644 index 00000000..e7a0633e --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/deploy-readfile4.txtar @@ -0,0 +1,17 @@ +-- metadata.yaml -- +#files: "files/" # these are some extra files we can use (is the default) +-- main.mcl -- +import "deploy" + +$f1 = "/files/file1" + +# the readfileabs method shouldn't be used often, it's here for testing... +$x1 = deploy.readfileabs($f1) + +# hide the newlines from our output +test $x1 {} +-- files/file1 -- +This is file1 in the files/ folder. +-- OUTPUT -- +Vertex: test[This is file1 in the files/ folder. +] diff --git a/lang/interpret_test/TestAstFunc2/lookup3.txtar b/lang/interpret_test/TestAstFunc2/lookup3.txtar index e3ff16ee..f926a587 100644 --- a/lang/interpret_test/TestAstFunc2/lookup3.txtar +++ b/lang/interpret_test/TestAstFunc2/lookup3.txtar @@ -11,4 +11,4 @@ $st = struct{ test $st->missing + "fail" {} # this can't unify! -- OUTPUT -- -# err: errUnify: 2 unconsumed generators +# err: errUnify: 1 unconsumed generators diff --git a/lang/interpret_test/TestAstFunc2/printfempty.txtar b/lang/interpret_test/TestAstFunc2/printfempty.txtar index 1b8b5f7d..a4dd3b08 100644 --- a/lang/interpret_test/TestAstFunc2/printfempty.txtar +++ b/lang/interpret_test/TestAstFunc2/printfempty.txtar @@ -7,4 +7,4 @@ import "fmt" test fmt.printf() {} -- OUTPUT -- -# err: errUnify: 2 unconsumed generators +# err: errUnify: 1 unconsumed generators diff --git a/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar b/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar index a90c2b2a..18b352bd 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: 2 unconsumed generators +# err: errUnify: 1 unconsumed generators diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar new file mode 100644 index 00000000..bceb61a7 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +# $id could theoretically have type func(int) int or func(str) str, but it +# can't be both because it is bound to a variable, which must have a single +# type. +$id = func($x) {$x} +test "test1" { + int8 => $id(42), +} +test "test2" { + anotherstr => $id("hello"), +} +-- OUTPUT -- +# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar new file mode 100644 index 00000000..79c21749 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar @@ -0,0 +1,15 @@ +-- main.mcl -- +# $id could theoretically have type func(int) int or func(str) str, but it +# can't be both because it is bound to a class parameter, which must have a +# single type. +class use_polymorphically($id) { + test "test1" { + int8 => $id(42), + } + test "test2" { + anotherstr => $id("hello"), + } +} +include use_polymorphically(func($x) {$x}) +-- OUTPUT -- +# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar new file mode 100644 index 00000000..fa2ef042 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +import "fmt" +# $id could theoretically have type func(int) int or func(str) str, but it +# can't be both because it is bound to a class parameter, which must have a +# single type. +func use_polymorphically($id) { + fmt.printf("%d %s", $id(42), $id("hello")) +} +test "test1" { + anotherstr => use_polymorphically(func($x) {$x}), +} +-- OUTPUT -- +# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar new file mode 100644 index 00000000..91359e19 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar @@ -0,0 +1,13 @@ +-- main.mcl -- +import "fmt" +# $id could theoretically have type func(int) int or func(str) str, but it +# can't be both because it is bound to a lambda parameter, which must have a +# single type. +$use_polymorphically = func($id) { + fmt.printf("%d %s", $id(42), $id("hello")) +} +test "test1" { + anotherstr => $use_polymorphically(func($x) {$x}), +} +-- OUTPUT -- +# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) diff --git a/lang/interpret_test/TestAstFunc2/test-one-instance.txtar b/lang/interpret_test/TestAstFunc2/test-one-instance-bind.txtar similarity index 100% rename from lang/interpret_test/TestAstFunc2/test-one-instance.txtar rename to lang/interpret_test/TestAstFunc2/test-one-instance-bind.txtar diff --git a/lang/interpret_test/TestAstFunc2/test-one-instance-class-arg.txtar b/lang/interpret_test/TestAstFunc2/test-one-instance-class-arg.txtar new file mode 100644 index 00000000..25a884d0 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-one-instance-class-arg.txtar @@ -0,0 +1,22 @@ +-- main.mcl -- +import "test" + +class use_twice($test1, $test2, $x) { + test $test1 { + anotherstr => $x, + } + test $test2 { + anotherstr => $x, + } +} + +# one_instance_a should only produce one value, and will error if initialized twice +include use_twice("test1", "test2", test.one_instance_a()) + +# one_instance_b should only produce one value, and will error if initialized twice +include use_twice("test3", "test4", test.one_instance_b()) +-- OUTPUT -- +Vertex: test[test1] +Vertex: test[test2] +Vertex: test[test3] +Vertex: test[test4] diff --git a/lang/interpret_test/TestAstFunc2/test-one-instance-func-arg.txtar b/lang/interpret_test/TestAstFunc2/test-one-instance-func-arg.txtar new file mode 100644 index 00000000..4bbcd173 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-one-instance-func-arg.txtar @@ -0,0 +1,19 @@ +-- main.mcl -- +import "test" + +func double($x) { + $x + $x +} + +# one_instance_a should only produce one value, and will error if initialized twice +test "test1" { + anotherstr => double(test.one_instance_a()), +} + +# one_instance_b should only produce one value, and will error if initialized twice +test "test2" { + anotherstr => double(test.one_instance_b()), +} +-- OUTPUT -- +Vertex: test[test1] +Vertex: test[test2] diff --git a/lang/interpret_test/TestAstFunc2/test-one-instance-lambda-arg.txtar b/lang/interpret_test/TestAstFunc2/test-one-instance-lambda-arg.txtar new file mode 100644 index 00000000..b189b1a8 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-one-instance-lambda-arg.txtar @@ -0,0 +1,19 @@ +-- main.mcl -- +import "test" + +$double = func($x) { + $x + $x +} + +# one_instance_a should only produce one value, and will error if initialized twice +test "test1" { + anotherstr => $double(test.one_instance_a()), +} + +# one_instance_b should only produce one value, and will error if initialized twice +test "test2" { + anotherstr => $double(test.one_instance_b()), +} +-- OUTPUT -- +Vertex: test[test1] +Vertex: test[test2] diff --git a/lang/interpret_test/TestAstFunc2/test-polymorphic-class.txtar b/lang/interpret_test/TestAstFunc2/test-polymorphic-class.txtar new file mode 100644 index 00000000..b9bf1a45 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-polymorphic-class.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +import "fmt" +class double($to_str, $x) { + test $to_str($x + $x) {} +} +include double(func($x) {fmt.printf("%d", $x)}, 42) +include double(func($x) {fmt.printf("%s", $x)}, "hello") +-- OUTPUT -- +Vertex: test[84] +Vertex: test[hellohello] diff --git a/lang/interpret_test/TestAstFunc2/test-polymorphic-func.txtar b/lang/interpret_test/TestAstFunc2/test-polymorphic-func.txtar new file mode 100644 index 00000000..b2226e19 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/test-polymorphic-func.txtar @@ -0,0 +1,10 @@ +-- main.mcl -- +import "fmt" +func double($x) { + $x + $x +} +test fmt.printf("%d", double(42)) {} +test double("hello") {} +-- OUTPUT -- +Vertex: test[84] +Vertex: test[hellohello]