1use runmat_builtins::{CharArray, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::format::{
7 decode_escape_sequences, extract_format_string, flatten_arguments, format_variadic_with_cursor,
8 ArgCursor,
9};
10use crate::builtins::common::map_control_flow_with_builtin;
11use crate::builtins::common::spec::{
12 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13 ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::strings::type_resolvers::string_scalar_type;
16use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::strings::core::sprintf")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20 name: "sprintf",
21 op_kind: GpuOpKind::Custom("format"),
22 supported_precisions: &[],
23 broadcast: BroadcastSemantics::None,
24 provider_hooks: &[],
25 constant_strategy: ConstantStrategy::InlineLiteral,
26 residency: ResidencyPolicy::GatherImmediately,
27 nan_mode: ReductionNaN::Include,
28 two_pass_threshold: None,
29 workgroup_size: None,
30 accepts_nan_mode: false,
31 notes: "Formatting runs on the CPU; GPU tensors are gathered before substitution.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::strings::core::sprintf")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36 name: "sprintf",
37 shape: ShapeRequirements::Any,
38 constant_strategy: ConstantStrategy::InlineLiteral,
39 elementwise: None,
40 reduction: None,
41 emits_nan: false,
42 notes: "Formatting is a residency sink and is not fused; callers should treat sprintf as a CPU-only builtin.",
43};
44
45fn sprintf_flow(message: impl Into<String>) -> RuntimeError {
46 build_runtime_error(message).with_builtin("sprintf").build()
47}
48
49fn remap_sprintf_flow(err: RuntimeError) -> RuntimeError {
50 map_control_flow_with_builtin(err, "sprintf")
51}
52
53#[runtime_builtin(
54 name = "sprintf",
55 category = "strings/core",
56 summary = "Format data into a character vector using printf-style placeholders.",
57 keywords = "sprintf,format,printf,text",
58 accel = "format",
59 sink = true,
60 type_resolver(string_scalar_type),
61 builtin_path = "crate::builtins::strings::core::sprintf"
62)]
63async fn sprintf_builtin(format_spec: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
64 let gathered_spec = gather_if_needed_async(&format_spec)
65 .await
66 .map_err(remap_sprintf_flow)?;
67 let raw_format =
68 extract_format_string(&gathered_spec, "sprintf").map_err(remap_sprintf_flow)?;
69 let format_string =
70 decode_escape_sequences("sprintf", &raw_format).map_err(remap_sprintf_flow)?;
71 let flattened_args = flatten_arguments(&rest, "sprintf")
72 .await
73 .map_err(remap_sprintf_flow)?;
74 let mut cursor = ArgCursor::new(&flattened_args);
75 let mut output = String::new();
76
77 loop {
78 let step =
79 format_variadic_with_cursor(&format_string, &mut cursor).map_err(remap_sprintf_flow)?;
80 output.push_str(&step.output);
81
82 if step.consumed == 0 {
83 if cursor.remaining() > 0 {
84 return Err(sprintf_flow(
85 "sprintf: formatSpec contains no conversion specifiers but additional arguments were supplied",
86 ));
87 }
88 break;
89 }
90
91 if cursor.remaining() == 0 {
92 break;
93 }
94 }
95
96 char_row_value(&output)
97}
98
99fn char_row_value(text: &str) -> BuiltinResult<Value> {
100 let chars: Vec<char> = text.chars().collect();
101 let len = chars.len();
102 let array = CharArray::new(chars, 1, len).map_err(|e| sprintf_flow(format!("sprintf: {e}")))?;
103 Ok(Value::CharArray(array))
104}
105
106#[cfg(test)]
107pub(crate) mod tests {
108 use super::*;
109 use crate::{builtins::common::test_support, make_cell};
110 use runmat_builtins::{CharArray, IntValue, ResolveContext, StringArray, Tensor, Type};
111
112 fn sprintf_builtin(format_spec: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
113 futures::executor::block_on(super::sprintf_builtin(format_spec, rest))
114 }
115
116 fn error_message(err: crate::RuntimeError) -> String {
117 err.message().to_string()
118 }
119
120 fn char_value_to_string(value: Value) -> String {
121 match value {
122 Value::CharArray(ca) => ca.data.into_iter().collect(),
123 other => panic!("expected char output, got {other:?}"),
124 }
125 }
126
127 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
128 #[test]
129 fn sprintf_basic_integer() {
130 let result = sprintf_builtin(
131 Value::String("Value: %d".to_string()),
132 vec![Value::Int(IntValue::I32(42))],
133 )
134 .expect("sprintf");
135 assert_eq!(char_value_to_string(result), "Value: 42");
136 }
137
138 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
139 #[test]
140 fn sprintf_float_precision() {
141 let result = sprintf_builtin(
142 Value::String("pi ~= %.3f".to_string()),
143 vec![Value::Num(std::f64::consts::PI)],
144 )
145 .expect("sprintf");
146 assert_eq!(char_value_to_string(result), "pi ~= 3.142");
147 }
148
149 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
150 #[test]
151 fn sprintf_array_repeat() {
152 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
153 let result = sprintf_builtin(
154 Value::String("%d ".to_string()),
155 vec![Value::Tensor(tensor)],
156 )
157 .expect("sprintf");
158 assert_eq!(char_value_to_string(result), "1 2 3 ");
159 }
160
161 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
162 #[test]
163 fn sprintf_star_width() {
164 let args = vec![
165 Value::Int(IntValue::I32(6)),
166 Value::Int(IntValue::I32(2)),
167 Value::Num(12.345),
168 ];
169 let result = sprintf_builtin(Value::String("%*.*f".to_string()), args).expect("sprintf");
170 assert_eq!(char_value_to_string(result), " 12.35");
171 }
172
173 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
174 #[test]
175 fn sprintf_literal_percent() {
176 let result =
177 sprintf_builtin(Value::String("%% complete".to_string()), Vec::new()).expect("sprintf");
178 assert_eq!(char_value_to_string(result), "% complete");
179 }
180
181 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
182 #[test]
183 fn sprintf_gpu_numeric() {
184 test_support::with_test_provider(|provider| {
185 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
186 let view = runmat_accelerate_api::HostTensorView {
187 data: &tensor.data,
188 shape: &tensor.shape,
189 };
190 let handle = provider.upload(&view).expect("upload");
191 let value = Value::GpuTensor(handle);
192 let result =
193 sprintf_builtin(Value::String("%0.1f,".to_string()), vec![value]).expect("sprintf");
194 assert_eq!(char_value_to_string(result), "1.0,2.0,");
195 });
196 }
197
198 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
199 #[test]
200 fn sprintf_matrix_column_major() {
201 let tensor = Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap();
202 let result = sprintf_builtin(
203 Value::String("%0.0f ".to_string()),
204 vec![Value::Tensor(tensor)],
205 )
206 .expect("sprintf");
207 assert_eq!(char_value_to_string(result), "1 3 2 4 ");
208 }
209
210 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
211 #[test]
212 fn sprintf_not_enough_arguments_error() {
213 let err = error_message(
214 sprintf_builtin(
215 Value::String("%d %d".to_string()),
216 vec![Value::Int(IntValue::I32(1))],
217 )
218 .expect_err("sprintf should error"),
219 );
220 assert!(
221 err.contains("not enough input arguments"),
222 "unexpected error: {err}"
223 );
224 }
225
226 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
227 #[test]
228 fn sprintf_extra_arguments_error() {
229 let err = error_message(
230 sprintf_builtin(
231 Value::String("literal text".to_string()),
232 vec![Value::Int(IntValue::I32(1))],
233 )
234 .expect_err("sprintf should error"),
235 );
236 assert!(
237 err.contains("contains no conversion specifiers"),
238 "unexpected error: {err}"
239 );
240 }
241
242 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
243 #[test]
244 fn sprintf_format_spec_multirow_error() {
245 let chars = CharArray::new("hi!".chars().collect(), 3, 1).unwrap();
246 let err = error_message(
247 sprintf_builtin(Value::CharArray(chars), Vec::new()).expect_err("sprintf"),
248 );
249 assert!(
250 err.contains("formatSpec must be a character row vector"),
251 "unexpected error: {err}"
252 );
253 }
254
255 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
256 #[test]
257 fn sprintf_percent_c_from_numeric() {
258 let result = sprintf_builtin(
259 Value::String("%c".to_string()),
260 vec![Value::Int(IntValue::I32(65))],
261 )
262 .expect("sprintf");
263 assert_eq!(char_value_to_string(result), "A");
264 }
265
266 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
267 #[test]
268 fn sprintf_cell_arguments() {
269 let cell = make_cell(
270 vec![
271 Value::Num(1.0),
272 Value::String("two".to_string()),
273 Value::Num(3.0),
274 ],
275 3,
276 1,
277 )
278 .expect("cell");
279 let result = sprintf_builtin(Value::String("%0.0f %s %0.0f".to_string()), vec![cell])
280 .expect("sprintf");
281 assert_eq!(char_value_to_string(result), "1 two 3");
282 }
283
284 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
285 #[test]
286 fn sprintf_string_array_column_major() {
287 let data = vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
288 let array =
289 StringArray::new(data, vec![3, 1]).expect("string array construction must succeed");
290 let result = sprintf_builtin(
291 Value::String("%s ".to_string()),
292 vec![Value::StringArray(array)],
293 )
294 .expect("sprintf");
295 assert_eq!(char_value_to_string(result), "alpha beta gamma ");
296 }
297
298 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
299 #[test]
300 fn sprintf_complex_s_conversion() {
301 let result = sprintf_builtin(
302 Value::String("%s".to_string()),
303 vec![Value::Complex(1.5, -2.0)],
304 )
305 .expect("sprintf");
306 assert_eq!(char_value_to_string(result), "1.5-2i");
307 }
308
309 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
310 #[test]
311 fn sprintf_escape_sequences() {
312 let result = sprintf_builtin(
313 Value::String("Line 1\\nLine 2\\t(tab)".to_string()),
314 Vec::new(),
315 )
316 .expect("sprintf");
317 assert_eq!(char_value_to_string(result), "Line 1\nLine 2\t(tab)");
318 }
319
320 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
321 #[test]
322 fn sprintf_hex_and_octal_escapes() {
323 let result =
324 sprintf_builtin(Value::String("\\x41\\101".to_string()), Vec::new()).expect("sprintf");
325 assert_eq!(char_value_to_string(result), "AA");
326 }
327
328 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
329 #[test]
330 fn sprintf_unknown_escape_preserved() {
331 let result =
332 sprintf_builtin(Value::String("Value\\q".to_string()), Vec::new()).expect("sprintf");
333 assert_eq!(char_value_to_string(result), "Value\\q");
334 }
335
336 #[test]
337 fn sprintf_type_is_string_scalar() {
338 assert_eq!(
339 string_scalar_type(&[Type::String], &ResolveContext::new(Vec::new())),
340 Type::String
341 );
342 }
343}