DCDart documentation

Release v0.1.3 · Cross-checked 13 September 2026.

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 Literals, comparisons, !, and short-circuit && / ||; no boolean C ABI
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 (implemented systems-language subset) or hosted (not yet implemented)
--target host or an alias/triple from the complete target registry below. Defaults to bare-x86_64.
--emit-header Also write the generated C header for every exported symbol
--prelude Explicit prelude file path; import the same file in your source.
-o Output object path

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

Compilation targets

v0.1.3 has eleven explicit targets, plus host to select the development machine. The compiler’s installation platform and the platform your code targets are separate. Cross-linking requires the destination platform’s SDK, libraries, and linker.

Target LLVM platform triple
bare-x86_64 x86_64-unknown-none-elf
bare-aarch64 aarch64-unknown-none-elf
linux-x86_64 x86_64-unknown-linux-gnu
linux-aarch64 aarch64-unknown-linux-gnu
macos-x86_64 x86_64-apple-macosx11.0.0
macos-arm64 arm64-apple-macosx11.0.0
ios-arm64 arm64-apple-ios16.0
ios-simulator-arm64 arm64-apple-ios16.0-simulator
android-arm64 aarch64-linux-android26
windows-x86_64 x86_64-pc-windows-msvc
windows-arm64 aarch64-pc-windows-msvc

--mode bare selects the implemented language subset; --target selects the machine and object format. Emitted objects use the C ABI. This does not implement the general hosted Dart runtime.

iOS & Android in v0.1.3

Compile the same systems code for iOS 16+ arm64 devices, iOS 16+ arm64 simulators, and Android arm64 API 26+. Device and simulator objects have distinct Apple platform triples. Android uses its Android ABI, not a renamed Linux object.

dcc build --mode bare --target ios-arm64 logic.dart -o logic-ios.o --emit-header logic-ios.h --prelude "$PRELUDE"
dcc build --mode bare --target ios-simulator-arm64 logic.dart -o logic-sim.o --emit-header logic-sim.h --prelude "$PRELUDE"
dcc build --mode bare --target android-arm64 logic.dart -o logic-android.o --emit-header logic-android.h --prelude "$PRELUDE"

Apple linking requires macOS and Xcode’s matching SDK. Android linking requires the NDK and its API-level compiler wrapper. These commands emit an object and C header; they do not create an IPA, APK, or signed application.

Swift/Objective-C projects can link the object and import the generated C header. Android projects can link it into a native library with the NDK and expose it through JNI. Swift Package Manager, CocoaPods, Maven, and Gradle are application/library integration choices; no DCDart package for those managers is currently published.

Verified scope: the shared mobile conformance source compiled and linked for macOS, iOS device, iOS Simulator, and Android arm64. Five behavioral cases ran on macOS and iOS Simulator. Android linked as an executable and shared library; Android and iOS device execution were not performed.

Exact toolchain commands and evidence: mobile target release notes and conformance harness. No Dart VM is linked into the generated application.

Build & run

Install v0.1.3 through the DotCorr Homebrew tap on macOS Apple Silicon or Linux x86-64/ARM64. Linux archives were built and tested on Ubuntu 24.04 and require glibc 2.39 or newer. A Dart SDK 3.12.2 must be available on PATH, or set DCDART_DART to its executable.

brew tap dotcorr/tap
brew trust --formula dotcorr/tap/dcdart
brew install dcdart
# Already installed?
brew update
brew upgrade dcdart
dcc --version  # dcc 0.1.3

# 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 packaged compiler is tested by compiling, linking, and executing this calculation on each advertised native host. Installing a compiler does not install Dart SDK 3.12.2, Clang, or a platform SDK; those remain prerequisites.

Windows x86-64 / Scoop

Install Scoop first. The official DotCorr bucket provides the native x86-64 compiler. Keep Dart SDK exactly 3.12.2 and Clang/LLVM available; use a Visual Studio Developer PowerShell with C++ build tools for native linking.

scoop bucket add dotcorr https://github.com/DotCorr/scoop-bucket
scoop install dotcorr/dcdart
dcc --version
# For later upgrades: scoop update; scoop update dcdart

$prelude = Join-Path (scoop prefix dcdart) 'core/runtime/dc-core-bare/prelude.dart'
$preludeUri = ([System.Uri]$prelude).AbsoluteUri
# Import the printed file URI in your DCDart source:
$preludeUri
dcc build --mode bare --target host main.dart -o main.obj --emit-header main.h --prelude "$prelude"
clang host.c main.obj -o example.exe
./example.exe

The source’s import must point to the same prelude file supplied to --prelude. Windows uses a file:///C:/… URI in the source import. A native Windows ARM64 compiler archive is not published; Windows ARM64 remains a code-generation target.

Other installation options

Download verified archives for macOS ARM64, Linux x86-64/ARM64, and Windows x86-64 from GitHub Releases. Keep the directory layout intact. macOS Intel currently requires a source build. Use the official DotCorr tap, Scoop bucket, or archives above instead of assuming a package exists in a community or operating-system repository.

DCDart is a native toolchain; npm, pub.dev, crates.io, and PyPI packages are not required to install it. The compiler can run on Linux under WSL; use the Linux package and Linux toolchain there.

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

git clone --branch v0.1.3 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 provides an editable source file with syntax highlighting, samples, and a Run button. Your source is sent to an isolated hosted DCDart compiler. The resulting WebAssembly executes on your device in a fresh browser worker.

Write @bare functions using the supported DCDart subset. The prelude is included automatically; omit file directives. Entry points accept and return u64 or f64. Choose a compiled function and change its arguments in the output pane. Compiler errors and runtime traps are displayed directly.

Compilation is limited to 32 KiB of source and 40 seconds, with six requests per minute. Browser execution has a two-second limit and 16 MiB of memory. No packages, operating-system APIs, Flutter widgets, or general Dart runtime are provided. The native compiler’s language limitations still apply.

LLVM may require the unavailable __multi3 helper for checked u64 multiplication. Such programs fail to link rather than silently changing arithmetic semantics. The precompiled examples remain available for exploring ARC accounting and fixed-width inputs without hosted compilation.

Limitations, stated plainly

v0.1.3 adds atomic-alignment checks, packed-memory alignment, error-propagation cleanup and Windows Result interoperability. It includes earlier fixes for temporary-object ownership, borrowed returns, method ownership, and freestanding buffer loops. Remaining safety and language work is tracked in the language audit.

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.