Repository: knurling-rs/defmt (Rust)

Issue:
support passing interned string literals
currently, binfmt only supports passing string values to a format string, like so:
```
info!("They said: {:str}", "Hi!"); // on-the-wire = [$index-they, $timestamp, $length, b'H', b'i', b'!']
```

This is not ideal, as the entire string value ("Hi!") has to be sent over the wire rather than just an index.
Therefore, we'd also like to support passing interned (i.e. predefined) string literals to a format string, like so:

```
let s: Str = intern!("Hi!");
info!("They said: {:istr}", s); // on-the-wire = [$index-they, $timestamp, $index-hi]
```

**To do this, we'll need to**
- [ ] introduce a new formatting parameter (for example `{:istr}`– preferrably a name that is less similar to the already-existing `{:str}`)
- [ ] extend `parser/lib.rs` to recognize the new formatting parameter
- [ ] extend `parse_args` to handle this parameter (implementation note from @japaric: &'t str in them would be pointing into the table: &'t [u8] (like the Format.format string does) not into bytes. Rationale: table lives for as long as the decoder/printer program is running (i.e. effectively it will never be deallocated); on the other hand, bytes may be a temporary stack allocated buffer.)
- [ ] extend docs to explain the difference
- [ ] add tests
qemu-run can drop log messages
Not sure why, but sometimes the last log messages get lost. This might require adding more messages to the test program to show up.


Discussion:

Patch:
diff --git a/decoder/src/lib.rs b/decoder/src/lib.rs
index 5dd331db..7e58fd16 100644
--- a/decoder/src/lib.rs
+++ b/decoder/src/lib.rs
@@ -100,18 +100,23 @@ impl core::fmt::Display for DisplayFrame<'_> {
 // NOTE follows `parser::Type`
 #[derive(Debug, PartialEq)]
 pub enum Arg<'t> {
-    // Bool
+    /// Bool
     Bool(bool),
     F32(f32),
-    // U8, U16, U24 and U32
+    /// U8, U16, U24 and U32
     Uxx(u64),
-    // I8, I16, I24 and I32
+    /// I8, I16, I24 and I32
     Ixx(i64),
-    // Str
+    /// Str
     Str(String),
-    // Format
-    Format { format: &'t str, args: Vec<Arg<'t>> },
-    // Slice
+    /// Interned string
+    IStr(&'t str),
+    /// Format
+    Format {
+        format: &'t str,
+        args: Vec<Arg<'t>>,
+    },
+    /// Slice
     Slice(Vec<u8>),
 }
 
@@ -123,6 +128,7 @@ impl fmt::Display for Arg<'_> {
             Arg::Uxx(x) => write!(f, "{}", x),
             Arg::Ixx(x) => write!(f, "{}", x),
             Arg::Str(x) => write!(f, "{}", x),
+            Arg::IStr(x) => write!(f, "{}", x),
             Arg::Format { format, args } => f.write_str(&format_args(format, args)),
             Arg::Slice(x) => write!(f, "{:?}", x),
         }
@@ -192,7 +198,7 @@ fn parse_args<'t>(bytes: &mut &[u8], format: &str, table: &'t Table) -> Result<V
             Type::Format => {
                 let index = leb128::read::unsigned(bytes).map_err(drop)?;
                 let (level, format) = table.get(index as usize)?;
-                // not well-formed
+                // Auxiliary format strings must not have a log level associated with them.
                 if level != None {
                     return Err(());
                 }
@@ -248,6 +254,17 @@ fn parse_args<'t>(bytes: &mut &[u8], format: &str, table: &'t Table) -> Result<V
 
                 args.push(Arg::Str(arg_str));
             }
