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::{IndexedTuples, RuntimeTuple, evaluate_indexed};
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>>,
285 pub children: Vec<Arc<PolydatProgram>>,
289 canonicals: Vec<Arc<PolydatKernel>>,
292 memo: Vec<Option<Arc<[RuntimeTuple]>>>,
296}
297
298impl std::fmt::Debug for TileProgram {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("TileProgram")
301 .field("spec", &self.spec)
302 .field("ops", &self.ops)
303 .field("children", &self.children.len())
304 .finish_non_exhaustive()
305 }
306}
307
308fn number_child_holes(ops: &mut [RtOp]) {
312 fn walk(ops: &mut [RtOp], next: &mut usize) {
313 for op in ops.iter_mut() {
314 match op {
315 RtOp::Hole(RtSource::Child(_, k), _) => {
316 *k = *next;
317 *next += 1;
318 }
319 RtOp::Branch {
320 cond,
321 then,
322 otherwise,
323 } => {
324 if let RtSource::Child(_, k) = cond {
325 *k = *next;
326 *next += 1;
327 }
328 walk(then, next);
329 walk(otherwise, next);
330 }
331 RtOp::Repeat { body, .. } => {
332 let mut inner = 0;
333 walk(body, &mut inner);
334 }
335 _ => {}
336 }
337 }
338 }
339 let mut top = 0;
340 walk(ops, &mut top);
341}
342
343enum Projection<'a> {
347 Memo(&'a [RuntimeTuple]),
348 Indexed(IndexedTuples),
349}
350
351impl Projection<'_> {
352 fn len(&self) -> u64 {
353 match self {
354 Projection::Memo(tuples) => tuples.len() as u64,
355 Projection::Indexed(tuples) => tuples.len(),
356 }
357 }
358
359 fn tuple<'b>(&'b self, i: u64, buf: &'b mut RuntimeTuple) -> &'b RuntimeTuple {
362 match self {
363 Projection::Memo(tuples) => &tuples[i as usize],
364 Projection::Indexed(tuples) => {
365 *buf = tuples.get(i).unwrap_or_default();
366 buf
367 }
368 }
369 }
370}
371
372fn memoize(
375 ops: &[RtOp],
376 canonicals: &[Arc<PolydatKernel>],
377 memo: &mut [Option<Arc<[RuntimeTuple]>>],
378) {
379 for op in ops {
380 match op {
381 RtOp::Repeat {
382 stream,
383 child,
384 body,
385 generators,
386 ..
387 } => {
388 if generators.is_empty()
392 && !stream.text.contains('{')
393 && let Ok(tuples) = evaluate_indexed(&stream.ast, &*canonicals[*child])
394 {
395 memo[*child] = Some(tuples.to_vec().into());
396 }
397 memoize(body, canonicals, memo);
398 }
399 RtOp::Branch {
400 then, otherwise, ..
401 } => {
402 memoize(then, canonicals, memo);
403 memoize(otherwise, canonicals, memo);
404 }
405 _ => {}
406 }
407 }
408}
409
410impl TileProgram {
411 pub fn from_parts(
418 spec: TileSpec,
419 bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
420 ) -> Result<Self, String> {
421 let mut children: Vec<Arc<PolydatProgram>> = Vec::with_capacity(bodies.len());
426 for (i, body) in bodies.iter().enumerate() {
427 let program = body
428 .program_on(crate::Engine::Interpreter(crate::JitMode::Auto))
429 .map_err(|e| format!("projection body {i} failed to compile: {e}"))?;
430 children.push(
431 program
432 .as_interpreter()
433 .ok_or_else(|| format!("projection body {i} is not an interpreter program"))?,
434 );
435 }
436 let canonicals: Vec<Arc<PolydatKernel>> = children
437 .iter()
438 .map(|p| Arc::new(PolydatKernel::from_program(p.clone())))
439 .collect();
440 let mut ops = lower_ops(&spec.ops);
441 number_child_holes(&mut ops);
442 let mut memo = vec![None; children.len()];
443 memoize(&ops, &canonicals, &mut memo);
444 Ok(TileProgram {
445 spec,
446 ops,
447 bodies,
448 children,
449 canonicals,
450 memo,
451 })
452 }
453
454 pub fn from_json(json: &str) -> Self {
465 Self::from_json_in(
466 json,
467 crate::kernel::CompileLedger::new(),
468 crate::resource::ResourceScope::new(),
469 )
470 }
471
472 pub fn from_json_for(json: &str, kernel: &dyn crate::Kernel) -> Self {
480 Self::from_json_in(json, kernel.ledger().clone(), kernel.resources().clone())
481 }
482
483 fn from_json_in(
486 json: &str,
487 ledger: Arc<crate::kernel::CompileLedger>,
488 resources: crate::resource::ResourceScope,
489 ) -> Self {
490 let spec: TileSpec = serde_json::from_str(json)
491 .unwrap_or_else(|e| panic!("tile_render: malformed skeleton payload: {e}"));
492 let name = spec.name.clone();
493 let bodies: Vec<Arc<crate::dsl::traversal::BodySource>> = spec
494 .children
495 .iter()
496 .enumerate()
497 .map(|(i, c)| {
498 Arc::new(
499 crate::dsl::traversal::BodySource::from_source_in(
500 &c.source,
501 &format!("tile '{name}' :: projection body {i}"),
502 ledger.clone(),
503 resources.clone(),
504 )
505 .unwrap_or_else(|e| {
506 panic!(
507 "tile '{name}': projection body failed to parse: {e}\n{}",
508 c.source
509 )
510 }),
511 )
512 })
513 .collect();
514 Self::from_parts(spec, bodies).unwrap_or_else(|e| panic!("tile '{name}': {e}"))
515 }
516
517 fn body_program_on(&self, child: usize, engine: crate::Engine) -> Arc<dyn KernelProgram> {
523 if matches!(engine, crate::Engine::Interpreter(_)) {
524 return self.children[child].clone();
525 }
526 self.bodies[child].program_on(engine).unwrap_or_else(|e| {
527 crate::library::support::audit::debug(&format!(
528 "tile '{}': projection body {child} renders on the interpreter: {e}",
529 self.spec.name
530 ));
531 self.children[child].clone()
532 })
533 }
534
535 pub fn has_projections(&self) -> bool {
537 fn walk(ops: &[RtOp]) -> bool {
538 ops.iter().any(|op| match op {
539 RtOp::Repeat { .. } => true,
540 RtOp::Branch {
541 then, otherwise, ..
542 } => walk(then) || walk(otherwise),
543 _ => false,
544 })
545 }
546 walk(&self.ops)
547 }
548
549 pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
553 let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
554 let mut out = String::new();
555 self.render_into(
556 &refs,
557 crate::Engine::Interpreter(crate::JitMode::Auto),
558 bodies,
559 &mut out,
560 );
561 out
562 }
563
564 pub fn render_into<W: std::fmt::Write>(
571 &self,
572 inputs: &[ValueRef<'_>],
573 engine: crate::Engine,
574 bodies: &mut BodyKernels,
575 out: &mut W,
576 ) {
577 self.render_ops(&self.ops, inputs, engine, bodies, None, out);
578 }
579
580 fn render_ops<W: std::fmt::Write>(
581 &self,
582 ops: &[RtOp],
583 inputs: &[ValueRef<'_>],
584 engine: crate::Engine,
585 bodies: &mut BodyKernels,
586 mut child: Option<&mut BodyEntry>,
587 out: &mut W,
588 ) {
589 for op in ops {
590 match op {
591 RtOp::Copy(s) => out.put(s),
594 RtOp::Hole(source, enc) => match source {
595 RtSource::Wire(i) => {
596 encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
597 }
598 RtSource::Child(name, k) => {
599 if let Some(entry) = child.as_deref_mut()
600 && let Some(i) = entry.hole(*k, name)
601 {
602 let v = entry.kernel.pull_at(i);
603 encode_ref(ValueRef::from(&v), enc, out)
604 }
605 }
606 },
607 RtOp::Branch {
608 cond,
609 then,
610 otherwise,
611 } => {
612 let c = self.truthy(cond, inputs, child.as_deref_mut());
613 let branch = if c { then } else { otherwise };
614 self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
615 }
616 RtOp::Repeat {
617 stream,
618 child: child_idx,
619 sep,
620 body,
621 generators,
622 } => {
623 let memoized = self.memo[*child_idx].clone();
632 let tuples = match &memoized {
633 Some(t) => Projection::Memo(&t[..]),
634 None => {
635 let mut streamer = (**stream).clone();
636 if !generators.is_empty() {
637 streamer.ast = bind_generators(&streamer.ast, generators, inputs);
638 }
639 Projection::Indexed(
640 evaluate_indexed(&streamer.ast, &*self.canonicals[*child_idx])
641 .unwrap_or_else(|e| {
642 panic!(
643 "tile '{}': projection `for {}` failed at render: {e}",
644 self.spec.name, streamer.text
645 )
646 }),
647 )
648 }
649 };
650 let child_spec = &self.spec.children[*child_idx];
651 let program = self.body_program_on(*child_idx, engine);
657 let mut first = true;
658 let fail = |name: &str, e: crate::kernel::WriteError| -> ! {
659 panic!(
660 "tile '{}': projection body input `{name}`: {e}",
661 self.spec.name
662 )
663 };
664 bodies.with(&program, engine, |entry, bodies| {
665 let mut buf = RuntimeTuple::new();
666 for index in 0..tuples.len() {
667 let tuple = tuples.tuple(index, &mut buf);
668 if !first {
669 out.put(sep);
670 }
671 first = false;
672 {
673 let BodyEntry {
676 kernel,
677 elements,
678 cascade,
679 ..
680 } = &mut *entry;
681 kernel.set_inputs(&[index]);
682 let elements = elements.get_or_insert_with(|| {
683 tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
684 });
685 for (k, (name, v)) in tuple.iter().enumerate() {
686 if let Some(i) = elements.get(k).copied().flatten() {
687 kernel
688 .set_input_at(i, v.clone())
689 .unwrap_or_else(|e| fail(name, e));
690 }
691 }
692 let cascade = cascade.get_or_insert_with(|| {
693 child_spec
694 .cascade
695 .iter()
696 .map(|(n, _, _)| kernel.input_index(n))
697 .collect()
698 });
699 for (k, (name, input_idx, ty)) in
700 child_spec.cascade.iter().enumerate()
701 {
702 if let Some(i) = cascade.get(k).copied().flatten()
703 && let Some(v) = inputs.get(*input_idx)
704 {
705 kernel
706 .set_input_at(i, typed_for(&owned(*v), ty))
707 .unwrap_or_else(|e| fail(name, e));
708 }
709 }
710 if !kernel.const_inits().is_empty()
713 && let Err(e) = kernel.init()
714 {
715 panic!("tile '{}': projection body: {e}", self.spec.name);
716 }
717 }
718 self.render_ops(body, inputs, engine, bodies, Some(entry), out);
719 }
720 });
721 }
722 }
723 }
724 }
725
726 fn truthy(
728 &self,
729 source: &RtSource,
730 inputs: &[ValueRef<'_>],
731 child: Option<&mut BodyEntry>,
732 ) -> bool {
733 match source {
734 RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
735 RtSource::Child(name, k) => match child {
736 Some(entry) => match entry.hole(*k, name) {
737 Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
738 None => false,
739 },
740 None => false,
741 },
742 }
743 }
744}
745
746fn owned(v: ValueRef<'_>) -> Value {
749 match v {
750 ValueRef::U64(n) => Value::U64(n),
751 ValueRef::I64(n) => Value::I64(n),
752 ValueRef::F64(f) => Value::F64(f),
753 ValueRef::Bool(b) => Value::Bool(b),
754 ValueRef::Str(s) => Value::Str(Arc::from(s)),
755 ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
756 ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
757 ValueRef::None => Value::None,
758 ValueRef::Other(v) => v.clone(),
759 }
760}
761
762fn typed_for(v: &Value, ty: &str) -> Value {
766 match (v, PortType::from_keyword(ty)) {
767 (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
768 _ => v.clone(),
769 }
770}
771
772fn bind_generators(
777 c: &crate::iteration::comprehension::Comprehension,
778 generators: &[(String, usize, String)],
779 inputs: &[ValueRef<'_>],
780) -> crate::iteration::comprehension::Comprehension {
781 use crate::iteration::comprehension::Comprehension as K;
782 use crate::iteration::comprehension::source::{LiteralValue, Source};
783 match c {
784 K::Clause {
785 name,
786 source: Source::Generator { .. },
787 } => {
788 let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
789 return c.clone();
790 };
791 let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
792 let items: Vec<Value> =
793 match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
794 Some(interior) => interior,
795 None => match &raw {
796 Value::Str(text) => {
797 match serde_json::from_str::<serde_json::Value>(text.trim()) {
798 Ok(serde_json::Value::Array(items)) => items
799 .iter()
800 .map(|j| {
801 retype(
802 &Value::Str(j.to_string().trim_matches('"').into()),
803 ty,
804 )
805 })
806 .collect(),
807 _ => vec![typed_for(&raw, ty)],
808 }
809 }
810 _ => vec![raw.clone()],
811 },
812 };
813 let json_items = ty == "json";
817 let values = items
818 .iter()
819 .map(|v| {
820 if json_items {
821 return LiteralValue::Json(json_of(v));
822 }
823 match v {
824 Value::U64(n) => LiteralValue::unsigned(*n),
825 Value::I64(n) => LiteralValue::Int(*n),
826 Value::F64(f) => LiteralValue::Float(*f),
827 Value::Bool(b) => LiteralValue::Bool(*b),
828 Value::Json(j) => match j.as_ref() {
830 serde_json::Value::Number(n) if n.is_u64() => {
831 LiteralValue::unsigned(n.as_u64().unwrap_or(0))
832 }
833 serde_json::Value::Number(n) if n.is_i64() => {
834 LiteralValue::Int(n.as_i64().unwrap_or(0))
835 }
836 serde_json::Value::Number(n) => {
837 LiteralValue::Float(n.as_f64().unwrap_or(0.0))
838 }
839 serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
840 serde_json::Value::String(s) => LiteralValue::String(s.clone()),
841 other => LiteralValue::String(other.to_string()),
842 },
843 other => LiteralValue::String(other.to_display_string()),
844 }
845 })
846 .collect();
847 K::Clause {
848 name: name.clone(),
849 source: Source::Literal { values },
850 }
851 }
852 K::Clause { .. } => c.clone(),
853 K::Cartesian { children } => K::Cartesian {
854 children: children
855 .iter()
856 .map(|ch| bind_generators(ch, generators, inputs))
857 .collect(),
858 },
859 K::Zip { children, mode } => K::Zip {
860 children: children
861 .iter()
862 .map(|ch| bind_generators(ch, generators, inputs))
863 .collect(),
864 mode: *mode,
865 },
866 K::Union { children } => K::Union {
867 children: children
868 .iter()
869 .map(|ch| bind_generators(ch, generators, inputs))
870 .collect(),
871 },
872 K::Filter { child, predicate } => K::Filter {
873 child: Box::new(bind_generators(child, generators, inputs)),
874 predicate: predicate.clone(),
875 },
876 K::Order {
877 child,
878 strategy,
879 truncation,
880 seed,
881 } => K::Order {
882 child: Box::new(bind_generators(child, generators, inputs)),
883 strategy: *strategy,
884 truncation: *truncation,
885 seed: *seed,
886 },
887 }
888}
889
890fn json_of(v: &Value) -> serde_json::Value {
895 match v {
896 Value::Json(j) => j.as_ref().clone(),
897 Value::U64(n) => serde_json::Value::from(*n),
898 Value::I64(n) => serde_json::Value::from(*n),
899 Value::F64(f) => serde_json::Number::from_f64(*f)
900 .map(serde_json::Value::Number)
901 .unwrap_or(serde_json::Value::Null),
902 Value::Bool(b) => serde_json::Value::Bool(*b),
903 Value::Str(s) => serde_json::Value::String(s.to_string()),
904 Value::None => serde_json::Value::Null,
905 other => serde_json::Value::String(other.to_display_string()),
906 }
907}
908
909fn retype(v: &Value, ty: &str) -> Value {
910 let text = v.to_display_string();
911 match PortType::from_keyword(ty) {
912 Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
913 Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
914 Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
915 Some(PortType::Str) | None => Value::Str(text.into()),
916 Some(_) => v.clone(),
917 }
918}
919
920struct BodyEntry {
924 program: Arc<dyn KernelProgram>,
925 kernel: Box<dyn Kernel>,
926 elements: Option<Vec<Option<usize>>>,
929 cascade: Option<Vec<Option<usize>>>,
931 holes: Vec<Option<Option<usize>>>,
933}
934
935impl BodyEntry {
936 fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
938 if self.holes.len() <= k {
939 self.holes.resize(k + 1, None);
940 }
941 if self.holes[k].is_none() {
942 self.holes[k] = Some(self.kernel.output_index(name));
943 }
944 self.holes[k].flatten()
945 }
946}
947
948#[derive(Default)]
956pub struct BodyKernels {
957 entries: HashMap<(usize, crate::Engine), BodyEntry>,
958 created: u64,
960}
961
962impl Clone for BodyKernels {
963 fn clone(&self) -> Self {
964 Self::default()
965 }
966}
967
968unsafe impl Sync for BodyKernels {}
975
976impl std::fmt::Debug for BodyKernels {
977 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
978 f.debug_struct("BodyKernels")
979 .field("entries", &self.entries.len())
980 .field("created", &self.created)
981 .finish()
982 }
983}
984
985fn body_engine_key(engine: crate::Engine) -> crate::Engine {
988 match engine {
989 crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
990 other => other,
991 }
992}
993
994impl BodyKernels {
995 pub fn created(&self) -> u64 {
997 self.created
998 }
999
1000 #[cfg(test)]
1002 fn clone_for_test(&self) -> (u64, BodyKernels) {
1003 (self.created, self.clone())
1004 }
1005
1006 fn with(
1010 &mut self,
1011 program: &Arc<dyn KernelProgram>,
1012 engine: crate::Engine,
1013 f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
1014 ) {
1015 let engine = body_engine_key(engine);
1016 let key = (Arc::as_ptr(program) as *const () as usize, engine);
1019 let mut entry = self
1020 .entries
1021 .remove(&key)
1022 .filter(|e| Arc::ptr_eq(&e.program, program))
1023 .unwrap_or_else(|| {
1024 self.created += 1;
1025 BodyEntry {
1026 program: program.clone(),
1027 kernel: program.clone().create_uninitialized(),
1028 elements: None,
1029 cascade: None,
1030 holes: Vec::new(),
1031 }
1032 });
1033 f(&mut entry, self);
1034 self.entries.insert(key, entry);
1035 }
1036}
1037
1038pub(crate) mod render_state {
1040 use super::{BodyKernels, TileRender};
1041 use crate::ast::{ScratchBuf, ScratchElem, Value};
1042
1043 pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
1044 vec![ScratchElem::Kernels]
1045 }
1046
1047 pub(crate) fn eval(
1048 node: &TileRender,
1049 scratch: &mut [ScratchBuf],
1050 inputs: &[Value],
1051 outputs: &mut [Value],
1052 ) {
1053 let bodies = bodies_of(&mut scratch[0]);
1054 outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1055 }
1056
1057 pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1059 match entry {
1060 ScratchBuf::Kernels(b) => b,
1061 other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1062 }
1063 }
1064}
1065
1066pub(crate) trait Sink: std::fmt::Write {
1070 fn put(&mut self, s: &str) {
1071 let _ = self.write_str(s);
1072 }
1073 fn put_char(&mut self, c: char) {
1074 let _ = self.write_char(c);
1075 }
1076}
1077
1078impl<W: std::fmt::Write> Sink for W {}
1079
1080pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1082 encode_ref(ValueRef::from(value), enc, out)
1083}
1084
1085pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1089 if enc.cond {
1090 out.put_char(if truthy_of(value) { '1' } else { '0' });
1091 return;
1092 }
1093 let ty = enc.ty.as_deref();
1094 if is_numeric_keyword(ty.unwrap_or("u64")) {
1103 match (enc.format.as_deref(), value) {
1104 (None, ValueRef::U64(n)) => {
1105 put_u64(n, out);
1106 return;
1107 }
1108 (None, ValueRef::I64(n)) => {
1109 if n < 0 {
1110 out.put_char('-');
1111 }
1112 put_u64(n.unsigned_abs(), out);
1113 return;
1114 }
1115 (None, ValueRef::F64(f)) => {
1116 let _ = float_text::write_shortest(f, out);
1117 return;
1118 }
1119 (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1120 if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1121 let _ = float_text::write_fixed(f, prec, out);
1122 return;
1123 }
1124 }
1125 _ => {}
1126 }
1127 }
1128 let text = formatted_text(value, ty, enc.format.as_deref());
1129 if enc.raw {
1130 out.put(&text);
1131 return;
1132 }
1133 match (enc.encoding.as_str(), enc.position) {
1134 ("json", HolePosition::InString) => push_json_escaped(&text, out),
1135 ("json", HolePosition::Value) => {
1136 let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1137 match (kind, value) {
1138 (_, ValueRef::None) => out.put("null"),
1139 ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1140 ("json", ValueRef::Json(j)) => {
1141 let _ = write!(out, "{j}");
1142 }
1143 ("str", _) | ("String", _) | ("string", _) => {
1144 out.put_char('"');
1145 push_json_escaped(&text, out);
1146 out.put_char('"');
1147 }
1148 (k, _) if is_numeric_keyword(k) => out.put(&text),
1149 (_, ValueRef::Json(j)) => {
1150 let _ = write!(out, "{j}");
1151 }
1152 (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1153 (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1154 _ => {
1155 out.put_char('"');
1156 push_json_escaped(&text, out);
1157 out.put_char('"');
1158 }
1159 }
1160 }
1161 ("csv", _) => {
1162 if text.contains([',', '"', '\n']) {
1163 out.put_char('"');
1164 for (i, piece) in text.split('"').enumerate() {
1165 if i > 0 {
1166 out.put("\"\"");
1167 }
1168 out.put(piece);
1169 }
1170 out.put_char('"');
1171 } else {
1172 out.put(&text);
1173 }
1174 }
1175 _ => out.put(&text),
1176 }
1177}
1178
1179fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1181 if n == 0 {
1182 out.put_char('0');
1183 return;
1184 }
1185 let mut buf = [0u8; 20];
1186 let mut i = buf.len();
1187 while n > 0 {
1188 i -= 1;
1189 buf[i] = b'0' + (n % 10) as u8;
1190 n /= 10;
1191 }
1192 out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1194}
1195
1196fn truthy_of(v: ValueRef<'_>) -> bool {
1197 match v {
1198 ValueRef::Bool(b) => b,
1199 ValueRef::U64(n) => n != 0,
1200 ValueRef::F64(f) => f != 0.0,
1201 ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1202 ValueRef::None => false,
1203 _ => true,
1204 }
1205}
1206
1207fn is_numeric_keyword(k: &str) -> bool {
1208 matches!(
1209 k,
1210 "u64"
1211 | "i64"
1212 | "f64"
1213 | "f32"
1214 | "u32"
1215 | "i32"
1216 | "u16"
1217 | "i16"
1218 | "u8"
1219 | "i8"
1220 | "u128"
1221 | "i128"
1222 | "f16"
1223 )
1224}
1225
1226fn formatted_text<'a>(
1230 value: ValueRef<'a>,
1231 ty: Option<&str>,
1232 format: Option<&str>,
1233) -> std::borrow::Cow<'a, str> {
1234 use std::borrow::Cow;
1235 let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1239 match (ty, value) {
1240 (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1241 (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1244 (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1245 (_, v) => v.display(),
1246 }
1247 };
1248 let Some(fmt) = format else {
1249 return base(value);
1250 };
1251 let fmt = fmt.trim();
1252 if let Some(prec) = precision_of(fmt) {
1253 if let Some(f) = as_f64(value) {
1254 return Cow::Owned(float_text::fixed_string(f, prec));
1255 }
1256 return base(value);
1257 }
1258 if fmt == "x" || fmt == "X" {
1259 if let ValueRef::U64(n) = value {
1260 return Cow::Owned(if fmt == "x" {
1261 format!("{n:x}")
1262 } else {
1263 format!("{n:X}")
1264 });
1265 }
1266 return base(value);
1267 }
1268 let base = base(value);
1269 if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1270 return Cow::Owned(format!("{base:0>w$}"));
1271 }
1272 if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1273 return Cow::Owned(format!("{base:>w$}"));
1274 }
1275 if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1276 return Cow::Owned(format!("{base:<w$}"));
1277 }
1278 if let Ok(w) = fmt.parse::<usize>() {
1279 return Cow::Owned(format!("{base:>w$}"));
1280 }
1281 base
1282}
1283
1284fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1285 match v {
1286 ValueRef::F64(f) => Some(f),
1287 ValueRef::U64(n) => Some(n as f64),
1288 _ => None,
1289 }
1290}
1291
1292fn precision_of(fmt: &str) -> Option<usize> {
1294 fmt.trim()
1295 .strip_prefix('.')
1296 .and_then(|p| p.parse::<usize>().ok())
1297}
1298
1299fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1300 for c in s.chars() {
1301 match c {
1302 '"' => out.put("\\\""),
1303 '\\' => out.put("\\\\"),
1304 '\n' => out.put("\\n"),
1305 '\r' => out.put("\\r"),
1306 '\t' => out.put("\\t"),
1307 c if (c as u32) < 0x20 => {
1308 let _ = write!(out, "\\u{:04x}", c as u32);
1309 }
1310 c => out.put_char(c),
1311 }
1312 }
1313}
1314
1315#[crate::polydat_node(category = Formatting)]
1323fn tile_encode(
1324 value: Value,
1325 spec: Const<&str>,
1326 #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1327) -> String {
1328 let mut out = String::new();
1329 encode(&value, enc, &mut out);
1330 out
1331}
1332
1333fn tile_render_compiled(
1341 node: &TileRender,
1342 wire_types: &[PortType],
1343 engine: crate::Engine,
1344) -> crate::ast::CompiledSlotKit {
1345 let program: &'static TileProgram = unsafe { &*std::sync::Arc::into_raw(node.program.clone()) };
1351 let mut reads: Vec<(usize, PortType)> = Vec::with_capacity(wire_types.len());
1354 let mut offset = 0usize;
1355 for &ty in wire_types {
1356 reads.push((offset, ty));
1357 offset += ty.slot_width().max(1);
1358 }
1359 crate::ast::CompiledSlotKit {
1360 scratch: vec![
1361 crate::ast::ScratchElem::Str,
1362 crate::ast::ScratchElem::Kernels,
1363 ],
1364 op: Box::new(
1365 move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [crate::ast::ScratchBuf]| {
1366 let owned_values: Vec<Value> = reads
1370 .iter()
1371 .filter(|(_, ty)| ty.slot_color() == crate::ast::SlotColor::Imm2)
1372 .map(|&(offset, ty)| crate::compile::marshal::decode_output(inputs, offset, ty))
1373 .collect();
1374 let mut next_owned = 0usize;
1375 let refs: Vec<ValueRef<'_>> = reads
1376 .iter()
1377 .map(|&(offset, ty)| {
1378 if ty.slot_color() == crate::ast::SlotColor::Imm2 {
1379 let v = ValueRef::from(&owned_values[next_owned]);
1380 next_owned += 1;
1381 v
1382 } else {
1383 unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1386 }
1387 })
1388 .collect();
1389 let (text, bodies) = scratch.split_at_mut(1);
1398 let crate::ast::ScratchBuf::Str(buf) = &mut text[0] else {
1399 unreachable!("the render step owns a string entry");
1400 };
1401 let bodies = render_state::bodies_of(&mut bodies[0]);
1402 buf.clear();
1403 let mut w = BytesSink(buf);
1404 program.render_into(&refs, engine, bodies, &mut w);
1405 let (p, l) = scratch[0].ptr_len();
1406 outputs[0] = p;
1407 outputs[1] = l;
1408 },
1409 ),
1410 }
1411}
1412
1413pub(crate) struct BytesSink<'a>(pub(crate) &'a mut Vec<u8>);
1416
1417impl std::fmt::Write for BytesSink<'_> {
1418 fn write_str(&mut self, s: &str) -> std::fmt::Result {
1419 self.0.extend_from_slice(s.as_bytes());
1420 Ok(())
1421 }
1422}
1423
1424#[crate::polydat_node(
1433 category = Formatting,
1434 variadic_min = 0,
1435 compiled_slot = tile_render_compiled,
1436 state = render_state
1437)]
1438fn tile_render(program: Const<Arc<TileProgram>>, values: &[Value]) -> String {
1439 program.render(values, &mut BodyKernels::default())
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446 use super::*;
1447
1448 #[test]
1452 fn body_kernels_are_created_once_per_state_and_reused() {
1453 let src =
1454 "input cycle: u64\ntile t : text := \"@for k in 0..3 sep \\\",\\\" {${k + cycle}}\"\n";
1455 let mut k = crate::dsl::compile_polydat_interpreter(src).unwrap();
1456 let program = k.program();
1457 let node = (0..program.node_count())
1458 .find(|&i| program.node_meta(i).name == "tile_render")
1459 .expect("the tile's render node");
1460 let bodies_of = |k: &mut PolydatKernel| match &k.state().core.node_scratch[node][0] {
1461 crate::ast::ScratchBuf::Kernels(b) => b.clone_for_test(),
1462 other => panic!("{other:?}"),
1463 };
1464 assert_eq!(bodies_of(&mut k).0, 0, "nothing before the first render");
1465 k.set_inputs(&[10]);
1466 assert_eq!(k.pull_ref("t").as_str(), "10,11,12");
1467 assert_eq!(bodies_of(&mut k).0, 1, "one kernel for the body");
1468 for c in 0..5u64 {
1469 k.set_inputs(&[c]);
1470 let _ = k.pull_ref("t");
1471 }
1472 let (created, clone) = bodies_of(&mut k);
1473 assert_eq!(created, 1, "reused across renders");
1474 assert_eq!(clone.created(), 0, "a clone is a new state's empty set");
1475 }
1476
1477 fn enc(
1478 encoding: &str,
1479 position: HolePosition,
1480 ty: Option<&str>,
1481 format: Option<&str>,
1482 raw: bool,
1483 ) -> HoleEncoding {
1484 HoleEncoding {
1485 encoding: encoding.into(),
1486 position,
1487 ty: ty.map(str::to_string),
1488 format: format.map(str::to_string),
1489 raw,
1490 cond: false,
1491 }
1492 }
1493
1494 #[test]
1495 fn json_value_and_string_positions_encode_by_type() {
1496 let mut out = String::new();
1497 encode(
1498 &Value::Str("a\"b".into()),
1499 &enc("json", HolePosition::Value, Some("str"), None, false),
1500 &mut out,
1501 );
1502 assert_eq!(out, "\"a\\\"b\"");
1503 out.clear();
1504 encode(
1505 &Value::U64(7),
1506 &enc("json", HolePosition::Value, None, None, false),
1507 &mut out,
1508 );
1509 assert_eq!(out, "7");
1510 out.clear();
1511 encode(
1512 &Value::Str("x\ny".into()),
1513 &enc("json", HolePosition::InString, None, None, false),
1514 &mut out,
1515 );
1516 assert_eq!(out, "x\\ny");
1517 out.clear();
1518 encode(
1519 &Value::F64(2.0 / 3.0),
1520 &enc("json", HolePosition::Value, None, Some(".2"), false),
1521 &mut out,
1522 );
1523 assert_eq!(out, "0.67");
1524 out.clear();
1525 encode(
1526 &Value::None,
1527 &enc("json", HolePosition::Value, None, None, false),
1528 &mut out,
1529 );
1530 assert_eq!(out, "null");
1531 }
1532
1533 #[test]
1534 fn spec_round_trips() {
1535 let e = enc(
1536 "json",
1537 HolePosition::InString,
1538 Some("u64"),
1539 Some(".2"),
1540 true,
1541 );
1542 assert_eq!(HoleEncoding::from_spec(&e.to_spec()), e);
1543 let c = HoleEncoding {
1544 cond: true,
1545 ..enc("text", HolePosition::Text, None, None, false)
1546 };
1547 assert_eq!(HoleEncoding::from_spec(&c.to_spec()), c);
1548 }
1549
1550 #[test]
1551 fn csv_quotes_when_needed_and_raw_skips_escaping() {
1552 let mut out = String::new();
1553 encode(
1554 &Value::Str("a,b".into()),
1555 &enc("csv", HolePosition::Text, None, None, false),
1556 &mut out,
1557 );
1558 assert_eq!(out, "\"a,b\"");
1559 out.clear();
1560 encode(
1561 &Value::Str("a\"b".into()),
1562 &enc("json", HolePosition::Value, None, None, true),
1563 &mut out,
1564 );
1565 assert_eq!(out, "a\"b");
1566 }
1567
1568 #[test]
1569 fn formats_apply_before_encoding() {
1570 assert_eq!(formatted_text(ValueRef::U64(5), None, Some("03")), "005");
1571 assert_eq!(formatted_text(ValueRef::U64(255), None, Some("x")), "ff");
1572 assert_eq!(
1573 formatted_text(ValueRef::Str("ab"), None, Some(">4")),
1574 " ab"
1575 );
1576 assert_eq!(
1577 formatted_text(ValueRef::F64(0.295), None, Some(".2")),
1578 "0.29"
1579 );
1580 assert_eq!(
1581 formatted_text(ValueRef::U64(7), None, Some(" .3 ")),
1582 "7.000"
1583 );
1584 }
1585
1586 #[test]
1589 fn float_holes_write_rust_text() {
1590 let cases: [(f64, Option<&str>, &str); 8] = [
1591 (100.0, None, "100.0"),
1592 (0.1, None, "0.1"),
1593 (5e-5, None, "5e-5"),
1594 (1e16, None, "1e16"),
1595 (-0.0, None, "-0.0"),
1596 (2.0 / 3.0, Some(".2"), "0.67"),
1597 (0.295, Some(".2"), "0.29"),
1598 (2.5, Some(".0"), "2"),
1599 ];
1600 for (f, fmt, want) in cases {
1601 for (encoding, position) in [
1602 ("text", HolePosition::Text),
1603 ("json", HolePosition::Value),
1604 ("json", HolePosition::InString),
1605 ("csv", HolePosition::Text),
1606 ] {
1607 for ty in [None, Some("f64")] {
1608 let mut out = String::new();
1609 encode(
1610 &Value::F64(f),
1611 &enc(encoding, position, ty, fmt, false),
1612 &mut out,
1613 );
1614 assert_eq!(out, want, "{f:?} {fmt:?} {encoding} {position:?} {ty:?}");
1615 }
1616 }
1617 let mut out = String::new();
1620 encode(
1621 &Value::F64(f),
1622 &enc("json", HolePosition::Value, Some("str"), fmt, false),
1623 &mut out,
1624 );
1625 assert_eq!(out, format!("\"{want}\""));
1626 }
1627 }
1628
1629 #[test]
1633 fn a_tile_reports_whether_any_op_re_runs_a_projection() {
1634 fn wire(index: usize) -> HoleSource {
1635 HoleSource::Wire {
1636 index,
1637 spec: "text|text".into(),
1638 }
1639 }
1640 fn program(ops: Vec<TileOp>, children: Vec<ChildSpec>) -> TileProgram {
1641 TileProgram::from_json(
1642 &TileSpec {
1643 name: "t".into(),
1644 encoding: "text".into(),
1645 ops,
1646 children,
1647 }
1648 .to_json(),
1649 )
1650 }
1651 let body = || ChildSpec {
1652 source: "input cycle: u64\nextern k: u64\nout := u64_add(k, 0)\n".to_string(),
1653 cascade: Vec::new(),
1654 };
1655 let repeat = |child: usize| TileOp::Repeat {
1656 stream: StreamerValue::parse_text("k in 0..3").unwrap().to_json(),
1657 child,
1658 sep: ",".into(),
1659 body: vec![TileOp::Hole(HoleSource::Child {
1660 name: "out".into(),
1661 spec: "text|text".into(),
1662 })],
1663 generators: Vec::new(),
1664 };
1665
1666 let flat = program(
1668 vec![
1669 TileOp::Static("a=".into()),
1670 TileOp::Hole(wire(0)),
1671 TileOp::Branch {
1672 cond: wire(1),
1673 then: vec![TileOp::Static("yes".into())],
1674 otherwise: vec![TileOp::Hole(wire(0))],
1675 },
1676 ],
1677 Vec::new(),
1678 );
1679 assert!(!flat.has_projections());
1680 assert!(flat.children.is_empty());
1681
1682 let top = program(vec![TileOp::Static("[".into()), repeat(0)], vec![body()]);
1684 assert!(top.has_projections());
1685 assert_eq!(top.children.len(), 1);
1686
1687 for (then, otherwise) in [
1690 (vec![repeat(0)], vec![TileOp::Static("none".into())]),
1691 (vec![TileOp::Static("none".into())], vec![repeat(0)]),
1692 ] {
1693 let branched = program(
1694 vec![TileOp::Branch {
1695 cond: wire(0),
1696 then,
1697 otherwise,
1698 }],
1699 vec![body()],
1700 );
1701 assert!(branched.has_projections());
1702 }
1703 }
1704}