use crate::node::{GkNode, NodeMeta, Port, PortType, Slot, Value};
#[derive(Debug, Clone)]
enum Segment {
Literal(String),
Placeholder(FormatSpec),
}
#[derive(Debug, Clone)]
struct FormatSpec {
index: usize,
width: Option<usize>,
precision: Option<usize>,
fill: char,
conversion: char,
}
pub struct Printf {
meta: NodeMeta,
segments: Vec<Segment>,
}
impl Printf {
pub fn new(fmt: &str, input_types: &[PortType]) -> Self {
let segments = parse_format(fmt);
let placeholder_count = segments.iter().filter(|s| matches!(s, Segment::Placeholder(_))).count();
assert_eq!(
placeholder_count, input_types.len(),
"format string has {placeholder_count} placeholders but {n} input types provided",
n = input_types.len()
);
let inputs: Vec<Port> = input_types
.iter()
.enumerate()
.map(|(i, &typ)| Port::new(format!("in_{i}"), typ))
.collect();
let slots: Vec<Slot> = inputs.iter().map(|p| Slot::Wire(p.clone())).collect();
Self {
meta: NodeMeta {
name: "printf".into(),
outs: vec![Port::new("output", PortType::Str)],
ins: slots,
},
segments,
}
}
pub fn variadic(fmt: &str, wire_count: usize) -> Self {
let segments = parse_format(fmt);
let inputs: Vec<Port> = (0..wire_count)
.map(|i| Port::new(format!("in_{i}"), PortType::U64))
.collect();
let slots: Vec<Slot> = inputs.iter().map(|p| Slot::Wire(p.clone())).collect();
Self {
meta: NodeMeta {
name: "printf".into(),
outs: vec![Port::new("output", PortType::Str)],
ins: slots,
},
segments,
}
}
}
impl GkNode for Printf {
fn meta(&self) -> &NodeMeta {
&self.meta
}
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
for seg in &self.segments {
if let Segment::Placeholder(spec) = seg
&& matches!(&inputs[spec.index], Value::None)
{
outputs[0] = Value::None;
return;
}
}
let mut result = String::new();
for seg in &self.segments {
match seg {
Segment::Literal(s) => result.push_str(s),
Segment::Placeholder(spec) => {
let val = &inputs[spec.index];
let formatted = format_value(val, spec);
result.push_str(&formatted);
}
}
}
outputs[0] = Value::Str(result.into());
}
}
fn format_value(val: &Value, spec: &FormatSpec) -> String {
match val {
Value::U64(v) => format_u64(*v, spec),
Value::F64(v) => format_f64(*v, spec),
Value::Bool(v) => v.to_string(),
Value::Str(v) => {
if let Some(w) = spec.width {
format!("{:>width$}", v, width = w)
} else {
v.to_string()
}
}
_ => 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
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
use crate::node::SlotType;
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "printf", category: C::Formatting,
outputs: 1, description: "printf-style formatting: printf(fmt, a, b, ...) -> String",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "format", slot_type: SlotType::ConstStr, required: true, example: "\"%d\"", constraint: None },
],
arity: Arity::VariadicWires { min_wires: 0 },
commutativity: crate::node::Commutativity::Positional,
help: "Printf-style string formatting with positional wire inputs.\nFormat string uses Rust-style {} placeholders with optional specifiers:\n {:05} zero-pad, {:.2} precision, {:x} hex, {:X} HEX, {:b} binary, {:o} octal.\nParameters:\n format — format string constant (e.g. \"user-{:05}-score-{:.1}\")\n input... — wire inputs matched positionally to placeholders\nExample: printf(\"id={:08x} val={:.2}\", hash(cycle), score)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
]
}
pub(crate) fn build_node(name: &str, wires: &[crate::assembly::WireRef], _wire_types: &[crate::node::PortType], consts: &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::node::GkNode>, String>> {
match name {
"printf" => {
let fmt = consts.first()
.map(|c| c.as_str())
.unwrap_or("{}");
let placeholder_count = parse_format(fmt)
.iter()
.filter(|s| matches!(s, Segment::Placeholder(_)))
.count();
if placeholder_count != wires.len() {
return Some(Err(format!(
"printf: format has {placeholder_count} placeholders but {} wire inputs supplied",
wires.len(),
)));
}
let types: Vec<crate::node::PortType> = (0..wires.len()).map(|_| crate::node::PortType::U64).collect();
Some(Ok(Box::new(Printf::new(fmt, &types))))
}
_ => None,
}
}
crate::register_nodes!(signatures, build_node);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn printf_simple() {
let node = Printf::new("hello {}", &[PortType::U64]);
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("{} + {} = {}", &[PortType::U64, PortType::U64, PortType::U64]);
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}", &[PortType::U64]);
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}", &[PortType::U64]);
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}", &[PortType::U64]);
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}", &[PortType::F64]);
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}", &[PortType::U64, PortType::F64]);
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}} {}", &[PortType::U64]);
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", &[]);
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 {}", &[PortType::Str]);
let mut out = [Value::None];
node.eval(&[Value::Str("world".into())], &mut out);
assert_eq!(out[0].as_str(), "hello world");
}
#[test]
fn printf_none_input_yields_none() {
let node = Printf::new("hello {}", &[PortType::Str]);
let mut out = [Value::Str("untouched".into())];
node.eval(&[Value::None], &mut out);
assert!(matches!(out[0], Value::None), "got: {:?}", out[0]);
}
#[test]
fn printf_partial_none_taints_whole_result() {
let node = Printf::new("a={} b={}", &[PortType::U64, PortType::U64]);
let mut out = [Value::Str("untouched".into())];
node.eval(&[Value::U64(1), Value::None], &mut out);
assert!(matches!(out[0], Value::None), "got: {:?}", out[0]);
}
#[test]
fn printf_all_present_unchanged() {
let node = Printf::new("a={} b={}", &[PortType::U64, PortType::U64]);
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", &[]);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str(), "static text");
}
}