Using format! strings at runtime (Rust stable)

Is there a way to use something like format! at runtime ?

I'm toying with a Lisp interpreter in Rust, and we have:

pub enum LispVal { ... }

at runtime, we generate s: String, v: Vec<LispVal> where

  • v.len() = number of {} / {:?} in s

So I'd like something that allows me to interpret s as a format string, and call either Display or Debug on the various elements of Vec<LispVal>

Aside: there is crates.io: Rust Package Registry , but it seems to require nightly, not updated in past 4 years, and not high download count.

I'm wondering if there is some way to exploit the fact the type LispVal is known at compile time, it is the only type passed to the format string, so really I just need stuff that parses the format string.

For this particular use case you can get the same effect with write!():

use std::fmt::Write as _;

fn main() {
    let v = vec![1, 2, 3];
    let mut s = String::new();

    for x in &v {
        write!(&mut s, "{x}, ").unwrap();
    }

    println!("{s}");
}

But then you get a trailing comma-and-space. That can be avoided with some cleverness:

use std::fmt::{Display, Write as _};
use std::iter::ExactSizeIterator;

/// In-house implementation of `std::slice::Join` because that trait is unstable.
/// See: https://github.com/rust-lang/rust/issues/27747
pub(crate) fn join<I, S>(iter: I, sep: &str) -> String
where
    I: Iterator<Item = S> + ExactSizeIterator,
    S: Display,
{
    let mut joined = String::new();

    let len = iter.len();

    for (i, item) in iter.enumerate() {
        write!(joined, "{item}").unwrap();

        if i < len - 1 {
            joined.push_str(sep);
        }
    }

    joined
}

fn main() {
    let v = vec![1, 2, 3];

    println!("{}", join(v.iter(), ", "));
}

In the broadest terms, this incrementally builds the final string rather than building the format string.

There are two others with large monthly downloads and no nightly requirement: strfmt and formatx.

2 Likes

Sorry, I don't think I stated the problem clearly, and as a result we may have different problems in mind. I want to be able to do things like:

(str-format "{} + {} = {}" x y (+ x y))
(str-format "{] is a {] with {} hit-points" (mob-name o) (mob-type o) (mob-hp o))

And in both cases, this hits a Rust fn with type sig

fn rust_str_format(s: &str, args: Vec<Lisp_Val>) { ... }

and if possible, I'd prefer to not parse the format string myself (for {} or {:?}) and instead just leverage the existing rust format! capabilities

Your examples are confusing with the desire for runtime formatting because the format strings are not runtime dependent. You only have n static format strings. Which implies that you could move the format string DSL to compile time (like the format!() machinery does) with n format functions.

There aren't too many things that you actually need runtime-dependent format strings for. Joining a list is the first thing that came to mind. And my point is that's better done by incrementally building the final string instead of the format string.

I think we have a very deep misunderstanding here. Lisps have REPLs. So what happens is as follows:

  1. we compile Rust program
  2. user runs Rust program, getting a Lisp REPL
  3. in the Lisp REPL, at runtime, the user types in:
(str-format SOME_FORMAT_STRING ... args)

and then our Rust code has to interpret it, at runtime.

I'm also interested to learn, given the current state, how you would have written the original, question to make this clear.

It's probably just my fault for not recognizing "Lisp interpreter" for what it is in your OP. The Lisp is literally a user input. Fair enough, the format strings are clearly heap-allocated in that context.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.