Name binding and variables

After a Starlark file is parsed, but before its execution begins, the Starlark interpreter checks statically that the program is well formed. For example, break and continue statements may appear only within a loop; a return statement may appear only within a function; and load statements may appear only outside any function.

Name resolution is the static checking process that resolves names to variable bindings. During execution, names refer to variables. Statically, names denote places in the code where variables are created; these places are called bindings. A name may denote different bindings at different places in the program. The region of text in which a particular name refers to the same binding is called that binding’s scope.

Four Starlark constructs bind names, as illustrated in the example below: load statements (a and b), def statements (c), function parameters (d), and assignments (e, h, including the augmented assignment e += 1). Variables may be assigned or re-assigned explicitly (e, h), or implicitly, as in a for-loop (f) or comprehension (g, i).

load("lib.star", "a", b="B")

def c(d):
  e = 0
  for f in d:
     print([True for g in f])
     e += 1

h = [2*i for i in a]

The environment of a Starlark program is structured as a tree of lexical blocks, each of which may contain name bindings. The tree of blocks is parallel to the syntax tree. Blocks are of five kinds.

At the root of the tree is the predeclared block, which binds several names implicitly. The set of predeclared names includes the universal constant values None, True, and False, and various built-in functions such as len and list; these functions are immutable and stateless. An application may pre-declare additional names to provide domain-specific functions to that file, for example. These additional functions may have side effects on the application. Starlark programs cannot change the set of predeclared bindings or assign new values to them.

Nested beneath the predeclared block is the module block, which contains the bindings of the current module. Bindings in the module block (such as c, and h in the example) are called global and may be visible to other modules. The module block is empty at the start of the file and is populated by top-level binding statements.

Nested beneath the module block is the file block, which contains bindings local to the current file. Names in this block (such as a and b in the example) are bound only by load statements. The sets of names bound in the file block and in the module block do not overlap: it is an error for a load statement to bind the name of a global, or for a top-level statement to assign to a name bound by a load statement.

A file block contains a function block for each top-level function, and a comprehension block for each top-level comprehension. Bindings in either of these kinds of block, and in the file block itself, are called local. (In the example, the bindings for e, f, g, and i are all local.) Additional functions and comprehensions, and their blocks, may be nested in any order, to any depth.

If name is bound anywhere within a block, all uses of the name within the block are treated as references to that binding, even if the use appears before the binding. This is true even at the top level, unlike Python. The binding of y on the last line of the example below makes y local to the function hello, so the use of y in the print statement also refers to the local y, even though it appears earlier.

y = "goodbye"

def hello():
  for x in (1, 2):
    if x == 2:
      print(y) # prints "hello"
    if x == 1:
      y = "hello"

It is a dynamic error to evaluate a reference to a local variable before it has been bound:

def f():
  print(x)              # dynamic error: local variable x referenced before assignment
  x = "hello"

The same is true for global variables:

print(x)                # dynamic error: global variable x referenced before assignment
x = "hello"

It is a static error to bind a global variable already explicitly bound in the file:

x = 1
x = 2                   # static error: cannot reassign global x declared on line 1

If a name was pre-bound by the application, the Starlark program may explicitly bind it, but only once.

An augmented assignment statement such as x += y is considered both a reference to x and a binding use of x, so it may not be used at top level.

Implementation note: The Go implementation of Starlark permits augmented assignments to appear at top level if the -globalreassign flag is enabled.

A function may refer to variables defined in an enclosing function. In this example, the inner function f refers to a variable x that is local to the outer function squarer. x is a free variable of f. The function value (f) created by a def statement holds a reference to each of its free variables so it may use them even after the enclosing function has returned.

def squarer():
    x = [0]
    def f():
      x[0] += 1
      return x[0]*x[0]
    return f

sq = squarer()
print(sq(), sq(), sq(), sq()) # "1 4 9 16"

An inner function cannot assign to a variable bound in an enclosing function, because the assignment would bind the variable in the inner function. In the example below, the x += 1 statement binds x within f, hiding the outer x. Execution fails because the inner x has not been assigned before the attempt to increment it.

def squarer():
    x = 0
    def f():
      x += 1            # dynamic error: local variable x referenced before assignment
      return x*x
    return f

sq = squarer()

(Starlark has no equivalent of Python’s nonlocal or global declarations, but as the first version of squarer showed, this omission can be worked around by using a list of a single element.)

A name appearing after a dot, such as split in get_filename().split('/'), is not resolved statically. The dot expression .split is a dynamic operation on the value returned by get_filename().