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.
Pipeline
Section titled “Pipeline”- 🟢 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
InternalTypewith 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.
- 🟢 Primitives —
num,str,bool,void, plusany. - 🟢 Collections —
list<T>,dict<K, V>as first-class annotated types. - 🟢 Tuple types —
(num, num), checked structurally and used to size proc return frames. - 🟢 Union types —
Target | 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)andpen.setAttr(pen.ColorParam.COLOR, 10)compile to the same blocks. The rule lives inisAssignable, so it covers arguments, initializers, assignments, returns, struct fields, list and dict elements, andcaselabels 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.
- 🔴 A non-literal of the backing type — a variable, a call, a concatenation — is not
assignable, and there is no
- 🟢 Type inference — every expression gets a type; annotations are optional on declarations with an initializer.
- 🟢 Generic stdlib typevars —
T,K,Vbind from receivers and arguments, sozip/enumerate/list.containsreturn concrete types. - 🟡 Structs — nominal record types; fully analyzed, not lowered.
Declarations
Section titled “Declarations”- 🟢 Variables —
public,private, andtempaccess 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_nameat codegen, since Scratch cannot represent the collision. - 🟢
tempinside a proc becomes a mangled global; scope-less by design, so recursion still clobbers it.
- 🟢 Procedures —
proc name(params) -> Type { ... }, with bodies lowered intoprocedures_definitionblocks.- 🟢 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
selfmakes the proc callable asreceiver.method(). - 🟢 Warp — on by default for user procs,
@warpdecorator overrides.
- 🟢 Sprites —
sprite 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_RIGHTresolves and folds; the enum stays out of global scope, so the bareRotationStyleis still undefined.
- 🟢 Enums declared inside a namespace —
- 🟢 Access control across files — only
publicsymbols cross a file boundary; imports are not re-exported. - 🔴 Stage block — no
stage { ... }declaration; stage state is implied by top-level variables.
Statements and control flow
Section titled “Statements and control flow”- 🟢
if/elif/else— lowers to nestedcontrol_if_else. - 🟢
while— lowers tocontrol_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. - 🟢
forover a counter —for (i, 4)lowers tocontrol_for_each. - 🟢
forover a list — binds each element by index. - 🟢
forover a string — binds each letter throughoperator_letter_of. - 🟢
forover a dict —for ((key, value), d)walks the keys and values columns together. - 🟢
foroverrange()— folded into the loop counter with constant folding on literalstart/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 bycontrol_stop. - 🟢 Event handlers —
events.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.
Expressions and operators
Section titled “Expressions and operators”- 🟢 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 tooperator_join. - 🟢 Interpolated strings —
f"{name} scored {score}"folds to a right-nestedoperator_joinchain. - 🟢 Enum member access — folded to a compile-time constant.
- 🟢 Namespace constants —
math.pifolds to its literal. - 🔴 Power — the
**operator parses and type-checks, but has no opcode and nobuildsproc, so the IR silently lowers it to an empty literal. Do not use it yet. - 🔴 Power assignment —
**=has the same gap.
Collections
Section titled “Collections”- 🟢 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_keysandname_vals. - 🟢 List indexing and assignment —
scores[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 indexing —
s[1]lowers tooperator_letter_of. - 🟢 List monitors —
show()andhide()toggle the project’s list monitors. - 🔴 Lists and dicts declared inside a script or proc — only top-level and sprite-level declarations are lowered.
- 🔴 Slices —
s[1:5:2]parses and gets a naive type, no lowering.
Procedures and the return ABI
Section titled “Procedures and the return ABI”- 🟢 Return strategy planning — Tarjan SCC over the resolved call graph decides each
proc’s ABI.
- 🟢
varstrategy — non-cyclic procs write dedicated return variables. - 🟢
vstackstrategy — procs in a call cycle push onto a per-proc Scratch list, so recursion returns correct values. - 🟢
@ret = "auto" | "var" | "vstack"— explicitvaron a cyclic proc is a hard error, because its failure mode is silently wrong values.
- 🟢
- 🟢 Recursion — verified end to end with
fibinexamples/codegen.knip. - 🟢 Argument reporter kinds —
%s,%n, and%ball 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.
Decorators
Section titled “Decorators”- 🟢
@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 ofreporter,command,userproc,builds;yieldsis accepted but unlowered. - 🟢
@operator— binds abuildsproc to a binary operator, so the IR routes that operator through it. - 🟢
@ret— picks the return strategy.
Modules
Section titled “Modules”- 🟢 Import resolution —
import "./thing.knip";andimport "../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
publicones 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.
Structs
Section titled “Structs”- 🟢 Parsed — fields with type annotations and/or defaults,
publicandprivate. - 🟢 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.
Standard library
Section titled “Standard library”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, theKeyandStopTypeenums,true/false, and thebuildsoperator procs. - 🟢
events—onFlag,onKey,onClick,onBackdropSwitch,onGreaterThan,onBroadcast,broadcast,broadcastAndWait; computed broadcast names supported. - 🟢
motion— movement, turning,goTo/glideTooverloads, 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. - 🟢
clone—onStart,create,delete. - 🟡
list—add,contains,length,clear,indexOf,show,hideare 🟢;removeis 🔴 (yields),mergeis 🔴 (katnip_list_merge). - 🟡
math—pi,e,taufold to literals 🟢;powis a stub with an empty body 🔴. - 🟡
str—containsis 🟢; the rest of the string surface is not written yet. - 🔴
dict—contains,length,keys,values,mergeall resolve tokatnip_*opcodes with no codegen metadata. - 🔴
console—log,warn,errorarekatnip_*;inputis a yields proc. - 🔴 Casts and helpers —
Num,Str,Bool,List,typeof,zip,enumerate,motion.getPosition, andrange()used as a value all type-check but have no codegen metadata; using one throwsno slot metadata.
Scratch project output
Section titled “Scratch project output”- 🟢 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;
penis the only one with stdlib bindings today. - 🟢 Packing —
fflatezipsproject.jsonplus the default costume into a.sb3that 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.
Comments
Section titled “Comments”- 🟢 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 AST —
NodeBase.commentexists but the parser discards comment tokens, so no comment ever reaches a node or the sb3 output.
Tooling
Section titled “Tooling”- 🟢 CLI —
katnip 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 .sb3command. - 🟢 Tests —
node --testcases across lexer, parser, semantic, callgraph, imports, IR, and codegen, plus 8todocases ingaps.test.tsthat assert each gap on this page is still a gap, so closing one fails loudly. - 🟢 Public API —
checkSource,compileToIRandcompileToSb3exported 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
.knipline.
Roadmap — nearest gaps, in rough order
Section titled “Roadmap — nearest gaps, in rough order”- Lower imported procs and variables, so
importstops failing at codegen. - Give the
katnip_*builtins real lowerings; casts,console,typeof, and the dict methods are the widest hole. - Lower structs; the analyzer is already complete for them.
- Implement
**, which today lowers silently to an empty literal. - Lower
@lower = "yields"procs, unblockinglist.removeandconsole.input. - Read tuple returns at a call site; the ABI already carries the extra slots.
- Attach comments to the AST and emit them into the sb3 comment map.