+            Type::IStr => {
+                let str_index = leb128::read::unsigned(bytes).map_err(drop)? as usize;
+
+                let (level, string) = table.get(str_index as usize)?;
+                // Interned strings must not have a log level associated with them.
+                if level != None {
+                    return Err(());
+                }
+
+                args.push(Arg::IStr(string));
+            }
             Type::Slice => {
                 // only supports byte slices
                 let num_elements = leb128::read::unsigned(bytes).map_err(drop)? as usize;
diff --git a/firmware/qemu/src/bin/log.rs b/firmware/qemu/src/bin/log.rs
index 5c48c24e..ab10b018 100644
--- a/firmware/qemu/src/bin/log.rs
+++ b/firmware/qemu/src/bin/log.rs
@@ -43,6 +43,10 @@ fn main() -> ! {
     binfmt::info!("{:?}", S { x: 1, y: 256 });
     binfmt::info!("{:?}", X { y: Y { z: 42 } });
 
+    let interned = binfmt::intern!("interned string");
+    binfmt::info!("&str = {:str}", "string slice");
+    binfmt::info!("&Str = {:istr}", interned);
+
     loop {
         debug::exit(debug::EXIT_SUCCESS)
     }
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 26172b38..3514114f 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -676,6 +676,9 @@ impl Codegen {
                 binfmt_parser::Type::Str => {
                     exprs.push(quote!(_fmt_.str(#arg)));
                 }
+                binfmt_parser::Type::IStr => {
+                    exprs.push(quote!(_fmt_.istr(#arg)));
+                }
                 binfmt_parser::Type::U16 => {
                     exprs.push(quote!(_fmt_.u16(#arg)));
                 }
diff --git a/parser/src/lib.rs b/parser/src/lib.rs
index bcce5e4f..4ce8c0e5 100644
--- a/parser/src/lib.rs
+++ b/parser/src/lib.rs
@@ -23,7 +23,10 @@ pub enum Type {
     I16,
     I32,
     I8,
-    Str, // used for string values (i.e. passed directly; not as interned string indices)
+    /// String slice (i.e. passed directly; not as interned string indices).
+    Str,
+    /// Interned string index.
+    IStr,
     U16,
     U24,
     U32,
@@ -117,6 +120,7 @@ pub fn parse(format_string: &str) -> Result<Vec<Parameter>, Cow<'static, str>> {
                         static I8: &str = "i8}";
                         static SLICE: &str = "[u8]}";
                         static STR: &str = "str}";
+                        static ISTR: &str = "istr}";
                         static U16: &str = "u16}";
                         static U24: &str = "u24}";
                         static U32: &str = "u32}";
@@ -130,6 +134,9 @@ pub fn parse(format_string: &str) -> Result<Vec<Parameter>, Cow<'static, str>> {
                         } else if s.starts_with(STR) {
                             (0..STR.len()).for_each(|_| drop(chars.next()));
                             Type::Str
+                        } else if s.starts_with(ISTR) {
+                            (0..ISTR.len()).for_each(|_| drop(chars.next()));
+                            Type::IStr
                         } else if s.starts_with(U8) {
                             (0..U8.len()).for_each(|_| drop(chars.next()));
                             Type::U8
diff --git a/qemu-run/src/main.rs b/qemu-run/src/main.rs
index 2504f77e..039f1e9f 100644
--- a/qemu-run/src/main.rs
+++ b/qemu-run/src/main.rs
@@ -5,6 +5,7 @@ use std::{
 };
 
 use anyhow::{anyhow, bail};
+use decoder::Table;
 use xmas_elf::ElfFile;
 
 fn main() -> Result<(), anyhow::Error> {
@@ -51,32 +52,40 @@ fn notmain() -> Result<Option<i32>, anyhow::Error> {
 
     let mut frames = vec![];
     let mut readbuf = [0; 256];
-    let mut exit_code = None;
-    let mut exited = false;
+    let exit_code;
     loop {
         let n = stdout.read(&mut readbuf)?;
 
         if n != 0 {
             frames.extend_from_slice(&readbuf[..n]);
 
-            if let Ok((frame, consumed)) = decoder::decode(&frames, &table) {
-                println!("{}", frame.display(true));
-                let n = frames.len();
-                frames.rotate_left(consumed);
-                frames.truncate(n - consumed);
-            }
-        }
-
-        if exited {
-            break;
+            decode(&mut frames, &table);
         }
 
         if let Some(status) = child.try_wait()? {
             exit_code = status.code();
-            // try to read once more before breaking out from this loop
-            exited = true;
+
+            stdout.read_to_end(&mut frames)?;
+            decode(&mut frames, &table);
+            if !frames.is_empty() {
+                return Err(anyhow!(
+                    "couldn't decode all data (remaining: {:x?})",
+                    frames
+                ));
+            }
+
+            break;
         }
     }
 
     Ok(exit_code)
 }
+
+fn decode(frames: &mut Vec<u8>, table: &Table) {
+    while let Ok((frame, consumed)) = decoder::decode(&frames, &table) {
+        println!("{}", frame.display(true));
+        let n = frames.len();
+        frames.rotate_left(consumed);
+        frames.truncate(n - consumed);
+    }
+}


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 2e8d58a4..8c6d870c 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -13,6 +13,9 @@ fn log_levels() {
 #[test]
 fn str() {
     binfmt::info!("Hello, {:str}", "world");
+
+    let world = binfmt::intern!("world");
+    binfmt::info!("Hello, {:istr}", world);
 }
 
 #[test]
diff --git a/tests/encode.rs b/tests/encode.rs
index 411018ec..93466523 100644
--- a/tests/encode.rs
+++ b/tests/encode.rs
@@ -376,3 +376,16 @@ fn format_primitives() {
         ],
     );
 }
+
+#[test]
+fn istr() {
+    let index = fetch_string_index();
+    let interned = binfmt::intern!("interned string contents");
+    cfi(
+        interned,
+        &[
+            inc(index, 1), // "{:istr}"
+            index,
+        ],
+    );
+}