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/orevaluate 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.