Operators and expressions
Arithmetic
Section titled “Arithmetic”| Operator | Meaning | Scratch block |
|---|---|---|
+ |
add, or join strings | operator_add / operator_join |
- |
subtract | operator_subtract |
* |
multiply | operator_multiply |
/ |
divide | operator_divide |
% |
modulo | operator_mod |
-x |
negate | operator_subtract from 0 |
private total: num = (a + b) * 2 % 10;Comparison
Section titled “Comparison”| Operator | Scratch block |
|---|---|
== |
operator_equals |
< |
operator_lt |
> |
operator_gt |
<= |
built — not (a > b) |
>= |
built — not (a < b) |
Scratch has no <= or >= block. Katnip declares them in the prelude as builds
procedures and inlines the body at every use site, so score <= 50 becomes a not
wrapping a > — a nested reporter, not a procedure call.
| Operator | Meaning | How |
|---|---|---|
&& |
and | operator_and |
|| |
or | operator_or |
! |
not | operator_not |
^ |
xor | built — not (a == b) |
!& |
nand | built — not (a && b) |
!| |
nor | built — not (a || b) |
!^ |
xnor | built — a == b |
if ((where >= 1) ^ (total <= 2)) { looks.say("exactly one");}Boolean shape coercion
Section titled “Boolean shape coercion”Scratch distinguishes round reporter slots from hexagonal boolean slots, and will not let a round block sit in a hexagonal one. Katnip handles this for you:
- a round reporter entering a boolean slot is wrapped automatically
- a literal in a boolean slot becomes a comparison
So you can write the natural thing and the compiler produces a shape Scratch accepts.
Strings
Section titled “Strings”+ on strings lowers to operator_join:
private label: str = "score: " + name;Interpolated strings
Section titled “Interpolated strings”Prefix a string with f and put expressions in {}:
looks.say(f"{name} scored {score}", 1);This folds into a right-nested join chain. It is also, today, the most practical way to
turn a num into a str, because the Str() cast is not lowered yet:
private s: str = f"{count}";An interpolation is a full expression, and it may contain strings — of either quote style, and f-strings of their own. The lexer keeps a stack of string frames, so nesting resumes the outer string correctly:
looks.say(f"{countChar("banana", "a")}"); # ✅looks.say(f"{countChar('banana', 'a')}"); # ✅looks.say(f"outer {f"inner {name}"} end"); # ✅An unterminated string — a missing closing quote, or a { interpolation never closed —
is reported as Unterminated string literal at the position the string opened, rather than
swallowing the rest of the file.
String indexing and length
Section titled “String indexing and length”private first: str = greeting[1]; # operator_letter_of — 1-basedprivate n: num = len(greeting); # operator_lengthprivate has: bool = greeting.contains("at");Scratch strings are 1-indexed, and so are Katnip’s.
Compile-time folding
Section titled “Compile-time folding”Two things are resolved before any block is emitted:
Enum members fold to constants:
private pick: Fruit = Fruit.banana; # the literal "Fruit.banana"private side: Team = Team.red; # the literal "R"Namespace constants fold to their literal value:
private turns: num = math.pi * 2; # 3.141592653589793 * 2range() in a for header is folded into the loop counter, with constant folding on
literal start / stop / step. It is never built as a list. Outside a for header it
silently produces an empty list — see
Known gaps.
Precedence
Section titled “Precedence”Katnip uses a Pratt parser with a binding-power table. Precedence follows the usual arithmetic-then-comparison-then-logic order:
unary - !* / %+ -< > <= >===&& ^ !& !^|| !|Every binary operator is left-associative, so 10 - 3 - 2 is (10 - 3) - 2. The one
exception is **, which binds to the right — and does not work anyway.
When in doubt, parenthesise. It costs nothing in the output — the block tree is the same either way.
** does not work
Section titled “** does not work”private x: num = base ** 2; # DO NOT USEThe ** operator parses and type-checks, but Scratch has no power block and Katnip has no
builds procedure for it yet, so the IR silently lowers it to an empty literal. No
error, wrong answer. math.pow was removed from the stdlib rather than fixed, so calling it
is at least a clean check error.
For a whole-number exponent, write the multiplication out or use a loop:
proc pow(base: num, exp: num) -> num { private result: num = 1; for (i, exp) { result = result * base; } return result;}For a fractional exponent, go through the natural log, which math does wrap:
private half: num = math.epow(0.5 * math.ln(16)); # 16 ** 0.5 -> 4Undefined for a negative base, so guard it if the base can go below zero.