1use std::collections::HashMap;
19use std::sync::Arc;
20
21use serde::{Deserialize, Serialize};
22
23use crate::ast::SlotShape;
24use crate::ast::{PortType, Value, ValueRef};
25use crate::iteration::comprehension::StreamerValue;
26use crate::iteration::comprehension::runtime::{RuntimeTuple, evaluate_for_iteration};
27use crate::kernel::{Kernel, KernelProgram, PolydatKernel, PolydatProgram};
28use crate::library::support::float_text;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum HolePosition {
33 Value,
35 InString,
37 Text,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct HoleEncoding {
46 pub encoding: String,
48 pub position: HolePosition,
50 pub ty: Option<String>,
52 pub format: Option<String>,
54 pub raw: bool,
56 pub cond: bool,
58}
59
60impl HoleEncoding {
61 pub fn to_spec(&self) -> String {
63 let pos = match self.position {
64 HolePosition::Value => "value",
65 HolePosition::InString => "string",
66 HolePosition::Text => "text",
67 };
68 let mut flags = String::new();
69 if self.raw {
70 flags.push('r');
71 }
72 if self.cond {
73 flags.push('c');
74 }
75 format!(
76 "{}|{}|{}|{}|{}",
77 self.encoding,
78 pos,
79 self.ty.as_deref().unwrap_or(""),
80 self.format.as_deref().unwrap_or(""),
81 flags
82 )
83 }
84
85 pub fn from_spec(spec: &str) -> Self {
87 let mut parts = spec.splitn(5, '|');
88 let encoding = parts.next().unwrap_or("text").to_string();
89 let position = match parts.next().unwrap_or("text") {
90 "value" => HolePosition::Value,
91 "string" => HolePosition::InString,
92 _ => HolePosition::Text,
93 };
94 let ty = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
95 let format = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
96 let flags = parts.next().unwrap_or("");
97 HoleEncoding {
98 encoding,
99 position,
100 ty,
101 format,
102 raw: flags.contains('r'),
103 cond: flags.contains('c'),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub enum HoleSource {
111 Wire {
114 index: usize,
116 spec: String,
118 },
119 Child {
122 name: String,
124 spec: String,
126 },
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub enum TileOp {
132 Static(String),
134 Hole(HoleSource),
136 Repeat {
138 stream: String,
140 child: usize,
142 sep: String,
144 body: Vec<TileOp>,
146 #[serde(default)]
150 generators: Vec<(String, usize, String)>,
151 },
152 Branch {
154 cond: HoleSource,
156 then: Vec<TileOp>,
158 otherwise: Vec<TileOp>,
160 },
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct ChildSpec {
168 pub source: String,
170 pub cascade: Vec<(String, usize, String)>,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct TileSpec {
177 pub name: String,
179 pub encoding: String,
181 pub ops: Vec<TileOp>,
183 pub children: Vec<ChildSpec>,
185}
186
187impl TileSpec {
188 pub fn to_json(&self) -> String {
190 serde_json::to_string(self).expect("TileSpec serializes")
191 }
192}
193
194#[derive(Debug)]
199enum RtOp {
200 Copy(&'static str),
202 Hole(RtSource, HoleEncoding),
204 Repeat {
205 stream: Arc<StreamerValue>,
206 child: usize,
207 sep: &'static str,
208 body: Vec<RtOp>,
209 generators: Vec<(String, usize, String)>,
210 },
211 Branch {
212 cond: RtSource,
213 then: Vec<RtOp>,
214 otherwise: Vec<RtOp>,
215 },
216}
217
218#[derive(Debug)]
220enum RtSource {
221 Wire(usize),
222 Child(String, usize),
225}
226
227fn lower_source(source: &HoleSource) -> (RtSource, HoleEncoding) {
228 match source {
229 HoleSource::Wire { index, spec } => (RtSource::Wire(*index), HoleEncoding::from_spec(spec)),
230 HoleSource::Child { name, spec } => (
231 RtSource::Child(name.clone(), 0),
232 HoleEncoding::from_spec(spec),
233 ),
234 }
235}
236
237fn lower_ops(ops: &[TileOp]) -> Vec<RtOp> {
240 use crate::kernel::StaticInterner;
241 ops.iter()
242 .map(|op| match op {
243 TileOp::Static(s) => RtOp::Copy(StaticInterner::intern(s)),
244 TileOp::Hole(h) => {
245 let (source, enc) = lower_source(h);
246 RtOp::Hole(source, enc)
247 }
248 TileOp::Repeat {
249 stream,
250 child,
251 sep,
252 body,
253 generators,
254 } => RtOp::Repeat {
255 stream: Arc::new(StreamerValue::from_json(stream)),
256 child: *child,
257 sep: StaticInterner::intern(sep),
258 body: lower_ops(body),
259 generators: generators.clone(),
260 },
261 TileOp::Branch {
262 cond,
263 then,
264 otherwise,
265 } => RtOp::Branch {
266 cond: lower_source(cond).0,
267 then: lower_ops(then),
268 otherwise: lower_ops(otherwise),
269 },
270 })
271 .collect()
272}
273
274pub struct TileProgram {
276 pub spec: TileSpec,
278 ops: Vec<RtOp>,
280 pub bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
288 pub children: Vec<Arc<PolydatProgram>>,
292 canonicals: Vec<Arc<PolydatKernel>>,
295 memo: Vec<Option<Arc<[RuntimeTuple]>>>,
299}
300
301impl std::fmt::Debug for TileProgram {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 f.debug_struct("TileProgram")
304 .field("spec", &self.spec)
305 .field("ops", &self.ops)
306 .field("children", &self.children.len())
307 .finish_non_exhaustive()
308 }
309}
310
311fn number_child_holes(ops: &mut [RtOp]) {
315 fn walk(ops: &mut [RtOp], next: &mut usize) {
316 for op in ops.iter_mut() {
317 match op {
318 RtOp::Hole(RtSource::Child(_, k), _) => {
319 *k = *next;
320 *next += 1;
321 }
322 RtOp::Branch {
323 cond,
324 then,
325 otherwise,
326 } => {
327 if let RtSource::Child(_, k) = cond {
328 *k = *next;
329 *next += 1;
330 }
331 walk(then, next);
332 walk(otherwise, next);
333 }
334 RtOp::Repeat { body, .. } => {
335 let mut inner = 0;
336 walk(body, &mut inner);
337 }
338 _ => {}
339 }
340 }
341 }
342 let mut top = 0;
343 walk(ops, &mut top);
344}
345
346fn memoize(
349 ops: &[RtOp],
350 canonicals: &[Arc<PolydatKernel>],
351 memo: &mut [Option<Arc<[RuntimeTuple]>>],
352) {
353 for op in ops {
354 match op {
355 RtOp::Repeat {
356 stream,
357 child,
358 body,
359 generators,
360 ..
361 } => {
362 if generators.is_empty()
363 && !stream.text.contains('{')
364 && let Ok(tuples) = evaluate_for_iteration(&stream.ast, &*canonicals[*child])
365 {
366 memo[*child] = Some(tuples.into());
367 }
368 memoize(body, canonicals, memo);
369 }
370 RtOp::Branch {
371 then, otherwise, ..
372 } => {
373 memoize(then, canonicals, memo);
374 memoize(otherwise, canonicals, memo);
375 }
376 _ => {}
377 }
378 }
379}
380
381impl TileProgram {
382 pub fn from_parts(
389 spec: TileSpec,
390 bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
391 ) -> Result<Self, String> {
392 let mut children: Vec<Arc<PolydatProgram>> = Vec::with_capacity(bodies.len());
397 for (i, body) in bodies.iter().enumerate() {
398 let program = body
399 .program_on(crate::Engine::Interpreter(crate::JitMode::Auto))
400 .map_err(|e| format!("projection body {i} failed to compile: {e}"))?;
401 children.push(
402 program
403 .as_interpreter()
404 .ok_or_else(|| format!("projection body {i} is not an interpreter program"))?,
405 );
406 }
407 let canonicals: Vec<Arc<PolydatKernel>> = children
408 .iter()
409 .map(|p| Arc::new(PolydatKernel::from_program(p.clone())))
410 .collect();
411 let mut ops = lower_ops(&spec.ops);
412 number_child_holes(&mut ops);
413 let mut memo = vec![None; children.len()];
414 memoize(&ops, &canonicals, &mut memo);
415 Ok(TileProgram {
416 spec,
417 ops,
418 bodies,
419 children,
420 canonicals,
421 memo,
422 })
423 }
424
425 pub fn from_json(json: &str) -> Self {
434 let spec: TileSpec = serde_json::from_str(json)
435 .unwrap_or_else(|e| panic!("tile_render: malformed skeleton payload: {e}"));
436 let name = spec.name.clone();
437 let bodies: Vec<Arc<crate::dsl::traversal::BodySource>> = spec
438 .children
439 .iter()
440 .enumerate()
441 .map(|(i, c)| {
442 Arc::new(
443 crate::dsl::traversal::BodySource::from_source(
444 &c.source,
445 &format!("tile '{name}' :: projection body {i}"),
446 )
447 .unwrap_or_else(|e| {
448 panic!(
449 "tile '{name}': projection body failed to parse: {e}\n{}",
450 c.source
451 )
452 }),
453 )
454 })
455 .collect();
456 Self::from_parts(spec, bodies).unwrap_or_else(|e| panic!("tile '{name}': {e}"))
457 }
458
459 fn body_program_on(&self, child: usize, engine: crate::Engine) -> Arc<dyn KernelProgram> {
469 if matches!(engine, crate::Engine::Interpreter(_)) {
470 return self.children[child].clone();
471 }
472 self.bodies[child].program_on(engine).unwrap_or_else(|e| {
473 crate::library::support::audit::debug(&format!(
474 "tile '{}': projection body {child} renders on the interpreter: {e}",
475 self.spec.name
476 ));
477 self.children[child].clone()
478 })
479 }
480
481 pub fn has_projections(&self) -> bool {
483 fn walk(ops: &[RtOp]) -> bool {
484 ops.iter().any(|op| match op {
485 RtOp::Repeat { .. } => true,
486 RtOp::Branch {
487 then, otherwise, ..
488 } => walk(then) || walk(otherwise),
489 _ => false,
490 })
491 }
492 walk(&self.ops)
493 }
494
495 pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
499 let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
500 let mut out = String::new();
501 self.render_into(
502 &refs,
503 crate::Engine::Interpreter(crate::JitMode::Auto),
504 bodies,
505 &mut out,
506 );
507 out
508 }
509
510 pub fn render_into<W: std::fmt::Write>(
517 &self,
518 inputs: &[ValueRef<'_>],
519 engine: crate::Engine,
520 bodies: &mut BodyKernels,
521 out: &mut W,
522 ) {
523 self.render_ops(&self.ops, inputs, engine, bodies, None, out);
524 }
525
526 fn render_ops<W: std::fmt::Write>(
527 &self,
528 ops: &[RtOp],
529 inputs: &[ValueRef<'_>],
530 engine: crate::Engine,
531 bodies: &mut BodyKernels,
532 mut child: Option<&mut BodyEntry>,
533 out: &mut W,
534 ) {
535 for op in ops {
536 match op {
537 RtOp::Copy(s) => out.put(s),
540 RtOp::Hole(source, enc) => match source {
541 RtSource::Wire(i) => {
542 encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
543 }
544 RtSource::Child(name, k) => {
545 if let Some(entry) = child.as_deref_mut()
546 && let Some(i) = entry.hole(*k, name)
547 {
548 let v = entry.kernel.pull_at(i);
549 encode_ref(ValueRef::from(&v), enc, out)
550 }
551 }
552 },
553 RtOp::Branch {
554 cond,
555 then,
556 otherwise,
557 } => {
558 let c = self.truthy(cond, inputs, child.as_deref_mut());
559 let branch = if c { then } else { otherwise };
560 self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
561 }
562 RtOp::Repeat {
563 stream,
564 child: child_idx,
565 sep,
566 body,
567 generators,
568 } => {
569 let memoized = self.memo[*child_idx].clone();
578 let tuples: std::borrow::Cow<'_, [RuntimeTuple]> = match &memoized {
579 Some(t) => std::borrow::Cow::Borrowed(&t[..]),
580 None => {
581 let mut streamer = (**stream).clone();
582 if !generators.is_empty() {
583 streamer.ast = bind_generators(&streamer.ast, generators, inputs);
584 }
585 std::borrow::Cow::Owned(
586 evaluate_for_iteration(
587 &streamer.ast,
588 &*self.canonicals[*child_idx],
589 )
590 .unwrap_or_else(|e| {
591 panic!(
592 "tile '{}': projection `for {}` failed at render: {e}",
593 self.spec.name, streamer.text
594 )
595 }),
596 )
597 }
598 };
599 let child_spec = &self.spec.children[*child_idx];
600 let program = self.body_program_on(*child_idx, engine);
606 let mut first = true;
607 let fail = |name: &str, e: crate::kernel::WriteError| -> ! {
608 panic!(
609 "tile '{}': projection body input `{name}`: {e}",
610 self.spec.name
611 )
612 };
613 bodies.with(&program, engine, |entry, bodies| {
614 for (index, tuple) in tuples.iter().enumerate() {
615 if !first {
616 out.put(sep);
617 }
618 first = false;
619 {
620 let BodyEntry {
623 kernel,
624 elements,
625 cascade,
626 ..
627 } = &mut *entry;
628 kernel.set_inputs(&[index as u64]);
629 let elements = elements.get_or_insert_with(|| {
630 tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
631 });
632 for (k, (name, v)) in tuple.iter().enumerate() {
633 if let Some(i) = elements.get(k).copied().flatten() {
634 kernel
635 .set_input_at(i, v.clone())
636 .unwrap_or_else(|e| fail(name, e));
637 }
638 }
639 let cascade = cascade.get_or_insert_with(|| {
640 child_spec
641 .cascade
642 .iter()
643 .map(|(n, _, _)| kernel.input_index(n))
644 .collect()
645 });
646 for (k, (name, input_idx, ty)) in
647 child_spec.cascade.iter().enumerate()
648 {
649 if let Some(i) = cascade.get(k).copied().flatten()
650 && let Some(v) = inputs.get(*input_idx)
651 {
652 kernel
653 .set_input_at(i, typed_for(&owned(*v), ty))
654 .unwrap_or_else(|e| fail(name, e));
655 }
656 }
657 if !kernel.const_inits().is_empty()
660 && let Err(e) = kernel.init()
661 {
662 panic!("tile '{}': projection body: {e}", self.spec.name);
663 }
664 }
665 self.render_ops(body, inputs, engine, bodies, Some(entry), out);
666 }
667 });
668 }
669 }
670 }
671 }
672
673 fn truthy(
675 &self,
676 source: &RtSource,
677 inputs: &[ValueRef<'_>],
678 child: Option<&mut BodyEntry>,
679 ) -> bool {
680 match source {
681 RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
682 RtSource::Child(name, k) => match child {
683 Some(entry) => match entry.hole(*k, name) {
684 Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
685 None => false,
686 },
687 None => false,
688 },
689 }
690 }
691}
692
693fn owned(v: ValueRef<'_>) -> Value {
696 match v {
697 ValueRef::U64(n) => Value::U64(n),
698 ValueRef::I64(n) => Value::I64(n),
699 ValueRef::F64(f) => Value::F64(f),
700 ValueRef::Bool(b) => Value::Bool(b),
701 ValueRef::Str(s) => Value::Str(Arc::from(s)),
702 ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
703 ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
704 ValueRef::None => Value::None,
705 ValueRef::Other(v) => v.clone(),
706 }
707}
708
709fn typed_for(v: &Value, ty: &str) -> Value {
713 match (v, PortType::from_keyword(ty)) {
714 (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
715 _ => v.clone(),
716 }
717}
718
719fn bind_generators(
724 c: &crate::iteration::comprehension::Comprehension,
725 generators: &[(String, usize, String)],
726 inputs: &[ValueRef<'_>],
727) -> crate::iteration::comprehension::Comprehension {
728 use crate::iteration::comprehension::Comprehension as K;
729 use crate::iteration::comprehension::source::{LiteralValue, Source};
730 match c {
731 K::Clause {
732 name,
733 source: Source::Generator { .. },
734 } => {
735 let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
736 return c.clone();
737 };
738 let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
739 let items: Vec<Value> =
740 match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
741 Some(interior) => interior,
742 None => match &raw {
743 Value::Str(text) => {
744 match serde_json::from_str::<serde_json::Value>(text.trim()) {
745 Ok(serde_json::Value::Array(items)) => items
746 .iter()
747 .map(|j| {
748 retype(
749 &Value::Str(j.to_string().trim_matches('"').into()),
750 ty,
751 )
752 })
753 .collect(),
754 _ => vec![typed_for(&raw, ty)],
755 }
756 }
757 _ => vec![raw.clone()],
758 },
759 };
760 let json_items = ty == "json";
764 let values = items
765 .iter()
766 .map(|v| {
767 if json_items {
768 return LiteralValue::Json(json_of(v));
769 }
770 match v {
771 Value::U64(n) => LiteralValue::Int(*n as i64),
772 Value::I64(n) => LiteralValue::Int(*n),
773 Value::F64(f) => LiteralValue::Float(*f),
774 Value::Bool(b) => LiteralValue::Bool(*b),
775 Value::Json(j) => match j.as_ref() {
777 serde_json::Value::Number(n) if n.is_u64() => {
778 LiteralValue::Int(n.as_u64().unwrap_or(0) as i64)
779 }
780 serde_json::Value::Number(n) if n.is_i64() => {
781 LiteralValue::Int(n.as_i64().unwrap_or(0))
782 }
783 serde_json::Value::Number(n) => {
784 LiteralValue::Float(n.as_f64().unwrap_or(0.0))
785 }
786 serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
787 serde_json::Value::String(s) => LiteralValue::String(s.clone()),
788 other => LiteralValue::String(other.to_string()),
789 },
790 other => LiteralValue::String(other.to_display_string()),
791 }
792 })
793 .collect();
794 K::Clause {
795 name: name.clone(),
796 source: Source::Literal { values },
797 }
798 }
799 K::Clause { .. } => c.clone(),
800 K::Cartesian { children } => K::Cartesian {
801 children: children
802 .iter()
803 .map(|ch| bind_generators(ch, generators, inputs))
804 .collect(),
805 },
806 K::Zip { children, mode } => K::Zip {
807 children: children
808 .iter()
809 .map(|ch| bind_generators(ch, generators, inputs))
810 .collect(),
811 mode: *mode,
812 },
813 K::Union { children } => K::Union {
814 children: children
815 .iter()
816 .map(|ch| bind_generators(ch, generators, inputs))
817 .collect(),
818 },
819 K::Filter { child, predicate } => K::Filter {
820 child: Box::new(bind_generators(child, generators, inputs)),
821 predicate: predicate.clone(),
822 },
823 K::Order {
824 child,
825 strategy,
826 truncation,
827 seed,
828 } => K::Order {
829 child: Box::new(bind_generators(child, generators, inputs)),
830 strategy: *strategy,
831 truncation: *truncation,
832 seed: *seed,
833 },
834 }
835}
836
837fn json_of(v: &Value) -> serde_json::Value {
842 match v {
843 Value::Json(j) => j.as_ref().clone(),
844 Value::U64(n) => serde_json::Value::from(*n),
845 Value::I64(n) => serde_json::Value::from(*n),
846 Value::F64(f) => serde_json::Number::from_f64(*f)
847 .map(serde_json::Value::Number)
848 .unwrap_or(serde_json::Value::Null),
849 Value::Bool(b) => serde_json::Value::Bool(*b),
850 Value::Str(s) => serde_json::Value::String(s.to_string()),
851 Value::None => serde_json::Value::Null,
852 other => serde_json::Value::String(other.to_display_string()),
853 }
854}
855
856fn retype(v: &Value, ty: &str) -> Value {
857 let text = v.to_display_string();
858 match PortType::from_keyword(ty) {
859 Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
860 Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
861 Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
862 Some(PortType::Str) | None => Value::Str(text.into()),
863 Some(_) => v.clone(),
864 }
865}
866
867struct BodyEntry {
871 program: Arc<dyn KernelProgram>,
872 kernel: Box<dyn Kernel>,
873 elements: Option<Vec<Option<usize>>>,
876 cascade: Option<Vec<Option<usize>>>,
878 holes: Vec<Option<Option<usize>>>,
880}
881
882impl BodyEntry {
883 fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
885 if self.holes.len() <= k {
886 self.holes.resize(k + 1, None);
887 }
888 if self.holes[k].is_none() {
889 self.holes[k] = Some(self.kernel.output_index(name));
890 }
891 self.holes[k].flatten()
892 }
893}
894
895#[derive(Default)]
903pub struct BodyKernels {
904 entries: HashMap<(usize, crate::Engine), BodyEntry>,
905 created: u64,
907}
908
909impl Clone for BodyKernels {
910 fn clone(&self) -> Self {
911 Self::default()
912 }
913}
914
915unsafe impl Sync for BodyKernels {}
922
923impl std::fmt::Debug for BodyKernels {
924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925 f.debug_struct("BodyKernels")
926 .field("entries", &self.entries.len())
927 .field("created", &self.created)
928 .finish()
929 }
930}
931
932fn body_engine_key(engine: crate::Engine) -> crate::Engine {
935 match engine {
936 crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
937 other => other,
938 }
939}
940
941impl BodyKernels {
942 pub fn created(&self) -> u64 {
944 self.created
945 }
946
947 #[cfg(test)]
949 fn clone_for_test(&self) -> (u64, BodyKernels) {
950 (self.created, self.clone())
951 }
952
953 fn with(
957 &mut self,
958 program: &Arc<dyn KernelProgram>,
959 engine: crate::Engine,
960 f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
961 ) {
962 let engine = body_engine_key(engine);
963 let key = (Arc::as_ptr(program) as *const () as usize, engine);
966 let mut entry = self
967 .entries
968 .remove(&key)
969 .filter(|e| Arc::ptr_eq(&e.program, program))
970 .unwrap_or_else(|| {
971 self.created += 1;
972 BodyEntry {
973 program: program.clone(),
974 kernel: program.clone().create_uninitialized(),
975 elements: None,
976 cascade: None,
977 holes: Vec::new(),
978 }
979 });
980 f(&mut entry, self);
981 self.entries.insert(key, entry);
982 }
983}
984
985pub(crate) mod render_state {
987 use super::{BodyKernels, TileRender};
988 use crate::ast::{ScratchBuf, ScratchElem, Value};
989
990 pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
991 vec![ScratchElem::Kernels]
992 }
993
994 pub(crate) fn eval(
995 node: &TileRender,
996 scratch: &mut [ScratchBuf],
997 inputs: &[Value],
998 outputs: &mut [Value],
999 ) {
1000 let bodies = bodies_of(&mut scratch[0]);
1001 outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1002 }
1003
1004 pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1006 match entry {
1007 ScratchBuf::Kernels(b) => b,
1008 other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1009 }
1010 }
1011}
1012
1013pub(crate) trait Sink: std::fmt::Write {
1017 fn put(&mut self, s: &str) {
1018 let _ = self.write_str(s);
1019 }
1020 fn put_char(&mut self, c: char) {
1021 let _ = self.write_char(c);
1022 }
1023}
1024
1025impl<W: std::fmt::Write> Sink for W {}
1026
1027pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1029 encode_ref(ValueRef::from(value), enc, out)
1030}
1031
1032pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1036 if enc.cond {
1037 out.put_char(if truthy_of(value) { '1' } else { '0' });
1038 return;
1039 }
1040 let ty = enc.ty.as_deref();
1041 if is_numeric_keyword(ty.unwrap_or("u64")) {
1050 match (enc.format.as_deref(), value) {
1051 (None, ValueRef::U64(n)) => {
1052 put_u64(n, out);
1053 return;
1054 }
1055 (None, ValueRef::I64(n)) => {
1056 if n < 0 {
1057 out.put_char('-');
1058 }
1059 put_u64(n.unsigned_abs(), out);
1060 return;
1061 }
1062 (None, ValueRef::F64(f)) => {
1063 let _ = float_text::write_shortest(f, out);
1064 return;
1065 }
1066 (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1067 if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1068 let _ = float_text::write_fixed(f, prec, out);
1069 return;
1070 }
1071 }
1072 _ => {}
1073 }
1074 }
1075 let text = formatted_text(value, ty, enc.format.as_deref());
1076 if enc.raw {
1077 out.put(&text);
1078 return;
1079 }
1080 match (enc.encoding.as_str(), enc.position) {
1081 ("json", HolePosition::InString) => push_json_escaped(&text, out),
1082 ("json", HolePosition::Value) => {
1083 let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1084 match (kind, value) {
1085 (_, ValueRef::None) => out.put("null"),
1086 ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1087 ("json", ValueRef::Json(j)) => {
1088 let _ = write!(out, "{j}");
1089 }
1090 ("str", _) | ("String", _) | ("string", _) => {
1091 out.put_char('"');
1092 push_json_escaped(&text, out);
1093 out.put_char('"');
1094 }
1095 (k, _) if is_numeric_keyword(k) => out.put(&text),
1096 (_, ValueRef::Json(j)) => {
1097 let _ = write!(out, "{j}");
1098 }
1099 (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1100 (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1101 _ => {
1102 out.put_char('"');
1103 push_json_escaped(&text, out);
1104 out.put_char('"');
1105 }
1106 }
1107 }
1108 ("csv", _) => {
1109 if text.contains([',', '"', '\n']) {
1110 out.put_char('"');
1111 for (i, piece) in text.split('"').enumerate() {
1112 if i > 0 {
1113 out.put("\"\"");
1114 }
1115 out.put(piece);
1116 }
1117 out.put_char('"');
1118 } else {
1119 out.put(&text);
1120 }
1121 }
1122 _ => out.put(&text),
1123 }
1124}
1125
1126fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1128 if n == 0 {
1129 out.put_char('0');
1130 return;
1131 }
1132 let mut buf = [0u8; 20];
1133 let mut i = buf.len();
1134 while n > 0 {
1135 i -= 1;
1136 buf[i] = b'0' + (n % 10) as u8;
1137 n /= 10;
1138 }
1139 out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1141}
1142
1143fn truthy_of(v: ValueRef<'_>) -> bool {
1144 match v {
1145 ValueRef::Bool(b) => b,
1146 ValueRef::U64(n) => n != 0,
1147 ValueRef::F64(f) => f != 0.0,
1148 ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1149 ValueRef::None => false,
1150 _ => true,
1151 }
1152}
1153
1154fn is_numeric_keyword(k: &str) -> bool {
1155 matches!(
1156 k,
1157 "u64"
1158 | "i64"
1159 | "f64"
1160 | "f32"
1161 | "u32"
1162 | "i32"
1163 | "u16"
1164 | "i16"
1165 | "u8"
1166 | "i8"
1167 | "u128"
1168 | "i128"
1169 | "f16"
1170 )
1171}
1172
1173fn formatted_text<'a>(
1177 value: ValueRef<'a>,
1178 ty: Option<&str>,
1179 format: Option<&str>,
1180) -> std::borrow::Cow<'a, str> {
1181 use std::borrow::Cow;
1182 let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1186 match (ty, value) {
1187 (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1188 (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1191 (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1192 (_, v) => v.display(),
1193 }
1194 };
1195 let Some(fmt) = format else {
1196 return base(value);
1197 };
1198 let fmt = fmt.trim();
1199 if let Some(prec) = precision_of(fmt) {
1200 if let Some(f) = as_f64(value) {
1201 return Cow::Owned(float_text::fixed_string(f, prec));
1202 }
1203 return base(value);
1204 }
1205 if fmt == "x" || fmt == "X" {
1206 if let ValueRef::U64(n) = value {
1207 return Cow::Owned(if fmt == "x" {
1208 format!("{n:x}")
1209 } else {
1210 format!("{n:X}")
1211 });
1212 }
1213 return base(value);
1214 }
1215 let base = base(value);
1216 if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1217 return Cow::Owned(format!("{base:0>w$}"));
1218 }
1219 if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1220 return Cow::Owned(format!("{base:>w$}"));
1221 }
1222 if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1223 return Cow::Owned(format!("{base:<w$}"));
1224 }
1225 if let Ok(w) = fmt.parse::<usize>() {
1226 return Cow::Owned(format!("{base:>w$}"));
1227 }
1228 base
1229}
1230
1231fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1232 match v {
1233 ValueRef::F64(f) => Some(f),
1234 ValueRef::U64(n) => Some(n as f64),
1235 _ => None,
1236 }
1237}
1238
1239fn precision_of(fmt: &str) -> Option<usize> {
1241 fmt.trim()
1242 .strip_prefix('.')
1243 .and_then(|p| p.parse::<usize>().ok())
1244}
1245
1246fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1247 for c in s.chars() {
1248 match c {
1249 '"' => out.put("\\\""),
1250 '\\' => out.put("\\\\"),
1251 '\n' => out.put("\\n"),
1252 '\r' => out.put("\\r"),
1253 '\t' => out.put("\\t"),
1254 c if (c as u32) < 0x20 => {
1255 let _ = write!(out, "\\u{:04x}", c as u32);
1256 }
1257 c => out.put_char(c),
1258 }
1259 }
1260}
1261
1262#[crate::polydat_node(category = Formatting)]
1270fn tile_encode(
1271 value: Value,
1272 spec: Const<&str>,
1273 #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1274) -> String {
1275 let mut out = String::new();
1276 encode(&value, enc, &mut out);
1277 out
1278}
1279
1280fn tile_render_compiled(
1288 node: &TileRender,
1289 wire_types: &[PortType],
1290 engine: crate::Engine,
1291) -> crate::ast::CompiledSlotKit {
1292 let program: &'static TileProgram = unsafe { &*std::sync::Arc::into_raw(node.program.clone()) };
1298 let mut reads: Vec<(usize, PortType)> = Vec::with_capacity(wire_types.len());
1301 let mut offset = 0usize;
1302 for &ty in wire_types {
1303 reads.push((offset, ty));
1304 offset += ty.slot_width().max(1);
1305 }
1306 crate::ast::CompiledSlotKit {
1307 scratch: vec![
1308 crate::ast::ScratchElem::Str,
1309 crate::ast::ScratchElem::Kernels,
1310 ],
1311 op: Box::new(
1312 move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [crate::ast::ScratchBuf]| {
1313 let owned_values: Vec<Value> = reads
1317 .iter()
1318 .filter(|(_, ty)| ty.slot_color() == crate::ast::SlotColor::Imm2)
1319 .map(|&(offset, ty)| crate::compile::marshal::decode_output(inputs, offset, ty))
1320 .collect();
1321 let mut next_owned = 0usize;
1322 let refs: Vec<ValueRef<'_>> = reads
1323 .iter()
1324 .map(|&(offset, ty)| {
1325 if ty.slot_color() == crate::ast::SlotColor::Imm2 {
1326 let v = ValueRef::from(&owned_values[next_owned]);
1327 next_owned += 1;
1328 v
1329 } else {
1330 unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1333 }
1334 })
1335 .collect();
1336 let (text, bodies) = scratch.split_at_mut(1);
1345 let crate::ast::ScratchBuf::Str(buf) = &mut text[0] else {
1346 unreachable!("the render step owns a string entry");
1347 };
1348 let bodies = render_state::bodies_of(&mut bodies[0]);
1349 buf.clear();
1350 let mut w = BytesSink(buf);
1351 program.render_into(&refs, engine, bodies, &mut w);
1352 let (p, l) = scratch[0].ptr_len();
1353 outputs[0] = p;
1354 outputs[1] = l;
1355 },
1356 ),
1357 }
1358}
1359
1360pub(crate) struct BytesSink<'a>(pub(crate) &'a mut Vec<u8>);
1363
1364impl std::fmt::Write for BytesSink<'_> {
1365 fn write_str(&mut self, s: &str) -> std::fmt::Result {
1366 self.0.extend_from_slice(s.as_bytes());
1367 Ok(())
1368 }
1369}
1370
1371#[crate::polydat_node(
1382 category = Formatting,
1383 variadic_min = 0,
1384 compiled_slot = tile_render_compiled,
1385 state = render_state
1386)]
1387fn tile_render(program: Const<Arc<TileProgram>>, values: &[Value]) -> 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_interpreter(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_ref("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_ref("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
1578 #[test]
1582 fn a_tile_reports_whether_any_op_re_runs_a_projection() {
1583 fn wire(index: usize) -> HoleSource {
1584 HoleSource::Wire {
1585 index,
1586 spec: "text|text".into(),
1587 }
1588 }
1589 fn program(ops: Vec<TileOp>, children: Vec<ChildSpec>) -> TileProgram {
1590 TileProgram::from_json(
1591 &TileSpec {
1592 name: "t".into(),
1593 encoding: "text".into(),
1594 ops,
1595 children,
1596 }
1597 .to_json(),
1598 )
1599 }
1600 let body = || ChildSpec {
1601 source: "input cycle: u64\nextern k: u64\nout := u64_add(k, 0)\n".to_string(),
1602 cascade: Vec::new(),
1603 };
1604 let repeat = |child: usize| TileOp::Repeat {
1605 stream: StreamerValue::parse_text("k in 0..3").unwrap().to_json(),
1606 child,
1607 sep: ",".into(),
1608 body: vec![TileOp::Hole(HoleSource::Child {
1609 name: "out".into(),
1610 spec: "text|text".into(),
1611 })],
1612 generators: Vec::new(),
1613 };
1614
1615 let flat = program(
1617 vec![
1618 TileOp::Static("a=".into()),
1619 TileOp::Hole(wire(0)),
1620 TileOp::Branch {
1621 cond: wire(1),
1622 then: vec![TileOp::Static("yes".into())],
1623 otherwise: vec![TileOp::Hole(wire(0))],
1624 },
1625 ],
1626 Vec::new(),
1627 );
1628 assert!(!flat.has_projections());
1629 assert!(flat.children.is_empty());
1630
1631 let top = program(vec![TileOp::Static("[".into()), repeat(0)], vec![body()]);
1633 assert!(top.has_projections());
1634 assert_eq!(top.children.len(), 1);
1635
1636 for (then, otherwise) in [
1639 (vec![repeat(0)], vec![TileOp::Static("none".into())]),
1640 (vec![TileOp::Static("none".into())], vec![repeat(0)]),
1641 ] {
1642 let branched = program(
1643 vec![TileOp::Branch {
1644 cond: wire(0),
1645 then,
1646 otherwise,
1647 }],
1648 vec![body()],
1649 );
1650 assert!(branched.has_projections());
1651 }
1652 }
1653}