lang: Improve graph shape with speculative execution

Most of the time, we don't need to have a dynamic call sub graph, since
the actual function call could be represented statically as it
originally was before lambda functions were implemented. Simplifying the
graph shape has important performance benefits in terms of both keep the
graph smaller (memory, etc) and in avoiding the need to run transactions
at runtime (speed) to reshape the graph.

Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
This commit is contained in:
James Shubin
2025-03-17 02:31:01 -04:00
parent 9c9f2f558a
commit 37bb67dffd
139 changed files with 1871 additions and 262 deletions

View File

@@ -321,6 +321,11 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) {
return expr.scope, nil
case *ExprVar:
return expr.scope, nil
//case *ExprParam:
//case *ExprIterated:
//case *ExprPoly:
//case *ExprTopLevel:
//case *ExprSingleton:
case *ExprIf:
return expr.scope, nil
@@ -329,6 +334,124 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) {
}
}
// CheckParamScope ensures that only the specified ExprParams are free in the
// expression. It is used for graph shape function speculation. This could have
// been an addition to the interfaces.Expr interface, but since it's mostly
// iteration, it felt cleaner like this.
// TODO: Can we replace this with a call to Apply instead.
func checkParamScope(node interfaces.Expr, freeVars map[interfaces.Expr]struct{}) error {
switch obj := node.(type) {
case *ExprBool:
return nil
case *ExprStr:
return nil
case *ExprInt:
return nil
case *ExprFloat:
return nil
case *ExprList:
for _, x := range obj.Elements {
if err := checkParamScope(x, freeVars); err != nil {
return err
}
}
return nil
case *ExprMap:
for _, x := range obj.KVs {
if err := checkParamScope(x.Key, freeVars); err != nil {
return err
}
if err := checkParamScope(x.Val, freeVars); err != nil {
return err
}
}
return nil
case *ExprStruct:
for _, x := range obj.Fields {
if err := checkParamScope(x.Value, freeVars); err != nil {
return err
}
}
return nil
case *ExprFunc:
if obj.Body != nil {
newFreeVars := make(map[interfaces.Expr]struct{})
for k, v := range freeVars {
newFreeVars[k] = v
}
for _, param := range obj.params {
newFreeVars[param] = struct{}{}
}
if err := checkParamScope(obj.Body, newFreeVars); err != nil {
return err
}
}
// XXX: Do we need to do anything for obj.Function ?
// XXX: Do we need to do anything for obj.Values ?
return nil
case *ExprCall:
if obj.expr != nil {
if err := checkParamScope(obj.expr, freeVars); err != nil {
return err
}
}
for _, x := range obj.Args {
if err := checkParamScope(x, freeVars); err != nil {
return err
}
}
return nil
case *ExprVar:
// XXX: is this still correct?
target := obj.scope.Variables[obj.Name]
return checkParamScope(target, freeVars)
case *ExprParam:
if _, exists := freeVars[obj]; !exists {
return fmt.Errorf("the body uses parameter $%s", obj.Name)
}
return nil
case *ExprIterated:
return checkParamScope(obj.Definition, freeVars) // XXX: is this what we want?
case *ExprPoly:
panic("checkParamScope(ExprPoly): should not happen, ExprVar should replace ExprPoly with a copy of its definition before calling checkParamScope")
case *ExprTopLevel:
return checkParamScope(obj.Definition, freeVars)
case *ExprSingleton:
return checkParamScope(obj.Definition, freeVars)
case *ExprIf:
if err := checkParamScope(obj.Condition, freeVars); err != nil {
return err
}
if err := checkParamScope(obj.ThenBranch, freeVars); err != nil {
return err
}
if err := checkParamScope(obj.ElseBranch, freeVars); err != nil {
return err
}
return nil
default:
return 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