dowel

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-failed now exist; section 2.1’s substitute-path does 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:

  1. JSON diagnostics (--message-format=json), including fix suggestions (span + replacement) as rustc does, in a form agents can apply mechanically
  2. A machine-readable schema: dowel schema dump prints every kind and 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
  3. 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.