Features That Disappear at Build Time
Some TypeScript features are designed to keep your code readable and safe — and then vanish completely when Cuttlefish writes the C++. The board ends up running lean, direct code, with no extra layers or overhead. You write readable TypeScript; the board runs tight C++.
emit() / rawCpp() — Put Raw C++ In a Specific Spot
The emit() function (also available as rawCpp()) drops a line of raw C++ right where you call it. The TypeScript itself never runs — the string just becomes part of the generated C++.
import { emit } from '@typecad/hal';
// This TypeScript never runs — the string becomes C++ at the call site
emit("PORTB |= (1 << PB5)");
// rawCpp() is an alias for emit() — both work identically
import { rawCpp } from '@typecad/hal';
rawCpp("__asm__ volatile ("nop")");Good for the rare case the hardware library doesn’t cover — direct register access, a line of inline assembly, or platform-specific code.
What happens at build time
- Cuttlefish spots the
emit()/rawCpp()call. - The string is placed directly into the generated C++ at that position.
- The call itself is removed — there’s no function call left at runtime.
What happens during type-checking/testing
emit() is a no-op function (function emit(code: string): void {}). Your TypeScript tests and type-checker simply skip over it.
include() — Add a C++ Library
The include() function adds a #include line (a C++ library import) to the generated output.
import { include } from '@typecad/hal';
include("<Wire.h>"); // → #include <Wire.h>
include('"mylib.h"'); // → #include "mylib.h"Calling include("<Wire.h>") twice only produces one #include.
board() — Read a Value From the Board Definition
The board() function looks up a value from the board definition and puts it directly into the C++ as a fixed constant.
import { board } from '@typecad/hal';
const pwmResolution = board("peripherals.pwm.resolution");
// Resolves to a compile-time constant from the board definitionResolvable paths
The path argument follows the board definition structure:
board("mcu") // → "ATmega328P"
board("clockSpeed") // → 16000000
board("memory.flash") // → 32768
board("memory.sram") // → 2048
board("peripherals.pwm.resolution") // → 8 (bits)
board("pins.all.13.name") // → "D13"
board("pins.all.13.number") // → 13At build time the path is resolved from the board package and put into the C++ as a constant. In a plain TypeScript test, the call returns undefined.
callback() — Mark a Function as a Callback
The callback() directive tells Cuttlefish that a function should be registered as a callback (for example, for an interrupt) rather than called directly.
import { callback } from '@typecad/hal';
callback(() => {
// This becomes an interrupt handler in C++
led.toggle();
});⚙️ Advanced details — what the optimizer does for you
These transformations all happen at build time. You don’t write any of it — it’s just useful to know what’s going on under the hood.
Phantom ownership types
Owned<T>, Shared<T>, and Mutable<T> are phantom types that exist only in the TypeScript type system for build-time validation. They evaporate completely in the C++ output:
Owned<T> — single ownership
The value is emitted as-is with no wrapper. The transpiler tracks ownership transfers (moves) and reports use-after-move errors.
let buf: Owned<Buffer> = new Buffer(64);
let other = buf; // ownership transferred (move)
// buf is now invalid — use-after-move diagnosticC++ output:
Buffer buf(64);
auto other = buf;
// No wrapper types, no reference counting
// The transpiler tracks ownership at compile time onlyShared<T> — immutable borrow
Adds const in C++ for compiler-enforced immutability. The transpiler prevents assignment to Shared variables.
const ref: Shared<Buffer> = buf;
// ref.write() → C++ const method (compiler enforces read-only)C++ output:
const Buffer& ref = buf;Mutable<T> — mutable borrow
Emitted as a regular reference. The transpiler enforces exclusivity — no other borrows can exist while a Mutable is active.
const mut: Mutable<Buffer> = buf;
mut.write(data); // Allowed — mutable borrowC++ output:
Buffer& mut = buf;No runtime cost
- No wrapper classes
- No reference counting
- No garbage collection
Shared<T>adds only aconstqualifier — the C++ compiler enforces immutability- All tracking happens at build time through the ownership validator
Pin handle erasure
TypeScript pin objects become direct function calls or register writes in the C++ output. The HAL class layer does not exist at runtime:
// TypeScript — typed, safe
import { D13 } from '@typecad/board-arduino-uno';
const led = D13.asOutput();
led.toggle();// C++ — no HAL class, no indirection
pinMode(13, OUTPUT);
digitalWrite(13, !digitalRead(13));The pin number is resolved from the board definition and inlined. The mode narrowing (asOutput() → OutputPin) is a TypeScript-only type change — it generates only pinMode() at the point of conversion.
Enum narrowing
The transpiler knows the full, closed value range of every enum at emit time — something the C++ compiler cannot recover. It picks the narrowest underlying type that fits:
enum State { Off, On, Blinking } // values 0–2// C++ — narrowed from default int (2 bytes on AVR) to uint8_t (1 byte)
enum class State : uint8_t { Off, On, Blinking };A 2-member state-machine enum halves from 2 bytes to 1 byte per field on AVR. On AVR, values exceeding the 16-bit int range widen to : long; values in [-128, 127] with negatives narrow to : int8_t.
let → const promotion
The transpiler performs whole-program reassignment analysis — it tracks every assignment to every let binding across all functions and scopes. When it proves a let is never written after initialization, it emits const in C++ automatically:
let threshold = 500; // never reassigned anywhere in the program// C++ — promoted to const, enabling ROM placement and constant folding
const int threshold = 500;This is information the C++ compiler cannot recover across translation units. The inverse demotion (const → non-const when a member is mutated) also applies automatically.
Automatic volatile inference
The classic embedded bug — a global written in an ISR and read in loop() becomes an infinite loop under -Os because the compiler caches it in a register. The transpiler knows which callbacks are interrupt handlers and which globals they write, so it marks shared variables volatile without any user annotation:
let flag = false;
function setup() {
D2.asInput().onFalling(() => { flag = true; });
}
function loop() {
if (flag) { flag = false; }
}// C++ — flag auto-marked volatile
volatile bool flag = false;The volatile keyword is emitted automatically. This eliminates an entire bug class that the C++ compiler cannot detect — “this function runs in interrupt context” is a call-graph property the transpiler sees but g++ does not.
Tree-shaking (removing unused code)
The transpiler performs dead-code elimination by default. Unused enums, classes, type aliases, and variables are removed from the C++ output.
Tree-shaking flags
| Flag | Effect |
|---|---|
--no-tree-shake | Disable all tree-shaking |
--keep-unused-enums | Preserve unused enum definitions |
--keep-unused-classes | Preserve unused class definitions |
--keep-unused-types | Preserve unused type aliases |
--keep-unused-variables | Preserve unused top-level variables |
--entry-point <name> | Additional entry point to keep (repeatable) |
How it works
- The transpiler builds a call graph from the internal representation.
- It identifies entry points (
setup(),loop(), and any functions they call). - Symbols not reachable from any entry point are marked for removal.
- The C++ emitter skips removed symbols entirely.
Entry point detection is controlled by the platform strategy — for Arduino, setup() and loop() are always entry points.
Free-function prototypes
When top-level code calls helper functions defined later in the file, the transpiler automatically inserts forward declarations before setup():
// TypeScript — definition after usage
led.toggle();
delay(500);
function blink() {
led.toggle();
}// C++ — forward declaration inserted
void blink(); // Auto-generated forward declaration
void setup() {
digitalWrite(13, !digitalRead(13));
delay(500);
}
void loop() {
blink();
}
void blink() {
digitalWrite(13, !digitalRead(13));
}Default arguments go on the forward declaration, not the definition, so they’re available throughout the file.
On This Page