Architecture Overview
Pipeline
Source (.tape)
│
▼
Region Splitter (#code, #view, #style, #localization,
#resource, #test, #bytecode markers)
│
├── #code region ──► Scanner ──► Parser (Pratt + recursive descent)
│ │
├── #view region ──► Scanner ──► View Parser (component declarations)
│ │
├── #style, #localization, #resource, #bytecode ──► their own parsers
│ │
│ ┌───────────────┘
│ ▼
│ Merged AST (tape_ast_module)
│ │
│ ┌───────────┼───────────┐
│ ▼ │ │
│ Module Resolver │ (single-module path)
│ (if imports) │ │
│ │ │ │
│ ▼ ▼ ▼
│ Comptime Execution (@tape blocks, @emit)
│ │
│ ▼
│ Style Resolution (#style rules → props at each site)
│ │
│ ▼
│ Type Checker
│ │
│ ▼
│ TAC Lowering (AST → three-address code)
│ │
│ ▼
│ Link Check (@link symbol verification)
│ │
│ ┌───────────┴───────────┐
│ ▼ ▼
│ VM Interpreter Native Codegen
│ (tape run) (TAC optimizer, inlining,
│ register allocation;
│ x86-64 / ARM64)
│ │
│ ▼
│ Binary Emitter
│ (PE64 / Mach-O / ELF64 / STO)
│ │
│ ▼
└── #test region ──► Output binary
tape test path
(separate pipeline)Key design decisions
- Single IR — both VM and native backends consume the same TAC representation
- Per-region parsers —
#codeuses the full parser,#viewa component parser,#stylea rule parser,#testa test parser,#bytecodean instruction parser (and#localization/#resourcetheir own entry parsers); all feed the same module - No LLVM — direct machine code emission, keeping the compiler self-contained and fast
- Comptime before typecheck —
@tapeblocks and@emitrun first, generating AST nodes that the type checker then validates like hand-written code
Region splitting
The first pass splits the file at #region markers (#code, #view, #style, #localization, #resource, #test, #bytecode). If no markers exist, the entire file is #code. A preamble (imports and attributes before the first marker) is prepended to #code. The #view region is parsed separately and its components are merged into the module’s component list. #style rules are resolved after comptime and before type checking: each instantiation site gets the matching rules’ values for the props it doesn’t set, so nothing of a style is left for run time.
Module resolution
When a module has import statements, the compiler enters the multi-module path:
- Resolve each import to a file path (include paths + relative paths)
- Parse each dependency recursively
- Build a dependency order
- Process each module in order: inject extern stubs → comptime → typecheck
- Build a unified dispatch registry across all modules
- Lower each module with shared dispatch info
- Merge all TAC modules into one
Single-module files (no imports) take a simpler path: comptime → typecheck → lower directly.
Comptime execution
After parsing but before type checking, the compiler evaluates @tape blocks and @tape fn calls. This stage can emit new AST nodes (@emit struct, @emit fn, etc.) that become part of the module. The type checker sees these generated declarations identically to hand-written ones.
TAC IR
The intermediate representation is linear three-address code:
%1 = load_local "x"
%2 = const_i64 1
%3 = add_i64 %1, %2
store_local "x", %3Explicit data flow, no implicit stack, easy to interpret and compile.
Native backends
Two native code generators read the TAC:
| Backend | Target | Output formats |
|---|---|---|
x64_codegen | x86-64 | PE64 executable and DLL (Windows), PE32+ UEFI application (uefi), ELF64 executable, shared object and relocatable object (Linux), Mach-O executable, dylib and object (macOS), ELF64 and STO (Uusi) |
aarch64_codegen | ARM64 | Mach-O executable, dylib and object (macOS), ELF64 executable, shared object and relocatable object (Linux) |
The target is selected with --target (win64, linux, linux-arm64, macos, macos-arm64, uusi, uefi).
VM backend
tape run interprets the TAC directly via tape_vm_run(). The VM also powers tape test — each test runs as its own program via tape_vm_run_test(): globals reset to their initial values, the module initializers, the optional setup, the test, the optional teardown.
Link checking
Before codegen, tape_link_check() verifies that all @link library references and their symbols can be resolved. This catches missing DLLs or undefined symbols at compile time rather than at runtime. It runs only when building for the host platform (a cross-compile cannot load the target’s libraries), and is skippable with --skip-link-check.
Why not stack-based bytecode?
Tape 1.0 used stack-machine bytecode. This caused:
- Reverse-engineering value flow for native codegen
- Phantom operand bugs
- Undebuggable calling conventions
TAC eliminates all of these by making data flow explicit.
Last modified: