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.18, VS Code extension 0.0.18) with a green
node --test run — 199 cases, 192 passing and 7 todo cases pinning the gaps below, one of
which (imports) now passes — and a clean katnip build of examples/all.knip and
examples/assets.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,
**, tuple destructuring,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 —
publicandprivateaccess modifiers, with or without a type annotation.- 🟢 The modifier picks the owner, wherever the declaration is written:
publicis stage-owned and visible to every sprite,privatebelongs to whatever encloses it. Two sprites claiming onepublicname is an error. - 🟢 Sprite members shadowing a global are renamed
Sprite_nameat codegen, since Scratch cannot represent the collision. - 🟢 A variable declared inside a proc becomes a mangled global; scope-less by design, so recursion still clobbers it.
- 🟢 The modifier picks the owner, wherever the declaration is written:
- 🟢 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 —
stage { ... }fills in the stage target: costumes, variables, procs and handlers all land on the stage.sprite stage { }is a parse error. - 🟢 Asset declarations —
costume "./file.svg" as name;andsound "./file.wav";at a sprite’s top level. The name defaults to the file stem; the extension is checked against Scratch’s costume and sound formats; a missing file is reported against its path.
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. 1-based and stop-inclusive like Scratch’s own counter, sorange(5)yields1 2 3 4 5. - 🟢
foroverzip()/enumerate()— walked by index instead of built:for ((a, b), zip(l1, l2))reads both lists under one counter runningl1.length()times;for ((i, v), enumerate(l))reuses the 1-based counter as the index, so it costs no extra block.- 🟡 A second list shorter than the first reads empty past its end, rather than the loop stopping early.
- 🟢
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 — hoisted and refilled on every run, rather than baked once like a top-level list.
- ⛔ Slices —
s[1:5:2]parses and gets a naive type, and lowers to an empty literal. Silently wrong, not an error.
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, and a tuple-pattern assignment —
(a, b) = f();— is skipped entirely, so the call never happens. Silently wrong; see Known gaps. - 🔴 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. - 🟢
@proccode— sets a user proc’s Scratch custom-block label. One%name(or bare%s/%n/%b) placeholder per parameter, in declaration order; the declared parameter type picks the final spec. Checked — a wrong count or a reordered placeholder is an error. - 🟢
@operator— binds abuildsproc to a binary operator, so the IR routes that operator through it. - 🟢
@ret— picks the return strategy. - 🔴
@doc,@constants— parse, and nothing reads them.
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 — an imported proc is lowered into every target that calls it, transitively, and an imported literal constant folds at the call site.
- 🔴 Imported module state — a variable or list declared in an imported module does not
cross. Reading one is a check error (only members with a literal initializer are
supported); an imported proc that touches its own module’s variable fails codegen with
undeclared variable.
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 — it crashes the IR rather than reporting),mergeis 🔴 (katnip_list_merge). - 🟢
math—pi,e,taufold to literals; theMathOpenum andopwrapoperator_mathop, andabs,floor,ceil,sqrt,sin,cos,tan,asin,acos,atan,ln,log,epowarebuildsprocs over it.powwas removed rather than fixed. Norandom,min,maxorroundyet. - 🟡
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 —
Num,StrandBoolarebuildsprocs that inline the value unchanged. They are type-checker assertions, not runtime conversions.Listwas removed from the prelude; there is no list cast. - 🔴 Helpers —
typeofandmotion.getPositionhave no codegen metadata; using one throwsno slot metadata. - 🟡 Iteration helpers — in a
forheaderrange(),zip()andenumerate()are 🟢, folded into the loop and never built. Used as a value,zipandenumeratethrowno slot metadata🔴, andrangeis worse — it silently produces an ⛔ empty list.
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.json, the default costume and every declared asset into a.sb3that loads in Scratch and MistWarp. - 🟢 Assets — declared costumes and sounds are keyed by md5, so one file used from
several sprites lands in the zip once. Rotation centres and sample rates are left for
Scratch to compute on load. A sprite with no
costumekeeps the default cat. - 🔴 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. - 🟢 Warnings — a non-blocking severity alongside errors;
no sprites declared, so this builds to an empty projectis the only one today. - 🟢 Tests —
node --testcases across lexer, parser, semantic, callgraph, imports, IR, and codegen, plus 7todocases ingaps.test.tsthat assert each gap on this page is still a gap, so closing one fails loudly. The imports case now passes. - 🟢 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”- Close the silently-wrong gaps —
**, slices,range()as a value, and tuple-pattern assignment — since each builds a project that runs and lies. - Give the remaining
katnip_*builtins real lowerings;console,typeof,list.mergeand the dict methods are the widest hole. - Lower structs; the analyzer is already complete for them.
- Lower imported module variables and lists, so a module can own state.
- Lower
@lower = "yields"procs, unblockinglist.removeandconsole.input. - Attach comments to the AST and emit them into the sb3 comment map.