nandi/rustnimpublic Fork 0
b66da6b1c3e50bd75a8d5010cb222724b5ce6c13
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

fmt.rs · 187 lines · 5.4 KBRust Blame HistoryRaw
Add the differential test runner, and a lowering to measure with it 8ac32af nandithebull 19h ago1//! Rust format strings (`println!`, `format!`) -> Nim string expressions.
2//!
3//! Only the subset that is reproduced exactly is accepted. An unrecognised
4//! format spec is an error, never a best-effort guess: `{:>8.3}` silently
5//! rendered as `{}` would produce plausible-looking wrong output, which is the
6//! failure mode this project exists to avoid.
7
8/// One piece of a parsed format string.
9#[derive(Debug, PartialEq)]
10pub enum Piece {
11 Lit(String),
12 Arg { r#ref: Ref, spec: Spec },
13}
14
15#[derive(Debug, PartialEq)]
16pub enum Ref {
17 /// `{}` — consumes the next positional argument.
18 Next,
19 /// `{0}` — an explicit index.
20 Index(usize),
21 /// `{name}` — an inline captured identifier.
22 Named(String),
23}
24
25#[derive(Debug, PartialEq, Default)]
26pub struct Spec {
27 pub debug: bool,
28 /// `x`, `X`, `b`, `o` — `None` for Display/Debug.
29 pub radix: Option<char>,
30 pub width: usize,
31 pub zero: bool,
32}
33
34pub fn parse(s: &str) -> Result<Vec<Piece>, String> {
35 let mut out = Vec::new();
36 let mut lit = String::new();
37 let mut it = s.chars().peekable();
38
39 while let Some(c) = it.next() {
40 match c {
41 '{' if it.peek() == Some(&'{') => {
42 it.next();
43 lit.push('{');
44 }
45 '}' if it.peek() == Some(&'}') => {
46 it.next();
47 lit.push('}');
48 }
49 '}' => return Err("unmatched `}` in format string".into()),
50 '{' => {
51 if !lit.is_empty() {
52 out.push(Piece::Lit(std::mem::take(&mut lit)));
53 }
54 let mut body = String::new();
55 let mut closed = false;
56 for c in it.by_ref() {
57 if c == '}' {
58 closed = true;
59 break;
60 }
61 body.push(c);
62 }
63 if !closed {
64 return Err("unmatched `{` in format string".into());
65 }
66 let (name, spec) = match body.split_once(':') {
67 Some((n, s)) => (n, parse_spec(s)?),
68 None => (body.as_str(), Spec::default()),
69 };
70 let r#ref = if name.is_empty() {
71 Ref::Next
72 } else if let Ok(i) = name.parse::<usize>() {
73 Ref::Index(i)
74 } else if name.chars().all(|c| c.is_alphanumeric() || c == '_') {
75 Ref::Named(name.to_string())
76 } else {
77 return Err(format!("unsupported format argument `{name}`"));
78 };
79 out.push(Piece::Arg { r#ref, spec });
80 }
81 _ => lit.push(c),
82 }
83 }
84 if !lit.is_empty() {
85 out.push(Piece::Lit(lit));
86 }
87 Ok(out)
88}
89
90fn parse_spec(s: &str) -> Result<Spec, String> {
91 let mut spec = Spec::default();
92 let mut rest = s;
93
94 match rest.chars().last() {
95 Some('?') => {
96 spec.debug = true;
97 rest = &rest[..rest.len() - 1];
98 }
99 Some(r @ ('x' | 'X' | 'b' | 'o')) => {
100 spec.radix = Some(r);
101 rest = &rest[..rest.len() - 1];
102 }
103 _ => {}
104 }
105
106 if let Some(r) = rest.strip_prefix('0') {
107 spec.zero = true;
108 rest = r;
109 }
110 if !rest.is_empty() {
111 spec.width = rest
112 .parse::<usize>()
113 .map_err(|_| format!("unsupported format spec `:{s}` (precision, alignment and fill are not implemented)"))?;
114 }
115 Ok(spec)
116}
117
118/// Build the Nim expression for one argument, given its already-lowered value.
119pub fn render_arg(value: &str, spec: &Spec) -> String {
120 let core = match spec.radix {
121 Some(r) => format!(
122 "rsRadix({}, {}, {})",
123 value,
124 match r {
125 'x' | 'X' => 16,
126 'b' => 2,
127 _ => 8,
128 },
129 r == 'X'
130 ),
131 None if spec.debug => format!("rsDebug({value})"),
132 None => format!("rsDisplay({value})"),
133 };
134 if spec.width > 0 {
135 format!("rsPad({}, {}, {})", core, spec.width, spec.zero)
136 } else {
137 core
138 }
139}
140
141/// Nim string literal with Rust's escaping rules applied to the bytes we emit.
142pub fn nim_str(s: &str) -> String {
143 let mut out = String::from("\"");
144 for c in s.chars() {
145 match c {
146 '"' => out.push_str("\\\""),
147 '\\' => out.push_str("\\\\"),
148 '\n' => out.push_str("\\n"),
149 '\t' => out.push_str("\\t"),
150 '\r' => out.push_str("\\r"),
151 c => out.push(c),
152 }
153 }
154 out.push('"');
155 out
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn braces_and_specs() {
164 assert_eq!(parse("a{{b").unwrap(), vec![Piece::Lit("a{b".into())]);
165 assert_eq!(
166 parse("{:02x}").unwrap(),
167 vec![Piece::Arg {
168 r#ref: Ref::Next,
169 spec: Spec { debug: false, radix: Some('x'), width: 2, zero: true }
170 }]
171 );
172 assert_eq!(
173 parse("{n:?}").unwrap(),
174 vec![Piece::Arg {
175 r#ref: Ref::Named("n".into()),
176 spec: Spec { debug: true, ..Spec::default() }
177 }]
178 );
179 }
180
181 #[test]
182 fn unsupported_specs_are_rejected_not_guessed() {
183 assert!(parse("{:>8}").is_err());
184 assert!(parse("{:.3}").is_err());
185 assert!(parse("{").is_err());
186 }
187}