DCDart documentation

A working subset of Dart, compiled ahead-of-time to native object files with a C ABI. This page documents what is real today; each limitation is one the compiler enforces loudly rather than silently mishandling.

Philosophy

DCDart exists to write systems code — kernels, firmware, drivers, high-performance native libraries — in a language that reads like Dart. Three decisions define it:

Divergence you will hit first: "héllo".length is 6 (UTF-8 bytes), not 5 (UTF-16 code units). DCDart counts bytes because that is what memory and wire formats contain.

Types & integers

DCDart provides fixed-width unsigned integers and IEEE-754 floating-point types. Integers require explicit conversions:

Type Width Operators
u8 u16 u32 u64 8/16/32/64-bit unsigned + - * ~/ % (trapping), < <= > >= == !=, & | ^ << >>
f32 f64 32/64-bit IEEE-754 + - * /, comparisons, explicit conversions; NaN and Infinity follow IEEE-754
bool i1 internally conditions only — ! is not yet supported (GAP-0023)
Pointer<T> machine word .fromAddress, .address, .value and .elementAt(n)

Integers do not silently promote. Convert explicitly with .toU8().toU64(); there are no signed sized-ints yet (see GAP-0026).

Arithmetic traps. Overflow, ~/ 0 and % 0 kill the process (SIGILL/SIGTRAP) rather than wrapping. This is deliberate. The specification’s proposed wrapping operator syntax is not part of the current prelude.
@bare
u64 factorial(u64 n) {
  var acc = u64(1);
  var i = u64(2);
  while (i < n + u64(1)) {
    acc = acc * i;          // traps at factorial(21) — by design
    i = i + u64(1);
  }
  return acc;
}

Ownership & ARC

Any class extends HeapObject is heap-allocated with automatic reference counting. You never write retain or release — the compiler inserts them, then removes the redundant ones:

Rule Meaning
Parameters borrow by default A callee may read a borrowed object; it stays alive for the caller
@owned parameters transfer The caller's reference moves in; a released @owned param is the callee's to free
Heap fields are retained Storing an object in a field retains it; the owning object's destructor cascade releases fields when it dies
Weak<T> nils out A weak reference to a dead object loads as dead rather than dangling; the dying object waits in a zombie slot until weak refs drop too

Elision passes then delete what ownership makes provably redundant — a retain(x); release(x) pair with no intervening release disappears entirely, and passing an object as an @owned argument can compile to zero ARC instructions.

class Box extends HeapObject {
  final u64 value;
  const Box(this.value);
}

@bare
u64 makeBoxAndReadValue(u64 v) {
  final b = Box(v);   // alloc=1
  return b.value;     // release=1 (elided to zero retains — checked)
}                     // heap returns to baseline: verified leak-free
Countable, not vibes. dc-objdump --arc file.dart prints the exact alloc/retain/release/weak operations per function at the DC-IR level. The conformance suite asserts these counts.

Strings

Str literals are UTF-8 bytes in .rodata, interned per exact byte sequence. You can measure, compare and slice them:

@bare
u64 greetLen() => Str("hello from DCDart").length;   // 17 — bytes, always bytes

An owning String type is not provided. Borrowed Str slices and a lower-level buffer implementation exist; the buffer is not exposed as a prelude StrBuf type (GAP-0045).

Result & errors

Dart has no ? operator and DCDart cannot add syntax, so error propagation is an explicit method:

@bare
Result withdraw(Account acct, u64 amount) {
  if (acct.balance < amount) {
    return Result.err(u64(1));        // code 1 = insufficient funds
  }
  acct.balance = acct.balance - amount;
  return Result.ok(acct.balance);
}

@bare
Result openAndWithdrawTwice(u64 initial, u64 amount) {
  final acct = Account(initial);
  withdraw(acct, amount).propagate(); // failure short-circuits out of the function
  final afterSecond = withdraw(acct, amount).propagate();
  return Result.ok(afterSecond);
}

Result returns by value across the C ABI as a real struct — the generated header spells the layout, and a C caller uses it without any special support.

Structs & MMIO

For C-layout interop, @packed ... extends Struct gives you a zero-cost view over an address. Getters/setters are never executed — dcc-lower reads their declaration order and types to compute byte offsets:

@packed
class Header extends Struct {
  const Header.fromAddress(u64 address) : super.fromAddress(address);
  u8 get a => throw UnimplementedError();   // offset 0 — bodies are never run
  set a(u8 v) => throw UnimplementedError();
  u32 get b => throw UnimplementedError();  // offset 1 — @packed: no padding
  set b(u32 v) => throw UnimplementedError();
}

@bare
void writeHeader(u64 address, u8 aVal, u32 bVal) {
  final h = Header.fromAddress(address);
  h.a = aVal;
  h.b = bVal;
}

This example's layout is verified byte-for-byte against a C reference struct compiled with #pragma pack(1).

