1use std::collections::HashMap;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::ast::SlotShape;
22use crate::ast::{PortType, Value, ValueRef};
23use crate::iteration::comprehension::StreamerValue;
24use crate::iteration::comprehension::runtime::{RuntimeTuple, evaluate_for_iteration};
25use crate::kernel::{Kernel, KernelProgram, PolydatKernel, PolydatProgram};
26use crate::library::support::float_text;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum HolePosition {
31 Value,
33 InString,
35 Text,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct HoleEncoding {
43 pub encoding: String,
45 pub position: HolePosition,
47 pub ty: Option<String>,
49 pub format: Option<String>,
51 pub raw: bool,
53 pub cond: bool,
55}
56
57impl HoleEncoding {
58 pub fn to_spec(&self) -> String {
60 let pos = match self.position {
61 HolePosition::Value => "value",
62 HolePosition::InString => "string",
63 HolePosition::Text => "text",
64 };
65 let mut flags = String::new();
66 if self.raw {
67 flags.push('r');
68 }
69 if self.cond {
70 flags.push('c');
71 }
72 format!(
73 "{}|{}|{}|{}|{}",
74 self.encoding,
75 pos,
76 self.ty.as_deref().unwrap_or(""),
77 self.format.as_deref().unwrap_or(""),
78 flags
79 )
80 }
81
82 pub fn interned(spec: &str) -> &'static HoleEncoding {
85 use std::sync::RwLock;
86 static ENCODINGS: RwLock<Option<HashMap<String, &'static HoleEncoding>>> =
87 RwLock::new(None);
88 if let Some(e) = ENCODINGS
89 .read()
90 .unwrap()
91 .as_ref()
92 .and_then(|m| m.get(spec).copied())
93 {
94 return e;
95 }
96 let mut guard = ENCODINGS.write().unwrap();
97 let map = guard.get_or_insert_with(HashMap::new);
98 if let Some(e) = map.get(spec).copied() {
99 return e;
100 }
101 let leaked: &'static HoleEncoding = Box::leak(Box::new(Self::from_spec(spec)));
102 map.insert(spec.to_string(), leaked);
103 leaked
104 }
105
106 pub fn from_spec(spec: &str) -> Self {
108 let mut parts = spec.splitn(5, '|');
109 let encoding = parts.next().unwrap_or("text").to_string();
110 let position = match parts.next().unwrap_or("text") {
111 "value" => HolePosition::Value,
112 "string" => HolePosition::InString,
113 _ => HolePosition::Text,
114 };
115 let ty = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
116 let format = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
117 let flags = parts.next().unwrap_or("");
118 HoleEncoding {
119 encoding,
120 position,
121 ty,
122 format,
123 raw: flags.contains('r'),
124 cond: flags.contains('c'),
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub enum HoleSource {
132 Wire {
135 index: usize,
137 spec: String,
139 },
140 Child {
143 name: String,
145 spec: String,
147 },
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub enum TileOp {
153 Static(String),
155 Hole(HoleSource),
157 Repeat {
159 stream: String,
161 child: usize,
163 sep: String,
165 body: Vec<TileOp>,
167 #[serde(default)]
171 generators: Vec<(String, usize, String)>,
172 },
173 Branch {
175 cond: HoleSource,
177 then: Vec<TileOp>,
179 otherwise: Vec<TileOp>,
181 },
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188pub struct ChildSpec {
189 pub source: String,
191 pub cascade: Vec<(String, usize, String)>,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct TileSpec {
198 pub name: String,
200 pub encoding: String,
202 pub ops: Vec<TileOp>,
204 pub children: Vec<ChildSpec>,
206}
207
208impl TileSpec {
209 pub fn to_json(&self) -> String {
211 serde_json::to_string(self).expect("TileSpec serializes")
212 }
213}
214
215#[derive(Debug)]
220enum RtOp {
221 Copy(&'static str),
223 Hole(RtSource, HoleEncoding),
225 Repeat {
226 stream: Arc<StreamerValue>,
227 child: usize,
228 sep: &'static str,
229 body: Vec<RtOp>,
230 generators: Vec<(String, usize, String)>,
231 },
232 Branch {
233 cond: RtSource,
234 then: Vec<RtOp>,
235 otherwise: Vec<RtOp>,
236 },
237}
238
239#[derive(Debug)]
241enum RtSource {
242 Wire(usize),
243 Child(String, usize),
246}
247
248fn lower_source(source: &HoleSource) -> (RtSource, HoleEncoding) {
249 match source {
250 HoleSource::Wire { index, spec } => (RtSource::Wire(*index), HoleEncoding::from_spec(spec)),
251 HoleSource::Child { name, spec } => (
252 RtSource::Child(name.clone(), 0),
253 HoleEncoding::from_spec(spec),
254 ),
255 }
256}
257
258fn lower_ops(ops: &[TileOp]) -> Vec<RtOp> {
261 use crate::kernel::StaticInterner;
262 ops.iter()
263 .map(|op| match op {
264 TileOp::Static(s) => RtOp::Copy(StaticInterner::intern(s)),
265 TileOp::Hole(h) => {
266 let (source, enc) = lower_source(h);
267 RtOp::Hole(source, enc)
268 }
269 TileOp::Repeat {
270 stream,
271 child,
272 sep,
273 body,
274 generators,
275 } => RtOp::Repeat {
276 stream: Arc::new(StreamerValue::from_json(stream)),
277 child: *child,
278 sep: StaticInterner::intern(sep),
279 body: lower_ops(body),
280 generators: generators.clone(),
281 },
282 TileOp::Branch {
283 cond,
284 then,
285 otherwise,
286 } => RtOp::Branch {
287 cond: lower_source(cond).0,
288 then: lower_ops(then),
289 otherwise: lower_ops(otherwise),
290 },
291 })
292 .collect()
293}
294
295pub struct TileProgram {
297 pub spec: TileSpec,
299 ops: Vec<RtOp>,
301 pub children: Vec<Arc<PolydatProgram>>,
303 canonicals: Vec<Arc<PolydatKernel>>,
306 compiled: Vec<Option<Arc<dyn KernelProgram>>>,
311 memo: Vec<Option<Arc<[RuntimeTuple]>>>,
315}
316
317impl std::fmt::Debug for TileProgram {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 f.debug_struct("TileProgram")
320 .field("spec", &self.spec)
321 .field("ops", &self.ops)
322 .field("children", &self.children.len())
323 .finish_non_exhaustive()
324 }
325}
326
327fn number_child_holes(ops: &mut [RtOp]) {
331 fn walk(ops: &mut [RtOp], next: &mut usize) {
332 for op in ops.iter_mut() {
333 match op {
334 RtOp::Hole(RtSource::Child(_, k), _) => {
335 *k = *next;
336 *next += 1;
337 }
338 RtOp::Branch {
339 cond,
340 then,
341 otherwise,
342 } => {
343 if let RtSource::Child(_, k) = cond {
344 *k = *next;
345 *next += 1;
346 }
347 walk(then, next);
348 walk(otherwise, next);
349 }
350 RtOp::Repeat { body, .. } => {
351 let mut inner = 0;
352 walk(body, &mut inner);
353 }
354 _ => {}
355 }
356 }
357 }
358 let mut top = 0;
359 walk(ops, &mut top);
360}
361
362fn memoize(
365 ops: &[RtOp],
366 canonicals: &[Arc<PolydatKernel>],
367 memo: &mut [Option<Arc<[RuntimeTuple]>>],
368) {
369 for op in ops {
370 match op {
371 RtOp::Repeat {
372 stream,
373 child,
374 body,
375 generators,
376 ..
377 } => {
378 if generators.is_empty()
379 && !stream.text.contains('{')
380 && let Ok(tuples) = evaluate_for_iteration(
381 &stream.ast,
382 &*canonicals[*child],
383 &HashMap::new(),
384 |_| Ok(()),
385 )
386 {
387 memo[*child] = Some(tuples.into());
388 }
389 memoize(body, canonicals, memo);
390 }
391 RtOp::Branch {
392 then, otherwise, ..
393 } => {
394 memoize(then, canonicals, memo);
395 memoize(otherwise, canonicals, memo);
396 }
397 _ => {}
398 }
399 }
400}
401
402impl TileProgram {
403 pub fn from_json(json: &str) -> Self {
407 let spec: TileSpec = serde_json::from_str(json)
408 .unwrap_or_else(|e| panic!("tile_render: malformed skeleton payload: {e}"));
409 let children: Vec<Arc<PolydatProgram>> = spec
410 .children
411 .iter()
412 .map(|c| {
413 crate::dsl::compile_polydat(&c.source)
414 .unwrap_or_else(|e| {
415 panic!(
416 "tile '{}': projection body failed to compile: {e}\n{}",
417 spec.name, c.source
418 )
419 })
420 .into_program()
421 })
422 .collect();
423 let canonicals: Vec<Arc<PolydatKernel>> = children
424 .iter()
425 .map(|p| Arc::new(PolydatKernel::from_program(p.clone())))
426 .collect();
427 let mut ops = lower_ops(&spec.ops);
428 number_child_holes(&mut ops);
429 let mut memo = vec![None; children.len()];
430 memoize(&ops, &canonicals, &mut memo);
431 let compiled = spec
432 .children
433 .iter()
434 .enumerate()
435 .map(|(i, c)| {
436 match crate::dsl::compile::compile_polydat_with(&c.source, crate::Engine::default())
437 {
438 Ok(kernel) => Some(kernel.into_program()),
439 Err(e) => {
440 crate::library::support::audit::debug(&format!(
441 "tile '{}': projection body {i} renders on the interpreter: {e}",
442 spec.name
443 ));
444 None
445 }
446 }
447 })
448 .collect();
449 TileProgram {
450 spec,
451 ops,
452 children,
453 canonicals,
454 compiled,
455 memo,
456 }
457 }
458
459 fn body_program_on(&self, child: usize, engine: crate::Engine) -> Arc<dyn KernelProgram> {
464 if matches!(engine, crate::Engine::Interpreter(_)) {
465 return self.children[child].clone();
466 }
467 self.compiled[child]
468 .clone()
469 .unwrap_or_else(|| self.children[child].clone())
470 }
471
472 pub fn interned(spec: &str) -> &'static TileProgram {
477 use std::sync::RwLock;
478 static PROGRAMS: RwLock<Option<HashMap<String, usize>>> = RwLock::new(None);
479 let found = PROGRAMS
480 .read()
481 .unwrap()
482 .as_ref()
483 .and_then(|m| m.get(spec).copied());
484 if let Some(p) = found {
485 return unsafe { &*(p as *const TileProgram) };
487 }
488 let built = Box::new(Self::from_json(spec));
494 let mut guard = PROGRAMS.write().unwrap();
495 let map = guard.get_or_insert_with(HashMap::new);
496 if let Some(&p) = map.get(spec) {
497 return unsafe { &*(p as *const TileProgram) };
499 }
500 let leaked: &'static TileProgram = Box::leak(built);
501 map.insert(spec.to_string(), leaked as *const TileProgram as usize);
502 leaked
503 }
504
505 pub fn has_projections(&self) -> bool {
508 fn walk(ops: &[RtOp]) -> bool {
509 ops.iter().any(|op| match op {
510 RtOp::Repeat { .. } => true,
511 RtOp::Branch {
512 then, otherwise, ..
513 } => walk(then) || walk(otherwise),
514 _ => false,
515 })
516 }
517 walk(&self.ops)
518 }
519
520 pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
524 let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
525 let mut out = String::new();
526 self.render_into(
527 &refs,
528 crate::Engine::Interpreter(crate::JitMode::Auto),
529 bodies,
530 &mut out,
531 );
532 out
533 }
534
535 pub fn render_into<W: std::fmt::Write>(
542 &self,
543 inputs: &[ValueRef<'_>],
544 engine: crate::Engine,
545 bodies: &mut BodyKernels,
546 out: &mut W,
547 ) {
548 self.render_ops(&self.ops, inputs, engine, bodies, None, out);
549 }
550
551 fn render_ops<W: std::fmt::Write>(
552 &self,
553 ops: &[RtOp],
554 inputs: &[ValueRef<'_>],
555 engine: crate::Engine,
556 bodies: &mut BodyKernels,
557 mut child: Option<&mut BodyEntry>,
558 out: &mut W,
559 ) {
560 for op in ops {
561 match op {
562 RtOp::Copy(s) => out.put(s),
565 RtOp::Hole(source, enc) => match source {
566 RtSource::Wire(i) => {
567 encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
568 }
569 RtSource::Child(name, k) => {
570 if let Some(entry) = child.as_deref_mut()
571 && let Some(i) = entry.hole(*k, name)
572 {
573 let v = entry.kernel.pull_at(i);
574 encode_ref(ValueRef::from(&v), enc, out)
575 }
576 }
577 },
578 RtOp::Branch {
579 cond,
580 then,
581 otherwise,
582 } => {
583 let c = self.truthy(cond, inputs, child.as_deref_mut());
584 let branch = if c { then } else { otherwise };
585 self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
586 }
587 RtOp::Repeat {
588 stream,
589 child: child_idx,
590 sep,
591 body,
592 generators,
593 } => {
594 let memoized = self.memo[*child_idx].clone();
603 let tuples: std::borrow::Cow<'_, [RuntimeTuple]> = match &memoized {
604 Some(t) => std::borrow::Cow::Borrowed(&t[..]),
605 None => {
606 let mut streamer = (**stream).clone();
607 if !generators.is_empty() {
608 streamer.ast = bind_generators(&streamer.ast, generators, inputs);
609 }
610 std::borrow::Cow::Owned(
611 evaluate_for_iteration(
612 &streamer.ast,
613 &*self.canonicals[*child_idx],
614 &HashMap::new(),
615 |_| Ok(()),
616 )
617 .unwrap_or_else(|e| {
618 panic!(
619 "tile '{}': projection `for {}` failed at render: {e}",
620 self.spec.name, streamer.text
621 )
622 }),
623 )
624 }
625 };
626 let child_spec = &self.spec.children[*child_idx];
627 let engine = match engine {
633 crate::Engine::Interpreter(_) => engine,
634 _ => crate::Engine::default(),
635 };
636 let program = self.body_program_on(*child_idx, engine);
637 let mut first = true;
638 let fail = |name: &str, e: String| -> ! {
639 panic!(
640 "tile '{}': projection body input `{name}`: {e}",
641 self.spec.name
642 )
643 };
644 bodies.with(&program, engine, |entry, bodies| {
645 for (index, tuple) in tuples.iter().enumerate() {
646 if !first {
647 out.put(sep);
648 }
649 first = false;
650 {
651 let BodyEntry {
654 kernel,
655 elements,
656 cascade,
657 ..
658 } = &mut *entry;
659 kernel.set_inputs(&[index as u64]);
660 let elements = elements.get_or_insert_with(|| {
661 tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
662 });
663 for (k, (name, v)) in tuple.iter().enumerate() {
664 if let Some(i) = elements.get(k).copied().flatten() {
665 kernel
666 .set_input_at(i, v.clone())
667 .unwrap_or_else(|e| fail(name, e));
668 }
669 }
670 let cascade = cascade.get_or_insert_with(|| {
671 child_spec
672 .cascade
673 .iter()
674 .map(|(n, _, _)| kernel.input_index(n))
675 .collect()
676 });
677 for (k, (name, input_idx, ty)) in
678 child_spec.cascade.iter().enumerate()
679 {
680 if let Some(i) = cascade.get(k).copied().flatten()
681 && let Some(v) = inputs.get(*input_idx)
682 {
683 kernel
684 .set_input_at(i, typed_for(&owned(*v), ty))
685 .unwrap_or_else(|e| fail(name, e));
686 }
687 }
688 }
689 self.render_ops(body, inputs, engine, bodies, Some(entry), out);
690 }
691 });
692 }
693 }
694 }
695 }
696
697 fn truthy(
699 &self,
700 source: &RtSource,
701 inputs: &[ValueRef<'_>],
702 child: Option<&mut BodyEntry>,
703 ) -> bool {
704 match source {
705 RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
706 RtSource::Child(name, k) => match child {
707 Some(entry) => match entry.hole(*k, name) {
708 Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
709 None => false,
710 },
711 None => false,
712 },
713 }
714 }
715}
716
717fn owned(v: ValueRef<'_>) -> Value {
720 match v {
721 ValueRef::U64(n) => Value::U64(n),
722 ValueRef::I64(n) => Value::I64(n),
723 ValueRef::F64(f) => Value::F64(f),
724 ValueRef::Bool(b) => Value::Bool(b),
725 ValueRef::Str(s) => Value::Str(Arc::from(s)),
726 ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
727 ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
728 ValueRef::None => Value::None,
729 ValueRef::Other(v) => v.clone(),
730 }
731}
732
733fn typed_for(v: &Value, ty: &str) -> Value {
737 match (v, PortType::from_keyword(ty)) {
738 (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
739 _ => v.clone(),
740 }
741}
742
743fn bind_generators(
748 c: &crate::iteration::comprehension::Comprehension,
749 generators: &[(String, usize, String)],
750 inputs: &[ValueRef<'_>],
751) -> crate::iteration::comprehension::Comprehension {
752 use crate::iteration::comprehension::Comprehension as K;
753 use crate::iteration::comprehension::source::{LiteralValue, Source};
754 match c {
755 K::Clause {
756 name,
757 source: Source::Generator { .. },
758 } => {
759 let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
760 return c.clone();
761 };
762 let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
763 let items: Vec<Value> =
764 match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
765 Some(interior) => interior,
766 None => match &raw {
767 Value::Str(text) => {
768 match serde_json::from_str::<serde_json::Value>(text.trim()) {
769 Ok(serde_json::Value::Array(items)) => items
770 .iter()
771 .map(|j| {
772 retype(
773 &Value::Str(j.to_string().trim_matches('"').into()),
774 ty,
775 )
776 })
777 .collect(),
778 _ => vec![typed_for(&raw, ty)],
779 }
780 }
781 _ => vec![raw.clone()],
782 },
783 };
784 let json_items = ty == "json";
788 let values = items
789 .iter()
790 .map(|v| {
791 if json_items {
792 return LiteralValue::Json(json_of(v));
793 }
794 match v {
795 Value::U64(n) => LiteralValue::Int(*n as i64),
796 Value::I64(n) => LiteralValue::Int(*n),
797 Value::F64(f) => LiteralValue::Float(*f),
798 Value::Bool(b) => LiteralValue::Bool(*b),
799 Value::Json(j) => match j.as_ref() {
801 serde_json::Value::Number(n) if n.is_u64() => {
802 LiteralValue::Int(n.as_u64().unwrap_or(0) as i64)
803 }
804 serde_json::Value::Number(n) if n.is_i64() => {
805 LiteralValue::Int(n.as_i64().unwrap_or(0))
806 }
807 serde_json::Value::Number(n) => {
808 LiteralValue::Float(n.as_f64().unwrap_or(0.0))
809 }
810 serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
811 serde_json::Value::String(s) => LiteralValue::String(s.clone()),
812 other => LiteralValue::String(other.to_string()),
813 },
814 other => LiteralValue::String(other.to_display_string()),
815 }
816 })
817 .collect();
818 K::Clause {
819 name: name.clone(),
820 source: Source::Literal { values },
821 }
822 }
823 K::Clause { .. } => c.clone(),
824 K::Cartesian { children } => K::Cartesian {
825 children: children
826 .iter()
827 .map(|ch| bind_generators(ch, generators, inputs))
828 .collect(),
829 },
830 K::Zip { children, mode } => K::Zip {
831 children: children
832 .iter()
833 .map(|ch| bind_generators(ch, generators, inputs))
834 .collect(),
835 mode: *mode,
836 },
837 K::Union { children } => K::Union {
838 children: children
839 .iter()
840 .map(|ch| bind_generators(ch, generators, inputs))
841 .collect(),
842 },
843 K::Filter { child, predicate } => K::Filter {
844 child: Box::new(bind_generators(child, generators, inputs)),
845 predicate: predicate.clone(),
846 },
847 K::Order {
848 child,
849 strategy,
850 truncation,
851 } => K::Order {
852 child: Box::new(bind_generators(child, generators, inputs)),
853 strategy: *strategy,
854 truncation: *truncation,
855 },
856 }
857}
858
859fn json_of(v: &Value) -> serde_json::Value {
864 match v {
865 Value::Json(j) => j.as_ref().clone(),
866 Value::U64(n) => serde_json::Value::from(*n),
867 Value::I64(n) => serde_json::Value::from(*n),
868 Value::F64(f) => serde_json::Number::from_f64(*f)
869 .map(serde_json::Value::Number)
870 .unwrap_or(serde_json::Value::Null),
871 Value::Bool(b) => serde_json::Value::Bool(*b),
872 Value::Str(s) => serde_json::Value::String(s.to_string()),
873 Value::None => serde_json::Value::Null,
874 other => serde_json::Value::String(other.to_display_string()),
875 }
876}
877
878fn retype(v: &Value, ty: &str) -> Value {
879 let text = v.to_display_string();
880 match PortType::from_keyword(ty) {
881 Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
882 Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
883 Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
884 Some(PortType::Str) | None => Value::Str(text.into()),
885 Some(_) => v.clone(),
886 }
887}
888
889struct BodyEntry {
893 program: Arc<dyn KernelProgram>,
894 kernel: Box<dyn Kernel>,
895 elements: Option<Vec<Option<usize>>>,
898 cascade: Option<Vec<Option<usize>>>,
900 holes: Vec<Option<Option<usize>>>,
902}
903
904impl BodyEntry {
905 fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
907 if self.holes.len() <= k {
908 self.holes.resize(k + 1, None);
909 }
910 if self.holes[k].is_none() {
911 self.holes[k] = Some(self.kernel.output_index(name));
912 }
913 self.holes[k].flatten()
914 }
915}
916
917#[derive(Default)]
925pub struct BodyKernels {
926 entries: HashMap<(usize, crate::Engine), BodyEntry>,
927 created: u64,
929}
930
931impl Clone for BodyKernels {
932 fn clone(&self) -> Self {
933 Self::default()
934 }
935}
936
937unsafe impl Sync for BodyKernels {}
944
945impl std::fmt::Debug for BodyKernels {
946 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947 f.debug_struct("BodyKernels")
948 .field("entries", &self.entries.len())
949 .field("created", &self.created)
950 .finish()
951 }
952}
953
954fn body_engine_key(engine: crate::Engine) -> crate::Engine {
957 match engine {
958 crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
959 other => other,
960 }
961}
962
963impl BodyKernels {
964 pub fn created(&self) -> u64 {
966 self.created
967 }
968
969 #[cfg(test)]
971 fn clone_for_test(&self) -> (u64, BodyKernels) {
972 (self.created, self.clone())
973 }
974
975 fn with(
979 &mut self,
980 program: &Arc<dyn KernelProgram>,
981 engine: crate::Engine,
982 f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
983 ) {
984 let engine = body_engine_key(engine);
985 let key = (Arc::as_ptr(program) as *const () as usize, engine);
988 let mut entry = self
989 .entries
990 .remove(&key)
991 .filter(|e| Arc::ptr_eq(&e.program, program))
992 .unwrap_or_else(|| {
993 self.created += 1;
994 BodyEntry {
995 program: program.clone(),
996 kernel: program.clone().create_kernel(),
997 elements: None,
998 cascade: None,
999 holes: Vec::new(),
1000 }
1001 });
1002 f(&mut entry, self);
1003 self.entries.insert(key, entry);
1004 }
1005}
1006
1007pub(crate) mod render_state {
1009 use super::{BodyKernels, TileRender};
1010 use crate::ast::{ScratchBuf, ScratchElem, Value};
1011
1012 pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
1013 vec![ScratchElem::Kernels]
1014 }
1015
1016 pub(crate) fn eval(
1017 node: &TileRender,
1018 scratch: &mut [ScratchBuf],
1019 inputs: &[Value],
1020 outputs: &mut [Value],
1021 ) {
1022 let bodies = bodies_of(&mut scratch[0]);
1023 outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1024 }
1025
1026 pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1028 match entry {
1029 ScratchBuf::Kernels(b) => b,
1030 other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1031 }
1032 }
1033}
1034
1035pub(crate) trait Sink: std::fmt::Write {
1039 fn put(&mut self, s: &str) {
1040 let _ = self.write_str(s);
1041 }
1042 fn put_char(&mut self, c: char) {
1043 let _ = self.write_char(c);
1044 }
1045}
1046
1047impl<W: std::fmt::Write> Sink for W {}
1048
1049pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1051 encode_ref(ValueRef::from(value), enc, out)
1052}
1053
1054pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1058 if enc.cond {
1059 out.put_char(if truthy_of(value) { '1' } else { '0' });
1060 return;
1061 }
1062 let ty = enc.ty.as_deref();
1063 if is_numeric_keyword(ty.unwrap_or("u64")) {
1072 match (enc.format.as_deref(), value) {
1073 (None, ValueRef::U64(n)) => {
1074 put_u64(n, out);
1075 return;
1076 }
1077 (None, ValueRef::I64(n)) => {
1078 if n < 0 {
1079 out.put_char('-');
1080 }
1081 put_u64(n.unsigned_abs(), out);
1082 return;
1083 }
1084 (None, ValueRef::F64(f)) => {
1085 let _ = float_text::write_shortest(f, out);
1086 return;
1087 }
1088 (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1089 if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1090 let _ = float_text::write_fixed(f, prec, out);
1091 return;
1092 }
1093 }
1094 _ => {}
1095 }
1096 }
1097 let text = formatted_text(value, ty, enc.format.as_deref());
1098 if enc.raw {
1099 out.put(&text);
1100 return;
1101 }
1102 match (enc.encoding.as_str(), enc.position) {
1103 ("json", HolePosition::InString) => push_json_escaped(&text, out),
1104 ("json", HolePosition::Value) => {
1105 let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1106 match (kind, value) {
1107 (_, ValueRef::None) => out.put("null"),
1108 ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1109 ("json", ValueRef::Json(j)) => {
1110 let _ = write!(out, "{j}");
1111 }
1112 ("str", _) | ("String", _) | ("string", _) => {
1113 out.put_char('"');
1114 push_json_escaped(&text, out);
1115 out.put_char('"');
1116 }
1117 (k, _) if is_numeric_keyword(k) => out.put(&text),
1118 (_, ValueRef::Json(j)) => {
1119 let _ = write!(out, "{j}");
1120 }
1121 (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1122 (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1123 _ => {
1124 out.put_char('"');
1125 push_json_escaped(&text, out);
1126 out.put_char('"');
1127 }
1128 }
1129 }
1130 ("csv", _) => {
1131 if text.contains([',', '"', '\n']) {
1132 out.put_char('"');
1133 for (i, piece) in text.split('"').enumerate() {
1134 if i > 0 {
1135 out.put("\"\"");
1136 }
1137 out.put(piece);
1138 }
1139 out.put_char('"');
1140 } else {
1141 out.put(&text);
1142 }
1143 }
1144 _ => out.put(&text),
1145 }
1146}
1147
1148fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1150 if n == 0 {
1151 out.put_char('0');
1152 return;
1153 }
1154 let mut buf = [0u8; 20];
1155 let mut i = buf.len();
1156 while n > 0 {
1157 i -= 1;
1158 buf[i] = b'0' + (n % 10) as u8;
1159 n /= 10;
1160 }
1161 out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1163}
1164
1165fn truthy_of(v: ValueRef<'_>) -> bool {
1166 match v {
1167 ValueRef::Bool(b) => b,
1168 ValueRef::U64(n) => n != 0,
1169 ValueRef::F64(f) => f != 0.0,
1170 ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1171 ValueRef::None => false,
1172 _ => true,
1173 }
1174}
1175
1176fn is_numeric_keyword(k: &str) -> bool {
1177 matches!(
1178 k,
1179 "u64"
1180 | "i64"
1181 | "f64"
1182 | "f32"
1183 | "u32"
1184 | "i32"
1185 | "u16"
1186 | "i16"
1187 | "u8"
1188 | "i8"
1189 | "u128"
1190 | "i128"
1191 | "f16"
1192 )
1193}
1194
1195fn formatted_text<'a>(
1199 value: ValueRef<'a>,
1200 ty: Option<&str>,
1201 format: Option<&str>,
1202) -> std::borrow::Cow<'a, str> {
1203 use std::borrow::Cow;
1204 let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1208 match (ty, value) {
1209 (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1210 (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1213 (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1214 (_, v) => v.display(),
1215 }
1216 };
1217 let Some(fmt) = format else {
1218 return base(value);
1219 };
1220 let fmt = fmt.trim();
1221 if let Some(prec) = precision_of(fmt) {
1222 if let Some(f) = as_f64(value) {
1223 return Cow::Owned(float_text::fixed_string(f, prec));
1224 }
1225 return base(value);
1226 }
1227 if fmt == "x" || fmt == "X" {
1228 if let ValueRef::U64(n) = value {
1229 return Cow::Owned(if fmt == "x" {
1230 format!("{n:x}")
1231 } else {
1232 format!("{n:X}")
1233 });
1234 }
1235 return base(value);
1236 }
1237 let base = base(value);
1238 if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1239 return Cow::Owned(format!("{base:0>w$}"));
1240 }
1241 if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1242 return Cow::Owned(format!("{base:>w$}"));
1243 }
1244 if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1245 return Cow::Owned(format!("{base:<w$}"));
1246 }
1247 if let Ok(w) = fmt.parse::<usize>() {
1248 return Cow::Owned(format!("{base:>w$}"));
1249 }
1250 base
1251}
1252
1253fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1254 match v {
1255 ValueRef::F64(f) => Some(f),
1256 ValueRef::U64(n) => Some(n as f64),
1257 _ => None,
1258 }
1259}
1260
1261fn precision_of(fmt: &str) -> Option<usize> {
1263 fmt.trim()
1264 .strip_prefix('.')
1265 .and_then(|p| p.parse::<usize>().ok())
1266}
1267
1268fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1269 for c in s.chars() {
1270 match c {
1271 '"' => out.put("\\\""),
1272 '\\' => out.put("\\\\"),
1273 '\n' => out.put("\\n"),
1274 '\r' => out.put("\\r"),
1275 '\t' => out.put("\\t"),
1276 c if (c as u32) < 0x20 => {
1277 let _ = write!(out, "\\u{:04x}", c as u32);
1278 }
1279 c => out.put_char(c),
1280 }
1281 }
1282}
1283
1284#[crate::polydat_node(category = Formatting)]
1287fn tile_encode(
1288 value: Value,
1289 spec: Const<&str>,
1290 #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1291) -> String {
1292 let mut out = String::new();
1293 encode(&value, enc, &mut out);
1294 out
1295}
1296
1297fn tile_render_compiled(node: &TileRender, wire_types: &[PortType]) -> crate::ast::CompiledSlotKit {
1304 let program: &'static TileProgram = TileProgram::interned(&node.spec);
1305 let mut reads: Vec<(usize, PortType)> = Vec::with_capacity(wire_types.len());
1308 let mut offset = 0usize;
1309 for &ty in wire_types {
1310 reads.push((offset, ty));
1311 offset += ty.slot_width().max(1);
1312 }
1313 crate::ast::CompiledSlotKit {
1314 scratch: vec![
1315 crate::ast::ScratchElem::Str,
1316 crate::ast::ScratchElem::Kernels,
1317 ],
1318 op: Box::new(
1319 move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [crate::ast::ScratchBuf]| {
1320 let owned_values: Vec<Value> = reads
1324 .iter()
1325 .filter(|(_, ty)| ty.slot_color() == crate::ast::SlotColor::Imm2)
1326 .map(|&(offset, ty)| crate::compile::marshal::decode_output(inputs, offset, ty))
1327 .collect();
1328 let mut next_owned = 0usize;
1329 let refs: Vec<ValueRef<'_>> = reads
1330 .iter()
1331 .map(|&(offset, ty)| {
1332 if ty.slot_color() == crate::ast::SlotColor::Imm2 {
1333 let v = ValueRef::from(&owned_values[next_owned]);
1334 next_owned += 1;
1335 v
1336 } else {
1337 unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1340 }
1341 })
1342 .collect();
1343 let (text, bodies) = scratch.split_at_mut(1);
1349 let crate::ast::ScratchBuf::Str(buf) = &mut text[0] else {
1350 unreachable!("the render step owns a string entry");
1351 };
1352 let bodies = render_state::bodies_of(&mut bodies[0]);
1353 buf.clear();
1354 let mut w = BytesSink(buf);
1355 program.render_into(&refs, crate::Engine::default(), bodies, &mut w);
1356 let (p, l) = scratch[0].ptr_len();
1357 outputs[0] = p;
1358 outputs[1] = l;
1359 },
1360 ),
1361 }
1362}
1363
1364pub(crate) struct BytesSink<'a>(pub(crate) &'a mut Vec<u8>);
1367
1368impl std::fmt::Write for BytesSink<'_> {
1369 fn write_str(&mut self, s: &str) -> std::fmt::Result {
1370 self.0.extend_from_slice(s.as_bytes());
1371 Ok(())
1372 }
1373}
1374
1375#[crate::polydat_node(
1378 category = Formatting,
1379 variadic_min = 0,
1380 compiled_slot = tile_render_compiled,
1381 state = render_state
1382)]
1383fn tile_render(
1384 spec: Const<&str>,
1385 #[poly_const(TileProgram::from_json, from = spec)] program: &TileProgram,
1386 values: &[Value],
1387) -> String {
1388 program.render(values, &mut BodyKernels::default())
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395 use super::*;
1396
1397 #[test]
1401 fn body_kernels_are_created_once_per_state_and_reused() {
1402 let src =
1403 "input cycle: u64\ntile t : text := \"@for k in 0..3 sep \\\",\\\" {${k + cycle}}\"\n";
1404 let mut k = crate::dsl::compile_polydat(src).unwrap();
1405 let program = k.program();
1406 let node = (0..program.node_count())
1407 .find(|&i| program.node_meta(i).name == "tile_render")
1408 .expect("the tile's render node");
1409 let bodies_of = |k: &mut PolydatKernel| match &k.state().core.node_scratch[node][0] {
1410 crate::ast::ScratchBuf::Kernels(b) => b.clone_for_test(),
1411 other => panic!("{other:?}"),
1412 };
1413 assert_eq!(bodies_of(&mut k).0, 0, "nothing before the first render");
1414 k.set_inputs(&[10]);
1415 assert_eq!(k.pull("t").as_str(), "10,11,12");
1416 assert_eq!(bodies_of(&mut k).0, 1, "one kernel for the body");
1417 for c in 0..5u64 {
1418 k.set_inputs(&[c]);
1419 let _ = k.pull("t");
1420 }
1421 let (created, clone) = bodies_of(&mut k);
1422 assert_eq!(created, 1, "reused across renders");
1423 assert_eq!(clone.created(), 0, "a clone is a new state's empty set");
1424 }
1425
1426 fn enc(
1427 encoding: &str,
1428 position: HolePosition,
1429 ty: Option<&str>,
1430 format: Option<&str>,
1431 raw: bool,
1432 ) -> HoleEncoding {
1433 HoleEncoding {
1434 encoding: encoding.into(),
1435 position,
1436 ty: ty.map(str::to_string),
1437 format: format.map(str::to_string),
1438 raw,
1439 cond: false,
1440 }
1441 }
1442
1443 #[test]
1444 fn json_value_and_string_positions_encode_by_type() {
1445 let mut out = String::new();
1446 encode(
1447 &Value::Str("a\"b".into()),
1448 &enc("json", HolePosition::Value, Some("str"), None, false),
1449 &mut out,
1450 );
1451 assert_eq!(out, "\"a\\\"b\"");
1452 out.clear();
1453 encode(
1454 &Value::U64(7),
1455 &enc("json", HolePosition::Value, None, None, false),
1456 &mut out,
1457 );
1458 assert_eq!(out, "7");
1459 out.clear();
1460 encode(
1461 &Value::Str("x\ny".into()),
1462 &enc("json", HolePosition::InString, None, None, false),
1463 &mut out,
1464 );
1465 assert_eq!(out, "x\\ny");
1466 out.clear();
1467 encode(
1468 &Value::F64(2.0 / 3.0),
1469 &enc("json", HolePosition::Value, None, Some(".2"), false),
1470 &mut out,
1471 );
1472 assert_eq!(out, "0.67");
1473 out.clear();
1474 encode(
1475 &Value::None,
1476 &enc("json", HolePosition::Value, None, None, false),
1477 &mut out,
1478 );
1479 assert_eq!(out, "null");
1480 }
1481
1482 #[test]
1483 fn spec_round_trips() {
1484 let e = enc(
1485 "json",
1486 HolePosition::InString,
1487 Some("u64"),
1488 Some(".2"),
1489 true,
1490 );
1491 assert_eq!(HoleEncoding::from_spec(&e.to_spec()), e);
1492 let c = HoleEncoding {
1493 cond: true,
1494 ..enc("text", HolePosition::Text, None, None, false)
1495 };
1496 assert_eq!(HoleEncoding::from_spec(&c.to_spec()), c);
1497 }
1498
1499 #[test]
1500 fn csv_quotes_when_needed_and_raw_skips_escaping() {
1501 let mut out = String::new();
1502 encode(
1503 &Value::Str("a,b".into()),
1504 &enc("csv", HolePosition::Text, None, None, false),
1505 &mut out,
1506 );
1507 assert_eq!(out, "\"a,b\"");
1508 out.clear();
1509 encode(
1510 &Value::Str("a\"b".into()),
1511 &enc("json", HolePosition::Value, None, None, true),
1512 &mut out,
1513 );
1514 assert_eq!(out, "a\"b");
1515 }
1516
1517 #[test]
1518 fn formats_apply_before_encoding() {
1519 assert_eq!(formatted_text(ValueRef::U64(5), None, Some("03")), "005");
1520 assert_eq!(formatted_text(ValueRef::U64(255), None, Some("x")), "ff");
1521 assert_eq!(
1522 formatted_text(ValueRef::Str("ab"), None, Some(">4")),
1523 " ab"
1524 );
1525 assert_eq!(
1526 formatted_text(ValueRef::F64(0.295), None, Some(".2")),
1527 "0.29"
1528 );
1529 assert_eq!(
1530 formatted_text(ValueRef::U64(7), None, Some(" .3 ")),
1531 "7.000"
1532 );
1533 }
1534
1535 #[test]
1538 fn float_holes_write_rust_text() {
1539 let cases: [(f64, Option<&str>, &str); 8] = [
1540 (100.0, None, "100.0"),
1541 (0.1, None, "0.1"),
1542 (5e-5, None, "5e-5"),
1543 (1e16, None, "1e16"),
1544 (-0.0, None, "-0.0"),
1545 (2.0 / 3.0, Some(".2"), "0.67"),
1546 (0.295, Some(".2"), "0.29"),
1547 (2.5, Some(".0"), "2"),
1548 ];
1549 for (f, fmt, want) in cases {
1550 for (encoding, position) in [
1551 ("text", HolePosition::Text),
1552 ("json", HolePosition::Value),
1553 ("json", HolePosition::InString),
1554 ("csv", HolePosition::Text),
1555 ] {
1556 for ty in [None, Some("f64")] {
1557 let mut out = String::new();
1558 encode(
1559 &Value::F64(f),
1560 &enc(encoding, position, ty, fmt, false),
1561 &mut out,
1562 );
1563 assert_eq!(out, want, "{f:?} {fmt:?} {encoding} {position:?} {ty:?}");
1564 }
1565 }
1566 let mut out = String::new();
1569 encode(
1570 &Value::F64(f),
1571 &enc("json", HolePosition::Value, Some("str"), fmt, false),
1572 &mut out,
1573 );
1574 assert_eq!(out, format!("\"{want}\""));
1575 }
1576 }
1577}