Skip to content

Feature status

Status of every language and toolchain feature, tracked against the compiler pipeline: lexer → parser → semantic analyzer → IR → SB3 codegen.

Emoji Meaning
🟢 Done; reaches real Scratch blocks in a built .sb3
🟡 Partial; implemented at some stages, named per row
🔴 Not implemented, or implemented only far enough to type-check

Last verified against main (compiler 0.1.12, VS Code extension 0.0.12) with a green node --test run — every case passing except the 8 todo cases that pin the gaps below — and a clean katnip build examples/codegen.knip.


  • 🟢 Lexer — hand-written state machine, operator trie for multi-character tokens, tracks line/column for every token.
    • 🟢 Interpolation nesting — a stack of string frames, so an interpolation may contain a string of either quote style, or another f-string.
    • 🟢 Unterminated string literals are reported instead of running off the end of the file.
  • 🟢 Parser — Pratt parser with a binding-power table, recovers from syntax errors into error nodes so analysis still runs.
  • 🟢 Semantic analyzer — two-pass (hoist, then walk), scoped symbol table, structured InternalType with unions, tuples, generics.
  • 🟡 IR generator — lowers everything Scratch-shaped, with named gaps below (structs, slices, **, imports, katnip_* builtins).
  • 🟡 SB3 codegen — emits blocks, variables, lists, procs, extensions, packs to .sb3; throws on any opcode missing slot metadata.
  • 🟢 Primitivesnum, str, bool, void, plus any.
  • 🟢 Collectionslist<T>, dict<K, V> as first-class annotated types.
  • 🟢 Tuple types(num, num), checked structurally and used to size proc return frames.
  • 🟢 Union typesTarget | str, with assignability in both directions.
  • 🟢 Enums — nominal, compared by name so two enums sharing a member never collide.
  • 🟢 Enum literal coercion — a literal whose value is one of an enum’s member values is assignable to that enum, so pen.setAttr("color", 10) and pen.setAttr(pen.ColorParam.COLOR, 10) compile to the same blocks. The rule lives in isAssignable, so it covers arguments, initializers, assignments, returns, struct fields, list and dict elements, and case labels alike. A literal that misses is rejected with the enum’s member list and a did-you-mean.
    • 🔴 A non-literal of the backing type — a variable, a call, a concatenation — is not assignable, and there is no Enum(x) cast to force one; the diagnostic points at the member form instead. See Known gaps.
  • 🟢 Type inference — every expression gets a type; annotations are optional on declarations with an initializer.
  • 🟢 Generic stdlib typevarsT, K, V bind from receivers and arguments, so zip/enumerate/list.contains return concrete types.
  • 🟡 Structs — nominal record types; fully analyzed, not lowered.
  • 🟢 Variablespublic, private, and temp access modifiers, with or without a type annotation.
    • 🟢 Top-level variables become stage-owned globals, visible from every sprite.
    • 🟢 Sprite members shadowing a global are renamed Sprite_name at codegen, since Scratch cannot represent the collision.
    • 🟢 temp inside a proc becomes a mangled global; scope-less by design, so recursion still clobbers it.
  • 🟢 Proceduresproc name(params) -> Type { ... }, with bodies lowered into procedures_definition blocks.
    • 🟢 Overloading by parameter list; dispatch resolved by argument types.
    • 🟢 Default parameter values.
    • 🟢 Named arguments — motion.goTo(y = 0, x = 0) routes by name, not position.
    • 🟢 Methods — a first parameter named self makes the proc callable as receiver.method().
    • 🟢 Warp — on by default for user procs, @warp decorator overrides.
  • 🟢 Spritessprite Name { ... } holding members, procs, and event handlers.
  • 🟢 Enums — implicit members fold to the qualified "Enum.member", explicit values are kept verbatim for sb3 fields and menus. A coerced literal folds to the identical value, so it takes the same lowering path as the member reference.
    • 🟢 Enums declared inside a namespace — motion.RotationStyle.LEFT_RIGHT resolves and folds; the enum stays out of global scope, so the bare RotationStyle is still undefined.
  • 🟢 Access control across files — only public symbols cross a file boundary; imports are not re-exported.
  • 🔴 Stage block — no stage { ... } declaration; stage state is implied by top-level variables.
  • 🟢 if / elif / else — lowers to nested control_if_else.
  • 🟢 while — lowers to control_while, condition taken as-is.
  • 🟢 do { } while ( ) — lowered by emitting the body once ahead of the loop, so it always runs at least once; note the body is duplicated in the output.
  • 🟢 for over a counterfor (i, 4) lowers to control_for_each.
  • 🟢 for over a list — binds each element by index.
  • 🟢 for over a string — binds each letter through operator_letter_of.
  • 🟢 for over a dictfor ((key, value), d) walks the keys and values columns together.
  • 🟢 for over range() — folded into the loop counter with constant folding on literal start/stop/step, never built as a list.
  • 🟢 switch / case / default — lowers to an if/else chain; a case can hold several values.
    • 🟢 Case labels are checked against the switch value’s type, so a label on an enum-typed value must be a member or a literal that coerces to one.
    • 🔴 Fallthrough keyword — not designed or implemented.
    • 🔴 Exhaustiveness checking over enums — a switch missing members is still not reported.
  • 🟢 return — scalar and tuple frames, followed by control_stop.
  • 🟢 Event handlersevents.onFlag() { } and friends, gated by the analyzer to sprite top level.
  • 🟡 Statement placement errors — handlers, imports, and switch placement are checked, but statements the IR cannot place outside a sprite are silently dropped rather than reported.
  • 🟡 forever / repeat — IR node kinds and codegen exist, no source syntax reaches them yet.
  • 🟢 Arithmetic+, -, *, /, %, and unary -.
  • 🟢 Comparison==, <, >.
  • 🟢 Logic&&, ||, and unary !.
  • 🟢 Composed operators<=, >=, ^, !&, !|, !^ have no Scratch block, so they come from @lower = "builds" stdlib procs inlined as nested reporters at each use site.
  • 🟢 Boolean shape coercion — round reporters entering a hexagonal slot are wrapped automatically, and literals become a comparison.
  • 🟢 Compound assignment+=, -=, *=, /=, %=, including through a list or dict index.
  • 🟢 String concatenation+ on strings lowers to operator_join.
  • 🟢 Interpolated stringsf"{name} scored {score}" folds to a right-nested operator_join chain.
  • 🟢 Enum member access — folded to a compile-time constant.
  • 🟢 Namespace constantsmath.pi folds to its literal.
  • 🔴 Power — the ** operator parses and type-checks, but has no opcode and no builds proc, so the IR silently lowers it to an empty literal. Do not use it yet.
  • 🔴 Power assignment**= has the same gap.
  • 🟢 List literals — all-literal contents bake straight into the project file, anything needing blocks is rebuilt by a green-flag script.
  • 🟢 Dict literals — backed by two parallel Scratch lists, name_keys and name_vals.
  • 🟢 List indexing and assignmentscores[1], scores[2] = ..., paws[1] += 1.
  • 🟢 Dict indexing and assignment — reads resolve the key column, writes replace in place or append when the key is missing.
  • 🟢 String indexings[1] lowers to operator_letter_of.
  • 🟢 List monitorsshow() and hide() toggle the project’s list monitors.
  • 🔴 Lists and dicts declared inside a script or proc — only top-level and sprite-level declarations are lowered.
  • 🔴 Slicess[1:5:2] parses and gets a naive type, no lowering.
  • 🟢 Return strategy planning — Tarjan SCC over the resolved call graph decides each proc’s ABI.
    • 🟢 var strategy — non-cyclic procs write dedicated return variables.
    • 🟢 vstack strategy — procs in a call cycle push onto a per-proc Scratch list, so recursion returns correct values.
    • 🟢 @ret = "auto" | "var" | "vstack" — explicit var on a cyclic proc is a hard error, because its failure mode is silently wrong values.
  • 🟢 Recursion — verified end to end with fib in examples/codegen.knip.
  • 🟢 Argument reporter kinds%s, %n, and %b all emitted from the declared parameter types.
  • 🟡 Tuple returns — the multi-slot frame is emitted, but a call site only reads the first element; no destructuring on the receiving side.
  • 🔴 List and dict returns — explicitly rejected by the analyzer, since the frame width is not statically known.
  • 🔴 @lower = "yields" procs — declared in the stdlib (list.remove, console.input) but the IR has no lowering for them.
  • 🟢 @opcode — binds a proc to a raw Scratch opcode.
  • 🟢 @hat — marks a proc usable only as an event handler; misuse is reported in both directions.
  • 🟢 @warp — runs the proc without screen refresh.
  • 🟢 @lower — one of reporter, command, userproc, builds; yields is accepted but unlowered.
  • 🟢 @operator — binds a builds proc to a binary operator, so the IR routes that operator through it.
  • 🟢 @ret — picks the return strategy.
  • 🟢 Import resolutionimport "./thing.knip"; and import "../lib/thing.knip" as alias;.
  • 🟢 Import graph walking — each file read and parsed once; unresolvable paths and cycles are reported against the importing file.
  • 🟢 Host-supplied resolvers — the CLI uses the filesystem, the editor uses open documents, so a browser playground can supply virtual files.
  • 🟢 Namespacing and visibility — imported symbols live under a namespace, only public ones cross, and imports are never re-exported.
  • 🔴 Lowering imported code — the IR walks only the entry file, so calling an imported proc type-checks and then fails codegen with call to unknown proc. Imports are analysis-only today.
  • 🟢 Parsed — fields with type annotations and/or defaults, public and private.
  • 🟢 Analyzed — literal construction with defaults filled, missing and unknown fields reported, field reads and writes type-checked, struct-typed params and returns, struct lists with per-field column access, non-scalar fields and duplicate fields rejected.
  • 🔴 Lowered — struct literals, field reads, and field writes all no-op in the IR. Nothing struct-shaped survives to sb3.

