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

  1. Cuttlefish spots the emit() / rawCpp() call.
  2. The string is placed directly into the generated C++ at that position.
  3. 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 definition

Resolvable 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")         // → 13

At 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 diagnostic

C++ output:

Buffer buf(64);
auto other = buf;
// No wrapper types, no reference counting
// The transpiler tracks ownership at compile time only

Shared<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 borrow

C++ output:

Buffer& mut = buf;

No runtime cost

  • No wrapper classes
  • No reference counting
  • No garbage collection
  • Shared<T> adds only a const qualifier — 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.

letconst 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

FlagEffect
--no-tree-shakeDisable all tree-shaking
--keep-unused-enumsPreserve unused enum definitions
--keep-unused-classesPreserve unused class definitions
--keep-unused-typesPreserve unused type aliases
--keep-unused-variablesPreserve unused top-level variables
--entry-point <name>Additional entry point to keep (repeatable)

How it works

  1. The transpiler builds a call graph from the internal representation.
  2. It identifies entry points (setup(), loop(), and any functions they call).
  3. Symbols not reachable from any entry point are marked for removal.
  4. 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.