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

5. Statements

A Korrin program is a sequence of statements, executed top to bottom. Each statement occupies one logical line (§1.2), except the compound statements (if, while, for, fn, class), whose header is one line and whose body is an indented block.

5.1 Expression statements

Any expression on its own line is a statement; its value is discarded. The common case is a call:

print("hello")
# => hello

5.2 Assignment

assignment = assignable , "=" , expression ;
assignable = NAME | attribute | index ;

target = value evaluates value, then stores it:

  • name = value — binds per the nearest-binding rule (ADR 0011): if name already exists in this or an enclosing scope, that binding is updated; otherwise a new binding is created in the current scope.
  • object.field = value — sets a field on an instance.
  • object[key] = value — sets a list element (index must be in range) or a map entry (creating it if absent).

There is no compound assignment (+= etc.) and no multiple assignment (a, b = ...) in Milestone 1. Assigning to anything else — a literal, a call, an operator expression — is E0104.

counter = 0
counter = counter + 1
print(counter)
# => 1

5.3 if / elif / else

if_statement =
    "if" , expression , block ,
    { "elif" , expression , block } ,
    [ "else" , block ] ;

Conditions are tested in order. The first truthy one's block runs; if none is truthy and an else is present, its block runs. Truthiness: only nil and false are falsy (ADR 0009).

n = 2
if n == 1:
    print("one")
elif n == 2:
    print("two")
else:
    print("many")
# => two

5.4 while

while_statement = "while" , expression , block ;

The condition is tested before each iteration; the block runs while it is truthy.

i = 0
while i < 3:
    print(i)
    i = i + 1
# => 0
# => 1
# => 2

5.5 for

for_statement = "for" , NAME , "in" , expression , block ;

NAME is bound to each element of the iterable in turn. Iterables in Milestone 1: lists (elements), strings (characters as one-character strings), maps (keys), and the ranges produced by the range builtin. A malformed header is E0106.

total = 0
for x in [10, 20, 30]:
    total = total + x
print(total)
# => 60

5.6 break and continue

break exits the innermost enclosing loop; continue skips to its next iteration. Outside any loop they are E0202 / E0203, reported by the resolver before the program runs.

5.7 pass

The do-nothing statement. Its only purpose is to be the body of a block that would otherwise be empty (ADR 0001):

fn todo():
    pass

5.8 return

Valid only inside a function body (E0201 otherwise). return with an expression yields that value; bare return yields nil. Reaching the end of a function body without return also yields nil. See §6.

5.9 fn and class

fn defines a function (§6) and class defines a class (§7). Both are statements: they bind a name in the current scope when executed.

5.10 try / except / finally

try_statement =
    "try" , block ,
    [ "except" , [ NAME ] , block ] ,
    [ "finally" , block ] ;

At least one of except and finally must be present; a bare try is E0107. A except or finally with no preceding try is the same error.

The try body runs. Then:

  • If it raised (via raise, §5.11, or a built-in fault) and there is an except, the handler runs. except NAME: binds the raised value to NAME for the handler body; except: catches without binding. There is exactly one except per try — it catches every exception. A caught built-in fault is bound as an Error value with .message and .code (§9).
  • If it raised and there is no except, the exception keeps propagating after finally runs.
  • finally, if present, runs on every exit path: normal completion, a caught exception, an uncaught one passing through, and a return / break / continue leaving the try. If the finally block itself raises or transfers control, that outcome replaces whatever was pending — so a return inside finally suppresses a propagating exception. Use finally for cleanup, not control flow.
fn classify(text):
    try:
        n = int(text)
        return "number"
    except:
        return "not a number"
    finally:
        print("classified {text}")

print(classify("42"))
print(classify("x"))
# => classified 42
# => number
# => classified x
# => not a number
try:
    total = 10 / 0
except err:
    print("{err.code}: caught")
# => E0305: caught

5.11 raise

raise_statement = "raise" , expression ;

Evaluates the expression and raises it as an exception, unwinding until a try with an except catches it (§5.10) or it reaches the top level, where it is E0312. Any value may be raised; Error("message") is the conventional choice. There is no bare raise — to re-raise in a handler, name the bound value: raise err.

fn checked_sqrt(x):
    if x < 0:
        raise Error("cannot take the square root of {x}")
    return x

try:
    print(checked_sqrt(-4))
except e:
    print(e.message)
# => cannot take the square root of -4