Files
next.js/turbopack/crates/turbo-tasks-backend/tests/turbofmt.rs
Matt MastracciandGitHub e4cae37084 Turbopack: turbofmt/turbobail macros (#90092)
# What
Add turbofmt! and turbobail! proc macros for async string formatting
with Vc<T> and ResolvedVc<T> values.

A new, agent-friendly `FORMATTING.md` file is added to help agents
figure out how these macros work.

# Why

Formatting strings that include turbo-tasks values currently requires
manually .await?ing each Vc, then calling format!, then converting to
RcStr via .into(). By using macros, we can reduce a lot of boilerplate
and potentially offer some opportunities for optimizations by reducing
intermediate, throwaway tasks.

# How

We make use of the "auto-deref" pattern in Rust to choose the "best"
formatting option: ValueToString or Display.

- Zero-alloc FormatIter: A shared iterator parser over format strings
(RawString, EscapedBrace, VarRef, VarRefFormat) that both macros and the
derive use, replacing duplicated manual parsing.
- Borrow-based capture: turbofmt! uses async { .. } (not async move), so
non-Copy types like FileSystemPath can be used in multiple turbofmt!
calls without cloning.
  - Debug on StringifyType: Enables {:?} format specs in turbofmt!.
- Converted ~30 call sites across next-core, turbo-tasks-fs,
turbopack-core, turbopack-ecmascript, and others.

# What's Left

- Constants cannot be formatted with turbofmt! yet because of arcane
Rust hygiene rules (similar issue to
https://github.com/rust-lang/rust/issues/131446)
- rust-analyzer doesn't show these format strings as "real" format
strings yet because of limitations of the analyzer, but this should be
possible to make work.
- Automatic derive for ValueToStringRef to avoid intermediate tasks in a
few places where formatting is trivial
 -  turbowrite!/turbowriteln! could clean up a bit more code
2026-03-04 15:11:04 +00:00

67 lines
1.9 KiB
Rust

#![feature(arbitrary_self_types)]
#![feature(arbitrary_self_types_pointers)]
#![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
use turbo_rcstr::RcStr;
use turbo_tasks::{ResolvedVc, ValueToString, Vc, turbobail, turbofmt};
use turbo_tasks_testing::{Registration, register, run_once};
static REGISTRATION: Registration = register!();
#[turbo_tasks::value(shared)]
#[derive(ValueToString)]
#[value_to_string("item {name} (count: {count})")]
struct FmtTest {
name: RcStr,
count: u32,
}
#[turbo_tasks::function(operation)]
async fn turbofmt_operation(value: ResolvedVc<FmtTest>) -> anyhow::Result<Vc<RcStr>> {
let s: RcStr = turbofmt!("prefix {} vc {}", 42u32, value).await?;
Ok(Vc::cell(s))
}
#[turbo_tasks::function(operation)]
async fn turbobail_operation(value: ResolvedVc<FmtTest>) -> anyhow::Result<Vc<RcStr>> {
turbobail!("error: {} with {}", 42u32, value)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_turbofmt() {
run_once(&REGISTRATION, || async {
let v = FmtTest {
name: "foo".into(),
count: 7,
}
.resolved_cell();
assert_eq!(
&*turbofmt_operation(v).read_strongly_consistent().await?,
"prefix 42 vc item foo (count: 7)"
);
anyhow::Ok(())
})
.await
.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_turbobail() {
run_once(&REGISTRATION, || async {
let v = FmtTest {
name: "bar".into(),
count: 3,
}
.resolved_cell();
let result = turbobail_operation(v).read_strongly_consistent().await;
let err = result.unwrap_err();
let root = err.root_cause().to_string();
assert_eq!(root, "error: 42 with item bar (count: 3)");
anyhow::Ok(())
})
.await
.unwrap()
}