Ready, Spec, Ship · built with Kiro

A real language, from spec to bytecode.

TinyLang is a complete programming language toolchain in TypeScript. A tree-walk interpreter, a 63-opcode bytecode VM and a WebAssembly target, cross-checked to byte-identical output. Plus a debugger, formatter, linter, test runner and a browser IDE. Not a prototype.

0
Tests passing
0
CLI commands
0
Execution backends
0
Bytecode opcodes
fibonacci.tiny
fn fibonacci(n) {
  if n <= 1 { return n }
  let a = 0, b = 1
  for i in 2..n + 1 {
    let t = b; b = a + b; a = t
  }
  return b
}

print(f"fib(20) = {fibonacci(20)}")
three backends, one answer
$ tinylang run fib.tiny        # interpreter
fib(20) = 6765
$ tinylang compile fib.tiny && tinylang exec fib.tinyc
fib(20) = 6765            # byte-identical
$ tinylang wasm fib.tiny       # → WebAssembly
fibonacci(20) === 6765 ✓   # asserted under WebAssembly
The narrative

Spec to ship, in that order.

TinyLang was not coded and then documented. Each subsystem was specified, designed and broken into tasks with Kiro before a line of it was written. Here is how it came together.

Phase 01 · Specify

Requirements, design and tasks first

Every subsystem starts as a spec in .kiro/specs/: user stories with acceptance criteria, a design doc with the data flow and the operator precedence table, then a task list with checkboxes. The compiler, VM, debugger, toolchain, Web IDE and test framework each got the full treatment before implementation.

Phase 02 · Core engine

Lexer, parser, tree-walk interpreter

The interpreter is the reference implementation, built test-first. Six data types, classes with inheritance, closures, pattern matching, f-strings, ranges and try/catch. Error messages are written for beginners: they name what went wrong and point at the fix.

Phase 03 · Compiler and VM

Bytecode, optimized, on a stack machine

A 63-opcode compiler with constant folding, dead-code elimination and a peephole pass, feeding a stack VM with call frames, upvalues captured by reference and a try/catch handler stack. Then the hard question: does it agree with the interpreter?

Phase 04 · The toolchain

17 commands around the language

A stepping debugger, a formatter that refuses to change meaning, a linter with auto-fix, a built-in test runner, a profiler, an AST viewer, a WebAssembly emitter and a 480KB browser IDE. One CLI, no external services.

Phase 05 · Prove it

Cross-validate, then trust

A differential suite runs the same programs through both backends and asserts byte-identical output. It found six real VM bugs that a wall of hand-written tests had missed. Correctness is measured by agreement between engines, not by the size of the test count.

Why differential testing

The VM passed every test it had and still returned fib(20) = 53.

The right answer is 6765. Forty-two hand-written VM tests were green. The bug hid in a place none of them looked. What caught it was running the same program through the interpreter and the VM and comparing the output character for character. Five more bugs surfaced the same way.

So the bar moved. Today 191 differential tests assert both backends produce identical output on every example. The formatter carries 149 round-trip and 98 comment-preservation checks. Nothing ships on a passing count alone.

differential.test.ts
// the interpreter is the reference.
// the VM must match it, byte for byte.
expect(runVM(src)).toEqual(runInterp(src))

✓ 191 passed  zero exclusions
✓ 1003 passed (21 files)
The toolchain

Everything around the language.

17 commands in one CLI. Compile, run, debug, format, lint, test, profile and ship to the browser.

⚙️

Bytecode compiler

63-opcode instruction set, constant pool, constant folding and dead-code passes. Emits .tinyc the VM executes.

🧮

Stack VM

Call frames, upvalues captured by reference, a try/catch handler stack and re-entrant dispatch for higher-order callbacks.

🌐

WebAssembly target

Compiles numeric functions to .wat, then assembles, validates, instantiates and calls them. It says out loud what it cannot compile rather than faking it.

🐛

Interactive debugger

Breakpoints, step over, into and out, watch expressions, locals and the live call stack.

Safe formatter

Re-parses its own output and compares the trees. It refuses to write if the format would change meaning or drop a comment.

🔍

Linter with auto-fix

Unused variables, unreachable code, prefer-const and naming rules. --fix applies the safe ones.

🧪

Built-in test runner

expect(x).toBe(y) with colored output and pass/fail counts. No external framework to install.

📊

Profiler and benchmarks

Timing, memory and instruction stats, with an interpreter-versus-VM comparison mode.

🖥️

Browser IDE

Editor, live output, AST view and a debugger panel in one page. The full language engine is embedded in the bundle, so programs run client-side.

Built with Kiro

Spec-driven from commit one.

The .kiro directory is not decoration. It is the plan the code was built against. It ships in the repo so you can read it.

Specs

Requirements → Design → Tasks

A full spec set for the compiler, VM, debugger, toolchain, Web IDE and test framework, written before the code.

10

Steering documents

Coding standards, the language spec, architecture decisions, the testing philosophy and performance notes that keep every session on the rails.

10

Automation hooks

Type-check, test, lint and format on save, plus a pre-commit gate, a benchmark check and a Web IDE rebuild.

.kiro/
.kiro/
├── specs/
│   ├── requirements.md      # user stories
│   ├── design.md            # data flow, precedence
│   ├── tasks.md             # phased checklist
│   ├── compiler-vm/         # bytecode ISA
│   ├── debugger/            # stepping protocol
│   ├── toolchain/           # CLI + formatter
│   ├── web-ide/             # editor + panels
│   └── testing-framework/   # assertions
├── steering/                # 10 knowledge files
└── hooks/                   # 10 automation hooks
  • Specs led the code. The design docs carry the operator precedence table and the opcode plan the implementation follows.
  • Steering held the line. The testing steering file mandates the cross-validation that caught the VM bugs.
  • Hooks kept it green. Tests, lint and format ran on save; a pre-commit gate blocked regressions.
  • Nothing is faked. Every command runs for real. The WASM target reports what it cannot compile instead of pretending.
Run it yourself

No Docker. No cloud. No keys.

Every feature runs from a fresh clone. Three commands and you have the whole toolchain.

terminal
$ git clone https://github.com/zkasuran/TinyLang.git
$ cd TinyLang && npm install && npm run build
$ node dist/cli/index.js run examples/07-fibonacci.tiny
=== Recursive Fibonacci ===
fib(10) = 55
$ npm test
Test Files  21 passed (21)
     Tests  1003 passed (1003)