Kagari
Type to search documentation.

Syntax

Control Flow and Loops

Kagari’s control flow is Rust’s structured control flow: if, if let, match, loop, while, while let, for, break, continue and return, with labels on loops. There is no goto. What a loop may do depends on the kind of the function it is in; the last section lists the rules.

if and if let

kis
if AFFECTION_MIO >= 3 {
MioSmiles();
} else if AFFECTION_MIO == 0 {
MioIgnores();
} else {
MioShrugs();
}
let label = if cleared { "Clear" } else { "New" };
  • The condition is a bool expression (E2001 otherwise), written without parentheses. The branches are blocks; braces are required.
  • With an else, if is an expression whose value is the value of the branch taken; both branches have one type. Without else, its type is (), and the block must have type ().
  • A struct expression in the condition must be parenthesized; see Expressions.

if let pattern = value { … } else { … } runs the first block when the pattern matches, with the pattern’s bindings in scope there. The pattern may be refutable. An if let cannot be combined with other conditions by &&; nest a second if instead.

kis
if let Some(slot) = last_slot {
show_slot(slot);
}

match

kix
match choice!{
id = "confess",
t!["Tell her the truth"] => Confess::Yes,
t!["Say nothing"] => Confess::No,
} {
Confess::Yes => AzusaConfession(),
Confess::No => s!(t!["I kept quiet."]),
}

match value { pattern => expression, … } runs the first arm whose pattern matches and whose guard (pattern if condition => …) is true. Its value is the value of that arm; all arms have one type. A match must be exhaustive (E2037). Patterns, guards and exhaustiveness are in Patterns.

loop

kis
let mut n = 0;
let first_big = loop {
n += 1;
if n * n > 50 {
break n;
}
};

loop { … } repeats its body until a break leaves it. break value makes value the value of the loop; all breaks of one loop give values of one type, and a plain break gives (). A loop without any break never ends; its type is !, so it fits wherever a value of any type is expected, such as the end of a scene that returns an ending.

while and while let

kis
while count < 10 {
count += 1;
}
while let Some(next) = queue.get(index) {
index += 1;
}

while condition { … } runs the body as long as the condition is true. while let pattern = value { … } runs it as long as the pattern matches. Both have type (); break inside them takes no value (E2001).

for loops

kis
for day in 0..7 { … }
for i in 1..=3 { … }
for name in ["Mio", "Azusa", "Hanako"] { … }
for score in scores { … }

for pattern in iterable { … } runs the body once per item. The pattern must be irrefutable (E2038). The loop has type (), and break inside it takes no value.

IterableItemsOrder
[T; N]The N elements, of type TFirst to last
Vec<T>The elements, of type TFirst to last
a..b, a..=b: a Range<T> of a Rust integer or Int typeThe integers from a to b (excluded or included)Ascending

Any other type is E2031, including references to arrays. A range over anything but integers (for example Decimal) is E2031 as well.

When the range is written in the for itself, the loop variable’s type covers exactly its values: day in for day in 0..7 is an Int<0, 6>, i in for i in 1..=3 is an Int<1, 3>, and with a and b of type Int<0, 9>, k in for k in a..b is an Int<0, 8>. A range value of type Range<Int<lo, hi>> from elsewhere, such as a parameter, gives an Int<lo, hi>.

Labels, break and continue

kis
'outer: for row in 0..4 {
for col in 0..4 {
if blocked(row, col) {
continue 'outer;
}
if done(row, col) {
break 'outer;
}
}
}
ExpressionEffect
breakLeaves the innermost loop
break valueLeaves the innermost loop with a value
break 'label, break 'label valueLeaves the loop labelled 'label
continueStarts the next iteration of the innermost loop; in a for loop, with the next item
continue 'labelStarts the next iteration of the loop labelled 'label

A label is 'name: written before loop, while or for. Labels exist only on loops: a label on a block or any other expression is a syntax error (E0001). A label that no enclosing loop declares is E1019; break or continue outside a loop is E1020. break and continue have type !.

return

return value leaves the function with value; return alone returns (). It has type !. Without return, a function returns the value of its body block. A function with a return type must produce a value of that type on every path; a path that reaches the end of the body without one is a type error (E2001), and so is return; in a function that returns a value.

Rules by function kind

The constructs above are available everywhere, with these restrictions:

Constructfnconst fnSceneInstant procedure, closure in the story
if, if let, matchyesyesyesyes
loop, while, while letyesyesyes, if every path around the loop passes a step (E5001)no (E3004)
for over an arrayyesyesyesyes
for over a rangeyesyesbounds must be constant expressions (E3006)bounds must be constant expressions (E3006)
for over a Vec<T>yesyesonly a constant one (E3006)only a constant one (E3006)
Recursionyesyesonly through a step (E5002)no (E3005)

In a pushdown fn, a for loop may iterate over any array, over a range whose two bounds are constant expressions, or over any other constant expression. A Vec<T> is not a finite type, so the story never holds one at runtime.

When a const fn is evaluated at compile time, the evaluation is limited to 1 000 000 steps (E4003), so a loop that does not end there is reported instead of hanging the build.

Termination in the story

Everything the story does between two steps runs instantly, without input from the player, so it must always finish. The language makes this checkable:

  • An instant procedure (a pushdown fn without #[scene]) and a closure in the story have no unbounded loop and no recursion: no loop or while (E3004), for only over arrays and constant ranges (E3006), and no cycle of calls among instant procedures (E3005). So every call of one finishes.
  • In a scene, loop and while are allowed, but every path around the loop body must pass a step (E5001), and a scene may call itself, directly or through other scenes, only after a step (E5002). A call of a scene that may return without passing a step counts as stepless.
kix
static mut READY: bool = false;
#[scene]
pushdown fn Wait() {
loop {
if READY {
s!(t!["Ready."]);
}
// E5001: when READY is false, the loop goes around without a step
}
}

A hub that waits for the player on every lap is accepted:

kix
enum Map { A, Leave }
#[scene]
pushdown fn FestivalHub() {
loop {
match choice!{
id = "map",
t!["Stall A"] => Map::A,
t!["Leave"] => Map::Leave,
} {
Map::A => StallA(),
Map::Leave => break,
}
}
}

The check counts every branch as possible; it does not try to prove that a stepless path is never taken. A for loop in a scene over more than 1 048 576 items makes the story analysis give up (W5007). The full rules are in Scenes and Instant Procedures.

No goto

There is no goto and no label that marks a position in the code. A hub is a loop, a scene that ends early returns, and a branch is an if or a match. Control can never jump into another function or into the middle of a block or loop, so calls and returns always nest, which the story analysis relies on.

Last updated Oct 29, 2026