ADR-0047: Unify @dbg and @compileLog
Status
Implemented
Summary
Merge @compileLog into @dbg so there is one debug-print intrinsic whose output routing depends on the evaluation phase. When @dbg runs at runtime it prints to stdout (today's behavior). When it runs inside a comptime context, the sema interpreter prints each message to stderr with a comptime dbg: prefix as it is evaluated (matching @compileLog's existing eprintln! behavior) and records the call for a post-sema "debug statement present" warning. The sema-side buffer that today records comptime @dbg output is preserved and exposed via a --capture-comptime-dbg CLI flag that suppresses the on-the-fly print, so the differential fuzzer can continue to consume structured output. @compileLog is removed; it is already gated behind the unstable comptime_meta preview feature, so the break is contained.
Context
Today the language ships two near-duplicate compile-time debug intrinsics:
| Intrinsic | Phase | Args | Default output | Warning | Preview gate |
|---|---|---|---|---|---|
@dbg(x) | runtime or comptime | single int/bool/string/struct/enum/array | runtime: stdout; comptime: silent buffer (only the fuzzer reads it) | no | none |
@compileLog(...) | comptime only | variadic, any comptime type | stderr with comptime log: prefix | yes, per call | comptime_meta |
The split is accidental. @compileLog was introduced in ADR-0042 to give users a way to debug comptime logic. @dbg-in-comptime was added by ADR-0040 as infrastructure for the differential fuzzer: the interpreter needs to honor @dbg side effects, so it routes them into a Vec<String> on Sema. Neither surfaces comptime output through the CLI by default, so from a user's perspective today @dbg simply vanishes inside a comptime { } block. Users who want to see a comptime value must remember to reach for @compileLog (and enable --preview comptime_meta).
This is a bad mental model. The natural question "when is this @dbg going to print?" has the pleasing answer "whenever the code containing it executes" — but only if the compiler actually prints what it saw. Once we route comptime @dbg output to stderr, @compileLog is strictly redundant: same destination, same warning-on-successful-compile nag, same variadic formatting. Deleting it removes a concept from the language.
A real constraint is the fuzzer. fuzz/fuzz_targets/comptime_differential.rs compares comptime vs. runtime @dbg output byte-for-byte by reading the structured buffer from SemaOutput. That buffer needs to keep existing; a CLI flag is the clean escape hatch for suppressing the driver-side print when a tool wants the structured form instead.
Decision
Unify the intrinsics under @dbg. Phase-specific output routing makes the print location predictable:
@dbgbecomes variadic at both phases. Zero or more arguments, each of an acceptable type for the phase. At runtime: integer, bool, string (existing supported types). At comptime: any typeformat_const_valuecan render (integer, bool, unit,comptime_str). Arguments are space-joined for a single output line.Runtime behavior (unchanged destination, extended arity).
@dbg(a, b, c)at runtime prints<a> <b> <c>\nto stdout. A single-argument call is byte-identical to today's@dbg(x).Comptime behavior (new, replacing
@compileLog).@dbg(...)inside a comptime evaluation context:- Evaluates each argument, formats via
format_const_value, joins with spaces. - Immediately prints
comptime dbg: <message>to stderr at the point of evaluation, unlessSema.suppress_comptime_dbg_printis set (which the driver flips on when--capture-comptime-dbgis passed). This matches how@compileLogis implemented today (eprintln!on the spot) and preserves partial output if comptime evaluation later errors or hits the step budget. - Appends the formatted string to
Sema.comptime_dbg_output(existing buffer, keep the name) regardless of the suppression flag — the buffer is the fuzzer's consumption point. - Records
(message, span)onSema.comptime_log_outputso a per-call warning is emitted after sema completes.
- Evaluates each argument, formats via
@compileLogis removed. Calls to@compileLogproduce an error diagnostic suggesting@dbg. Thecompile_logentry inknown_symbols.rsis removed, as isanalyze_compile_log_intrinsic. Thecomptime_metapreview feature continues to gate the remaining metaprogramming intrinsics (@typeName,@typeInfo,@field,@compileError,comptime_unroll for) —@dbgitself is not gated, since it is already stable.Warning kind is kept but renamed.
WarningKind::ComptimeLogPresentbecomesWarningKind::ComptimeDbgPresentwith the same text ("debug statement present — remove before release").
Phase detection
There is no new phase-detection machinery. Sema already has two distinct @dbg handlers — analyze_dbg_intrinsic (runtime path, produces an AIR intrinsic) and the evaluate_comptime_inst branch for known.dbg (comptime path, populates the buffer). This ADR widens both to accept variadic args and, in the comptime path, records for warning emission too.
CLI surface
--capture-comptime-dbg Suppress the on-the-fly stderr print of @dbg output
from comptime evaluation. The buffer is still populated
and accessible through the compilation state. Intended
for tools and fuzz harnesses that consume structured output.
The driver sets Sema.suppress_comptime_dbg_print = true when this flag is present; otherwise the intrinsic prints inline as it evaluates. No post-sema "replay" step — the CLI does not walk comptime_dbg_output for printing.
Migration
Within the repo: update crates/gruel-spec/cases/expressions/comptime_meta.toml (compile_log tests become dbg tests or are folded into existing dbg tests), fuzz/src/lib.rs (only uses @dbg, no change needed), and fuzz/fuzz_targets/comptime_differential.rs (add --capture-comptime-dbg when invoking the compiler). Spec prose in docs/spec/src/04-expressions/13-intrinsics.md and docs/spec/src/04-expressions/14-comptime.md is rewritten to describe the unified intrinsic. Tutorial at website/content/tutorial/14-comptime.md is updated.
External users are unlikely: @compileLog required --preview comptime_meta, which is explicitly unstable and documented as subject to breaking changes.
Implementation Phases
Phase 1: Spec rewrite. Update
docs/spec/src/04-expressions/13-intrinsics.mdanddocs/spec/src/04-expressions/14-comptime.md: describe variadic@dbg, phase-dependent output routing, warning behavior. Remove@compileLogparagraphs (4.14:52, 4.14:53, 4.14:54); renumber or retire the IDs per spec conventions. Add new paragraphs covering the unified behavior and the--capture-comptime-dbgflag.Phase 2: Variadic runtime
@dbg. Relaxanalyze_dbg_intrinsicingruel-air/src/sema/analysis.rsto accept zero or more args. Update codegen ingruel-codegen-llvm/src/codegen.rsto emit a sequence of type-dispatched__gruel_dbg_*calls interleaved with a space-writing call (add__gruel_dbg_spaceingruel-runtime/src/debug.rs) and a final newline. Update spec tests covering@dbgruntime behavior.Phase 3: Variadic comptime
@dbg+ on-the-fly print + warning. Widen theknown.dbgbranch inevaluate_comptime_instto accept variadic args and useformat_const_valueon each, space-joining. At the point of evaluation:eprintln!("comptime dbg: {msg}")unlessself.suppress_comptime_dbg_printis set. Always append tocomptime_dbg_output. Push a(msg, span)pair ontocomptime_log_outputso the existing warning-emission pass fires. RenameWarningKind::ComptimeLogPresent→WarningKind::ComptimeDbgPresent(and the text).Phase 4:
--capture-comptime-dbgflag wiring. Addsuppress_comptime_dbg_print: booltoSema(default false). Add the CLI flag incrates/gruel/src/main.rs, thread throughgruel-compilerinto sema construction. No driver-side "print the buffer" step — the printing already happened in Phase 3.Phase 5: Remove
@compileLog. Deletecompile_logfromknown_symbols.rs, deleteanalyze_compile_log_intrinsic, delete the@compileLogbranch inevaluate_comptime_inst. Any remaining use produces "unknown intrinsiccompileLog"; add a targeted diagnostic that suggests@dbgwhen the name is exactlycompileLog. Remove thecomptime_metagate on the now-deleted code paths.Phase 6: Migrate tests and fuzz. Convert
@compileLogusages incrates/gruel-spec/cases/expressions/comptime_meta.tomlto@dbg(or delete as redundant with existing dbg tests). Add UI tests incrates/gruel-ui-tests/cases/for: (a) default comptime@dbgprints to stderr with prefix, (b) warning emitted, (c)--capture-comptime-dbgsuppresses the print, (d) helpful diagnostic for@compileLogmisuse. Updatefuzz/fuzz_targets/comptime_differential.rsto pass--capture-comptime-dbg. Update the tutorial atwebsite/content/tutorial/14-comptime.md.Phase 7: Traceability and
make test. Run the traceability check; ensure every new spec paragraph has a test and no removed paragraph is still referenced. Runmake testgreen.
Consequences
Positive
- One debug-print intrinsic instead of two. Mental model: "
@dbgprints when the code runs." - Comptime
@dbgfinally does something observable from the CLI by default — today it silently vanishes into a buffer. - On-the-fly printing preserves output when comptime evaluation later fails (step budget exceeded,
@compileError, type error in a subsequent instruction). A buffer-then-replay design would lose everything on error. - The fuzz harness keeps working via an explicit, documented flag instead of an implicit "the CLI doesn't surface this" behavior.
comptime_metapreview shrinks by one intrinsic, reducing the surface area remaining to stabilize.
Negative
- Breaking change for anyone using
@compileLog(mitigated: it was unstable and preview-gated). - New subtle behavior: a regular function containing
@dbgthat gets called from both comptime and runtime code prints in both phases. This is a consequence of phase-consistent execution semantics, not a bug, but it may surprise users the first time. - Runtime variadic
@dbgrequires a small codegen change and one new runtime function (__gruel_dbg_space) — minor but non-zero complexity. - The sema interpreter now unconditionally prints comptime
@dbgoutput (unless suppressed), which changes compiler stderr behavior for any program using comptime@dbg. Existing spec/fuzz tests that call comptime@dbgwithout--capture-comptime-dbgwill start emitting stderr content; they'll need the flag or test updates. - Output ordering interleaves with any other stderr the compiler emits during sema (e.g. warnings emitted mid-analysis). In practice sema runs single-threaded per module and most other diagnostics are emitted at pass boundaries, so interleaving is not a meaningful concern.
Open Questions
- Prefix format.
comptime dbg:vscomptime log:vs something else?@compileLogusescomptime log:today, but "log" is no longer the name of anything after this change. Proposal:comptime dbg:. Consistent with the intrinsic name. - Runtime variadic separator policy. Space-join matches
@compileLogprecedent. Alternative: newline-join at runtime ("print each arg on its own line") for ergonomics when printing large values. Proposal: space-join for symmetry with comptime; users can call@dbgmultiple times for per-line output. - Should the warning fire for every call or once per
fn?@compileLogfires once per call site. Keep that.
Future Work
- Once
comptime_metastabilizes more broadly (ADR-0042 completion), reconsider whether@dbgat comptime should require any gate at all. Current proposal: no gate, since@dbgitself is stable and phase-dependent routing is a natural extension. - A structured
@trace(event_name, fields...)intrinsic for higher-fidelity comptime debugging is a separate, larger design and is explicitly out of scope here.
References
- ADR-0040: Comptime Expansion — introduced the comptime
@dbgbuffer - ADR-0042: Comptime Metaprogramming — introduced
@compileLog - Spec §4.13 (intrinsics), §4.14 (compile-time expressions)
crates/gruel-air/src/sema/analysis.rs:3438(runtime@dbg),:7431(comptime@dbg),:7467(@compileLog)