lang: Add a for loop statement for iterating over a list

This adds a for statement which is used to iterate over a list with a
body of statements. This is an important data transformation tool which
should be used sparingly, but is important to have.

An import statement inside of a for loop is not currently supported. We
have a simple hack to detect the obvious cases, but more deeply nested
scenarios probably won't be caught, and you'll get an obscure error
message if you try to do this.

This was incredibly challenging to get right, and it's all thanks to Sam
for his brilliance.

Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
This commit is contained in:
James Shubin
2025-03-04 21:55:29 -05:00
parent c456a5ab97
commit cf7e73bbf6
63 changed files with 2814 additions and 203 deletions

View File

@@ -85,8 +85,9 @@ type Stmt interface {
// child statements, and Infer/Check for child expressions.
TypeCheck() ([]*UnificationInvariant, error)
// Graph returns the reactive function graph expressed by this node.
Graph() (*pgraph.Graph, error)
// Graph returns the reactive function graph expressed by this node. It
// takes in the environment of any functions in scope.
Graph(env *Env) (*pgraph.Graph, error)
// Output returns the output that this "program" produces. This output
// is what is used to build the output graph. It requires the input
@@ -148,7 +149,7 @@ type Expr interface {
// Graph returns the reactive function graph expressed by this node. It
// takes in the environment of any functions in scope. It also returns
// the function for this node.
Graph(env map[string]Func) (*pgraph.Graph, Func, error)
Graph(env *Env) (*pgraph.Graph, Func, error)
// SetValue stores the result of the last computation of this expression
// node.
@@ -277,9 +278,21 @@ func (obj *Data) AbsFilename() string {
// An interesting note about these is that they exist in a distinct namespace
// from the variables, which could actually contain lambda functions.
type Scope struct {
// Variables maps the scope of name to Expr.
Variables map[string]Expr
Functions map[string]Expr // the Expr will usually be an *ExprFunc (actually it's usually (or always) an *ExprSingleton, which wraps an *ExprFunc now)
Classes map[string]Stmt
// Functions is the scope of functions.
//
// The Expr will usually be an *ExprFunc. (Actually it's usually or
// always an *ExprSingleton, which wraps an *ExprFunc now.)
Functions map[string]Expr
// Classes map the name of the class to the class.
Classes map[string]Stmt
// Iterated is a flag that is true if this scope is inside of a for
// loop.
Iterated bool
Chain []Node // chain of previously seen node's
}
@@ -291,6 +304,7 @@ func EmptyScope() *Scope {
Variables: make(map[string]Expr),
Functions: make(map[string]Expr),
Classes: make(map[string]Stmt),
Iterated: false,
Chain: []Node{},
}
}
@@ -307,6 +321,7 @@ func (obj *Scope) Copy() *Scope {
variables := make(map[string]Expr)
functions := make(map[string]Expr)
classes := make(map[string]Stmt)
iterated := obj.Iterated
chain := []Node{}
for k, v := range obj.Variables { // copy
@@ -326,6 +341,7 @@ func (obj *Scope) Copy() *Scope {
Variables: variables,
Functions: functions,
Classes: classes,
Iterated: iterated,
Chain: chain,
}
}
@@ -377,6 +393,10 @@ func (obj *Scope) Merge(scope *Scope) error {
obj.Classes[name] = scope.Classes[name]
}
if scope.Iterated { // XXX: how should we merge this?
obj.Iterated = scope.Iterated
}
return err
}
@@ -398,6 +418,94 @@ func (obj *Scope) IsEmpty() bool {
return true
}
// Env is an environment which contains the relevant mappings. This is used at
// the Graph(...) stage of the compiler. It does not contain classes.
type Env struct {
// Variables map and Expr to a *FuncSingleton which deduplicates the
// use of a function.
Variables map[Expr]*FuncSingleton
// Functions contains the captured environment, because when we're
// recursing into a StmtFunc which is defined inside a for loop, we can
// use that to get the right Env.Variables map. As for the function
// itself, it's the same in each loop iteration, therefore, we find it
// in obj.expr of ExprCall. (Functions map[string]*Env) But actually,
// our new version is now this:
Functions map[Expr]*Env
}
// EmptyEnv returns the zero, empty value for the scope, with all the internal
// lists initialized appropriately.
func EmptyEnv() *Env {
return &Env{
Variables: make(map[Expr]*FuncSingleton),
Functions: make(map[Expr]*Env),
}
}
// Copy makes a copy of the Env struct. This ensures that if the internal maps
// are changed, it doesn't affect other copies of the Env. It does *not* copy or
// change the pointers contained within, since these are references, and we need
// those to be consistently pointing to the same things after copying.
func (obj *Env) Copy() *Env {
if obj == nil { // allow copying nil envs
return EmptyEnv()
}
variables := make(map[Expr]*FuncSingleton)
functions := make(map[Expr]*Env)
for k, v := range obj.Variables { // copy
variables[k] = v // we don't copy the func's!
}
for k, v := range obj.Functions { // copy
functions[k] = v // we don't copy the generator func's
}
return &Env{
Variables: variables,
Functions: functions,
}
}
// FuncSingleton is a singleton system for storing a singleton func and its
// corresponding graph. You must pass in a `MakeFunc` builder method to generate
// these. The graph which is returned from this must contain that Func as a
// node.
type FuncSingleton struct {
// MakeFunc builds and returns a Func and a graph that it must be
// contained within.
// XXX: Add Txn as an input arg?
MakeFunc func() (*pgraph.Graph, Func, error)
g *pgraph.Graph
f Func
}
// GraphFunc returns the previously saved graph and func if they exist. If they
// do not, then it calls the MakeFunc method to get them, and saves a copy for
// next time.
// XXX: Add Txn as an input arg?
func (obj *FuncSingleton) GraphFunc() (*pgraph.Graph, Func, error) {
// If obj.f already exists, just use that.
if obj.f != nil { // && obj.g != nil
return obj.g, obj.f, nil
}
var err error
obj.g, obj.f, err = obj.MakeFunc() // XXX: Add Txn as an input arg?
if err != nil {
return nil, nil, err
}
if obj.g == nil {
return nil, nil, fmt.Errorf("unexpected nil graph")
}
if obj.f == nil {
return nil, nil, fmt.Errorf("unexpected nil function")
}
return obj.g, obj.f, nil
}
// Arg represents a name identifier for a func or class argument declaration and
// is sometimes accompanied by a type. This does not satisfy the Expr interface.
type Arg struct {