For memory-mapped device registers, use Volatile<T>. Ordinary Pointer<T> accesses may be optimized; volatility and ordering are separate concerns.

Static data

Constants can be pinned into .rodata as bare arrays — no length word, no header, exactly the bytes you declared:

@rodata final List<u32> TABLE = const [1, 2, 3, 5, 8];

@bare
u32 lookup(u64 i) => TABLE[i.toInt()];   // raw pointer arithmetic under the hood

C FFI

FFI works in both directions and is checked, not trusted:

DCBool is refused in headers rather than mapped to C bool — LLVM i1 and C's byte-sized _Bool would produce a header that compiles, links, and is silently wrong.

Port I/O

x86 port I/O is a first-class pair for kernel work (built for a real downstream OS):

@bare
void uartInit(u64 port) {
  Port.outb(port + u64(1), u8(0x00));   // disable interrupts
  Port.outb(port + u64(3), u8(0x80));   // DLAB on
  // … a real 16550 UART init, 7 outb + 1 inb, verified by disassembly
}

The toolchain: dcc

dcc build --mode bare --target host input.dart -o out.o --emit-header out.h
Flag Meaning
--mode bare (freestanding C-ABI object — the working mode) or hosted (not yet implemented)
--target host, macos-arm64, linux-x86_64, windows-x86_64, darwin-x86_64, linux-aarch64, windows-aarch64, bare-x86_64 (the original default)
--emit-header Also write the generated C header for every exported symbol
-o Output object path

Supporting tools: dc-objdump --arc (per-function ARC counts), scripts/verify-freestanding.sh (the zero-undefined-symbols spine check).

Targets

All eight targets cross-compile from one machine and are zero-undefined-symbol clean — ELF, Mach-O and COFF across x86-64 and aarch64. --mode (language subset) and --target (machine) are orthogonal: a @bare object is a plain C-ABI object, which is why DCDart code links into ordinary hosted programs just as happily as into freestanding ones.

Build & run

On macOS with Apple Silicon, install v0.1.0 from the DotCorr tap. A Dart SDK 3.12.2 must be available on PATH, or set DCDART_DART to its executable.

brew tap dotcorr/tap https://github.com/DotCorr/homebrew-tap
brew install dotcorr/tap/dcdart

# compile a program — pass the shipped prelude; its path is matched lexically,
# so spell it the same in your source's import
PRELUDE="$(brew --prefix)/opt/dcdart/libexec/core/runtime/dc-core-bare/prelude.dart"
dcc build --mode bare --target host main.dart -o main.o --emit-header main.h --prelude "$PRELUDE"

A Dart SDK 3.12.2 on PATH (or DCDART_DART) is required for the kernel-frontend stage.

Create main.dart with the same absolute prelude path printed by echo "$PRELUDE". On a standard Apple Silicon Homebrew installation:

import '/opt/homebrew/opt/dcdart/libexec/core/runtime/dc-core-bare/prelude.dart';

@bare
u64 sumTo(u64 n) {
  var i = u64(0);
  var total = u64(0);
  while (i < n) {
    total = total + i;
    i = i + u64(1);
  }
  return total;
}

Run the dcc build command above. It creates an object file and header. To run it, save this C host as host.c:

#include <stdio.h>
#include "main.h"
int main(void) {
  printf("%llu\n", (unsigned long long)sumTo(100));
  return 0;
}
clang host.c main.o -o example
./example
# 4950

The installed Homebrew v0.1.0 compiler was verified with this calculation on 13 September 2026.

The canonical source walkthrough is core/docs/testing-setup.md. The short version:

git clone https://github.com/DotCorr/dcdart
cd dcdart
bash core/scripts/vendor-frontend.sh   # required — restores the vendored frontend
source core/scripts/dcdart-env.sh
bash core/tests/run-conformance.sh     # inspect the current results

Every program is then compiled to an object and linked into an ordinary C binary — main lives in a .c file because there is no runtime to start one.

Browser playground

The playground runs real DCDart-compiled WebAssembly with editable function inputs. It includes the compiler-generated allocator and ARC for heap examples; live allocations are read from WASM memory after each return.

Compilation is performed at site build time. The browser does not compile edited source. The experimental adapter uses the unchanged DCDart lowerer and LLVM emitter, then LLVM’s wasm32 backend; it is not a general WASM target in dcc. Arbitrary source compilation, hosted features and operating-system APIs are unavailable here.

Checked unsigned 64-bit multiplication may require LLVM’s __multi3 helper on wasm32. That helper is not bundled, so this playground exposes verified division, remainder and GCD functions instead. Every published module has zero unresolved imports.

Limitations, stated plainly

Each limitation has a number and an entry in known-gaps.md ↗. Unsupported features are reported by the compiler. The gap records distinguish implemented behavior from planned language features.