January 13, 2022·5 min read·engineeringrustformattingmacroswindows

Rust 1.58 captures format names with a deliberate limit

Rust 1.58 lets format strings refer to local identifiers while retaining explicit arguments for expressions. Its Windows command lookup change addresses a separate source of implicit behavior.

Writing the same name twice in a formatting call communicates little when both occurrences identify the same local value. Rust 1.58 removes that repetition by letting a format string refer directly to an identifier in scope. The useful design choice is where the shorthand stops. It captures names without turning a format string into another place to write arbitrary Rust expressions.

The 13 January release announcement introduces captured format identifiers alongside a narrower Windows executable search and expanded must-use annotations. These changes affect different operations, but each asks how much implicit behavior an everyday API should permit.

I am impressed by Rust's semantics and its suitability for systems work. Go remains my first choice for many general-purpose programs, so Rust's smaller usability changes interest me where they reduce ceremony without making the underlying rule harder to explain. Captured names fit that description when the values already have useful local names.

The shorthand preserves the formatting model

The format argument RFC proposes implicit named arguments inferred from the format literal. Its explanation relates the shorthand to an explicitly supplied named argument with the same identifier. Existing explicit named arguments retain precedence, so a caller can still supply a value different from the local binding with that name.

That relationship is important for compatibility and reading. The format placeholder does not introduce a new variable. It uses an identifier that must already resolve in the scope of the macro invocation. The compiler can therefore diagnose a missing binding through the normal name-resolution mechanism.

let filename = "settings.toml";
let message = format!("Loading {filename}");
assert_eq!(message, "Loading settings.toml");

The release also permits captured identifiers in width and precision positions. That extends the existing formatting parameter model rather than introducing a separate formatting API. The same names that describe output values can describe how those values are presented.

For code review, the improvement is local. A message can keep the relevant value's name next to the text describing it. This reduces the positional bookkeeping needed when several fields appear in a message. Explicit arguments remain useful where the value is computed or where a different placeholder name makes the output easier to understand.

Expressions remain outside the literal

The release restricts capture to plain identifiers. A field access or function call still needs an explicit formatting argument, or a local binding before the macro invocation. That boundary avoids embedding a second expression notation within the string.

The distinction is useful when expressions have meaningful work or failure behavior. Keeping a computation in ordinary Rust syntax leaves its evaluation visible beside the formatting operation. A string remains a description of output with references to named values, rather than an alternate place to hide logic.

That does not mean an additional binding is always necessary. The existing explicit named-argument form accepts an expression and assigns it a placeholder name. The shorthand supplements that form. It does not remove the more explicit route when the relationship needs to be stated.

My preference is for convenience features with a small explanatory rule. Capturing a binding is easy to connect to the surrounding code. Allowing arbitrary expressions would ask the formatting parser and its users to account for substantially more language syntax inside the literal.

Macro hygiene limits generated strings

The RFC's discussion of generated format strings explains another boundary. When an expression expands into the format string, implicit capture does not occur. The design considers cases involving concatenation and macro-generated strings, where the identifiers could acquire surprising hygiene behavior.

That limitation preserves a relationship between text the caller writes and names the caller expects to capture. A macro that assembles a placeholder from pieces should not silently acquire authority to capture a binding because the final text happens to spell its name. Explicit named arguments provide a route for such generated formats.

The release announcement also identifies an edition-specific panic case. In the 2015 and 2018 editions, a single literal passed to panic remains an unformatted string in this situation, with a compiler warning about the likely intent. The 2021 edition's revised panic behavior supports the expected formatting interpretation.

That is an observable compatibility detail. A formatting example copied into a panic call needs the edition context. Assuming that every macro invocation has identical behavior would conceal the older panic contract preserved by those editions.

Windows command lookup becomes more explicit

The release's process change addresses a different implicit lookup. On Windows, Command no longer automatically searches the parent process's current directory for an executable. The announcement links that historical behavior to the underlying CreateProcess search and describes the security risk when the directory is untrusted.

The released Windows process implementation performs executable resolution itself. A simple filename follows its search paths. A path with directory components takes a different route, so the change should not be described as prohibiting every explicitly relative executable path.

Its search order considers child PATH settings, the application's directory and Windows system locations, followed by the parent's PATH. The code filters empty PATH components. A directory deliberately present in PATH remains part of the configured lookup, so the correction does not make the executable search independent of environment policy.

The practical improvement is the removal of an ambient location from the automatic search. Changing into a directory to inspect its contents should not independently grant that directory the ability to supply an executable with a familiar name. Explicit paths and configured search locations remain decisions the caller needs to understand.

Ignored return values receive more diagnostics

Rust 1.58 also completes the standard library audit described in library proposal 35. The release explains that must-use annotations identify operations whose result is usually the point of calling them. Ignoring the return can indicate that a caller expected mutation when the operation instead produced a new value.

This is a diagnostic policy rather than a general proof of functional purity. A return value can deserve attention without the function satisfying every formal property associated with purity. The useful question is whether discarding that value is likely to be an unintended operation.

I regard these diagnostics as consistent with Rust's appeal for systems work. They make a likely mistake visible where the value is ignored. Together with the formatting shorthand and executable lookup change, the release gives ordinary source a more direct relationship to the behavior its reader expects.