Resource Ownership
Cuttlefish has an optional set of features that help you share memory and hardware buses safely between different parts of your program. They’re turned off by default — if you don’t use them, Cuttlefish behaves exactly like ordinary TypeScript.
Who this is for. These features are aimed at larger or more demanding programs where you’re sharing data or buses between several pieces of code and want the tool to catch mix-ups for you. If you’re just blinking an LED or reading a sensor, you can skip this whole section.
Hardware Ownership (take & release)
In embedded systems, multiple tasks (such as a sensor reader and a display driver) often share the same hardware bus (e.g., I2C0). This can lead to race conditions where one task interrupts another’s communication, causing bus collisions or corrupted data.
The Problem
// Task A: Reading a Sensor
I2C0.device(0x76).readReg(0x00);
// Task B: Updating a Display (could potentially interrupt Task A)
I2C0.device(0x3C).writeReg(0x00, 0x55);The Solution: Exclusive Claims
Cuttlefish provides an exclusive acquisition pattern. When you take() a bus, you receive a handle that uniquely owns that resource.
import { I2C0 } from '@typecad/board';
// Claim exclusive access
const bus = I2C0.take();
if (bus) {
// We now exclusively own I2C0
sensor.readReg(0x00);
// Return the bus to the shared pool when finished
bus.release();
}Implementation Details:
take()/release()are compile-time ownership markers — nothing is emitted into the C++. The ownership pass validates the discipline instead: no double-take(), norelease()without atake(), no bus taken and never released, and no I/O outside the claimed region.- The system is opt-in: if
take()never appears, no diagnostics are generated.
Memory Safety Types
Cuttlefish introduces Phantom Types that allow you to specify how data is owned and borrowed. These types are erased during transpilation—resulting in zero runtime overhead—but they guide the transpiler’s static analyzer to prevent common embedded programming errors.
Owned<T>: Single Ownership
The Owned type indicates that a variable is the sole owner of its data.
- Benefit: Prevents Use-After-Move errors.
- How it works: When you assign an
Ownedvariable to another, ownership is “moved.” The original variable becomes invalid, preventing bugs where two different parts of your code try to manage the same memory simultaneously.
import { Owned } from '@typecad/board';
function process(data: Owned<Uint8Array>) { /* ... */ }
let buffer: Owned<Uint8Array> = new Uint8Array(32);
process(buffer); // Ownership is MOVED into the function
// ERROR: 'buffer' is no longer valid here because 'process' now owns it.
// UART0.writeLine(buffer[0]); Shared<T>: Immutable Borrowing
The Shared type is used when you want to share data for reading without allowing any modifications.
- Benefit: Prevents Accidental Mutation and Memory Bloat.
- How it works: Cuttlefish ensures that any variable marked as
Sharedcannot be the target of an assignment or update.
Why not just use const? While const in TypeScript prevents the variable itself from being reassigned, Shared<T> provides three additional layers of safety and performance:
- Deep Immutability: Unlike a standard
constarray, you cannot modify the contents (elements) of aSharedarray. - Zero-Copy (C++ References): For non-primitive types, the transpiler emits a C++ reference (
const T&). This means the data is shared by address rather than being copied, saving CPU cycles and RAM. - Stability Guarantees: The analyzer ensures that while a
Sharedborrow is active, no other part of the code can obtain aMutableto the same data, preventing “spooky” bugs where data changes while you are reading it.
import { Shared } from '@typecad/board';
const config: Shared<number[]> = [1, 2, 3];
function readConfig(c: Shared<number[]>) {
UART0.writeLine(c[0]);
// c[0] = 5; // ERROR: Shared prevents modifying elements (Deep Immutability)
}
readConfig(config); // Pass-by-reference (Zero-Copy)Mutable<T>: Mutable Borrowing
A Mutable is an explicit “mutable borrow.” It allows you to pass data to a function or variable that needs to modify it in-place.
- Benefit: Prevents Shared Mutability bugs.
- How it works: While
Sharedallows many readers,Mutabledocuments that a specific part of your code has temporary, exclusive permission to change the data. It makes data flow explicit and easier to debug.
import { Mutable } from '@typecad/board';
function calibrate(val: Mutable<number>) {
val = val + 10; // OK: Mutable specifically allows mutation
}
let offset: number = 5;
calibrate(offset);When to Use These Types
If you are a developer coming from high-level languages like JavaScript, you might be used to objects being shared automatically. In the embedded world, this “hidden sharing” often leads to:
- Race Conditions: Two tasks changing the same variable at once.
- Memory Corruption: A function modifying a buffer that another function thought was constant.
- Dangling Pointers: Using a piece of memory after it has been deleted or repurposed.
By using Owned, Shared, and Mutable, you turn these dangerous runtime crashes into simple red squiggles in your editor.
Zero-Cost Abstractions
A core principle of Cuttlefish is that safety should not come at the cost of performance. Because these rules are enforced at the transpiler level, the generated C++ is identical to hand-written code:
Owned<number>emits asint.Shared<number>emits asconst int.take()andrelease()emit nothing into the C++ on any platform.
API & Type Reference
Bus Ownership Handle
| Method | Description |
|---|---|
bus.take() | Claims exclusive ownership of the bus from this point. Taking a bus that is already taken is an error. |
bus.release() | Returns ownership of the bus to the system. |
Ownership Phantom Types
| Type | C++ Emission | Rule Enforced |
|---|---|---|
Owned<T> | T | Prevents use-after-move (Transfer of ownership). |
Shared<T> | const T (primitives), const T& (complex types) | Prevents reassignment and modification (Immutability). |
Mutable<T> | T& (complex types) | Explicitly marks a mutable reference for clarity. |
Complex Example: Safety Across Functions
import { Owned, Shared } from '@typecad/board';
/** Reads data without taking ownership (Borrowing) */
function analyze(data: Shared<Uint8Array>) {
const first = data[0];
UART0.writeLine(`Analyzing: ${first}`);
}
/** Processes data and takes full ownership (Moving) */
function archive(data: Owned<Uint8Array>) {
// Logic to move data to long-term storage...
}
function run() {
const myData: Owned<Uint8Array> = new Uint8Array([0x01, 0x02]);
analyze(myData); // OK: Borrowing allowed while owned
archive(myData); // OK: Ownership transferred (MOVE)
// analyze(myData); // Error: Cannot borrow 'myData' after ownership was moved
}On This Page