Bundled .knip declaration files, generated into the compiler at build time.

Menu parameters are typed by the enum alone where the Scratch menu is a closed set (ColorParam, RotationStyle, GraphicEffect, LayerPosition, LayerDirection, NumberName, DragMode, TimeUnit, GreaterThanProperty, Key), so a bare literal is checked against the members. Six menus also list sprite, costume, or backdrop names, which the compiler cannot enumerate — Target, TouchTarget, DistanceTarget, ObjectTarget, CloneTarget and Backdrop keep an explicit | str arm for those, and are not checked.

  • 🟢 prelude (no namespace) — wait, stop, len, showVariable, hideVariable, the Key and StopType enums, true/false, and the builds operator procs.
  • 🟢 eventsonFlag, onKey, onClick, onBackdropSwitch, onGreaterThan, onBroadcast, broadcast, broadcastAndWait; computed broadcast names supported.
  • 🟢 motion — movement, turning, goTo/glideTo overloads, pointing, x/y, edge bounce, rotation style.
  • 🟢 looks — say/think with both overloads, costumes, backdrops, size, graphic effects, show/hide, layers.
  • 🟢 sensing — touching, colors, distance, ask/answer, keys, mouse, drag mode, loudness, timer, sensing_of, date parts, online, username.
  • 🟢 pen — down, up, clear, stamp, hex color, color params, size; the extension is declared in the project automatically.
  • 🟢 cloneonStart, create, delete.
  • 🟡 listadd, contains, length, clear, indexOf, show, hide are 🟢; remove is 🔴 (yields), merge is 🔴 (katnip_list_merge).
  • 🟡 mathpi, e, tau fold to literals 🟢; pow is a stub with an empty body 🔴.
  • 🟡 strcontains is 🟢; the rest of the string surface is not written yet.
  • 🔴 dictcontains, length, keys, values, merge all resolve to katnip_* opcodes with no codegen metadata.
  • 🔴 consolelog, warn, error are katnip_*; input is a yields proc.
  • 🔴 Casts and helpersNum, Str, Bool, List, typeof, zip, enumerate, motion.getPosition, and range() used as a value all type-check but have no codegen metadata; using one throws no slot metadata.
  • 🟢 Project structure — stage plus one target per sprite, variables and lists declared on the right target, scripts laid out vertically.
  • 🟢 Custom procs — proccode, argument ids, argument defaults, and warp mutation all emitted; signatures registered up front so forward calls resolve.
  • 🟢 Input shapes — shadow primitives per slot kind (number, whole, positive, angle, color, string), menu shadows with dynamic-reporter overlay, broadcast primitives registered on the stage.
  • 🟢 Extension declaration — opcode prefixes are detected and added to the project’s extension list; pen is the only one with stdlib bindings today.
  • 🟢 Packingfflate zips project.json plus the default costume into a .sb3 that loads in Scratch and TurboWarp.
  • 🔴 Assets — every sprite gets the same default costume; no costume, backdrop, or sound import.
  • 🔴 Monitor layout — monitors can be shown and hidden, but not positioned or styled.
  • 🔴 Comments in output — nothing is written to the sb3 comment map.
  • 🟢 Lexing — six variants, single-line and multi-line, each in expanded, collapsed, and ignored forms; ignored ones are dropped at the lexer.
  • 🔴 Attaching to the ASTNodeBase.comment exists but the parser discards comment tokens, so no comment ever reaches a node or the sb3 output.
  • 🟢 CLIkatnip tokenize, parse, check, lower, build, help.
  • 🟢 Error reporting — source spans with line and column, colorized output, multiple errors per run; analysis continues past syntax errors so semantic errors surface alongside them.
  • 🟢 VS Code extension — live diagnostics on type or on save with a configurable debounce, syntax highlighting, language configuration, and a Katnip: Build .sb3 command.
  • 🟢 Testsnode --test cases across lexer, parser, semantic, callgraph, imports, IR, and codegen, plus 8 todo cases in gaps.test.ts that assert each gap on this page is still a gap, so closing one fails loudly.
  • 🟢 Public APIcheckSource, compileToIR and compileToSb3 exported for embedding, with a pluggable import resolver.
  • 🔴 Language server — the extension shells the compiler directly; no LSP, no completion, hover, or go-to-definition.
  • 🔴 Formatter.
  • 🔴 Source maps — no mapping from a Scratch block back to a .knip line.

  1. Lower imported procs and variables, so import stops failing at codegen.
  2. Give the katnip_* builtins real lowerings; casts, console, typeof, and the dict methods are the widest hole.
  3. Lower structs; the analyzer is already complete for them.
  4. Implement **, which today lowers silently to an empty literal.
  5. Lower @lower = "yields" procs, unblocking list.remove and console.input.
  6. Read tuple returns at a call site; the ABI already carries the extra slots.
  7. Attach comments to the AST and emit them into the sb3 comment map.