Developer experience
This is a design document for runners, debugger integration, and editor integration. Usage of runners and editors is in 63-guides.md; what is implemented is in 91-implementation-status.md.
dowel debug(ADR-0024) and section 2.3’s--debug-failednow exist; section 2.1’ssubstitute-pathdoes not.
1. The runner abstraction
Declare an execution wrapper per target triple, and let
dowel test --target <triple> run through the wrapper transparently.
[runner.riscv64gc-unknown-linux-gnu]
command = "qemu-riscv64"
args = ["-L", "/usr/riscv64-linux-gnu"]
Intended instantiations: qemu-user, qemu-system, SSH to real hardware, serial-port flashing and execution. Making real hardware declarable is what covers embedded use.
Runners with transfer
When the target machine cannot see the build machine’s file system, the artifact is transferred before launch. Source and destination paths are not written in the manifest; the implementation appends them (ADR-0008).
[runner.aarch64-unknown-linux-gnu]
host = "board.local"
remote_dir = "/tmp/dowel"
transfer = ["scp", "-q"]
command = "ssh"
args = ["board.local"]
This expands to:
scp -q <build>/bin/unit_test board.local:/tmp/dowel/unit_test
ssh board.local /tmp/dowel/unit_test
transfer and remote_dir are specified together. The exit status is
whatever the launch command returns, so with ssh the target machine’s exit
status is the verdict.
Prior art (the capability itself is established):
| System | Equivalent |
|---|---|
| Cargo | target.<triple>.runner |
| Meson | exe_wrapper |
| CMake | CMAKE_CROSSCOMPILING_EMULATOR |
2. Debugger integration
This is the real differentiator. The build system knows every input of the action that produced an artifact, so it can generate debugger configuration.
2.1 Resolving the reproducibility-vs-debugging trade-off
Normalizing source paths with -ffile-prefix-map for reproducibility breaks
the debugger’s source resolution. The one party that knows the correct
substitute-path to compensate is the party that applied the mapping — the
build system.
Reproducibility and debugging experience are inherently a trade-off, and this layer is the only place it can be resolved.
2.2 What dowel debug <target> does
- Pins down the sysroot,
substitute-path, and shared-library search paths - For cross execution, starts qemu’s gdbstub and connects the gdb version tied to the toolchain
- Alternatively emits a DAP (Debug Adapter Protocol) launch configuration so an editor reproduces the same environment
Implemented, with two departures
(ADR-0024, 60-cli.md). The gdb
tied to the toolchain is selected — debug is a toolchain tool, defaulting
to gdb and declarable per triple. The DAP configuration is emitted with
--dap. But the stub is declared, not started by guesswork: dowel
cannot know the flag that turns a given runner into a gdbstub, so
debug_args and debug_connect say it, and a cross target declaring
neither is refused instead of hanging. And no substitute-path is emitted,
because dowel does not remap paths yet — there is nothing to compensate for,
and section 2.1’s trade-off has not been taken.
2.3 Derived features
dowel test --debug-failed — rerun a failing test directly under the
debugger. Implemented as the join it was described as: the failing case’s
job (its arguments, environment, and working directory) becomes the debug
launch, through the same prepare the debug command uses
(60-cli.md). The selection has to come to exactly one case —
a debugger attaches to one process — and --dap writes the configuration,
case arguments included, instead of starting anything.
3. Editor integration
Three paths; do not conflate them.
| Target | Role |
|---|---|
| the manifest language LSP | implemented in-house (another frontend of the core) |
| the C/C++ LSP | delegated to clangd; the build system is the supplier |
| debugging | supply information to DAP |
3.1 Supplying clangd
The only current interface is compile_commands.json, with these limits:
- It expresses a single configuration only
- It carries no C++20 module information
Immediate updates that track configuration switches can be improved on our side; module support is also immature on clangd’s side and cannot be solved unilaterally.
3.2 The manifest language LSP
Initially restricted to diagnostics and hover. As long as the four constraints in 20-architecture.md are respected, features can be added incrementally.
Plan on this part never being finished (it carries a permanent maintenance cost).
Started as dowel lsp; speaks LSP on stdin/stdout. The editor is the
starting party and it exits with the editor, which distinguishes it from the
resident daemon rejected by ADR-0002. The CLI never
depends on the language server’s existence.
Hover explains the schema itself: property types and merge rules, each level
of a table header, builtin function signatures, configuration key domains,
and the nested tables that are not property blocks (cases, harness,
artifacts, inspect). The source is the same table dowel schema dump
reads; nothing is kept twice. That is the point of the arrangement — the one
table dowel kept in two places instead drifted, and the editor went silent
inside cases while the type checker knew every key (issue #90). Word identification walks the CST rather than evaluated values,
because explanations must appear even in files that contain errors.
The VS Code client lives in editors/vscode/. It starts dowel lsp,
receives diagnostics and hover, and adds syntax highlighting for
dowel.build.
Diagnostics come from a workspace model rebuilt per change: the open buffers
overlay the disk, and the model is loaded from every open manifest’s
directory, so cross-file checks (undeclared-dependency, the feature
vocabulary, merge conflicts, cycles) reach the editor. The plan stage then
runs over the model, so glob expansion, path resolution, and toolchain
existence are checked from real file-system scans — the same depth as
check. The editor session only reads: it never fetches, never touches the
store, starts no external processes, and is dropped after each change.
What is still not produced — checks that need fetching, --target, or
external processes — is listed with reasons in dowel_lsp::UNSUPPORTED,
and the check fails if a listed diagnostic is in fact being emitted.
4. Designing for LLM assistance
LLMs generate from the distribution of their training corpus, so novel syntax with no public corpus is their weakest ground. “LLMs exist, so unfamiliar syntax is fine” does not hold.
What LLMs are reliably good at is the repair loop: with located, structured diagnostics, convergence from wrong output to correct text is fast.
The LLM premise therefore justifies not looser syntax but investment in diagnostic quality. Concretely:
- JSON diagnostics (
--message-format=json), including fix suggestions (span + replacement) as rustc does, in a form agents can apply mechanically - A machine-readable schema:
dowel schema dumpprints everykindand property with types and merge rules, including the nested tables (case_properties,harness_properties,artifact_properties,inspection_properties,runner_properties). Supplying it as context compensates for the missing corpus - A fast
dowel check: the faster the generate-verify loop, the faster the convergence. Incremental evaluation pays off here
5. C++20 modules
With modules, dependencies are unknown until sources are scanned. ninja can
express this with dyndep, but the generated structure gets complex, and
neither CMake nor Meson has matured here (the same shape of problem Fortran
has carried for decades).
Treating scan actions as first-class citizens of the graph fits incremental evaluation well and is clearly open territory today — though parts depend on the state of clangd support.