Skip to content

Control flow

if (score == 0) {
looks.say("zero");
} elif (score <= 50) {
looks.say("low");
} else {
looks.say("high");
}

Lowers to nested control_if_else blocks. Parentheses around the condition are required; braces are required even for a single statement.

while (score > 100) {
score -= 5;
}

Lowers to control_while, with the condition taken as-is.

do {
score += 1;
} while (score < 10);

Runs the body at least once. Scratch has no do-while block, so Katnip emits the body once before the loop, then again inside it. Note the semicolon — this is a statement.

Katnip’s for takes a binding and something to walk. There are five things it can walk.

for (step, 4) {
motion.forward(10);
}

Runs four times with step = 1, 2, 3, 4. Lowers to control_for_each, Scratch’s own counted loop. The count can be any num expression, not just a literal.

for (n, range(2, 10, 2)) {
score += n;
}

range(stop) or range(start, stop, step). The range is folded into the loop counter, with constant folding when the bounds are literals — no list is ever built.

for (s, scores) {
score += s;
}

Binds each element in turn, by index.

for (letter, greeting) {
if (letter == "a") {
score += 1;
}
}

Binds each character, through operator_letter_of.

for ((name, count), stock) {
report(name, count);
}

Binds the key and the value together, walking the keys and values columns in step. The double parentheses are the tuple pattern — see Lists and dicts.

switch (score % 3) {
case (0, 1) { looks.switchCostume("costume1"); }
case (2) { looks.nextCostume(); }
default { looks.setSize(100); }
}

A case can hold several values, separated by commas — it matches if any of them do. Cases work on enums too:

switch (pick) {
case (Fruit.apple, Fruit.cherry) { looks.think("red-ish", 1); }
default { looks.think("something else", 1); }
}

Every label is checked against the switch value’s type, so a label that could never match is an error. On an enum-typed value a label must be a member or a literal that coerces to one:

enum Team { red = "R", blue = "B" }
switch (side) { # side: Team
case (Team.red) { ... } # ✅
case ("B") { ... } # ✅ Team.blue's value
case ("R!") { ... } # error, with a did-you-mean
case (Fruit.apple) { ... } # error, wrong enum
}

switch lowers to an if/else chain, which has two consequences:

  • There is no fallthrough. Each case is independent; there is no keyword to fall into the next one.
  • There is no exhaustiveness check. A switch over an enum that misses a member compiles without complaint. Add a default.
proc double(n: num) -> num {
return n * 2;
}

return in a void procedure ends the script early:

proc guard() -> void {
if (score < 0) { return; }
looks.say("ok");
}

Both forms emit a control_stop after writing the return value. The mechanics of how the value gets back to the caller are in Procedures → Returning values.

stop(StopType.ALL);
stop(StopType.THIS_SCRIPT);
stop(StopType.OTHER_SCRIPTS_IN_SPRITE);

forever and repeat have IR nodes and codegen, but no source syntax reaches them. Write the equivalent:

while (true) { ... } # forever
for (i, 10) { ... } # repeat 10

break and continue do not exist.