DreamFXLang
Generation

The build pipeline

What a build actually does — discovery, ordering, schema probing, writing, the Niagara compile, saving, stamping, and why an unchanged source is skipped.

A build turns source into assets through six stages — and the first digit of a diagnostic code is the stage that raised it:

StageWhat it doesCodes
1Driver and file I/ODFX1xxx
2Lexer and syntaxDFX2xxx
3Declarations and document structureDFX3xxx
4Values, types and expressionsDFX4xxx
5Generation and asset writingDFX5xxx
6Niagara compilationDFX6xxx

Two more families sit outside that line: DFX7xxx is provenance, drift and lint, and DFX8xxx is the decompiler.

Order: modules → emitters → systems

build -All (and Rebuild DFX in the editor) builds in that order for a concrete reason: when a .dfm and the .dfs that calls it are queued together, the module has to exist first or the system cannot resolve it.

The editor-side watcher uses the same order — it stamps changed files into a pending queue, and the debounce ticker drains it.

One file's journey

Parse

Lexer plus parser produce a document. Position information (file, line, column) is attached here and carried to the last diagnostic, which is why a failed build can toast an Open in VSCode link that lands on the offending column.

Resolve module names and schemas

Short module names resolve on ModulePaths (L4). Once a name resolves, the input signature is probed, not assumed: the module is added to a transient system and the inputs it really exposes in that stack are read back.

That detour is necessary — the asset-level schema misses inline edit conditions and inputs revealed by a static switch entirely. dfx schema <Module> -Stack <Stack> reproduces this exact probe.

Write

The system, emitters, stacks, modules and inputs are written through the Niagara external edit API. Write order carries meaning: a static switch has to be written before the inputs it reveals exist (see .dfs).

Compile

The system compiles once it is written. A compile that is not clean is a DFX6xxx — including the engine's own stack issues (unmet dependencies, unbound parameters), which DreamFX restates as positioned diagnostics.

Save and stamp

SavePackage writes the package and stamps provenance on the asset: the source hash, the generator version, and module version GUIDs. A failed save is DFX5030, a build error — not a fatal, so "the file is in use by another program" no longer takes the editor down with it.

Unchanged sources are skipped

Building an untouched file a second time reports 0 built, 1 up to date: the hash stamped on the asset matches the source's, so there is nothing to do.

ToUse
Rebuild regardless of the hash-Force
Build without writing packages-NoSave
Run static checks only, touching no assetlint
Check assets against sources, writing nothingverify

Building in the editor vs on the command line

One pipeline, two entry points. There is exactly one difference, and it is hard:

Do not run package-writing commands with the editor open. Two processes saving the same packages race silently, and whichever saves second wins. build, corpus, mirror-diff and decompile-all all write packages.

One more that is equally hard: despawn a system's instances before rebuilding it — see the daily loop.

Performance: why a full-tree build is not linear

In a whole-tree build, most of the time is neither parsing nor writing — it is the Niagara compile. Measured on a single asset, 84% of the time went there, with roughly four system compiles triggered per asset.

Several knobs exist, and the defaults are already the fast side. They are there mainly for A/B and for escape:

SwitchEffect
-Window=<n>pipeline depth: how many systems may sit between compile request and finalize. 1 restores the fully serial build
-NoWriteScoperebuild the edit context for every write (the old behaviour, much slower)
-RebuildOnStructuraldrop the edit context after every structural write (the old behaviour)
-RebuildOnSwitchdrop the edit context after every static-switch write (the old behaviour)
-RebuildPerAddpay the engine's per-add stack refresh instead of one batch refresh per stack

These are not day-to-day switches. Their value is that the fast path and the slow path can both be run on one machine from one binary — so "did that optimisation actually help" is measured rather than read out of the code.

On this page