How to Fix: Wiggle: build failure with `tracing: false` with async functions
The build breaks because Wiggle generates different code paths when tracing is disabled, and the async function path currently assumes tracing-related tokens still exist. The result is a code generation mismatch that surfaces as a compile failure when tracing: false is added to the from_witx!() definition in crates/wiggle/tests/atoms_async.rs.
Reproducing the Failure
The issue is easy to trigger:
- Open
crates/wiggle/tests/atoms_async.rs. - Add
tracing: falseinside thefrom_witx!()macro invocation. - Run
cargo test -p wiggle.
You will see that the async test target fails to compile, even though disabling tracing should be a supported configuration.
A reduced version of the triggering pattern looks like this:
from_witx!({
witx: [".../atoms.witx"],
async: {
atoms::double_int_return_float,
},
tracing: false,
});
Expected behavior: the code should compile and tests should pass with tracing enabled or disabled.
Understanding the Root Cause
This bug is fundamentally a macro code generation consistency problem.
Inside Wiggle, the from_witx!() macro expands into generated Rust for host traits, function shims, and optional instrumentation. When async functions are involved, the generated wrapper logic typically differs from the synchronous path because it must produce or await a future.
The failure occurs because the async code path still depends on tracing-specific generation assumptions even when tracing: false is set. In practice, this usually means one of the following is happening:
- A tracing-related variable, import, or token stream is referenced unconditionally in the async generator.
- The non-tracing branch omits code that the async wrapper still expects.
- The sync path correctly gates tracing code, but the async path does not apply the same conditional branching.
In other words, the feature gate logic is incomplete: tracing is disabled at the macro configuration layer, but not fully disabled in every generated async fragment.
This is why the problem only appears with the combination of async + tracing disabled. Sync functions may compile fine, and async functions may compile fine when tracing is enabled, but that specific combination exposes the missing conditional handling.
Step-by-Step Solution
The fix is to make the generated async path fully independent from tracing when tracing: false is configured.
1. Locate the async code generation path
Search the Wiggle macro implementation for the code that emits async wrappers and tracing instrumentation. You are looking for the place where function bodies are generated for async host calls.
Typically, this lives in the macro/codegen area under the Wiggle crate, where quote! output is built for function shims.
2. Find tracing-specific emitted tokens
Look for patterns like:
tracing::instrument
tracing::span!
tracing::event!
let _span = ...
Also inspect any helper variables that are only meaningful when tracing is enabled.
3. Ensure conditional generation is symmetrical
If the sync code already does this correctly, mirror that same branching in the async path.
The implementation pattern should look conceptually like this:
if settings.tracing {
quote! {
// async wrapper with tracing instrumentation
}
} else {
quote! {
// async wrapper without any tracing-specific code
}
}
The key point is that the non-tracing branch must be self-contained. It must not reference tracing spans, attributes, helper bindings, or imports.
4. Remove unconditional tracing references from async wrappers
If the current code mixes common async logic with tracing variables, refactor it so shared logic is separated from optional instrumentation.
For example, this pattern is fragile:
let tracing_setup = if settings.tracing {
quote! { let _span = tracing::span!(...); }
} else {
quote! {}
};
quote! {
#tracing_setup
let result = self.#func(...).await;
_span.in_scope(|| result)
}
That fails because _span is still referenced even when tracing is disabled.
Refactor it into fully separated branches:
let body = if settings.tracing {
quote! {
let _span = tracing::span!(tracing::Level::TRACE, "wiggle_async");
let result = self.#func(#args).await;
_span.in_scope(|| result)
}
} else {
quote! {
self.#func(#args).await
}
};
This is the safest fix because each branch compiles independently.
5. Verify generated attributes are also gated
If async functions receive emitted attributes such as instrumentation decorators, ensure those are omitted too:
let maybe_instrument = if settings.tracing {
quote! { #[tracing::instrument] }
} else {
quote! {}
};
Then apply them only where valid:
quote! {
#maybe_instrument
async fn generated_wrapper(...) -> Result<_, _> {
...
}
}
Do not leave any tracing path partially active.
6. Add or update the regression test
The issue already gives you the perfect regression case. Update crates/wiggle/tests/atoms_async.rs so it explicitly exercises async generation with tracing disabled:
from_witx!({
witx: ["tests/atoms.witx"],
async: {
atoms::double_int_return_float,
},
tracing: false,
});
Then run:
cargo test -p wiggle
If the fix is correct, the test suite should now compile and pass.
7. Validate both matrix combinations
Before considering the issue solved, verify these combinations:
- sync + tracing enabled
- sync + tracing disabled
- async + tracing enabled
- async + tracing disabled
This confirms that your code generation branches remain consistent across all supported modes.
Common Edge Cases
Even after fixing the main bug, several adjacent issues can still appear.
1. Hidden imports only used in one branch
If use tracing::... or fully qualified tracing paths are emitted outside the branch check, compilation can still fail or produce unused import noise. Keep all tracing-specific imports conditional.
2. Attribute placement on generated async functions
Procedural or declarative macro output can be sensitive to where attributes are emitted. If instrumentation attributes are inserted above the wrong generated item, Rust may report confusing parser or trait errors.
3. Await placement differences
When splitting tracing and non-tracing branches, make sure both branches preserve the same await semantics. A missing .await in one branch can turn a compile error into a type mismatch.
4. Lifetimes and borrowed arguments
If tracing code captures references for spans or events, removing it may change inferred lifetimes. Usually this improves things, but in tightly generated code you should still verify signatures remain identical.
5. Feature flag interactions
If Wiggle also supports crate-level Cargo features around tracing, check that macro-level tracing: false does not accidentally emit tracing code when the crate feature is enabled, or vice versa.
FAQ
Why does this only fail for async functions?
Because the async wrapper generation uses a separate code path from synchronous functions. The sync path likely already handles tracing: false correctly, while the async path still references tracing-generated tokens.
Is this a Cargo feature problem or a macro bug?
It is primarily a macro/code generation bug. Cargo features may influence whether tracing is available, but the core issue is that the generated async code does not fully respect the tracing: false setting.
What is the safest long-term fix?
The safest approach is to generate fully separate tracing and non-tracing async bodies instead of mixing optional snippets into a shared body. That prevents dangling references and makes future maintenance easier.
Once this change is in place, cargo test -p wiggle should pass for the reported reproduction case, and Wiggle’s async code generation will correctly honor tracing configuration in all supported modes.