Statements and Control Flow
Klyn uses indentation-defined blocks and a compact statement vocabulary. This page covers the control-flow statements you will use constantly in real code.
Indentation and Submitted Source
A block body starts on a new line after :, with four spaces
per nesting level. Declarations in a class follow the same rule;
an annotation and its declaration may share one correctly indented line.
Application.compile and Application.eval compile
the supplied String without further reindentation. Triple-quoted
literals already remove their common physical margin when creating the string,
while preserving indentation differences between lines. Ordinary quoted
strings and text read from files do not receive this transformation.
See Literals for text blocks.
source = "def answer() as Int:\n return 42\nanswer()"
assert (Application.eval(source) as Int) == 42
Assignment
count = 10
name as String = "Ada"
count += 1
count--
Assignment updates the nearest visible variable while preserving its established type. If no visible variable has that name, the assignment declares it in the current lexical block. Such a declaration is not visible after that block ends.
A nested scope cannot declare another local variable with the same name as a visible local or parameter. This includes explicit local declarations, loop declarations, catch variables and lambda parameters. Reusing a name in separate sibling scopes is allowed. Assignment to an existing variable remains legal: it updates that variable rather than declaring a second one.
Comprehensions reuse visible variables instead of declaring a second variable with the same name.
Only iteration names that do not already exist are local to the comprehension.
For example, after x = 100 and values = [x * 2 for x in [1, 2]],
x is 2 and values is [2, 4].
If, Elif, Else
score = 72
if score >= 90:
print("excellent")
elif score >= 50:
print("pass")
else:
print("retry")
Every branch owns a lexical scope. A variable declared before the conditional can be updated
by any branch; a variable first declared in an if, elif, or
else branch remains local to that branch.
Conditions are strictly typed as Boolean. Klyn does not apply truthiness to numbers,
strings, collections, objects, or null; the same rule applies to while,
assert, conditional expressions, Boolean operators, and comprehension filters.
score = 72
label = "unknown"
if score >= 50:
label = "pass"
congratulation = "well done"
else:
label = "retry"
print(label)
# print(congratulation) # TypeError: the name belongs to the if branch.
Match
value = 2
match value:
case 0:
print("zero")
case 1 | 2:
print("small")
case 3 to 10:
print("range")
default:
print("other")
match compares one subject against ordered cases. A case can use an exact value,
several alternatives separated by |, or an inclusive interval with to.
default handles the fallback branch.
Both statement and expression forms require at least one case.
Cases and default are indented exactly one level (four spaces) below
match, and their bodies start on a new line at the next indentation
level. A default clause alone is not a valid match.
case is Type tests the runtime type, with the same meaning as
subject is Type. The tested type must be the subject's declared
type or one of its subtypes; an unrelated type or a supertype is a compile-time
TypeError. This includes interface implementations. Numeric
widening conversions do not establish a subtype relation.
Repeated type patterns and type patterns entirely covered by an earlier type
pattern are rejected with SyntaxError, including alternatives within
the same case. When testing both a subtype and its parent, put the subtype first.
Short and fully qualified spellings of the same type do not make distinct patterns.
obj as Object = "hello"
kind = match obj:
case is String:
"text"
case is Int | is Long:
"integer"
default:
"other"
assert kind == "text"
The subject is evaluated once and the first matching case wins.
A null reference does not match a type pattern; use case null
explicitly if needed. Type patterns do not redeclare or change the static
type of the subject variable; use an explicit cast to access subtype members.
case String remains an equality comparison with the type object,
not a type pattern. Use match type(obj) with case String
when testing the exact runtime type, rather than its compatibility with a base type.
The compiler rejects case nan, case nanf and case nanh with a
SyntaxError. IEEE 754 defines every equality comparison involving NaN, including
NaN with itself, as false, so these constant patterns could never match. Test a value with
is nan, is nanf or is nanh before the match instead.
text = "dominique@example.com"
match text:
case /^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$/:
print("email")
case /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/:
print("date")
default:
print("unknown")
Regex cases use the same literal syntax as regular expressions and test the subject with pattern matching.
command = "ls"
action = match command:
case "ls" | "dir":
"listing"
case "ps":
"processes"
default:
"unknown"
A match expression returns the expression produced by the first matching branch.
Use this form when a value must remain available after the match. Variables first
declared inside a case or default block remain local to that branch.
value = 1
result = 0
match value:
case 0:
result = 10
explanation = "zero"
case 1:
result = 20
default:
result = 30
print(result)
# print(explanation) # TypeError: explanation belongs to one case only.
While Loops
i = 0
while i < 3:
print(i)
i += 1
Use while when the continuation condition depends on values updated in the body.
Variables declared before the loop can be updated by it; names first declared in the body are
local to the body.
Klyn has no separate do or until loop form. When a body must execute
before its termination test, use while true with an explicit conditional
break.
Range Loops
for i = 0 to 5:
print(i)
Both bounds are inclusive. The example prints 0 through 5.
The binding i exists only in the loop body. When iterating over indices, use an
explicit upper bound such as values.size - 1ul, after checking that
values.size > 0ul so an empty collection does not underflow.
Without an explicit type annotation, the binding uses the common numeric type of the
bounds: 1u to 3u produces UInt values, and
1ul to 3ul produces ULong values. Matching character bounds
retain Char or CodeUnit. A floating-point step promotes a
numeric binding to the corresponding common floating-point type.
for value = 1u to 5u step 2:
assert type(value) == UInt
print(value) # 1, 3, 5
for value = 5l to 1l step -2:
print(value) # 5, 3, 1
The bounds and step are evaluated once. A zero step raises ValueException.
Integer loops stop before advancing beyond the final bound, including when the next
addition would otherwise wrap at an integer limit.
Collection Iteration
for also consumes lazy, single-pass generators, such as
range(10) (values 0 through 9). Unlike for i = 0 to 5,
the stop bound of range is exclusive. No collection is allocated.
See Yield and Generators for suspension, manual
iteration and automatic cleanup when leaving a loop.
for value in [10, 20, 30]:
print(value)
config = {"theme": "dark", "fullscreen": true}
for key in config:
print(key)
for key, value in config:
print(key + " = " + value)
records = [
Tuple(1, "ready", true),
Tuple(2, "waiting", false)
]
for identifier as Int, label as String, enabled as Boolean in records:
print(f"{identifier}: {label} ({enabled})")
Iterating directly over a map yields keys. Use keys(), values(), or
items() when you want a specific view, or provide two bindings to destructure its
key and value directly. An iterable of positional tuples can be destructured into any number of
bindings; the number of bindings must equal the tuple arity. Each iteration binding is local to
its loop and cannot be read after that loop ends.
Break and Continue
while true:
text = input("> ")
if text == "":
continue
if text == "quit":
break
print(text)
Assert and Pass
count = 1
featurePending = true
assert count > 0
if featurePending:
pass
assert is appropriate for invariants and tests. pass is an explicit
placeholder that leaves a block intentionally empty.