Types
Every expression in Katnip has a type, and the compiler checks them before emitting a single block. Scratch itself has no types at all — it coerces everything at runtime — so this whole layer exists purely at compile time and costs nothing in the output.
Primitives
Section titled “Primitives”| Type | Holds | Scratch equivalent |
|---|---|---|
num |
numbers, integer or decimal | a value in a number slot |
str |
text | a value in a text slot |
bool |
true / false |
a hexagonal boolean slot |
void |
nothing; a proc’s “returns no value” | a stack block |
any |
anything; opts out of checking | — |
temp count: num = 3;temp name: str = "Katnip";temp ready: bool = true;true and false are declared in the prelude, not keywords. They exist because Scratch
has no boolean literal: true is "true" == "true" and false is "false" == "".
Collections
Section titled “Collections”public scores: list<num> = [3, 1, 4, 1, 5];public stock: dict<str, num> = {"apple": 2, "banana": 5};list<T> is a Scratch list. dict<K, V> is a Katnip invention backed by two Scratch
lists — stock_keys and stock_vals — kept in step.
Full detail in Lists and dicts.
Tuples
Section titled “Tuples”A fixed-length, positionally-typed group:
temp pair: (num, num) = ...;temp columns: (list<str>, list<num>) = zip(names, powers);Tuple types are checked structurally — (num, num) and (num, num) are the same type no
matter where they were written. They are used for multi-value returns and for the
(key, value) binding in a dict for loop.
Unions
Section titled “Unions”A value that may be one of several types, written with |:
proc goTo(position: Target | str) -> void {}Assignability works in both directions: a str fits a Target | str parameter, and a
Target | str fits a str parameter only if every member is assignable.
The standard library uses unions to let you pass either a named target or a raw string:
motion.goTo("_random_"); # str armsensing.touching("_edge_"); # str armclone.create("_myself_"); # str armNominal, not structural — two enums that share a member name never compare equal.
enum Fruit { apple, banana, cherry }enum Team { red = "R", blue = "B" }A literal that matches one of an enum’s member values is assignable to that enum, so
temp t: Team = "R"; and temp t: Team = Team.red; mean the same thing. A computed
value of the backing type is not — see Enums and structs.
Structs
Section titled “Structs”Nominal record types with named fields:
struct Point { x: num, y: num }Inference
Section titled “Inference”An annotation is optional whenever there is an initializer to infer from:
temp count = 3; # numtemp name = "Katnip"; # strtemp scores = [3, 1, 4]; # list<num>Annotate when you want the compiler to hold you to something:
temp scores: list<num> = []; # otherwise the element type is openProcedure parameters and return types are always annotated. There is no inference across a procedure boundary.
Generics in the standard library
Section titled “Generics in the standard library”The stdlib declares typevars T, K, and V, which bind from the receiver and the
arguments at each call:
temp fighters: list<str> = ["Ember", "Splash"];
fighters.contains("Ember"); # T binds to str -> boolfighters.length(); # T binds to str -> numzip(fighters, powers); # -> (list<str>, list<num>)enumerate(fighters); # -> (list<num>, list<str>)You cannot declare your own generic procedures — typevars are a stdlib facility.
Type conversion
Section titled “Type conversion”Katnip does not silently convert. num into a str slot is an error, and the casts
that would fix it are declared but not yet lowered:
temp s: str = Str(42); # type-checks, then fails at codegenUntil they land, use an interpolated string, which lowers to join and produces a str:
temp s: str = f"{42}"; # works today