use crate::ast::Value;
use crate::derive_support::PolydatSetup;
#[derive(Debug, Clone)]
pub enum Segment {
Literal(String),
Placeholder(FormatSpec),
}
#[derive(Debug, Clone)]
pub struct FormatSpec {
index: usize,
width: Option<usize>,
precision: Option<usize>,
fill: char,
conversion: char,
}
#[derive(Debug, Clone)]
pub struct ParsedFormat {
segments: Vec<Segment>,
}
impl PolydatSetup for ParsedFormat {}
#[crate::polydat_node(category = Formatting)]
fn printf(
format: Const<&str>,
#[poly_const(ParsedFormat::from_format_str, from = format)] parsed: &ParsedFormat,
parts: &[polydat::ast::Value],
) -> String {
parsed.render_with(parts.len(), |i| FmtArg::from(&parts[i]))
}
pub enum FmtArg<'a> {
U64(u64),
F64(f64),
Bool(bool),
Str(&'a str),
Value(Value),
}
impl<'a> From<&'a Value> for FmtArg<'a> {
fn from(v: &'a Value) -> Self {
match v {
Value::U64(x) => FmtArg::U64(*x),
Value::F64(x) => FmtArg::F64(*x),
Value::Bool(b) => FmtArg::Bool(*b),
Value::Str(s) => FmtArg::Str(s),
other => FmtArg::Value(other.clone()),
}
}
}
impl ParsedFormat {
pub fn from_format_str(fmt: &str) -> Self {
Self {
segments: parse_format(fmt),
}
}
pub fn interned(fmt: &str) -> &'static ParsedFormat {
use std::sync::RwLock;
static FORMATS: RwLock<Option<std::collections::HashMap<String, &'static ParsedFormat>>> =
RwLock::new(None);
if let Some(p) = FORMATS
.read()
.unwrap()
.as_ref()
.and_then(|m| m.get(fmt).copied())
{
return p;
}
let mut guard = FORMATS.write().unwrap();
let map = guard.get_or_insert_with(std::collections::HashMap::new);
if let Some(p) = map.get(fmt).copied() {
return p;
}
let leaked: &'static ParsedFormat = Box::leak(Box::new(Self::from_format_str(fmt)));
map.insert(fmt.to_string(), leaked);
leaked
}
pub fn render_with<'a>(&self, argc: usize, arg: impl Fn(usize) -> FmtArg<'a>) -> String {
let mut result = String::new();
self.render_into(argc, arg, &mut result);
result
}
pub fn render_into<'a, W: std::fmt::Write>(
&self,
argc: usize,
arg: impl Fn(usize) -> FmtArg<'a>,
out: &mut W,
) {
for seg in &self.segments {
match seg {
Segment::Literal(s) => {
let _ = out.write_str(s);
}
Segment::Placeholder(spec) => {
if spec.index >= argc {
panic!(
"printf: format references input #{} but only {argc} wire input(s) supplied",
spec.index,
);
}
let _ = out.write_str(&format_arg(&arg(spec.index), spec));
}
}
}
}
}
fn format_arg(arg: &FmtArg<'_>, spec: &FormatSpec) -> String {
match arg {
FmtArg::U64(v) => format_u64(*v, spec),
FmtArg::F64(v) => format_f64(*v, spec),
FmtArg::Bool(v) => v.to_string(),
FmtArg::Str(v) => {
if let Some(w) = spec.width {
format!("{:>width$}", v, width = w)
} else {
v.to_string()
}
}
FmtArg::Value(val @ Value::Ext(_)) => val.to_display_string(),
FmtArg::Value(val) => format!("{val:?}"),
}
}
fn format_u64(v: u64, spec: &FormatSpec) -> String {
let raw = match spec.conversion {
'x' => format!("{v:x}"),
'X' => format!("{v:X}"),
'b' => format!("{v:b}"),
'o' => format!("{v:o}"),
_ => v.to_string(),
};
apply_width(&raw, spec)
}
fn format_f64(v: f64, spec: &FormatSpec) -> String {
let raw = if let Some(prec) = spec.precision {
format!("{v:.prec$}")
} else {
format!("{v:?}")
};
apply_width(&raw, spec)
}
fn apply_width(s: &str, spec: &FormatSpec) -> String {
if let Some(w) = spec.width {
if s.len() < w {
let pad = w - s.len();
let fill = spec.fill;
format!("{}{s}", std::iter::repeat_n(fill, pad).collect::<String>())
} else {
s.to_string()
}
} else {
s.to_string()
}
}
fn parse_format(fmt: &str) -> Vec<Segment> {
let mut segments = Vec::new();
let mut literal = String::new();
let chars: Vec<char> = fmt.chars().collect();
let mut i = 0;
let mut placeholder_idx = 0;
while i < chars.len() {
if chars[i] == '{' && i + 1 < chars.len() && chars[i + 1] == '{' {
literal.push('{');
i += 2;
} else if chars[i] == '{' {
if !literal.is_empty() {
segments.push(Segment::Literal(std::mem::take(&mut literal)));
}
let start = i + 1;
while i < chars.len() && chars[i] != '}' {
i += 1;
}
let spec_str: String = chars[start..i].iter().collect();
let spec = parse_spec(&spec_str, placeholder_idx);
segments.push(Segment::Placeholder(spec));
placeholder_idx += 1;
i += 1; } else if chars[i] == '}' && i + 1 < chars.len() && chars[i + 1] == '}' {
literal.push('}');
i += 2;
} else {
literal.push(chars[i]);
i += 1;
}
}
if !literal.is_empty() {
segments.push(Segment::Literal(literal));
}
segments
}
fn parse_spec(spec: &str, index: usize) -> FormatSpec {
let mut result = FormatSpec {
index,
width: None,
precision: None,
fill: ' ',
conversion: 'd',
};
if spec.is_empty() {
return result;
}
let spec = spec.strip_prefix(':').unwrap_or(spec);
if spec.is_empty() {
return result;
}
let chars: Vec<char> = spec.chars().collect();
let mut pos = 0;
if pos < chars.len()
&& chars[pos] == '0'
&& pos + 1 < chars.len()
&& chars[pos + 1].is_ascii_digit()
{
result.fill = '0';
pos += 1;
}
let width_start = pos;
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
}
if pos > width_start {
let w: String = chars[width_start..pos].iter().collect();
result.width = Some(w.parse().unwrap());
}
if pos < chars.len() && chars[pos] == '.' {
pos += 1;
let prec_start = pos;
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
}
if pos > prec_start {
let p: String = chars[prec_start..pos].iter().collect();
result.precision = Some(p.parse().unwrap());
}
}
if pos < chars.len() {
result.conversion = chars[pos];
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::PolydatNode;
#[test]
fn printf_simple() {
let node = Printf::new("hello {}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "hello 42");
}
#[test]
fn printf_multiple() {
let node = Printf::new("{} + {} = {}".to_string(), 3);
let mut out = [Value::None];
node.eval(&[Value::U64(1), Value::U64(2), Value::U64(3)], &mut out);
assert_eq!(out[0].as_str(), "1 + 2 = 3");
}
#[test]
fn printf_zero_pad() {
let node = Printf::new("{:05}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "00042");
}
#[test]
fn printf_hex() {
let node = Printf::new("{:x}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::U64(255)], &mut out);
assert_eq!(out[0].as_str(), "ff");
}
#[test]
fn printf_hex_upper() {
let node = Printf::new("{:X}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::U64(255)], &mut out);
assert_eq!(out[0].as_str(), "FF");
}
#[test]
fn printf_precision() {
let node = Printf::new("{:.2}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::F64(3.14159)], &mut out);
assert_eq!(out[0].as_str(), "3.14");
}
#[test]
fn printf_mixed() {
let node = Printf::new("id={:05} val={:.1}".to_string(), 2);
let mut out = [Value::None];
node.eval(&[Value::U64(7), Value::F64(98.6)], &mut out);
assert_eq!(out[0].as_str(), "id=00007 val=98.6");
}
#[test]
fn printf_literal_braces() {
let node = Printf::new("{{escaped}} {}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::U64(1)], &mut out);
assert_eq!(out[0].as_str(), "{escaped} 1");
}
#[test]
fn printf_no_placeholders() {
let node = Printf::new("just text".to_string(), 0);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str(), "just text");
}
#[test]
fn printf_string_input() {
let node = Printf::new("hello {}".to_string(), 1);
let mut out = [Value::None];
node.eval(&[Value::Str("world".into())], &mut out);
assert_eq!(out[0].as_str(), "hello world");
}
#[test]
fn printf_all_present_unchanged() {
let node = Printf::new("a={} b={}".to_string(), 2);
let mut out = [Value::None];
node.eval(&[Value::U64(1), Value::U64(2)], &mut out);
assert_eq!(out[0].as_str(), "a=1 b=2");
}
#[test]
fn printf_no_placeholders_still_renders() {
let node = Printf::new("static text".to_string(), 0);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str(), "static text");
}
}