Embedding the compiler
The compiler is a normal ES module. Three functions are exported, plus the error type and a resolver hook — enough to build a browser playground, a different editor integration, or a build step.
npm install @katnip-org/compilerimport { checkSource, compileToIR, compileToSb3, KatnipError } from "@katnip-org/compiler";Requires Node 20+. The package is ESM-only.
checkSource
Section titled “checkSource”checkSource(source: string, options?: Options): readonly KatnipError[]Lex, parse, analyze. Returns the diagnostics; an empty array means clean.
const errors = checkSource(source);for (const e of errors) { console.log(`${e.message} at ${e.location.line}:${e.location.column}`);}compileToSb3
Section titled “compileToSb3”compileToSb3(source: string, options?: Options): { errors: readonly KatnipError[]; sb3?: Uint8Array }The whole pipeline. sb3 is present only when errors is empty.
import { writeFile } from "node:fs/promises";
const { errors, sb3 } = compileToSb3(source, { path: "/proj/main.knip" });if (!sb3) throw new Error(errors.map(e => e.message).join("\n"));await writeFile("out.sb3", sb3);In a browser, hand the bytes straight to a download or to TurboWarp:
const url = URL.createObjectURL(new Blob([sb3], { type: "application/zip" }));compileToIR
Section titled “compileToIR”compileToIR(source: string, options?: Options): { errors: readonly KatnipError[]; ir?: IRProgram }Stops after lowering. Useful for tooling that wants the Scratch-shaped tree without the zip.
Options
Section titled “Options”interface Options { path?: string; // the entry file's path, for resolving relative imports resolve?: ImportResolver; // how to fetch an imported file}Imports are only followed when you supply a resolve. Without it, the entry file is
compiled alone.
Import resolvers
Section titled “Import resolvers”type ImportResolver = (specifier: string, fromPath: string) => { path: string; source: string } | null;Return null for an unresolvable specifier; the compiler reports it against the importing
file with a source span.
Filesystem, as the CLI does
Section titled “Filesystem, as the CLI does”import { readFileSync } from "node:fs";import path from "node:path";
const fileResolver = (specifier, fromPath) => { const resolved = path.resolve( path.dirname(fromPath), specifier.endsWith(".knip") ? specifier : `${specifier}.knip`, ); try { return { path: resolved, source: readFileSync(resolved, "utf8") }; } catch { return null; }};Virtual files, for a playground
Section titled “Virtual files, for a playground”const files = { "/main.knip": "import \"./lib.knip\";", "/lib.knip": "public proc double(n: num) -> num { return n * 2; }",};
const virtualResolver = (specifier, fromPath) => { const resolved = new URL(specifier, `file://${fromPath}`).pathname; const key = resolved.endsWith(".knip") ? resolved : `${resolved}.knip`; return files[key] ? { path: key, source: files[key] } : null;};Each file is read and parsed exactly once per compile, no matter how many times it is imported. Cycles are detected and reported.
Errors
Section titled “Errors”class KatnipError { message: string; location: { line: number; column: number };}Errors are collected, not thrown — the parser recovers into error nodes so a syntax error
does not hide the semantic errors after it. Both compileToIR and compileToSb3 catch the
KatnipErrors that codegen throws and return them in errors; anything else propagates.
Performance note
Section titled “Performance note”The standard library is parsed once and cached across calls in module scope. The first
checkSource in a process pays for it; the rest do not. That is what makes an
on-every-keystroke editor integration viable.