Owned, Shared & Mutable

A hal program becomes C++, and these three type annotations decide the memory behind it: who owns a value, who may read it, and who may write it. They are checked at transpile time — the mistakes that become dangling pointers and surprise copies in C++ are errors and warnings here.

  • Owned<T> — the value is yours. Assigning it to someone else moves it; using it afterward is refused.
  • Shared<T> — a read-only borrow. Zero copy, any number of them, and nothing can be written through one.
  • Mutable<T> — a writable borrow. Writes through it reach the original.
class

Owned<T>

Marks a value you own. Assignment transfers ownership, and the transpiler tracks the transfer: the source becomes unusable, so use-after-move bugs never reach the board.

Example first

owned.ts
import { UART0 } from '@typecad/hal';

let packet: Owned<number[]> = buildPacket();
let archived = packet;   // ownership moves — 'packet' no longer has it

UART0.writeLine(String(packet.length));
// ✗ error: 'packet' was moved and cannot be used again

Every unannotated variable already behaves this way — assignment copies, and each copy is its own value. What Owned adds is the tracking: the move is recorded and a later use of the source is an error.

class

Shared<T>

A read-only borrow: the data stays where it is and you get a const view of it. Non-primitives lower to const T& — no copy at all; primitives borrow by value as const.

Example first

shared.ts
import { UART0 } from '@typecad/hal';

const view: Shared<number[]> = readings;   // zero-copy, read-only
const limit: Shared = config.threshold;    // primitives borrow by value

UART0.writeLine(String(view.length));      // reading is what it's for

view = filtered;
// ✗ error: Cannot assign to 'view' — it is an immutable borrow
//   hint: change 'view: Shared<number[]>' → 'view: Mutable'

Any number of Shared borrows of one value can coexist — that is the point of immutable data. Writing through one is refused at transpile time, with a hint that names the Mutable annotation that would allow it.

class

Mutable<T>

A writable borrow. The function that receives one can change what it points at, and the changes are visible to the caller — no copies, no return values, no out parameters.

Example first

mutable.ts
function scale(values: Mutable<number[]>, factor: number) {
  for (let i = 0; i < values.length; i++) {
    values[i] = values[i] * factor;   // writes reach the caller's array
  }
}

scale(samples, 0.5);   // in place — no copy, nothing returned

Non-primitives lower to T&. Primitives stay passed by value — a borrow of an int saves nothing — so annotate the arrays, strings, and structs.

Why parameters want it

An unannotated struct parameter arrives as a copy, and mutating that copy silently does nothing to the caller's value:

by-value.ts
function zero(reading: Reading) {   // no annotation — passed by value
  reading.value = 0;
  // ⚠ changes are invisible to the caller
  //   hint: use 'reading: Mutable<Reading>' to pass by reference (T&)
}

The transpiler catches this pattern — changes are invisible to the caller — and the fix is the annotation: reading: Mutable<Reading> passes by reference so the write lands.

pattern

bus ownership — take / release

Memory isn't the only thing worth owning. Buses are shared hardware, and the same opt-in model guards them: take a bus before using it, release it when you're done. I2C0, I2C1, SPI0, SPI1, and UART0 are recognized.

Example first

bus-ownership.ts
const i2c = I2C0.take();

const sensor = i2c.device(0x44);
const reading = sensor.readReg(0x00);

i2c.release();   // other code may take it now

Nothing is emitted — take() returns the same bus object; the ownership is a compile-time claim. The validator checks the discipline:

  • Taking an already-taken bus — error, with the fix in the hint
  • Releasing, or doing I/O on, a bus nobody took — error
  • Taken and never released — warning (a leak, not necessarily a bug)

The configuration is the setup

There is no begin(). Peripherals are ready the moment your program runs, because construction is the configuration — the arguments you pass become devicetree nodes, Kconfig lines, and guarded one-time driver calls, not runtime state.

  • Use-before-configure can't happen — there is no configure step to skip
  • Double-configure can't happen — the facts are compile-time, applied once before main()
  • Two peripherals claiming the same pins is a build error (conflict detection)
  • Requesting a peripheral the board lacks is a module-resolution error

Runtime verbs that genuinely reconfigure — PWM.setPeriod(), WiFi.leave() — are ordinary methods on a configured object, not a second constructor.

What each one lowers to

AnnotationMeaningC++ the transpiler emits
(no annotation)A private copy — every assignment copiesC++ value
Owned<T>You own the value; assigning it moves itC++ value — use after the move is an error
Shared<T>Read-only borrow — as many as you likeconst — non-primitives borrow as const T&
Mutable<T>A writable borrow — the one place writes are allowedT& for non-primitives; writes reach the source

The checks

All of this runs while your program is transpiled — nothing on the board, nothing at runtime. Errors stop the build; warnings and infos explain themselves in the output.

CheckSeverityWhen it fires
Use after moveerrorReading a variable whose ownership already moved — 'was moved and cannot be used again'
Assign to SharederrorWriting through an immutable borrow — the hint says exactly what to change to Mutable
Update to Sharederror++ / -- through an immutable borrow
Shared passed as MutableerrorA read-only borrow handed to something that writes — 'expects a mutable borrow'
Dangling borrowerrorReturning a borrow of a local — 'destroyed when this function returns'
Borrowing a temporarywarningShared/Mutable initialized from a call result — C++ cannot reference an rvalue, so it falls back to a copy
Silent copyinfoAn unannotated non-primitive assigned from a variable — suggests Shared for zero-copy
Const suggestioninfoA let never reassigned emits as C++ const — the compiler can place it in ROM and fold it
© 2026 typeCAD — code-defined electronics github