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:
-
No VM, no JIT, no tracing GC. Output is a plain
object file.
@bareobjects are linkable into freestanding binaries — even OS kernels. - Dart is the syntax and the type system, not the runtime. Where Dart's semantics are hostile to systems code (UTF-16 strings, wrapping arithmetic, a garbage collector), DCDart diverges and documents the divergence in the spec.
- The C ABI is the boundary. Every export is callable from C with a header generated from the same IR as the code, so declarations cannot drift from the real ABI.
"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).
~/ 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
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:
-
Outbound: your source declares
@externsymbols (e.g. libc'stoupper);dccrecords them in a per-object manifest and refuses to produce an object with undeclared undefined symbols. -
Inbound:
dcc build --emit-header out.hwrites C prototypes generated from the same DC-IR as the object. A C program that includes only the generated header compiles with-Werror— wrong generated types are build failures, not silent ABI corruption.
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.
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.
- Capturing closures are a compile error by design until value ownership is specified (non-capturing closures and function pointers work).
- Generic functions and supported generic classes are monomorphized. Unsupported receiver shapes are documented in GAP-0056.
-
No dynamic dispatch or vtables — every heap object's concrete class
is statically known; cycle collection and
unownedare sequenced after the weak/destructor groundwork that now exists. -
No signed sized-ints or general
asm/@naked. Narrow port I/O and extern FFI are done. - Foreign heap-pointer validation, read-only pointer provenance, borrowed-text lifetimes, and linking several allocating objects still have open work. Qualified fields and managed reference arrays also have remaining restrictions; consult the current gap records before relying on these forms.
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.