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 {
507 fn walk(ops: &[RtOp]) -> bool {
508 ops.iter().any(|op| match op {
509 RtOp::Repeat { .. } => true,
510 RtOp::Branch {
511 then, otherwise, ..
512 } => walk(then) || walk(otherwise),
513 _ => false,
514 })
515 }
516 walk(&self.ops)
517 }
518
519 pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
523 let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
524 let mut out = String::new();
525 self.render_into(
526 &refs,
527 crate::Engine::Interpreter(crate::JitMode::Auto),
528 bodies,
529 &mut out,
530 );
531 out
532 }
533
534 pub fn render_into<W: std::fmt::Write>(
541 &self,
542 inputs: &[ValueRef<'_>],
543 engine: crate::Engine,
544 bodies: &mut BodyKernels,
545 out: &mut W,
546 ) {
547 self.render_ops(&self.ops, inputs, engine, bodies, None, out);
548 }
549
550 fn render_ops<W: std::fmt::Write>(
551 &self,
552 ops: &[RtOp],
553 inputs: &[ValueRef<'_>],
554 engine: crate::Engine,
555 bodies: &mut BodyKernels,
556 mut child: Option<&mut BodyEntry>,
557 out: &mut W,
558 ) {
559 for op in ops {
560 match op {
561 RtOp::Copy(s) => out.put(s),
564 RtOp::Hole(source, enc) => match source {
565 RtSource::Wire(i) => {
566 encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
567 }
568 RtSource::Child(name, k) => {
569 if let Some(entry) = child.as_deref_mut()
570 && let Some(i) = entry.hole(*k, name)
571 {
572 let v = entry.kernel.pull_at(i);
573 encode_ref(ValueRef::from(&v), enc, out)
574 }
575 }
576 },
577 RtOp::Branch {
578 cond,
579 then,
580 otherwise,
581 } => {
582 let c = self.truthy(cond, inputs, child.as_deref_mut());
583 let branch = if c { then } else { otherwise };
584 self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
585 }
586 RtOp::Repeat {
587 stream,
588 child: child_idx,
589 sep,
590 body,
591 generators,
592 } => {
593 let memoized = self.memo[*child_idx].clone();
602 let tuples: std::borrow::Cow<'_, [RuntimeTuple]> = match &memoized {
603 Some(t) => std::borrow::Cow::Borrowed(&t[..]),
604 None => {
605 let mut streamer = (**stream).clone();
606 if !generators.is_empty() {
607 streamer.ast = bind_generators(&streamer.ast, generators, inputs);
608 }
609 std::borrow::Cow::Owned(
610 evaluate_for_iteration(
611 &streamer.ast,
612 &*self.canonicals[*child_idx],
613 &HashMap::new(),
614 |_| Ok(()),
615 )
616 .unwrap_or_else(|e| {
617 panic!(
618 "tile '{}': projection `for {}` failed at render: {e}",
619 self.spec.name, streamer.text
620 )
621 }),
622 )
623 }
624 };
625 let child_spec = &self.spec.children[*child_idx];
626 let engine = match engine {
632 crate::Engine::Interpreter(_) => engine,
633 _ => crate::Engine::default(),
634 };
635 let program = self.body_program_on(*child_idx, engine);
636 let mut first = true;
637 let fail = |name: &str, e: String| -> ! {
638 panic!(
639 "tile '{}': projection body input `{name}`: {e}",
640 self.spec.name
641 )
642 };
643 bodies.with(&program, engine, |entry, bodies| {
644 for (index, tuple) in tuples.iter().enumerate() {
645 if !first {
646 out.put(sep);
647 }
648 first = false;
649 {
650 let BodyEntry {
653 kernel,
654 elements,
655 cascade,
656 ..
657 } = &mut *entry;
658 kernel.set_inputs(&[index as u64]);
659 let elements = elements.get_or_insert_with(|| {
660 tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
661 });
662 for (k, (name, v)) in tuple.iter().enumerate() {
663 if let Some(i) = elements.get(k).copied().flatten() {
664 kernel
665 .set_input_at(i, v.clone())
666 .unwrap_or_else(|e| fail(name, e));
667 }
668 }
669 let cascade = cascade.get_or_insert_with(|| {
670 child_spec
671 .cascade
672 .iter()
673 .map(|(n, _, _)| kernel.input_index(n))
674 .collect()
675 });
676 for (k, (name, input_idx, ty)) in
677 child_spec.cascade.iter().enumerate()
678 {
679 if let Some(i) = cascade.get(k).copied().flatten()
680 && let Some(v) = inputs.get(*input_idx)
681 {
682 kernel
683 .set_input_at(i, typed_for(&owned(*v), ty))
684 .unwrap_or_else(|e| fail(name, e));
685 }
686 }
687 }
688 self.render_ops(body, inputs, engine, bodies, Some(entry), out);
689 }
690 });
691 }
692 }
693 }
694 }
695
696 fn truthy(
698 &self,
699 source: &RtSource,
700 inputs: &[ValueRef<'_>],
701 child: Option<&mut BodyEntry>,
702 ) -> bool {
703 match source {
704 RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
705 RtSource::Child(name, k) => match child {
706 Some(entry) => match entry.hole(*k, name) {
707 Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
708 None => false,
709 },
710 None => false,
711 },
712 }
713 }
714}
715
716fn owned(v: ValueRef<'_>) -> Value {
719 match v {
720 ValueRef::U64(n) => Value::U64(n),
721 ValueRef::I64(n) => Value::I64(n),
722 ValueRef::F64(f) => Value::F64(f),
723 ValueRef::Bool(b) => Value::Bool(b),
724 ValueRef::Str(s) => Value::Str(Arc::from(s)),
725 ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
726 ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
727 ValueRef::None => Value::None,
728 ValueRef::Other(v) => v.clone(),
729 }
730}
731
732fn typed_for(v: &Value, ty: &str) -> Value {
736 match (v, PortType::from_keyword(ty)) {
737 (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
738 _ => v.clone(),
739 }
740}
741
742fn bind_generators(
747 c: &crate::iteration::comprehension::Comprehension,
748 generators: &[(String, usize, String)],
749 inputs: &[ValueRef<'_>],
750) -> crate::iteration::comprehension::Comprehension {
751 use crate::iteration::comprehension::Comprehension as K;
752 use crate::iteration::comprehension::source::{LiteralValue, Source};
753 match c {
754 K::Clause {
755 name,
756 source: Source::Generator { .. },
757 } => {
758 let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
759 return c.clone();
760 };
761 let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
762 let items: Vec<Value> =
763 match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
764 Some(interior) => interior,
765 None => match &raw {
766 Value::Str(text) => {
767 match serde_json::from_str::<serde_json::Value>(text.trim()) {
768 Ok(serde_json::Value::Array(items)) => items
769 .iter()
770 .map(|j| {
771 retype(
772 &Value::Str(j.to_string().trim_matches('"').into()),
773 ty,
774 )
775 })
776 .collect(),
777 _ => vec![typed_for(&raw, ty)],
778 }
779 }
780 _ => vec![raw.clone()],
781 },
782 };
783 let json_items = ty == "json";
787 let values = items
788 .iter()
789 .map(|v| {
790 if json_items {
791 return LiteralValue::Json(json_of(v));
792 }
793 match v {
794 Value::U64(n) => LiteralValue::Int(*n as i64),
795 Value::I64(n) => LiteralValue::Int(*n),
796 Value::F64(f) => LiteralValue::Float(*f),
797 Value::Bool(b) => LiteralValue::Bool(*b),
798 Value::Json(j) => match j.as_ref() {
800 serde_json::Value::Number(n) if n.is_u64() => {
801 LiteralValue::Int(n.as_u64().unwrap_or(0) as i64)
802 }
803 serde_json::Value::Number(n) if n.is_i64() => {
804 LiteralValue::Int(n.as_i64().unwrap_or(0))
805 }
806 serde_json::Value::Number(n) => {
807 LiteralValue::Float(n.as_f64().unwrap_or(0.0))
808 }
809 serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
810 serde_json::Value::String(s) => LiteralValue::String(s.clone()),
811 other => LiteralValue::String(other.to_string()),
812 },
813 other => LiteralValue::String(other.to_display_string()),
814 }
815 })
816 .collect();
817 K::Clause {
818 name: name.clone(),
819 source: Source::Literal { values },
820 }
821 }
822 K::Clause { .. } => c.clone(),
823 K::Cartesian { children } => K::Cartesian {
824 children: children
825 .iter()
826 .map(|ch| bind_generators(ch, generators, inputs))
827 .collect(),
828 },
829 K::Zip { children, mode } => K::Zip {
830 children: children
831 .iter()
832 .map(|ch| bind_generators(ch, generators, inputs))
833 .collect(),
834 mode: *mode,
835 },
836 K::Union { children } => K::Union {
837 children: children
838 .iter()
839 .map(|ch| bind_generators(ch, generators, inputs))
840 .collect(),
841 },
842 K::Filter { child, predicate } => K::Filter {
843 child: Box::new(bind_generators(child, generators, inputs)),
844 predicate: predicate.clone(),
845 },
846 K::Order {
847 child,
848 strategy,
849 truncation,
850 } => K::Order {
851 child: Box::new(bind_generators(child, generators, inputs)),
852 strategy: *strategy,
853 truncation: *truncation,
854 },
855 }
856}
857
858fn json_of(v: &Value) -> serde_json::Value {
863 match v {
864 Value::Json(j) => j.as_ref().clone(),
865 Value::U64(n) => serde_json::Value::from(*n),
866 Value::I64(n) => serde_json::Value::from(*n),
867 Value::F64(f) => serde_json::Number::from_f64(*f)
868 .map(serde_json::Value::Number)
869 .unwrap_or(serde_json::Value::Null),
870 Value::Bool(b) => serde_json::Value::Bool(*b),
871 Value::Str(s) => serde_json::Value::String(s.to_string()),
872 Value::None => serde_json::Value::Null,
873 other => serde_json::Value::String(other.to_display_string()),
874 }
875}
876
877fn retype(v: &Value, ty: &str) -> Value {
878 let text = v.to_display_string();
879 match PortType::from_keyword(ty) {
880 Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
881 Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
882 Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
883 Some(PortType::Str) | None => Value::Str(text.into()),
884 Some(_) => v.clone(),
885 }
886}
887
888struct BodyEntry {
892 program: Arc<dyn KernelProgram>,
893 kernel: Box<dyn Kernel>,
894 elements: Option<Vec<Option<usize>>>,
897 cascade: Option<Vec<Option<usize>>>,
899 holes: Vec<Option<Option<usize>>>,
901}
902
903impl BodyEntry {
904 fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
906 if self.holes.len() <= k {
907 self.holes.resize(k + 1, None);
908 }
909 if self.holes[k].is_none() {
910 self.holes[k] = Some(self.kernel.output_index(name));
911 }
912 self.holes[k].flatten()
913 }
914}
915
916#[derive(Default)]
924pub struct BodyKernels {
925 entries: HashMap<(usize, crate::Engine), BodyEntry>,
926 created: u64,
928}
929
930impl Clone for BodyKernels {
931 fn clone(&self) -> Self {
932 Self::default()
933 }
934}
935
936unsafe impl Sync for BodyKernels {}
943
944impl std::fmt::Debug for BodyKernels {
945 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
946 f.debug_struct("BodyKernels")
947 .field("entries", &self.entries.len())
948 .field("created", &self.created)
949 .finish()
950 }
951}
952
953fn body_engine_key(engine: crate::Engine) -> crate::Engine {
956 match engine {
957 crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
958 other => other,
959 }
960}
961
962impl BodyKernels {
963 pub fn created(&self) -> u64 {
965 self.created
966 }
967
968 #[cfg(test)]
970 fn clone_for_test(&self) -> (u64, BodyKernels) {
971 (self.created, self.clone())
972 }
973
974 fn with(
978 &mut self,
979 program: &Arc<dyn KernelProgram>,
980 engine: crate::Engine,
981 f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
982 ) {
983 let engine = body_engine_key(engine);
984 let key = (Arc::as_ptr(program) as *const () as usize, engine);
987 let mut entry = self
988 .entries
989 .remove(&key)
990 .filter(|e| Arc::ptr_eq(&e.program, program))
991 .unwrap_or_else(|| {
992 self.created += 1;
993 BodyEntry {
994 program: program.clone(),
995 kernel: program.clone().create_kernel(),
996 elements: None,
997 cascade: None,
998 holes: Vec::new(),
999 }
1000 });
1001 f(&mut entry, self);
1002 self.entries.insert(key, entry);
1003 }
1004}
1005
1006pub(crate) mod render_state {
1008 use super::{BodyKernels, TileRender};
1009 use crate::ast::{ScratchBuf, ScratchElem, Value};
1010
1011 pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
1012 vec![ScratchElem::Kernels]
1013 }
1014
1015 pub(crate) fn eval(
1016 node: &TileRender,
1017 scratch: &mut [ScratchBuf],
1018 inputs: &[Value],
1019 outputs: &mut [Value],
1020 ) {
1021 let bodies = bodies_of(&mut scratch[0]);
1022 outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1023 }
1024
1025 pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1027 match entry {
1028 ScratchBuf::Kernels(b) => b,
1029 other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1030 }
1031 }
1032}
1033
1034pub(crate) trait Sink: std::fmt::Write {
1038 fn put(&mut self, s: &str) {
1039 let _ = self.write_str(s);
1040 }
1041 fn put_char(&mut self, c: char) {
1042 let _ = self.write_char(c);
1043 }
1044}
1045
1046impl<W: std::fmt::Write> Sink for W {}
1047
1048pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1050 encode_ref(ValueRef::from(value), enc, out)
1051}
1052
1053pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1057 if enc.cond {
1058 out.put_char(if truthy_of(value) { '1' } else { '0' });
1059 return;
1060 }
1061 let ty = enc.ty.as_deref();
1062 if is_numeric_keyword(ty.unwrap_or("u64")) {
1071 match (enc.format.as_deref(), value) {
1072 (None, ValueRef::U64(n)) => {
1073 put_u64(n, out);
1074 return;
1075 }
1076 (None, ValueRef::I64(n)) => {
1077 if n < 0 {
1078 out.put_char('-');
1079 }
1080 put_u64(n.unsigned_abs(), out);
1081 return;
1082 }
1083 (None, ValueRef::F64(f)) => {
1084 let _ = float_text::write_shortest(f, out);
1085 return;
1086 }
1087 (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1088 if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1089 let _ = float_text::write_fixed(f, prec, out);
1090 return;
1091 }
1092 }
1093 _ => {}
1094 }
1095 }
1096 let text = formatted_text(value, ty, enc.format.as_deref());
1097 if enc.raw {
1098 out.put(&text);
1099 return;
1100 }
1101 match (enc.encoding.as_str(), enc.position) {
1102 ("json", HolePosition::InString) => push_json_escaped(&text, out),
1103 ("json", HolePosition::Value) => {
1104 let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1105 match (kind, value) {
1106 (_, ValueRef::None) => out.put("null"),
1107 ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1108 ("json", ValueRef::Json(j)) => {
1109 let _ = write!(out, "{j}");
1110 }
1111 ("str", _) | ("String", _) | ("string", _) => {
1112 out.put_char('"');
1113 push_json_escaped(&text, out);
1114 out.put_char('"');
1115 }
1116 (k, _) if is_numeric_keyword(k) => out.put(&text),
1117 (_, ValueRef::Json(j)) => {
1118 let _ = write!(out, "{j}");
1119 }
1120 (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1121 (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1122 _ => {
1123 out.put_char('"');
1124 push_json_escaped(&text, out);
1125 out.put_char('"');
1126 }
1127 }
1128 }
1129 ("csv", _) => {
1130 if text.contains([',', '"', '\n']) {
1131 out.put_char('"');
1132 for (i, piece) in text.split('"').enumerate() {
1133 if i > 0 {
1134 out.put("\"\"");
1135 }
1136 out.put(piece);
1137 }
1138 out.put_char('"');
1139 } else {
1140 out.put(&text);
1141 }
1142 }
1143 _ => out.put(&text),
1144 }
1145}
1146
1147fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1149 if n == 0 {
1150 out.put_char('0');
1151 return;
1152 }
1153 let mut buf = [0u8; 20];
1154 let mut i = buf.len();
1155 while n > 0 {
1156 i -= 1;
1157 buf[i] = b'0' + (n % 10) as u8;
1158 n /= 10;
1159 }
1160 out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1162}
1163
1164fn truthy_of(v: ValueRef<'_>) -> bool {
1165 match v {
1166 ValueRef::Bool(b) => b,
1167 ValueRef::U64(n) => n != 0,
1168 ValueRef::F64(f) => f != 0.0,
1169 ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1170 ValueRef::None => false,
1171 _ => true,
1172 }
1173}
1174
1175fn is_numeric_keyword(k: &str) -> bool {
1176 matches!(
1177 k,
1178 "u64"
1179 | "i64"
1180 | "f64"
1181 | "f32"
1182 | "u32"
1183 | "i32"
1184 | "u16"
1185 | "i16"
1186 | "u8"
1187 | "i8"
1188 | "u128"
1189 | "i128"
1190 | "f16"
1191 )
1192}
1193
1194fn formatted_text<'a>(
1198 value: ValueRef<'a>,
1199 ty: Option<&str>,
1200 format: Option<&str>,
1201) -> std::borrow::Cow<'a, str> {
1202 use std::borrow::Cow;
1203 let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1207 match (ty, value) {
1208 (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1209 (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1212 (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1213 (_, v) => v.display(),
1214 }
1215 };
1216 let Some(fmt) = format else {
1217 return base(value);
1218 };
1219 let fmt = fmt.trim();
1220 if let Some(prec) = precision_of(fmt) {
1221 if let Some(f) = as_f64(value) {
1222 return Cow::Owned(float_text::fixed_string(f, prec));
1223 }
1224 return base(value);
1225 }
1226 if fmt == "x" || fmt == "X" {
1227 if let ValueRef::U64(n) = value {
1228 return Cow::Owned(if fmt == "x" {
1229 format!("{n:x}")
1230 } else {
1231 format!("{n:X}")
1232 });
1233 }
1234 return base(value);
1235 }
1236 let base = base(value);
1237 if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1238 return Cow::Owned(format!("{base:0>w$}"));
1239 }
1240 if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1241 return Cow::Owned(format!("{base:>w$}"));
1242 }
1243 if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1244 return Cow::Owned(format!("{base:<w$}"));
1245 }
1246 if let Ok(w) = fmt.parse::<usize>() {
1247 return Cow::Owned(format!("{base:>w$}"));
1248 }
1249 base
1250}
1251
1252fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1253 match v {
1254 ValueRef::F64(f) => Some(f),
1255 ValueRef::U64(n) => Some(n as f64),
1256 _ => None,
1257 }
1258}
1259
1260fn precision_of(fmt: &str) -> Option<usize> {
1262 fmt.trim()
1263 .strip_prefix('.')
1264 .and_then(|p| p.parse::<usize>().ok())
1265}
1266
1267fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1268 for c in s.chars() {
1269 match c {
1270 '"' => out.put("\\\""),
1271 '\\' => out.put("\\\\"),
1272 '\n' => out.put("\\n"),
1273 '\r' => out.put("\\r"),
1274 '\t' => out.put("\\t"),
1275 c if (c as u32) < 0x20 => {
1276 let _ = write!(out, "\\u{:04x}", c as u32);
1277 }
1278 c => out.put_char(c),
1279 }
1280 }
1281}
1282
1283#[crate::polydat_node(category = Formatting)]
1286fn tile_encode(
1287 value: Value,
1288 spec: Const<&str>,
1289 #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1290) -> String {
1291 let mut out = String::new();
1292 encode(&value, enc, &mut out);
1293 out
1294}
1295
1296fn 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}