Control flow
if / elif / else
Section titled “if / elif / else”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 / while
Section titled “do / while”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.
A counter
Section titled “A counter”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.
A range
Section titled “A range”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.
A list
Section titled “A list”for (s, scores) { score += s;}Binds each element in turn, by index.
A string
Section titled “A string”for (letter, greeting) { if (letter == "a") { score += 1; }}Binds each character, through operator_letter_of.
A dict
Section titled “A dict”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
Section titled “switch”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
switchover an enum that misses a member compiles without complaint. Add adefault.
return
Section titled “return”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.
Stopping
Section titled “Stopping”stop(StopType.ALL);stop(StopType.THIS_SCRIPT);stop(StopType.OTHER_SCRIPTS_IN_SPRITE);What is missing
Section titled “What is missing”forever and repeat have IR nodes and codegen, but no source syntax reaches them.
Write the equivalent:
while (true) { ... } # foreverfor (i, 10) { ... } # repeat 10break and continue do not exist.