1use indexmap::IndexMap;
28use serde_json::Value;
29
30use crate::spec::{Document, FuncDecl, FuncKind, NodeSpec, SourceDecl};
31
32pub const MAX_EXPANDED_NODES: usize = 10_000;
36
37#[derive(Debug, thiserror::Error)]
38pub enum ExpandError {
39 #[error("call `{call}`: unknown function `{func}`")]
40 UnknownFunction { call: String, func: String },
41
42 #[error("call `{call}` of `{func}`: missing required input `{input}`")]
43 MissingInput {
44 call: String,
45 func: String,
46 input: String,
47 },
48
49 #[error("call `{call}` of `{func}`: unknown input `{input}` (declared inputs: {declared})")]
50 UnknownInput {
51 call: String,
52 func: String,
53 input: String,
54 declared: String,
55 },
56
57 #[error(
58 "call `{call}` of `{func}`: input `{input}` is `{kind}`, so the argument must be a `@node` reference"
59 )]
60 NodeArgExpected {
61 call: String,
62 func: String,
63 input: String,
64 kind: &'static str,
65 },
66
67 #[error("function `{func}`: input `{input}` is `{kind}` — `default` is only allowed for scalar inputs")]
68 NonScalarDefault {
69 func: String,
70 input: String,
71 kind: &'static str,
72 },
73
74 #[error("function `{func}`: output node `{output}` is not in its body")]
75 UnknownOutputNode { func: String, output: String },
76
77 #[error("function `{func}`: `{name}` is both an input and a body node")]
78 InputBodyCollision { func: String, name: String },
79
80 #[error("function `{func}`, node `{node}`: unknown reference `@{reference}` (not an input, body node, or source)")]
81 UnknownRef {
82 func: String,
83 node: String,
84 reference: String,
85 },
86
87 #[error("recursive function call: {path}")]
88 RecursiveCall { path: String },
89
90 #[error("node id `{id}` contains `/`, which is reserved for expanded function bodies")]
91 ReservedIdSeparator { id: String },
92
93 #[error("function call `{call}`: missing `fn` field naming the function")]
94 MissingFnName { call: String },
95
96 #[error(
97 "expansion produced more than {MAX_EXPANDED_NODES} nodes — check for heavily nested function calls"
98 )]
99 TooManyNodes,
100}
101
102#[derive(Debug, Clone)]
106pub struct KindCheck {
107 pub node: String,
110 pub declared: FuncKind,
111 pub call: String,
113 pub func: String,
115 pub input: Option<String>,
118}
119
120#[derive(Debug)]
122pub struct Expanded {
123 pub doc: Document,
124 pub kind_checks: Vec<KindCheck>,
125}
126
127pub fn expand_functions(doc: &Document) -> Result<Option<Expanded>, ExpandError> {
131 if doc.functions.is_empty() {
132 return Ok(None);
133 }
134
135 for id in doc.nodes.keys() {
137 if id.contains('/') {
138 return Err(ExpandError::ReservedIdSeparator { id: id.clone() });
139 }
140 }
141 for (fname, f) in &doc.functions {
142 for id in f.nodes.keys() {
143 if id.contains('/') {
144 return Err(ExpandError::ReservedIdSeparator { id: id.clone() });
145 }
146 if f.inputs.contains_key(id) {
147 return Err(ExpandError::InputBodyCollision {
148 func: fname.clone(),
149 name: id.clone(),
150 });
151 }
152 }
153 if !f.nodes.contains_key(f.output.as_str()) {
154 return Err(ExpandError::UnknownOutputNode {
155 func: fname.clone(),
156 output: f.output.as_str().to_string(),
157 });
158 }
159 for (iname, input) in &f.inputs {
160 if input.default.is_some() && input.kind != FuncKind::Scalar {
161 return Err(ExpandError::NonScalarDefault {
162 func: fname.clone(),
163 input: iname.clone(),
164 kind: input.kind.as_str(),
165 });
166 }
167 }
168 }
169
170 check_call_cycles(&doc.functions)?;
171
172 let mut cx = Expander {
173 functions: &doc.functions,
174 sources: &doc.sources,
175 out: IndexMap::new(),
176 kind_checks: Vec::new(),
177 };
178
179 for (id, spec) in &doc.nodes {
180 if spec.op == "func" {
181 cx.expand_call(id, &spec.fields)?;
182 } else {
183 cx.push(id.clone(), spec.clone())?;
184 }
185 }
186
187 Ok(Some(Expanded {
188 doc: Document {
189 name: doc.name.clone(),
190 version: doc.version.clone(),
191 tile_size: doc.tile_size,
192 pad: doc.pad,
193 params: doc.params.clone(),
194 attribution: doc.attribution.clone(),
195 functions: IndexMap::new(),
196 legend: doc.legend.clone(),
197 sources: doc.sources.clone(),
198 nodes: cx.out,
199 output: doc.output.clone(),
200 },
201 kind_checks: cx.kind_checks,
202 }))
203}
204
205fn check_call_cycles(functions: &IndexMap<String, FuncDecl>) -> Result<(), ExpandError> {
207 fn visit(
208 name: &str,
209 functions: &IndexMap<String, FuncDecl>,
210 stack: &mut Vec<String>,
211 done: &mut Vec<String>,
212 ) -> Result<(), ExpandError> {
213 if done.iter().any(|d| d == name) {
214 return Ok(());
215 }
216 if let Some(pos) = stack.iter().position(|s| s == name) {
217 let mut path: Vec<&str> = stack[pos..].iter().map(String::as_str).collect();
218 path.push(name);
219 return Err(ExpandError::RecursiveCall {
220 path: path.join(" → "),
221 });
222 }
223 let Some(f) = functions.get(name) else {
224 return Ok(());
227 };
228 stack.push(name.to_string());
229 for spec in f.nodes.values() {
230 if spec.op == "func" {
231 if let Some(callee) = spec.fields.get("fn").and_then(Value::as_str) {
232 visit(callee, functions, stack, done)?;
233 }
234 }
235 }
236 stack.pop();
237 done.push(name.to_string());
238 Ok(())
239 }
240
241 let mut done = Vec::new();
242 for name in functions.keys() {
243 visit(name, functions, &mut Vec::new(), &mut done)?;
244 }
245 Ok(())
246}
247
248struct Expander<'a> {
249 functions: &'a IndexMap<String, FuncDecl>,
250 sources: &'a IndexMap<String, SourceDecl>,
251 out: IndexMap<String, NodeSpec>,
252 kind_checks: Vec<KindCheck>,
253}
254
255impl Expander<'_> {
256 fn push(&mut self, id: String, spec: NodeSpec) -> Result<(), ExpandError> {
257 if self.out.len() >= MAX_EXPANDED_NODES {
258 return Err(ExpandError::TooManyNodes);
259 }
260 self.out.insert(id, spec);
261 Ok(())
262 }
263
264 fn expand_call(
269 &mut self,
270 call_id: &str,
271 fields: &serde_json::Map<String, Value>,
272 ) -> Result<(), ExpandError> {
273 let func_name =
274 fields
275 .get("fn")
276 .and_then(Value::as_str)
277 .ok_or_else(|| ExpandError::MissingFnName {
278 call: call_id.to_string(),
279 })?;
280 let func = self
281 .functions
282 .get(func_name)
283 .ok_or_else(|| ExpandError::UnknownFunction {
284 call: call_id.to_string(),
285 func: func_name.to_string(),
286 })?;
287
288 for key in fields.keys() {
290 if key == "op" || key == "fn" {
291 continue;
292 }
293 if !func.inputs.contains_key(key) {
294 return Err(ExpandError::UnknownInput {
295 call: call_id.to_string(),
296 func: func_name.to_string(),
297 input: key.clone(),
298 declared: func
299 .inputs
300 .keys()
301 .map(String::as_str)
302 .collect::<Vec<_>>()
303 .join(", "),
304 });
305 }
306 }
307
308 let mut subst: IndexMap<String, Value> = IndexMap::new();
310 for (iname, input) in &func.inputs {
311 let arg = match fields.get(iname) {
312 Some(v) => v.clone(),
313 None => match &input.default {
314 Some(d) => d.clone(),
315 None => {
316 return Err(ExpandError::MissingInput {
317 call: call_id.to_string(),
318 func: func_name.to_string(),
319 input: iname.clone(),
320 });
321 }
322 },
323 };
324 let arg_node = arg
325 .as_str()
326 .and_then(|s| s.strip_prefix('@'))
327 .map(str::to_string);
328 if input.kind != FuncKind::Scalar && arg_node.is_none() {
329 return Err(ExpandError::NodeArgExpected {
330 call: call_id.to_string(),
331 func: func_name.to_string(),
332 input: iname.clone(),
333 kind: input.kind.as_str(),
334 });
335 }
336 if let Some(src) = arg_node {
340 self.kind_checks.push(KindCheck {
341 node: src,
342 declared: input.kind,
343 call: call_id.to_string(),
344 func: func_name.to_string(),
345 input: Some(iname.clone()),
346 });
347 }
348 subst.insert(iname.clone(), arg);
349 }
350
351 let output_id = func.output.as_str();
353 let mangled = |body_id: &str| -> String {
354 if body_id == output_id {
355 call_id.to_string()
356 } else {
357 format!("{call_id}/{body_id}")
358 }
359 };
360
361 self.kind_checks.push(KindCheck {
362 node: call_id.to_string(),
363 declared: func.output_kind,
364 call: call_id.to_string(),
365 func: func_name.to_string(),
366 input: None,
367 });
368
369 for (body_id, spec) in &func.nodes {
370 let new_id = mangled(body_id);
371 let mut new_fields = serde_json::Map::with_capacity(spec.fields.len());
372 for (k, v) in &spec.fields {
373 if let Some(name) = v.as_str().and_then(|s| s.strip_prefix('@')) {
378 if subst.get(name) == Some(&Value::Null) {
379 continue;
380 }
381 }
382 new_fields.insert(
383 k.clone(),
384 self.rewrite(v, &subst, func, func_name, body_id, &mangled)?,
385 );
386 }
387 if spec.op == "func" {
388 self.expand_call(&new_id, &new_fields)?;
389 } else {
390 self.push(
391 new_id,
392 NodeSpec {
393 op: spec.op.clone(),
394 fields: new_fields,
395 },
396 )?;
397 }
398 }
399 Ok(())
400 }
401
402 fn rewrite(
408 &self,
409 v: &Value,
410 subst: &IndexMap<String, Value>,
411 func: &FuncDecl,
412 func_name: &str,
413 body_id: &str,
414 mangled: &dyn Fn(&str) -> String,
415 ) -> Result<Value, ExpandError> {
416 match v {
417 Value::String(s) => {
418 let Some(name) = s.strip_prefix('@') else {
419 return Ok(v.clone());
420 };
421 if let Some(arg) = subst.get(name) {
422 return Ok(arg.clone());
423 }
424 if func.nodes.contains_key(name) {
425 return Ok(Value::String(format!("@{}", mangled(name))));
426 }
427 if self.sources.contains_key(name) {
428 return Ok(v.clone());
429 }
430 Err(ExpandError::UnknownRef {
431 func: func_name.to_string(),
432 node: body_id.to_string(),
433 reference: name.to_string(),
434 })
435 }
436 Value::Array(items) => Ok(Value::Array(
437 items
438 .iter()
439 .map(|item| self.rewrite(item, subst, func, func_name, body_id, mangled))
440 .collect::<Result<_, _>>()?,
441 )),
442 Value::Object(map) => {
443 let mut out = serde_json::Map::with_capacity(map.len());
444 for (k, item) in map {
445 out.insert(
446 k.clone(),
447 self.rewrite(item, subst, func, func_name, body_id, mangled)?,
448 );
449 }
450 Ok(Value::Object(out))
451 }
452 _ => Ok(v.clone()),
453 }
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 fn expand(json: &str) -> Result<Option<Expanded>, ExpandError> {
462 let doc = Document::from_json(json).unwrap();
463 expand_functions(&doc)
464 }
465
466 fn expanded(json: &str) -> Document {
467 expand(json).unwrap().expect("functions present").doc
468 }
469
470 const BASIC: &str = r##"{
471 "name": "demo",
472 "functions": {
473 "tinted": {
474 "inputs": {
475 "base": { "kind": "raster" },
476 "color": { "kind": "scalar", "default": "#ff0000" }
477 },
478 "output": "@mix",
479 "output-kind": "raster",
480 "nodes": {
481 "tint": { "op": "solid", "color": "@color" },
482 "mix": { "op": "blend", "base": "@base", "over": "@tint" }
483 }
484 }
485 },
486 "nodes": {
487 "bg": { "op": "solid", "color": "#ffffff" },
488 "out": { "op": "func", "fn": "tinted", "base": "@bg", "color": "#00ff00" }
489 },
490 "output": "@out"
491 }"##;
492
493 #[test]
494 fn expands_with_mangling_and_output_alias() {
495 let doc = expanded(BASIC);
496 assert!(doc.functions.is_empty());
497 let ids: Vec<&str> = doc.nodes.keys().map(String::as_str).collect();
498 assert_eq!(ids, ["bg", "out/tint", "out"]);
499 assert_eq!(doc.nodes["out"].fields["base"], "@bg");
502 assert_eq!(doc.nodes["out"].fields["over"], "@out/tint");
503 assert_eq!(doc.nodes["out/tint"].fields["color"], "#00ff00");
505 }
506
507 #[test]
508 fn default_fills_missing_scalar_arg() {
509 let json = BASIC.replace(r##", "color": "#00ff00""##, "");
510 let doc = expanded(&json);
511 assert_eq!(doc.nodes["out/tint"].fields["color"], "#ff0000");
512 }
513
514 #[test]
515 fn param_args_stay_params() {
516 let json = BASIC.replace(r##""color": "#00ff00""##, r##""color": "$ink""##);
517 let doc = expanded(&json);
518 assert_eq!(doc.nodes["out/tint"].fields["color"], "$ink");
519 }
520
521 #[test]
522 fn missing_required_input_errors() {
523 let json = BASIC.replace(r#", "base": "@bg""#, "");
524 let err = expand(&json).unwrap_err();
525 assert!(matches!(err, ExpandError::MissingInput { input, .. } if input == "base"));
526 }
527
528 #[test]
529 fn unknown_input_errors() {
530 let json = BASIC.replace(r##""color": "#00ff00""##, r##""colour": "#00ff00""##);
531 let err = expand(&json).unwrap_err();
532 assert!(matches!(err, ExpandError::UnknownInput { input, .. } if input == "colour"));
533 }
534
535 #[test]
536 fn non_scalar_input_requires_node_arg() {
537 let json = BASIC.replace(r#""base": "@bg""#, r#""base": 3"#);
538 let err = expand(&json).unwrap_err();
539 assert!(matches!(err, ExpandError::NodeArgExpected { input, .. } if input == "base"));
540 }
541
542 #[test]
543 fn unknown_body_ref_errors() {
544 let json = BASIC.replace(r#""base": "@base""#, r#""base": "@nope""#);
545 let err = expand(&json).unwrap_err();
546 assert!(matches!(err, ExpandError::UnknownRef { reference, .. } if reference == "nope"));
547 }
548
549 #[test]
550 fn nested_function_calls_expand() {
551 let json = r##"{
552 "name": "demo",
553 "functions": {
554 "white": {
555 "inputs": {},
556 "output": "@w",
557 "output-kind": "raster",
558 "nodes": { "w": { "op": "solid", "color": "#ffffff" } }
559 },
560 "framed": {
561 "inputs": {},
562 "output": "@mix",
563 "output-kind": "raster",
564 "nodes": {
565 "fill": { "op": "func", "fn": "white" },
566 "mix": { "op": "blend", "base": "@fill", "over": "@fill" }
567 }
568 }
569 },
570 "nodes": { "out": { "op": "func", "fn": "framed" } },
571 "output": "@out"
572 }"##;
573 let doc = expanded(json);
574 let ids: Vec<&str> = doc.nodes.keys().map(String::as_str).collect();
575 assert_eq!(ids, ["out/fill", "out"]);
578 assert_eq!(doc.nodes["out"].fields["base"], "@out/fill");
579 }
580
581 #[test]
582 fn recursive_calls_error_with_path() {
583 let json = r##"{
584 "name": "demo",
585 "functions": {
586 "a": { "inputs": {}, "output": "@n", "output-kind": "raster",
587 "nodes": { "n": { "op": "func", "fn": "b" } } },
588 "b": { "inputs": {}, "output": "@n", "output-kind": "raster",
589 "nodes": { "n": { "op": "func", "fn": "a" } } }
590 },
591 "nodes": { "out": { "op": "func", "fn": "a" } },
592 "output": "@out"
593 }"##;
594 let err = expand(json).unwrap_err();
595 let msg = err.to_string();
596 assert!(
597 msg.contains("a → b → a") || msg.contains("b → a → b"),
598 "{msg}"
599 );
600 }
601
602 #[test]
603 fn no_functions_is_a_noop() {
604 let json = r##"{
605 "name": "demo",
606 "nodes": { "out": { "op": "solid", "color": "#ffffff" } },
607 "output": "@out"
608 }"##;
609 assert!(expand(json).unwrap().is_none());
610 }
611
612 #[test]
613 fn substitution_recurses_into_arrays() {
614 let json = r##"{
615 "name": "demo",
616 "functions": {
617 "ramp": {
618 "inputs": { "lo": { "kind": "scalar", "default": "#000000" } },
619 "output": "@g",
620 "output-kind": "raster",
621 "nodes": {
622 "g": { "op": "gradient-linear",
623 "stops": [[0, "@lo"], [1, "#ffffff"]] }
624 }
625 }
626 },
627 "nodes": { "out": { "op": "func", "fn": "ramp", "lo": "#101010" } },
628 "output": "@out"
629 }"##;
630 let doc = expanded(json);
631 assert_eq!(doc.nodes["out"].fields["stops"][0][1], "#101010");
632 }
633
634 #[test]
635 fn null_arg_drops_the_substituted_field() {
636 let json = r##"{
637 "name": "demo",
638 "functions": {
639 "stroke": {
640 "inputs": {
641 "curve": { "kind": "scalar", "default": null }
642 },
643 "output": "@n",
644 "output-kind": "raster",
645 "nodes": {
646 "n": { "op": "solid", "color": "#ffffff",
647 "radius-stroke-curve": "@curve" }
648 }
649 }
650 },
651 "nodes": {
652 "a": { "op": "func", "fn": "stroke" },
653 "b": { "op": "func", "fn": "stroke", "curve": [[0, -1.0], [1, 0.0]] }
654 },
655 "output": "@a"
656 }"##;
657 let doc = expanded(json);
658 assert!(!doc.nodes["a"].fields.contains_key("radius-stroke-curve"));
660 assert_eq!(doc.nodes["b"].fields["radius-stroke-curve"][0][1], -1.0);
662 }
663
664 #[test]
665 fn kind_checks_record_call_sites() {
666 let e = expand(BASIC).unwrap().unwrap();
667 assert_eq!(e.kind_checks.len(), 2);
669 let arg = &e.kind_checks[0];
670 assert_eq!(arg.node, "bg");
671 assert_eq!(arg.declared, FuncKind::Raster);
672 assert_eq!(arg.input.as_deref(), Some("base"));
673 let out = &e.kind_checks[1];
674 assert_eq!(out.node, "out");
675 assert!(out.input.is_none());
676 }
677}