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:
-
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 |
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).
~/ 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 (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
- 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, no boolean
!, no generalasm/@naked. Narrow port I/O and extern FFI are done. - Qualified fields and managed reference arrays 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.