Skip to main content

polydat_core/library/
tile_render.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The tile nodes (SRD 114 §6, §7.1).
5//!
6//! The compiler lowers a `tile` statement to one `tile_render` binding
7//! over the hole wires: the render node carries each hole's encoding
8//! spec and encodes the value at the hole, concatenates static runs
9//! with encoded holes, selects branches, and re-runs projection bodies
10//! per tuple over a scratch state, using a skeleton it parses once at
11//! setup.
12//!
13//! `tile_encode` encodes one value under one hole's spec. The compiler
14//! emitted one per hole until encoding moved into the renderer; it is
15//! a library node now, which a program may call by name and nothing
16//! generates.
17
18use 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/// Where a hole sits in a `json` skeleton, which decides its encoding.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum HolePosition {
33    /// A JSON value position: numbers bare, strings quoted.
34    Value,
35    /// Inside a JSON string literal: escaped text only.
36    InString,
37    /// Plain text (text and csv encodings).
38    Text,
39}
40
41/// The encoder for one hole: what the renderer applies at the hole,
42/// and what the `tile_encode` node takes as its spec.
43/// Serialized compactly as `encoding|position|type|format|flags`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct HoleEncoding {
46    /// The tile's encoding.
47    pub encoding: String,
48    /// Where in the output the hole sits: a value, inside a string, or text.
49    pub position: HolePosition,
50    /// The declared type, if any.
51    pub ty: Option<String>,
52    /// The format, if any.
53    pub format: Option<String>,
54    /// Whether the value is emitted without the encoding's escaping.
55    pub raw: bool,
56    /// Encode as a branch condition: `1` or `0`.
57    pub cond: bool,
58}
59
60impl HoleEncoding {
61    /// The compact spec form, `encoding|position|type|format|flags`.
62    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    /// The encoding a spec names; a missing part takes its default.
86    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/// Where a hole's encoded text comes from at render time.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub enum HoleSource {
111    /// The render node's wire input at this index, encoded per `spec`
112    /// (a [`HoleEncoding`] spec) as it is rendered.
113    Wire {
114        /// The input's index among the render node's wires.
115        index: usize,
116        /// The hole's encoding spec.
117        spec: String,
118    },
119    /// An output of the enclosing projection's body program, encoded
120    /// per `spec` as it is rendered.
121    Child {
122        /// The body program's output.
123        name: String,
124        /// The hole's encoding spec.
125        spec: String,
126    },
127}
128
129/// One skeleton instruction. Holes are values, encoded at the hole.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub enum TileOp {
132    /// Text copied as is.
133    Static(String),
134    /// A hole, already encoded.
135    Hole(HoleSource),
136    /// A projection: the body rendered once per tuple.
137    Repeat {
138        /// The comprehension, as a serialized [`StreamerValue`].
139        stream: String,
140        /// Index into [`TileSpec::children`].
141        child: usize,
142        /// The separator between tuples.
143        sep: String,
144        /// The body skeleton.
145        body: Vec<TileOp>,
146        /// Generator-call clauses whose expressions compiled to wires of
147        /// the enclosing program: `(element, node input index, type)`.
148        /// At render the input's value stands in for the clause.
149        #[serde(default)]
150        generators: Vec<(String, usize, String)>,
151    },
152    /// A branch on a condition hole.
153    Branch {
154        /// The condition, encoded as `1` or `0`.
155        cond: HoleSource,
156        /// The skeleton when the condition holds.
157        then: Vec<TileOp>,
158        /// The skeleton otherwise.
159        otherwise: Vec<TileOp>,
160    },
161}
162
163/// A projection body: a program compiled once at setup, and the outer
164/// wires it imports from the render node's inputs, each with the type
165/// its extern declares so the transported text can be re-typed.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct ChildSpec {
168    /// The body program's source.
169    pub source: String,
170    /// `(extern name, render-node input index, port-type keyword)`.
171    pub cascade: Vec<(String, usize, String)>,
172}
173
174/// The serialized skeleton.
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct TileSpec {
177    /// The tile's name.
178    pub name: String,
179    /// The tile's encoding.
180    pub encoding: String,
181    /// The skeleton.
182    pub ops: Vec<TileOp>,
183    /// The projection body programs, by index.
184    pub children: Vec<ChildSpec>,
185}
186
187impl TileSpec {
188    /// The spec as JSON, the form the `tile_render` node's spec argument carries.
189    pub fn to_json(&self) -> String {
190        serde_json::to_string(self).expect("TileSpec serializes")
191    }
192}
193
194/// One skeleton instruction in its runtime form (SRD 114 §6): static
195/// runs are interned once at build and copied from the interner, the
196/// comprehension of a projection is parsed once, and separators are
197/// interned too.
198#[derive(Debug)]
199enum RtOp {
200    /// Copy an interned static run.
201    Copy(&'static str),
202    /// Encode a value at the hole.
203    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/// Where a hole's value comes from at render time.
219#[derive(Debug)]
220enum RtSource {
221    Wire(usize),
222    /// A body output and its ordinal among the body's holes, which a
223    /// body kernel's entry resolves to an output index once.
224    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
237/// Intern every static run and separator of a skeleton and parse every
238/// projection stream, once, at construction.
239fn 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
274/// The runtime form: the spec, compiled body programs, and parsed streams.
275pub struct TileProgram {
276    /// The serialized skeleton.
277    pub spec: TileSpec,
278    /// The skeleton with statics interned and streams parsed.
279    ops: Vec<RtOp>,
280    /// Each projection body, as the one carrier a `for` body uses:
281    /// its statements, the settings it compiles under, and its
282    /// program per engine, built on first use.
283    ///
284    /// This used to be two eager compiles per body — one interpreter
285    /// program and one on `Engine::default()` — made at node
286    /// construction whether or not either engine ever rendered it.
287    pub bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
288    /// The body programs for the interpreter, which construction
289    /// needs: the canonical kernels are built over them, and
290    /// memoisation runs against those.
291    pub children: Vec<Arc<PolydatProgram>>,
292    /// One kernel over each body program, the canonical kernel the
293    /// comprehension evaluator installs tuple values into.
294    canonicals: Vec<Arc<PolydatKernel>>,
295    /// Per body, its projection's tuples when the comprehension is the
296    /// same every render: no generator clause and no placeholder in
297    /// its sources. Evaluated once at construction.
298    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
311/// Give every body hole an ordinal within its body, so a body kernel's
312/// entry can resolve the hole's output index once and keep it by
313/// position (SRD 117 step 3).
314fn 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
346/// Evaluate every projection whose tuples cannot change between
347/// renders, once.
348fn 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    /// A program from a skeleton and the bodies its projections run.
383    ///
384    /// The compiler's path: it lowered the bodies, so it hands them
385    /// over as they are. Each body carries the settings the parent
386    /// compiled under, and its program per engine is built when a
387    /// render on that engine first asks for it.
388    pub fn from_parts(
389        spec: TileSpec,
390        bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
391    ) -> Result<Self, String> {
392        // Construction needs the interpreter's program: the canonical
393        // kernel the comprehension evaluator installs tuples into is
394        // built over it, and memoisation runs there. Every other
395        // engine's is built on the first render that asks for it.
396        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    /// A program from a serialized skeleton, whose projection bodies
426    /// are rebuilt from the source text it carries under the default
427    /// settings.
428    ///
429    /// The route for a skeleton that reaches the runtime as text — a
430    /// host that stored one, a test that wrote one. The compiler's own
431    /// path is [`Self::from_parts`], which hands the bodies over
432    /// rather than describing them.
433    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    /// The body program of projection `child` for a render on
460    /// `engine` — the engine the kernel doing the rendering runs on,
461    /// which is the rule a `for` body already followed.
462    ///
463    /// A tile's body used to render on `Engine::default()` whatever
464    /// engine the kernel was, because both of its programs were built
465    /// at construction and the default one was the only compiled
466    /// program there was. An engine that refuses the body falls back
467    /// to the interpreter's, as before, and says so once.
468    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    /// True when any op re-runs a projection body.
482    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    /// Render with the node's wire inputs, the hole values, on the
496    /// interpreter, over `bodies`, the rendering state's own kernels
497    /// for the projection bodies.
498    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    /// Render into any text sink from borrowed views of the hole
511    /// values: a `String` at P1, a step's own scratch in a compiled
512    /// closure. Every hole is encoded here, from the view straight into
513    /// the sink, and a projection's body runs on `engine`, the engine
514    /// of the kernel rendering, in a kernel the rendering state owns
515    /// (`bodies`) and reuses across renders.
516    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                // `Copy`: a memcpy from the static interner (SRD 114 §6,
538                // SRD 115 step 3). The bytes were interned at build.
539                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                    // The tuples: memoized when the comprehension is the
570                    // same every render, otherwise evaluated now with the
571                    // same evaluator the `for` construct opens a traversal
572                    // with, which applies order strategies, samples
573                    // continuous sources, and runs predicates over the
574                    // tuple. Generators are bound first, so the parent
575                    // kernel it sees is empty and the canonical kernel is
576                    // the body program.
577                    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                    // The body runs on the engine the kernel rendering
601                    // it runs on, which is the rule a `for` body
602                    // already followed. The kernel over its program is
603                    // owned by the rendering state and reused across
604                    // its renders.
605                    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                                // The body's inputs by index: the names are
621                                // resolved on the first tuple and kept.
622                                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                                // Each element is a binding of its own, so
658                                // the body's consts are evaluated for it.
659                                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    /// A branch condition's truth, as the `cond` encoding decides it.
674    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
693/// A borrowed view as an owned value, for the paths that bind values
694/// into a body program or a comprehension.
695fn 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
709/// A cascaded value as the body's extern expects it. Values arrive on
710/// the render node's inputs as they are, so this is the value itself;
711/// text is parsed only when a `Str` reaches a non-string extern.
712fn 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
719/// Replace each generator-call clause with the literal values its wire
720/// carries at this render: a list value (a stream, a vector, a JSON
721/// array) contributes its items, a scalar contributes itself. Text that
722/// spells a JSON array is read as one.
723fn 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            // An element declared `json` takes every item as the JSON
761            // value it is, its kind kept, so the body's extern receives
762            // what it declares; another declared type takes the scalar.
763            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                        // JSON scalars carry their own kind.
776                        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
837/// Recover a typed value from the display text a cascaded wire arrives
838/// as, using the child extern's declared type.
839/// A generator item as a JSON value: a JSON item as it is, a scalar as
840/// the JSON of its kind.
841fn 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
867/// A cached body kernel: the program it was created from, the kernel,
868/// and the body's names resolved to indices once (SRD 117 step 3), so
869/// a tuple is bound and its holes read with no lookup per tuple.
870struct BodyEntry {
871    program: Arc<dyn KernelProgram>,
872    kernel: Box<dyn Kernel>,
873    /// The tuple elements' input indices, by position in the tuple;
874    /// `None` for an element the body does not declare.
875    elements: Option<Vec<Option<usize>>>,
876    /// The cascade's input indices, by position in the cascade.
877    cascade: Option<Vec<Option<usize>>>,
878    /// The body holes' output indices, by ordinal.
879    holes: Vec<Option<Option<usize>>>,
880}
881
882impl BodyEntry {
883    /// The output index of body hole `k`, named `name`, resolved once.
884    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/// The kernels one rendering state keeps over its projection bodies:
896/// one per body program and engine, created on the first render that
897/// reaches the body and reused by every render after, so a projection
898/// creates nothing per tuple. A tile render node owns one of these in
899/// its scratch (axiom S3): the storage belongs to the state that
900/// renders, never to the node, which every state of the program
901/// shares. A clone is empty, since a clone of a state is a new state.
902#[derive(Default)]
903pub struct BodyKernels {
904    entries: HashMap<(usize, crate::Engine), BodyEntry>,
905    /// Kernels created so far, for the tests.
906    created: u64,
907}
908
909impl Clone for BodyKernels {
910    fn clone(&self) -> Self {
911        Self::default()
912    }
913}
914
915// SAFETY: the kernels are reached only through `&mut self` (`with`),
916// which the owning state holds exclusively; every `&self` method
917// (`created`, `clone`, `Debug`) reads a count and touches no kernel. A
918// set inside a program shared across threads is therefore never used
919// from more than one thread, and a state created from that program
920// starts with an empty set of its own.
921unsafe 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
932/// The engine a body kernel is kept under: the interpreter's body
933/// program is one program whatever the enclosing kernel's cone mode.
934fn 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    /// Kernels this state has created so far.
943    pub fn created(&self) -> u64 {
944        self.created
945    }
946
947    /// The count and a clone, for the unit test of both.
948    #[cfg(test)]
949    fn clone_for_test(&self) -> (u64, BodyKernels) {
950        (self.created, self.clone())
951    }
952
953    /// Run `f` over the kernel for `program` on `engine`, created on
954    /// first use. The entry is taken out for the call, so a nested
955    /// projection's body finds the set free for its own kernels.
956    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        // The entry pins its program so the address cannot be reused by
964        // a later program while a kernel built for this one is kept.
965        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
985/// The tile render node's state: its projection bodies' kernels.
986pub(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    /// The body kernel set a scratch entry holds.
1005    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
1013/// A text sink that cannot fail: a `String`, or the `BytesSink` over a
1014/// step's string scratch in the compiled closure. `fmt::Write`'s
1015/// results are ignored because neither sink reports an error.
1016pub(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
1027/// Encode one value per a hole's encoding, into any text sink.
1028pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1029    encode_ref(ValueRef::from(value), enc, out)
1030}
1031
1032/// Encode a borrowed view of a value (SRD 115 §6.1): the compiled
1033/// closure calls this on its slot without owning a `Value`, and a
1034/// string hole is encoded from its producer's scratch in place.
1035pub 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    // A number with no format, or a float under a `.N` precision,
1042    // writes its digits straight into the sink (SRD 117 step 3):
1043    // digits, a sign, and a point need no escaping in any encoding or
1044    // position, and a numeric type is written bare in a JSON value
1045    // position, so the text is the same as the general path's, without
1046    // the `String` the general path builds. The float writer is
1047    // byte-identical to `format!` (`support::float_text`, proved by
1048    // `tests/float_text.rs`).
1049    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
1126/// The decimal digits of `n`, written without an allocation.
1127fn 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    // SAFETY-free: the buffer holds ASCII digits only.
1140    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
1173/// Display text for a value under an optional printf-style format:
1174/// `.N` precision for floats, `0N` zero-padded width, `N` width, `>N`
1175/// and `<N` alignment, `x`/`X` hex for integers.
1176fn 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    // A string with no format is borrowed as it is; everything else is
1183    // owned text. The base text is produced only where a format needs
1184    // it: a precision or a hex format writes the number once, itself.
1185    let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1186        match (ty, value) {
1187            (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1188            // Text quotes nothing: a JSON string in a text position is
1189            // its text, as a `str` hole is.
1190            (_, 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
1239/// The `N` of a `.N` precision format, after trimming.
1240fn 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/// Encode one value under a hole's spec
1263/// (`encoding|position|type|format|flags`).
1264///
1265/// A library node a program may call. The compiler emitted one of
1266/// these per hole until encoding moved into the renderer, which
1267/// encodes each hole where it stands in the skeleton; nothing
1268/// generates this node now.
1269#[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
1280/// The closure-tier form of `tile_render` (SRD 117 step 1): every hole
1281/// value is read from its slot as a borrowed view, by the wire type the
1282/// kernel fixed, and the document is rendered straight into the step's
1283/// own string scratch through a `BytesSink`; nothing is decoded into
1284/// an owned `Value` on the way. A wire
1285/// wider than one slot, or of a kind without a view, is read as a value
1286/// through the typed decoder.
1287fn tile_render_compiled(
1288    node: &TileRender,
1289    wire_types: &[PortType],
1290    engine: crate::Engine,
1291) -> crate::ast::CompiledSlotKit {
1292    // Native code bakes this address, so it must outlive every kernel
1293    // compiled from the program: one reference count of the node's own
1294    // `Arc` is given up here and never taken back. This used to be a
1295    // process-wide table keyed by the whole JSON payload, which made
1296    // "the same tile" mean "the same bytes of JSON".
1297    let program: &'static TileProgram = unsafe { &*std::sync::Arc::into_raw(node.program.clone()) };
1298    // Per wire: its first slot and its type; a one-slot carrier or a
1299    // `Ref2` kind is viewed in place, a two-slot immediate is decoded.
1300    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                // Owned values only for the two-slot immediates; they keep
1314                // their positions, so the views are built once they are
1315                // all in place.
1316                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                            // SAFETY: a pair in the buffer was published by
1331                            // a producer whose storage is alive (S3, S4).
1332                            unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1333                        }
1334                    })
1335                    .collect();
1336                // The document is rendered straight into this step's own
1337                // scratch (axiom S3). A projection body runs on the
1338                // engine this kit was built for — the engine of the
1339                // kernel doing the rendering — which is the rule a
1340                // `for` body already follows. The closure had no engine
1341                // to ask until `compiled_slot` was given the one it is
1342                // building for; before that every compiled kernel
1343                // rendered its bodies on `Engine::default()`.
1344                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
1360/// A text sink over the bytes of a step's string scratch: what a
1361/// compiled render writes into.
1362pub(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/// Render a compiled tile skeleton over its encoded hole texts.
1372///
1373/// The compiler emits this for a `tile` statement and hands it the
1374/// skeleton it built, projection bodies and all. The skeleton used to
1375/// travel as JSON in a string constant, which this node parsed back
1376/// and compiled at construction: a malformed payload was a panic here
1377/// rather than a compile error, the body's source lived in three
1378/// places, and what the compiler knew about the body that JSON cannot
1379/// carry — its source directory, library paths, strict flag, pragmas,
1380/// and the modules the program had resolved — was lost on the way.
1381#[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    // A render without a state's scratch (a node evaluated on its
1389    // own): body kernels of the call's own.
1390    program.render(values, &mut BodyKernels::default())
1391}
1392
1393#[cfg(test)]
1394mod tests {
1395    use super::*;
1396
1397    /// A rendering state's body kernels are created on the first
1398    /// render that reaches a projection and reused by every render
1399    /// after; a clone of the set is a new, empty set.
1400    #[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    /// A float hole's bytes are `format!`'s, on the direct path and on
1536    /// the general one, in every encoding and position.
1537    #[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            // A non-numeric declared type takes the general path; the
1567            // text is the same, quoted where the position quotes.
1568            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    /// A tile's ops say whether rendering it re-runs a projection body
1579    /// (`TileProgram::has_projections`): a body inside a branch arm
1580    /// counts, and the answer agrees with the compiled body programs.
1581    #[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        // Statics, holes, and a branch over them: nothing re-runs.
1616        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        // A projection at the top level.
1632        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        // A projection inside a branch arm: the walk recurses, so the
1637        // arm that holds it is found whichever arm it is.
1638        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}