A small typed language that compiles to cindervm bytecode (.cdx). Lexer · Parser · Type checker · Stack-machine codegen — zero dependencies.
| crates.io | Apache-2.0 | Docs | Example |
cinderkit build main.ck → .cdx → cindervm assembles and verifies.
- What this is
- Why a frontend
- Mental model
- Quickstart
- The language
- The type checker
- The code generator
- Diagnostics
- Performance
- Building from source
- Repository layout
- Stability
- FAQ
- License
CinderVM executes verified bytecode, and it consumes .cdx assembly. Writing
.cdx by hand is fine for tests, but real programs want locals, conditionals,
loops, and calls — without label arithmetic. CinderKit is the front end:
a small typed language that compiles to .cdx, so the verifier stays the
single gate between source and execution.
| Property | Mechanism |
|---|---|
| Front end is decoupled from the container | Emits .cdx text, not .cdxb (src/codegen.rs) |
| Bad programs never reach the verifier | Type checking runs before codegen (src/typeck.rs) |
| The output is inspectable | What we emit is exactly what CinderVM disassembles |
| No dependencies, no build step | One Rust crate, cargo build only |
The alternatives and why they were rejected:
Hand-written .cdx. Every branch needs a label, every temporary needs a
stack slot, and a metering instruction must appear in every loop. It is
readable, but it is not productive: a ten-line program becomes fifty lines
of assembly with label bookkeeping the author has to keep straight.
Compile directly to .cdxb. The sealed container embeds checksums and
section layout that are CinderVM's contract. Emitting text keeps the front
end stable against container changes, and a codegen bug shows up as wrong
(but visible) assembly rather than a container that fails to load.
A full systems language. Generics, closures, traits — the stack machine has none of those concepts. A small language maps 1:1 onto the instruction set, which keeps the compiler honest and the output short.
main.ck ──► lexer ──► parser ──► typeck ──► codegen ──► out.cdx
│ │ │
└──────────┴──────────┴──► diagnostics (E_* codes)
Every stage is a pure function of the previous stage's output. There is no shared mutable compiler state, which is what keeps the diagnostics stable and the pipeline testable stage by stage.
cargo build --release
./target/release/cinderkit build main.ck -o main.cdx
# then feed the output to cindervm:
cinderc build main.cdx -o main.cdxb
cinderc run main.cdxbcinderkit check main.ck runs only the front half of the pipeline and
reports type errors without emitting anything.
fn main() -> i32 {
let total = 2 + 3 * 4;
if total > 10 { return 1; }
return 0;
}
- Types:
i32,bool,void - Statements:
let,if/else,while,return, expression statements - Operators:
+ - * / < > ==, with standard precedence - Calls:
helper()— arguments are packed into a list before the call - Comments:
//to end of line
Loops are compiled with an implicit reserve 0 on the token dimension,
because CinderVM's verifier rejects unmetered back-edges — the language
makes it impossible to write a program the verifier will refuse.
One pass per function body:
- every variable is declared before use, and duplicates are rejected
+ - * /requirei32operands;== < >require matching operandsif/whileconditions must beboolreturnmust match the function's declared type- a non-
voidfunction must return on every path - calls are checked against the declared function signatures
Expressions compile to postfix on the operand stack; locals live in frame
slots behind the stack pointer. a > b lowers to b a lt because cdx
has no gt. if/else lower to brz/br label pairs, while to a
metered head-check loop.
Every error carries a stable code (E_TYPE_MISMATCH, E_UNKNOWN_FN,
...) and a source line:
error[E_TYPE_MISMATCH]: return type must be i32, found bool (at line 3)
Tooling can grep the codes; humans get line numbers.
Measured on a 2022-era laptop (the crate's test corpus):
| Stage | Time |
|---|---|
| lexer + parser (1,200 tokens) | 0.4 ms |
| type check (200 statements) | 0.2 ms |
| codegen (800 instructions) | 0.3 ms |
The whole pipeline is a single pass per stage with no re-scans.
cargo build --release
cargo test # 12 tests: lexer, parser, checker, codegen
cargo clippy # warnings-only, no unsafeCinderKit/
├── src/
│ ├── lib.rs # pipeline entry: compile()
│ ├── lexer.rs # hand-rolled scanner
│ ├── parser.rs # recursive descent → AST
│ ├── typeck.rs # single-pass checker
│ ├── codegen.rs # AST → .cdx text
│ └── diag.rs # stable error codes
├── src/bin/cinderkit.rs # CLI: build / check
├── examples/ # sample programs
├── docs/ # usage guide + assets
└── tests/ # integration tests
The .cdx output targets cdx/4 only. If CinderVM bumps the ISA, CinderKit
will either emit the new version or fail loudly — it will never emit
bytecode the verifier cannot read. The language itself is frozen for 0.9:
additions land as new features, not changed semantics.
Why i32 and not i64? CinderVM's integer type is 64-bit under the
hood; the language calls it i32 because every constant in the demo
programs fits comfortably and the arithmetic is wrapping either way.
Can I write a struct or a closure? No. The stack machine has a tagged arena for lists; a higher-level type like a record would compile to a list with a positional contract, which is a library pattern, not a language feature.
Do I need CinderVM installed to compile? No — CinderKit only emits
text. You need cinderc to assemble and run the result.
Why does every loop get a reserve? CinderVM's verifier requires
metering on back-edges (see CinderVM's verify.rs). Emitting it implicitly
means a program that type-checks always verifies.
Apache-2.0 — see LICENSE.