Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10. Execution model

How a Korrin program runs, once it has lexed, parsed, and resolved without error.

This section describes the language, not an implementation. Korrin has two engines — a tree-walking interpreter and a bytecode virtual machine — and everything here holds for both; every runnable example below is executed under each of them on every build (ADR 0024).

10.1 Program start

A program starts at one file — the entry module. Its top-level statements execute in order, top to bottom, in that module's global scope. fn and class statements bind their names when reached, so a function must be defined above its first call at the top level (though mutual recursion between functions works, because the calls happen later, at run time).

An import statement runs another module to completion (once) before the importing statement continues; each module has its own global scope (§11).

fn even(n):
    if n == 0:
        return true
    return odd(n - 1)

fn odd(n):
    if n == 0:
        return false
    return even(n - 1)

print(even(10))
# => true

10.2 Scopes and name resolution

There are exactly two kinds of scope: the global scope, and a function-call scope (ADR 0011). if, while, and for do not create scopes. Each call gets a fresh scope, and a function can see the scope it was defined in, not the one it was called from — this is what makes closures work.

A name refers to the nearest binding of that name: the current function scope, then each enclosing function scope, then global. = updates the binding it finds, or creates one in the current scope if there is none. That is why a function can update a global without any keyword:

count = 0

fn bump():
    count = count + 1

bump()
bump()
print(count)
# => 2

Which binding a name refers to is fixed by the program text. A scope binds the names that appear in binding position anywhere in its own body: an assignment target, a for variable, an except binding, a parameter, or the name of a fn, class, or import ... as. Position within the body does not matter, so a name assigned late in a function is that function's local from its first line, and reading it before the assignment runs is E0301:

fn f():
    print(later)     # E0301: `later` is f's, and has no value yet
    later = 1

Names not bound anywhere nearer are global, and reading a global that nothing has assigned yet is also E0301.

The tree-walking interpreter answers "is there a nearer binding?" while the program runs rather than from the text, which differs in one narrow case: a function that assigns a name the module also binds, called before the module's own assignment has run. See ADR 0022.

10.3 Evaluation order

  • Binary operators evaluate the left operand, then the right, then combine.
  • A call evaluates the callee, then each argument left to right, then calls.
  • and / or evaluate the left operand, and the right only if needed.
  • List and map literals evaluate their elements in source order (for a map, each key before its value).

10.4 Value semantics

Scalars (nil, bool, int, float) and str are effectively immutable — operations produce new values. list, map, and instance are reference types: the same object can be reached through several names, and a mutation through one is visible through all (§3.4).

A for loop iterates over a snapshot of a list taken when the loop starts, so appending to the list inside the loop does not extend the iteration.

xs = [1, 2, 3]
for x in xs:
    xs.push(x)
print(xs.len())
# => 6

10.5 Runtime limits

Function-call nesting is bounded: a call depth beyond the limit (currently 10 000) is reported as E0313 rather than aborting the process, which turns an infinite recursion into a diagnostic.

Programs compiled for the virtual machine also meet the limits of the bytecode itself — how many locals one function may have, how many constants one chunk may hold (E04xx). Reaching any of them takes a generated program; they are limits of the instruction encoding, not of the language.

10.6 Termination

A program ends when its last top-level statement finishes, or at the point an exception reaches the top level uncaught (§9.6). There is no explicit exit function.