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_encode` binding
7//! per hole and one `tile_render` binding for the tile. `tile_encode`
8//! takes the hole's value on a polymorphic wire and produces the
9//! encoded text for the hole's position under the tile's encoding, its
10//! declared or observed type, its format, and its raw flag. Encoding is
11//! therefore an ordinary node on the graph, visible to provenance and
12//! to the engines. `tile_render` then concatenates static runs with
13//! encoded holes, selects branches, and re-runs projection bodies per
14//! tuple over a scratch state, using a skeleton it parses once at setup.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::ast::SlotShape;
22use crate::ast::{PortType, Value, ValueRef};
23use crate::iteration::comprehension::StreamerValue;
24use crate::iteration::comprehension::runtime::{RuntimeTuple, evaluate_for_iteration};
25use crate::kernel::{Kernel, KernelProgram, PolydatKernel, PolydatProgram};
26use crate::library::support::float_text;
27
28/// Where a hole sits in a `json` skeleton, which decides its encoding.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum HolePosition {
31    /// A JSON value position: numbers bare, strings quoted.
32    Value,
33    /// Inside a JSON string literal: escaped text only.
34    InString,
35    /// Plain text (text and csv encodings).
36    Text,
37}
38
39/// The encoder for one hole, carried as the `tile_encode` node's spec.
40/// Serialized compactly as `encoding|position|type|format|flags`.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct HoleEncoding {
43    /// The tile's encoding.
44    pub encoding: String,
45    /// Where in the output the hole sits: a value, inside a string, or text.
46    pub position: HolePosition,
47    /// The declared type, if any.
48    pub ty: Option<String>,
49    /// The format, if any.
50    pub format: Option<String>,
51    /// Whether the value is emitted without the encoding's escaping.
52    pub raw: bool,
53    /// Encode as a branch condition: `1` or `0`.
54    pub cond: bool,
55}
56
57impl HoleEncoding {
58    /// The compact spec form, `encoding|position|type|format|flags`.
59    pub fn to_spec(&self) -> String {
60        let pos = match self.position {
61            HolePosition::Value => "value",
62            HolePosition::InString => "string",
63            HolePosition::Text => "text",
64        };
65        let mut flags = String::new();
66        if self.raw {
67            flags.push('r');
68        }
69        if self.cond {
70            flags.push('c');
71        }
72        format!(
73            "{}|{}|{}|{}|{}",
74            self.encoding,
75            pos,
76            self.ty.as_deref().unwrap_or(""),
77            self.format.as_deref().unwrap_or(""),
78            flags
79        )
80    }
81
82    /// The encoding for a spec, interned for the process (SRD 115 §6)
83    /// so the compiled lowering of `tile_encode` can bake its address.
84    pub fn interned(spec: &str) -> &'static HoleEncoding {
85        use std::sync::RwLock;
86        static ENCODINGS: RwLock<Option<HashMap<String, &'static HoleEncoding>>> =
87            RwLock::new(None);
88        if let Some(e) = ENCODINGS
89            .read()
90            .unwrap()
91            .as_ref()
92            .and_then(|m| m.get(spec).copied())
93        {
94            return e;
95        }
96        let mut guard = ENCODINGS.write().unwrap();
97        let map = guard.get_or_insert_with(HashMap::new);
98        if let Some(e) = map.get(spec).copied() {
99            return e;
100        }
101        let leaked: &'static HoleEncoding = Box::leak(Box::new(Self::from_spec(spec)));
102        map.insert(spec.to_string(), leaked);
103        leaked
104    }
105
106    /// The encoding a spec names; a missing part takes its default.
107    pub fn from_spec(spec: &str) -> Self {
108        let mut parts = spec.splitn(5, '|');
109        let encoding = parts.next().unwrap_or("text").to_string();
110        let position = match parts.next().unwrap_or("text") {
111            "value" => HolePosition::Value,
112            "string" => HolePosition::InString,
113            _ => HolePosition::Text,
114        };
115        let ty = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
116        let format = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
117        let flags = parts.next().unwrap_or("");
118        HoleEncoding {
119            encoding,
120            position,
121            ty,
122            format,
123            raw: flags.contains('r'),
124            cond: flags.contains('c'),
125        }
126    }
127}
128
129/// Where a hole's encoded text comes from at render time.
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub enum HoleSource {
132    /// The render node's wire input at this index, encoded per `spec`
133    /// (a [`HoleEncoding`] spec) as it is rendered.
134    Wire {
135        /// The input's index among the render node's wires.
136        index: usize,
137        /// The hole's encoding spec.
138        spec: String,
139    },
140    /// An output of the enclosing projection's body program, encoded
141    /// per `spec` as it is rendered.
142    Child {
143        /// The body program's output.
144        name: String,
145        /// The hole's encoding spec.
146        spec: String,
147    },
148}
149
150/// One skeleton instruction. Holes are values, encoded at the hole.
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub enum TileOp {
153    /// Text copied as is.
154    Static(String),
155    /// A hole, already encoded.
156    Hole(HoleSource),
157    /// A projection: the body rendered once per tuple.
158    Repeat {
159        /// The comprehension, as a serialized [`StreamerValue`].
160        stream: String,
161        /// Index into [`TileSpec::children`].
162        child: usize,
163        /// The separator between tuples.
164        sep: String,
165        /// The body skeleton.
166        body: Vec<TileOp>,
167        /// Generator-call clauses whose expressions compiled to wires of
168        /// the enclosing program: `(element, node input index, type)`.
169        /// At render the input's value stands in for the clause.
170        #[serde(default)]
171        generators: Vec<(String, usize, String)>,
172    },
173    /// A branch on a condition hole.
174    Branch {
175        /// The condition, encoded as `1` or `0`.
176        cond: HoleSource,
177        /// The skeleton when the condition holds.
178        then: Vec<TileOp>,
179        /// The skeleton otherwise.
180        otherwise: Vec<TileOp>,
181    },
182}
183
184/// A projection body: a program compiled once at setup, and the outer
185/// wires it imports from the render node's inputs, each with the type
186/// its extern declares so the transported text can be re-typed.
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188pub struct ChildSpec {
189    /// The body program's source.
190    pub source: String,
191    /// `(extern name, render-node input index, port-type keyword)`.
192    pub cascade: Vec<(String, usize, String)>,
193}
194
195/// The serialized skeleton.
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct TileSpec {
198    /// The tile's name.
199    pub name: String,
200    /// The tile's encoding.
201    pub encoding: String,
202    /// The skeleton.
203    pub ops: Vec<TileOp>,
204    /// The projection body programs, by index.
205    pub children: Vec<ChildSpec>,
206}
207
208impl TileSpec {
209    /// The spec as JSON, the form the `tile_render` node's spec argument carries.
210    pub fn to_json(&self) -> String {
211        serde_json::to_string(self).expect("TileSpec serializes")
212    }
213}
214
215/// One skeleton instruction in its runtime form (SRD 114 §6): static
216/// runs are interned once at build and copied from the interner, the
217/// comprehension of a projection is parsed once, and separators are
218/// interned too.
219#[derive(Debug)]
220enum RtOp {
221    /// Copy an interned static run.
222    Copy(&'static str),
223    /// Encode a value at the hole.
224    Hole(RtSource, HoleEncoding),
225    Repeat {
226        stream: Arc<StreamerValue>,
227        child: usize,
228        sep: &'static str,
229        body: Vec<RtOp>,
230        generators: Vec<(String, usize, String)>,
231    },
232    Branch {
233        cond: RtSource,
234        then: Vec<RtOp>,
235        otherwise: Vec<RtOp>,
236    },
237}
238
239/// Where a hole's value comes from at render time.
240#[derive(Debug)]
241enum RtSource {
242    Wire(usize),
243    /// A body output and its ordinal among the body's holes, which a
244    /// body kernel's entry resolves to an output index once.
245    Child(String, usize),
246}
247
248fn lower_source(source: &HoleSource) -> (RtSource, HoleEncoding) {
249    match source {
250        HoleSource::Wire { index, spec } => (RtSource::Wire(*index), HoleEncoding::from_spec(spec)),
251        HoleSource::Child { name, spec } => (
252            RtSource::Child(name.clone(), 0),
253            HoleEncoding::from_spec(spec),
254        ),
255    }
256}
257
258/// Intern every static run and separator of a skeleton and parse every
259/// projection stream, once, at construction.
260fn lower_ops(ops: &[TileOp]) -> Vec<RtOp> {
261    use crate::kernel::StaticInterner;
262    ops.iter()
263        .map(|op| match op {
264            TileOp::Static(s) => RtOp::Copy(StaticInterner::intern(s)),
265            TileOp::Hole(h) => {
266                let (source, enc) = lower_source(h);
267                RtOp::Hole(source, enc)
268            }
269            TileOp::Repeat {
270                stream,
271                child,
272                sep,
273                body,
274                generators,
275            } => RtOp::Repeat {
276                stream: Arc::new(StreamerValue::from_json(stream)),
277                child: *child,
278                sep: StaticInterner::intern(sep),
279                body: lower_ops(body),
280                generators: generators.clone(),
281            },
282            TileOp::Branch {
283                cond,
284                then,
285                otherwise,
286            } => RtOp::Branch {
287                cond: lower_source(cond).0,
288                then: lower_ops(then),
289                otherwise: lower_ops(otherwise),
290            },
291        })
292        .collect()
293}
294
295/// The runtime form: the spec, compiled body programs, and parsed streams.
296pub struct TileProgram {
297    /// The serialized skeleton.
298    pub spec: TileSpec,
299    /// The skeleton with statics interned and streams parsed.
300    ops: Vec<RtOp>,
301    /// The body programs, compiled once at setup, for the interpreter.
302    pub children: Vec<Arc<PolydatProgram>>,
303    /// One kernel over each body program, the canonical kernel the
304    /// comprehension evaluator installs tuple values into.
305    canonicals: Vec<Arc<PolydatKernel>>,
306    /// Per body, its program on the default engine (SRD 117 step 2),
307    /// compiled here, at construction, so the first render pays no
308    /// compile. `None` where the engine refused the body, which then
309    /// renders interpreted.
310    compiled: Vec<Option<Arc<dyn KernelProgram>>>,
311    /// Per body, its projection's tuples when the comprehension is the
312    /// same every render: no generator clause and no placeholder in
313    /// its sources. Evaluated once at construction.
314    memo: Vec<Option<Arc<[RuntimeTuple]>>>,
315}
316
317impl std::fmt::Debug for TileProgram {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.debug_struct("TileProgram")
320            .field("spec", &self.spec)
321            .field("ops", &self.ops)
322            .field("children", &self.children.len())
323            .finish_non_exhaustive()
324    }
325}
326
327/// Give every body hole an ordinal within its body, so a body kernel's
328/// entry can resolve the hole's output index once and keep it by
329/// position (SRD 117 step 3).
330fn number_child_holes(ops: &mut [RtOp]) {
331    fn walk(ops: &mut [RtOp], next: &mut usize) {
332        for op in ops.iter_mut() {
333            match op {
334                RtOp::Hole(RtSource::Child(_, k), _) => {
335                    *k = *next;
336                    *next += 1;
337                }
338                RtOp::Branch {
339                    cond,
340                    then,
341                    otherwise,
342                } => {
343                    if let RtSource::Child(_, k) = cond {
344                        *k = *next;
345                        *next += 1;
346                    }
347                    walk(then, next);
348                    walk(otherwise, next);
349                }
350                RtOp::Repeat { body, .. } => {
351                    let mut inner = 0;
352                    walk(body, &mut inner);
353                }
354                _ => {}
355            }
356        }
357    }
358    let mut top = 0;
359    walk(ops, &mut top);
360}
361
362/// Evaluate every projection whose tuples cannot change between
363/// renders, once.
364fn memoize(
365    ops: &[RtOp],
366    canonicals: &[Arc<PolydatKernel>],
367    memo: &mut [Option<Arc<[RuntimeTuple]>>],
368) {
369    for op in ops {
370        match op {
371            RtOp::Repeat {
372                stream,
373                child,
374                body,
375                generators,
376                ..
377            } => {
378                if generators.is_empty()
379                    && !stream.text.contains('{')
380                    && let Ok(tuples) = evaluate_for_iteration(
381                        &stream.ast,
382                        &*canonicals[*child],
383                        &HashMap::new(),
384                        |_| Ok(()),
385                    )
386                {
387                    memo[*child] = Some(tuples.into());
388                }
389                memoize(body, canonicals, memo);
390            }
391            RtOp::Branch {
392                then, otherwise, ..
393            } => {
394                memoize(then, canonicals, memo);
395                memoize(otherwise, canonicals, memo);
396            }
397            _ => {}
398        }
399    }
400}
401
402impl TileProgram {
403    /// Parse a skeleton and compile its projection bodies. Panics with
404    /// the compiler's diagnostic on a malformed payload, which only the
405    /// compiler produces.
406    pub fn from_json(json: &str) -> Self {
407        let spec: TileSpec = serde_json::from_str(json)
408            .unwrap_or_else(|e| panic!("tile_render: malformed skeleton payload: {e}"));
409        let children: Vec<Arc<PolydatProgram>> = spec
410            .children
411            .iter()
412            .map(|c| {
413                crate::dsl::compile_polydat(&c.source)
414                    .unwrap_or_else(|e| {
415                        panic!(
416                            "tile '{}': projection body failed to compile: {e}\n{}",
417                            spec.name, c.source
418                        )
419                    })
420                    .into_program()
421            })
422            .collect();
423        let canonicals: Vec<Arc<PolydatKernel>> = children
424            .iter()
425            .map(|p| Arc::new(PolydatKernel::from_program(p.clone())))
426            .collect();
427        let mut ops = lower_ops(&spec.ops);
428        number_child_holes(&mut ops);
429        let mut memo = vec![None; children.len()];
430        memoize(&ops, &canonicals, &mut memo);
431        let compiled = spec
432            .children
433            .iter()
434            .enumerate()
435            .map(|(i, c)| {
436                match crate::dsl::compile::compile_polydat_with(&c.source, crate::Engine::default())
437                {
438                    Ok(kernel) => Some(kernel.into_program()),
439                    Err(e) => {
440                        crate::library::support::audit::debug(&format!(
441                            "tile '{}': projection body {i} renders on the interpreter: {e}",
442                            spec.name
443                        ));
444                        None
445                    }
446                }
447            })
448            .collect();
449        TileProgram {
450            spec,
451            ops,
452            children,
453            canonicals,
454            compiled,
455            memo,
456        }
457    }
458
459    /// The body program of projection `child` for a render on `engine`:
460    /// the interpreter's for the interpreter, the default engine's for
461    /// every compiled kernel, and the interpreter's again where the
462    /// default engine refused the body.
463    fn body_program_on(&self, child: usize, engine: crate::Engine) -> Arc<dyn KernelProgram> {
464        if matches!(engine, crate::Engine::Interpreter(_)) {
465            return self.children[child].clone();
466        }
467        self.compiled[child]
468            .clone()
469            .unwrap_or_else(|| self.children[child].clone())
470    }
471
472    /// The program for a skeleton payload, interned for the process
473    /// (SRD 115 §6): the compiled lowering of `tile_render` bakes its
474    /// address, so it must outlive every kernel compiled from it, and
475    /// the same payload is parsed and its bodies compiled once.
476    pub fn interned(spec: &str) -> &'static TileProgram {
477        use std::sync::RwLock;
478        static PROGRAMS: RwLock<Option<HashMap<String, usize>>> = RwLock::new(None);
479        let found = PROGRAMS
480            .read()
481            .unwrap()
482            .as_ref()
483            .and_then(|m| m.get(spec).copied());
484        if let Some(p) = found {
485            // SAFETY: the address was leaked below and is never freed.
486            return unsafe { &*(p as *const TileProgram) };
487        }
488        // Built with no lock held: constructing a program compiles its
489        // projection bodies, and a body's own tile interns its program
490        // through this same table (SRD 117 step 2). Two threads may
491        // build the same program at once; the first to insert wins and
492        // the other's build is dropped.
493        let built = Box::new(Self::from_json(spec));
494        let mut guard = PROGRAMS.write().unwrap();
495        let map = guard.get_or_insert_with(HashMap::new);
496        if let Some(&p) = map.get(spec) {
497            // SAFETY: as above.
498            return unsafe { &*(p as *const TileProgram) };
499        }
500        let leaked: &'static TileProgram = Box::leak(built);
501        map.insert(spec.to_string(), leaked as *const TileProgram as usize);
502        leaked
503    }
504
505    /// True when any op re-runs a projection body: such a skeleton
506    /// stays on P1 until projection bodies activate as `for` bodies do.
507    pub fn has_projections(&self) -> bool {
508        fn walk(ops: &[RtOp]) -> bool {
509            ops.iter().any(|op| match op {
510                RtOp::Repeat { .. } => true,
511                RtOp::Branch {
512                    then, otherwise, ..
513                } => walk(then) || walk(otherwise),
514                _ => false,
515            })
516        }
517        walk(&self.ops)
518    }
519
520    /// Render with the node's wire inputs, the hole values, on the
521    /// interpreter, over `bodies`, the rendering state's own kernels
522    /// for the projection bodies.
523    pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
524        let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
525        let mut out = String::new();
526        self.render_into(
527            &refs,
528            crate::Engine::Interpreter(crate::JitMode::Auto),
529            bodies,
530            &mut out,
531        );
532        out
533    }
534
535    /// Render into any text sink from borrowed views of the hole
536    /// values: a `String` at P1, a step's own scratch in a compiled
537    /// closure. Every hole is encoded here, from the view straight into
538    /// the sink, and a projection's body runs on `engine`, the engine
539    /// of the kernel rendering, in a kernel the rendering state owns
540    /// (`bodies`) and reuses across renders.
541    pub fn render_into<W: std::fmt::Write>(
542        &self,
543        inputs: &[ValueRef<'_>],
544        engine: crate::Engine,
545        bodies: &mut BodyKernels,
546        out: &mut W,
547    ) {
548        self.render_ops(&self.ops, inputs, engine, bodies, None, out);
549    }
550
551    fn render_ops<W: std::fmt::Write>(
552        &self,
553        ops: &[RtOp],
554        inputs: &[ValueRef<'_>],
555        engine: crate::Engine,
556        bodies: &mut BodyKernels,
557        mut child: Option<&mut BodyEntry>,
558        out: &mut W,
559    ) {
560        for op in ops {
561            match op {
562                // `Copy`: a memcpy from the static interner (SRD 114 §6,
563                // SRD 115 step 3). The bytes were interned at build.
564                RtOp::Copy(s) => out.put(s),
565                RtOp::Hole(source, enc) => match source {
566                    RtSource::Wire(i) => {
567                        encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
568                    }
569                    RtSource::Child(name, k) => {
570                        if let Some(entry) = child.as_deref_mut()
571                            && let Some(i) = entry.hole(*k, name)
572                        {
573                            let v = entry.kernel.pull_at(i);
574                            encode_ref(ValueRef::from(&v), enc, out)
575                        }
576                    }
577                },
578                RtOp::Branch {
579                    cond,
580                    then,
581                    otherwise,
582                } => {
583                    let c = self.truthy(cond, inputs, child.as_deref_mut());
584                    let branch = if c { then } else { otherwise };
585                    self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
586                }
587                RtOp::Repeat {
588                    stream,
589                    child: child_idx,
590                    sep,
591                    body,
592                    generators,
593                } => {
594                    // The tuples: memoized when the comprehension is the
595                    // same every render, otherwise evaluated now with the
596                    // same evaluator the `for` construct opens a traversal
597                    // with, which applies order strategies, samples
598                    // continuous sources, and runs predicates over the
599                    // tuple. Generators are bound first, so the parent
600                    // kernel it sees is empty and the canonical kernel is
601                    // the body program.
602                    let memoized = self.memo[*child_idx].clone();
603                    let tuples: std::borrow::Cow<'_, [RuntimeTuple]> = match &memoized {
604                        Some(t) => std::borrow::Cow::Borrowed(&t[..]),
605                        None => {
606                            let mut streamer = (**stream).clone();
607                            if !generators.is_empty() {
608                                streamer.ast = bind_generators(&streamer.ast, generators, inputs);
609                            }
610                            std::borrow::Cow::Owned(
611                                evaluate_for_iteration(
612                                    &streamer.ast,
613                                    &*self.canonicals[*child_idx],
614                                    &HashMap::new(),
615                                    |_| Ok(()),
616                                )
617                                .unwrap_or_else(|e| {
618                                    panic!(
619                                        "tile '{}': projection `for {}` failed at render: {e}",
620                                        self.spec.name, streamer.text
621                                    )
622                                }),
623                            )
624                        }
625                    };
626                    let child_spec = &self.spec.children[*child_idx];
627                    // The body runs compiled wherever the kernel rendering
628                    // is compiled, as a kernel over the body's program for
629                    // the default engine, owned by the rendering state and
630                    // reused across its renders; on the interpreter it runs
631                    // interpreted.
632                    let engine = match engine {
633                        crate::Engine::Interpreter(_) => engine,
634                        _ => crate::Engine::default(),
635                    };
636                    let program = self.body_program_on(*child_idx, engine);
637                    let mut first = true;
638                    let fail = |name: &str, e: String| -> ! {
639                        panic!(
640                            "tile '{}': projection body input `{name}`: {e}",
641                            self.spec.name
642                        )
643                    };
644                    bodies.with(&program, engine, |entry, bodies| {
645                        for (index, tuple) in tuples.iter().enumerate() {
646                            if !first {
647                                out.put(sep);
648                            }
649                            first = false;
650                            {
651                                // The body's inputs by index: the names are
652                                // resolved on the first tuple and kept.
653                                let BodyEntry {
654                                    kernel,
655                                    elements,
656                                    cascade,
657                                    ..
658                                } = &mut *entry;
659                                kernel.set_inputs(&[index as u64]);
660                                let elements = elements.get_or_insert_with(|| {
661                                    tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
662                                });
663                                for (k, (name, v)) in tuple.iter().enumerate() {
664                                    if let Some(i) = elements.get(k).copied().flatten() {
665                                        kernel
666                                            .set_input_at(i, v.clone())
667                                            .unwrap_or_else(|e| fail(name, e));
668                                    }
669                                }
670                                let cascade = cascade.get_or_insert_with(|| {
671                                    child_spec
672                                        .cascade
673                                        .iter()
674                                        .map(|(n, _, _)| kernel.input_index(n))
675                                        .collect()
676                                });
677                                for (k, (name, input_idx, ty)) in
678                                    child_spec.cascade.iter().enumerate()
679                                {
680                                    if let Some(i) = cascade.get(k).copied().flatten()
681                                        && let Some(v) = inputs.get(*input_idx)
682                                    {
683                                        kernel
684                                            .set_input_at(i, typed_for(&owned(*v), ty))
685                                            .unwrap_or_else(|e| fail(name, e));
686                                    }
687                                }
688                            }
689                            self.render_ops(body, inputs, engine, bodies, Some(entry), out);
690                        }
691                    });
692                }
693            }
694        }
695    }
696
697    /// A branch condition's truth, as the `cond` encoding decides it.
698    fn truthy(
699        &self,
700        source: &RtSource,
701        inputs: &[ValueRef<'_>],
702        child: Option<&mut BodyEntry>,
703    ) -> bool {
704        match source {
705            RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
706            RtSource::Child(name, k) => match child {
707                Some(entry) => match entry.hole(*k, name) {
708                    Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
709                    None => false,
710                },
711                None => false,
712            },
713        }
714    }
715}
716
717/// A borrowed view as an owned value, for the paths that bind values
718/// into a body program or a comprehension.
719fn owned(v: ValueRef<'_>) -> Value {
720    match v {
721        ValueRef::U64(n) => Value::U64(n),
722        ValueRef::I64(n) => Value::I64(n),
723        ValueRef::F64(f) => Value::F64(f),
724        ValueRef::Bool(b) => Value::Bool(b),
725        ValueRef::Str(s) => Value::Str(Arc::from(s)),
726        ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
727        ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
728        ValueRef::None => Value::None,
729        ValueRef::Other(v) => v.clone(),
730    }
731}
732
733/// A cascaded value as the body's extern expects it. Values arrive on
734/// the render node's inputs as they are, so this is the value itself;
735/// text is parsed only when a `Str` reaches a non-string extern.
736fn typed_for(v: &Value, ty: &str) -> Value {
737    match (v, PortType::from_keyword(ty)) {
738        (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
739        _ => v.clone(),
740    }
741}
742
743/// Replace each generator-call clause with the literal values its wire
744/// carries at this render: a list value (a stream, a vector, a JSON
745/// array) contributes its items, a scalar contributes itself. Text that
746/// spells a JSON array is read as one.
747fn bind_generators(
748    c: &crate::iteration::comprehension::Comprehension,
749    generators: &[(String, usize, String)],
750    inputs: &[ValueRef<'_>],
751) -> crate::iteration::comprehension::Comprehension {
752    use crate::iteration::comprehension::Comprehension as K;
753    use crate::iteration::comprehension::source::{LiteralValue, Source};
754    match c {
755        K::Clause {
756            name,
757            source: Source::Generator { .. },
758        } => {
759            let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
760                return c.clone();
761            };
762            let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
763            let items: Vec<Value> =
764                match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
765                    Some(interior) => interior,
766                    None => match &raw {
767                        Value::Str(text) => {
768                            match serde_json::from_str::<serde_json::Value>(text.trim()) {
769                                Ok(serde_json::Value::Array(items)) => items
770                                    .iter()
771                                    .map(|j| {
772                                        retype(
773                                            &Value::Str(j.to_string().trim_matches('"').into()),
774                                            ty,
775                                        )
776                                    })
777                                    .collect(),
778                                _ => vec![typed_for(&raw, ty)],
779                            }
780                        }
781                        _ => vec![raw.clone()],
782                    },
783                };
784            // An element declared `json` takes every item as the JSON
785            // value it is, its kind kept, so the body's extern receives
786            // what it declares; another declared type takes the scalar.
787            let json_items = ty == "json";
788            let values = items
789                .iter()
790                .map(|v| {
791                    if json_items {
792                        return LiteralValue::Json(json_of(v));
793                    }
794                    match v {
795                        Value::U64(n) => LiteralValue::Int(*n as i64),
796                        Value::I64(n) => LiteralValue::Int(*n),
797                        Value::F64(f) => LiteralValue::Float(*f),
798                        Value::Bool(b) => LiteralValue::Bool(*b),
799                        // JSON scalars carry their own kind.
800                        Value::Json(j) => match j.as_ref() {
801                            serde_json::Value::Number(n) if n.is_u64() => {
802                                LiteralValue::Int(n.as_u64().unwrap_or(0) as i64)
803                            }
804                            serde_json::Value::Number(n) if n.is_i64() => {
805                                LiteralValue::Int(n.as_i64().unwrap_or(0))
806                            }
807                            serde_json::Value::Number(n) => {
808                                LiteralValue::Float(n.as_f64().unwrap_or(0.0))
809                            }
810                            serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
811                            serde_json::Value::String(s) => LiteralValue::String(s.clone()),
812                            other => LiteralValue::String(other.to_string()),
813                        },
814                        other => LiteralValue::String(other.to_display_string()),
815                    }
816                })
817                .collect();
818            K::Clause {
819                name: name.clone(),
820                source: Source::Literal { values },
821            }
822        }
823        K::Clause { .. } => c.clone(),
824        K::Cartesian { children } => K::Cartesian {
825            children: children
826                .iter()
827                .map(|ch| bind_generators(ch, generators, inputs))
828                .collect(),
829        },
830        K::Zip { children, mode } => K::Zip {
831            children: children
832                .iter()
833                .map(|ch| bind_generators(ch, generators, inputs))
834                .collect(),
835            mode: *mode,
836        },
837        K::Union { children } => K::Union {
838            children: children
839                .iter()
840                .map(|ch| bind_generators(ch, generators, inputs))
841                .collect(),
842        },
843        K::Filter { child, predicate } => K::Filter {
844            child: Box::new(bind_generators(child, generators, inputs)),
845            predicate: predicate.clone(),
846        },
847        K::Order {
848            child,
849            strategy,
850            truncation,
851        } => K::Order {
852            child: Box::new(bind_generators(child, generators, inputs)),
853            strategy: *strategy,
854            truncation: *truncation,
855        },
856    }
857}
858
859/// Recover a typed value from the display text a cascaded wire arrives
860/// as, using the child extern's declared type.
861/// A generator item as a JSON value: a JSON item as it is, a scalar as
862/// the JSON of its kind.
863fn json_of(v: &Value) -> serde_json::Value {
864    match v {
865        Value::Json(j) => j.as_ref().clone(),
866        Value::U64(n) => serde_json::Value::from(*n),
867        Value::I64(n) => serde_json::Value::from(*n),
868        Value::F64(f) => serde_json::Number::from_f64(*f)
869            .map(serde_json::Value::Number)
870            .unwrap_or(serde_json::Value::Null),
871        Value::Bool(b) => serde_json::Value::Bool(*b),
872        Value::Str(s) => serde_json::Value::String(s.to_string()),
873        Value::None => serde_json::Value::Null,
874        other => serde_json::Value::String(other.to_display_string()),
875    }
876}
877
878fn retype(v: &Value, ty: &str) -> Value {
879    let text = v.to_display_string();
880    match PortType::from_keyword(ty) {
881        Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
882        Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
883        Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
884        Some(PortType::Str) | None => Value::Str(text.into()),
885        Some(_) => v.clone(),
886    }
887}
888
889/// A cached body kernel: the program it was created from, the kernel,
890/// and the body's names resolved to indices once (SRD 117 step 3), so
891/// a tuple is bound and its holes read with no lookup per tuple.
892struct BodyEntry {
893    program: Arc<dyn KernelProgram>,
894    kernel: Box<dyn Kernel>,
895    /// The tuple elements' input indices, by position in the tuple;
896    /// `None` for an element the body does not declare.
897    elements: Option<Vec<Option<usize>>>,
898    /// The cascade's input indices, by position in the cascade.
899    cascade: Option<Vec<Option<usize>>>,
900    /// The body holes' output indices, by ordinal.
901    holes: Vec<Option<Option<usize>>>,
902}
903
904impl BodyEntry {
905    /// The output index of body hole `k`, named `name`, resolved once.
906    fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
907        if self.holes.len() <= k {
908            self.holes.resize(k + 1, None);
909        }
910        if self.holes[k].is_none() {
911            self.holes[k] = Some(self.kernel.output_index(name));
912        }
913        self.holes[k].flatten()
914    }
915}
916
917/// The kernels one rendering state keeps over its projection bodies:
918/// one per body program and engine, created on the first render that
919/// reaches the body and reused by every render after, so a projection
920/// creates nothing per tuple. A tile render node owns one of these in
921/// its scratch (axiom S3): the storage belongs to the state that
922/// renders, never to the node, which every state of the program
923/// shares. A clone is empty, since a clone of a state is a new state.
924#[derive(Default)]
925pub struct BodyKernels {
926    entries: HashMap<(usize, crate::Engine), BodyEntry>,
927    /// Kernels created so far, for the tests.
928    created: u64,
929}
930
931impl Clone for BodyKernels {
932    fn clone(&self) -> Self {
933        Self::default()
934    }
935}
936
937// SAFETY: the kernels are reached only through `&mut self` (`with`),
938// which the owning state holds exclusively; every `&self` method
939// (`created`, `clone`, `Debug`) reads a count and touches no kernel. A
940// set inside a program shared across threads is therefore never used
941// from more than one thread, and a state created from that program
942// starts with an empty set of its own.
943unsafe impl Sync for BodyKernels {}
944
945impl std::fmt::Debug for BodyKernels {
946    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947        f.debug_struct("BodyKernels")
948            .field("entries", &self.entries.len())
949            .field("created", &self.created)
950            .finish()
951    }
952}
953
954/// The engine a body kernel is kept under: the interpreter's body
955/// program is one program whatever the enclosing kernel's cone mode.
956fn body_engine_key(engine: crate::Engine) -> crate::Engine {
957    match engine {
958        crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
959        other => other,
960    }
961}
962
963impl BodyKernels {
964    /// Kernels this state has created so far.
965    pub fn created(&self) -> u64 {
966        self.created
967    }
968
969    /// The count and a clone, for the unit test of both.
970    #[cfg(test)]
971    fn clone_for_test(&self) -> (u64, BodyKernels) {
972        (self.created, self.clone())
973    }
974
975    /// Run `f` over the kernel for `program` on `engine`, created on
976    /// first use. The entry is taken out for the call, so a nested
977    /// projection's body finds the set free for its own kernels.
978    fn with(
979        &mut self,
980        program: &Arc<dyn KernelProgram>,
981        engine: crate::Engine,
982        f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
983    ) {
984        let engine = body_engine_key(engine);
985        // The entry pins its program so the address cannot be reused by
986        // a later program while a kernel built for this one is kept.
987        let key = (Arc::as_ptr(program) as *const () as usize, engine);
988        let mut entry = self
989            .entries
990            .remove(&key)
991            .filter(|e| Arc::ptr_eq(&e.program, program))
992            .unwrap_or_else(|| {
993                self.created += 1;
994                BodyEntry {
995                    program: program.clone(),
996                    kernel: program.clone().create_kernel(),
997                    elements: None,
998                    cascade: None,
999                    holes: Vec::new(),
1000                }
1001            });
1002        f(&mut entry, self);
1003        self.entries.insert(key, entry);
1004    }
1005}
1006
1007/// The tile render node's state: its projection bodies' kernels.
1008pub(crate) mod render_state {
1009    use super::{BodyKernels, TileRender};
1010    use crate::ast::{ScratchBuf, ScratchElem, Value};
1011
1012    pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
1013        vec![ScratchElem::Kernels]
1014    }
1015
1016    pub(crate) fn eval(
1017        node: &TileRender,
1018        scratch: &mut [ScratchBuf],
1019        inputs: &[Value],
1020        outputs: &mut [Value],
1021    ) {
1022        let bodies = bodies_of(&mut scratch[0]);
1023        outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1024    }
1025
1026    /// The body kernel set a scratch entry holds.
1027    pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1028        match entry {
1029            ScratchBuf::Kernels(b) => b,
1030            other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1031        }
1032    }
1033}
1034
1035/// A text sink that cannot fail: a `String`, or the cycle arena writer
1036/// in a compiled helper. `fmt::Write`'s results are ignored because
1037/// neither sink reports an error.
1038pub(crate) trait Sink: std::fmt::Write {
1039    fn put(&mut self, s: &str) {
1040        let _ = self.write_str(s);
1041    }
1042    fn put_char(&mut self, c: char) {
1043        let _ = self.write_char(c);
1044    }
1045}
1046
1047impl<W: std::fmt::Write> Sink for W {}
1048
1049/// Encode one value per a hole's encoding, into any text sink.
1050pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1051    encode_ref(ValueRef::from(value), enc, out)
1052}
1053
1054/// Encode a borrowed view of a value (SRD 115 §6.1): the compiled
1055/// helper calls this on its slot without owning a `Value`, and a
1056/// string hole is encoded from the arena in place.
1057pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1058    if enc.cond {
1059        out.put_char(if truthy_of(value) { '1' } else { '0' });
1060        return;
1061    }
1062    let ty = enc.ty.as_deref();
1063    // A number with no format, or a float under a `.N` precision,
1064    // writes its digits straight into the sink (SRD 117 step 3):
1065    // digits, a sign, and a point need no escaping in any encoding or
1066    // position, and a numeric type is written bare in a JSON value
1067    // position, so the text is the same as the general path's, without
1068    // the `String` the general path builds. The float writer is
1069    // byte-identical to `format!` (`support::float_text`, proved by
1070    // `tests/float_text.rs`).
1071    if is_numeric_keyword(ty.unwrap_or("u64")) {
1072        match (enc.format.as_deref(), value) {
1073            (None, ValueRef::U64(n)) => {
1074                put_u64(n, out);
1075                return;
1076            }
1077            (None, ValueRef::I64(n)) => {
1078                if n < 0 {
1079                    out.put_char('-');
1080                }
1081                put_u64(n.unsigned_abs(), out);
1082                return;
1083            }
1084            (None, ValueRef::F64(f)) => {
1085                let _ = float_text::write_shortest(f, out);
1086                return;
1087            }
1088            (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1089                if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1090                    let _ = float_text::write_fixed(f, prec, out);
1091                    return;
1092                }
1093            }
1094            _ => {}
1095        }
1096    }
1097    let text = formatted_text(value, ty, enc.format.as_deref());
1098    if enc.raw {
1099        out.put(&text);
1100        return;
1101    }
1102    match (enc.encoding.as_str(), enc.position) {
1103        ("json", HolePosition::InString) => push_json_escaped(&text, out),
1104        ("json", HolePosition::Value) => {
1105            let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1106            match (kind, value) {
1107                (_, ValueRef::None) => out.put("null"),
1108                ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1109                ("json", ValueRef::Json(j)) => {
1110                    let _ = write!(out, "{j}");
1111                }
1112                ("str", _) | ("String", _) | ("string", _) => {
1113                    out.put_char('"');
1114                    push_json_escaped(&text, out);
1115                    out.put_char('"');
1116                }
1117                (k, _) if is_numeric_keyword(k) => out.put(&text),
1118                (_, ValueRef::Json(j)) => {
1119                    let _ = write!(out, "{j}");
1120                }
1121                (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1122                (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1123                _ => {
1124                    out.put_char('"');
1125                    push_json_escaped(&text, out);
1126                    out.put_char('"');
1127                }
1128            }
1129        }
1130        ("csv", _) => {
1131            if text.contains([',', '"', '\n']) {
1132                out.put_char('"');
1133                for (i, piece) in text.split('"').enumerate() {
1134                    if i > 0 {
1135                        out.put("\"\"");
1136                    }
1137                    out.put(piece);
1138                }
1139                out.put_char('"');
1140            } else {
1141                out.put(&text);
1142            }
1143        }
1144        _ => out.put(&text),
1145    }
1146}
1147
1148/// The decimal digits of `n`, written without an allocation.
1149fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1150    if n == 0 {
1151        out.put_char('0');
1152        return;
1153    }
1154    let mut buf = [0u8; 20];
1155    let mut i = buf.len();
1156    while n > 0 {
1157        i -= 1;
1158        buf[i] = b'0' + (n % 10) as u8;
1159        n /= 10;
1160    }
1161    // SAFETY-free: the buffer holds ASCII digits only.
1162    out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1163}
1164
1165fn truthy_of(v: ValueRef<'_>) -> bool {
1166    match v {
1167        ValueRef::Bool(b) => b,
1168        ValueRef::U64(n) => n != 0,
1169        ValueRef::F64(f) => f != 0.0,
1170        ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1171        ValueRef::None => false,
1172        _ => true,
1173    }
1174}
1175
1176fn is_numeric_keyword(k: &str) -> bool {
1177    matches!(
1178        k,
1179        "u64"
1180            | "i64"
1181            | "f64"
1182            | "f32"
1183            | "u32"
1184            | "i32"
1185            | "u16"
1186            | "i16"
1187            | "u8"
1188            | "i8"
1189            | "u128"
1190            | "i128"
1191            | "f16"
1192    )
1193}
1194
1195/// Display text for a value under an optional printf-style format:
1196/// `.N` precision for floats, `0N` zero-padded width, `N` width, `>N`
1197/// and `<N` alignment, `x`/`X` hex for integers.
1198fn formatted_text<'a>(
1199    value: ValueRef<'a>,
1200    ty: Option<&str>,
1201    format: Option<&str>,
1202) -> std::borrow::Cow<'a, str> {
1203    use std::borrow::Cow;
1204    // A string with no format is borrowed as it is; everything else is
1205    // owned text. The base text is produced only where a format needs
1206    // it: a precision or a hex format writes the number once, itself.
1207    let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1208        match (ty, value) {
1209            (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1210            // Text quotes nothing: a JSON string in a text position is
1211            // its text, as a `str` hole is.
1212            (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1213            (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1214            (_, v) => v.display(),
1215        }
1216    };
1217    let Some(fmt) = format else {
1218        return base(value);
1219    };
1220    let fmt = fmt.trim();
1221    if let Some(prec) = precision_of(fmt) {
1222        if let Some(f) = as_f64(value) {
1223            return Cow::Owned(float_text::fixed_string(f, prec));
1224        }
1225        return base(value);
1226    }
1227    if fmt == "x" || fmt == "X" {
1228        if let ValueRef::U64(n) = value {
1229            return Cow::Owned(if fmt == "x" {
1230                format!("{n:x}")
1231            } else {
1232                format!("{n:X}")
1233            });
1234        }
1235        return base(value);
1236    }
1237    let base = base(value);
1238    if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1239        return Cow::Owned(format!("{base:0>w$}"));
1240    }
1241    if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1242        return Cow::Owned(format!("{base:>w$}"));
1243    }
1244    if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1245        return Cow::Owned(format!("{base:<w$}"));
1246    }
1247    if let Ok(w) = fmt.parse::<usize>() {
1248        return Cow::Owned(format!("{base:>w$}"));
1249    }
1250    base
1251}
1252
1253fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1254    match v {
1255        ValueRef::F64(f) => Some(f),
1256        ValueRef::U64(n) => Some(n as f64),
1257        _ => None,
1258    }
1259}
1260
1261/// The `N` of a `.N` precision format, after trimming.
1262fn precision_of(fmt: &str) -> Option<usize> {
1263    fmt.trim()
1264        .strip_prefix('.')
1265        .and_then(|p| p.parse::<usize>().ok())
1266}
1267
1268fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1269    for c in s.chars() {
1270        match c {
1271            '"' => out.put("\\\""),
1272            '\\' => out.put("\\\\"),
1273            '\n' => out.put("\\n"),
1274            '\r' => out.put("\\r"),
1275            '\t' => out.put("\\t"),
1276            c if (c as u32) < 0x20 => {
1277                let _ = write!(out, "\\u{:04x}", c as u32);
1278            }
1279            c => out.put_char(c),
1280        }
1281    }
1282}
1283
1284/// Encode one hole's value per its spec (`encoding|position|type|format|flags`).
1285/// Authors do not call this directly; the compiler emits it for each hole.
1286#[crate::polydat_node(category = Formatting)]
1287fn tile_encode(
1288    value: Value,
1289    spec: Const<&str>,
1290    #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1291) -> String {
1292    let mut out = String::new();
1293    encode(&value, enc, &mut out);
1294    out
1295}
1296
1297/// The closure-tier form of `tile_render` (SRD 117 step 1): every hole
1298/// value is read from its slot as a borrowed view, by the wire type the
1299/// kernel fixed, and the document is rendered straight into the cycle
1300/// arena; nothing is decoded into an owned `Value` on the way. A wire
1301/// wider than one slot, or of a kind without a view, is read as a value
1302/// through the typed decoder.
1303fn tile_render_compiled(node: &TileRender, wire_types: &[PortType]) -> crate::ast::CompiledSlotKit {
1304    let program: &'static TileProgram = TileProgram::interned(&node.spec);
1305    // Per wire: its first slot and its type; a one-slot carrier or a
1306    // `Ref2` kind is viewed in place, a two-slot immediate is decoded.
1307    let mut reads: Vec<(usize, PortType)> = Vec::with_capacity(wire_types.len());
1308    let mut offset = 0usize;
1309    for &ty in wire_types {
1310        reads.push((offset, ty));
1311        offset += ty.slot_width().max(1);
1312    }
1313    crate::ast::CompiledSlotKit {
1314        scratch: vec![
1315            crate::ast::ScratchElem::Str,
1316            crate::ast::ScratchElem::Kernels,
1317        ],
1318        op: Box::new(
1319            move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [crate::ast::ScratchBuf]| {
1320                // Owned values only for the two-slot immediates; they keep
1321                // their positions, so the views are built once they are
1322                // all in place.
1323                let owned_values: Vec<Value> = reads
1324                    .iter()
1325                    .filter(|(_, ty)| ty.slot_color() == crate::ast::SlotColor::Imm2)
1326                    .map(|&(offset, ty)| crate::compile::marshal::decode_output(inputs, offset, ty))
1327                    .collect();
1328                let mut next_owned = 0usize;
1329                let refs: Vec<ValueRef<'_>> = reads
1330                    .iter()
1331                    .map(|&(offset, ty)| {
1332                        if ty.slot_color() == crate::ast::SlotColor::Imm2 {
1333                            let v = ValueRef::from(&owned_values[next_owned]);
1334                            next_owned += 1;
1335                            v
1336                        } else {
1337                            // SAFETY: a pair in the buffer was published by
1338                            // a producer whose storage is alive (S3, S4).
1339                            unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1340                        }
1341                    })
1342                    .collect();
1343                // The document is rendered straight into this step's own
1344                // scratch (axiom S3). A body runs compiled wherever the
1345                // kernel rendering is compiled: this closure serves the
1346                // closure tier and a hybrid kernel's closure steps alike,
1347                // so the body takes the default engine.
1348                let (text, bodies) = scratch.split_at_mut(1);
1349                let crate::ast::ScratchBuf::Str(buf) = &mut text[0] else {
1350                    unreachable!("the render step owns a string entry");
1351                };
1352                let bodies = render_state::bodies_of(&mut bodies[0]);
1353                buf.clear();
1354                let mut w = BytesSink(buf);
1355                program.render_into(&refs, crate::Engine::default(), bodies, &mut w);
1356                let (p, l) = scratch[0].ptr_len();
1357                outputs[0] = p;
1358                outputs[1] = l;
1359            },
1360        ),
1361    }
1362}
1363
1364/// A text sink over the bytes of a step's string scratch: what a
1365/// compiled render writes into.
1366pub(crate) struct BytesSink<'a>(pub(crate) &'a mut Vec<u8>);
1367
1368impl std::fmt::Write for BytesSink<'_> {
1369    fn write_str(&mut self, s: &str) -> std::fmt::Result {
1370        self.0.extend_from_slice(s.as_bytes());
1371        Ok(())
1372    }
1373}
1374
1375/// Render a compiled tile skeleton over its encoded hole texts. Authors
1376/// do not call this directly; the compiler emits it for `tile` statements.
1377#[crate::polydat_node(
1378    category = Formatting,
1379    variadic_min = 0,
1380    compiled_slot = tile_render_compiled,
1381    state = render_state
1382)]
1383fn tile_render(
1384    spec: Const<&str>,
1385    #[poly_const(TileProgram::from_json, from = spec)] program: &TileProgram,
1386    values: &[Value],
1387) -> String {
1388    // 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(src).unwrap();
1405        let program = k.program();
1406        let node = (0..program.node_count())
1407            .find(|&i| program.node_meta(i).name == "tile_render")
1408            .expect("the tile's render node");
1409        let bodies_of = |k: &mut PolydatKernel| match &k.state().core.node_scratch[node][0] {
1410            crate::ast::ScratchBuf::Kernels(b) => b.clone_for_test(),
1411            other => panic!("{other:?}"),
1412        };
1413        assert_eq!(bodies_of(&mut k).0, 0, "nothing before the first render");
1414        k.set_inputs(&[10]);
1415        assert_eq!(k.pull("t").as_str(), "10,11,12");
1416        assert_eq!(bodies_of(&mut k).0, 1, "one kernel for the body");
1417        for c in 0..5u64 {
1418            k.set_inputs(&[c]);
1419            let _ = k.pull("t");
1420        }
1421        let (created, clone) = bodies_of(&mut k);
1422        assert_eq!(created, 1, "reused across renders");
1423        assert_eq!(clone.created(), 0, "a clone is a new state's empty set");
1424    }
1425
1426    fn enc(
1427        encoding: &str,
1428        position: HolePosition,
1429        ty: Option<&str>,
1430        format: Option<&str>,
1431        raw: bool,
1432    ) -> HoleEncoding {
1433        HoleEncoding {
1434            encoding: encoding.into(),
1435            position,
1436            ty: ty.map(str::to_string),
1437            format: format.map(str::to_string),
1438            raw,
1439            cond: false,
1440        }
1441    }
1442
1443    #[test]
1444    fn json_value_and_string_positions_encode_by_type() {
1445        let mut out = String::new();
1446        encode(
1447            &Value::Str("a\"b".into()),
1448            &enc("json", HolePosition::Value, Some("str"), None, false),
1449            &mut out,
1450        );
1451        assert_eq!(out, "\"a\\\"b\"");
1452        out.clear();
1453        encode(
1454            &Value::U64(7),
1455            &enc("json", HolePosition::Value, None, None, false),
1456            &mut out,
1457        );
1458        assert_eq!(out, "7");
1459        out.clear();
1460        encode(
1461            &Value::Str("x\ny".into()),
1462            &enc("json", HolePosition::InString, None, None, false),
1463            &mut out,
1464        );
1465        assert_eq!(out, "x\\ny");
1466        out.clear();
1467        encode(
1468            &Value::F64(2.0 / 3.0),
1469            &enc("json", HolePosition::Value, None, Some(".2"), false),
1470            &mut out,
1471        );
1472        assert_eq!(out, "0.67");
1473        out.clear();
1474        encode(
1475            &Value::None,
1476            &enc("json", HolePosition::Value, None, None, false),
1477            &mut out,
1478        );
1479        assert_eq!(out, "null");
1480    }
1481
1482    #[test]
1483    fn spec_round_trips() {
1484        let e = enc(
1485            "json",
1486            HolePosition::InString,
1487            Some("u64"),
1488            Some(".2"),
1489            true,
1490        );
1491        assert_eq!(HoleEncoding::from_spec(&e.to_spec()), e);
1492        let c = HoleEncoding {
1493            cond: true,
1494            ..enc("text", HolePosition::Text, None, None, false)
1495        };
1496        assert_eq!(HoleEncoding::from_spec(&c.to_spec()), c);
1497    }
1498
1499    #[test]
1500    fn csv_quotes_when_needed_and_raw_skips_escaping() {
1501        let mut out = String::new();
1502        encode(
1503            &Value::Str("a,b".into()),
1504            &enc("csv", HolePosition::Text, None, None, false),
1505            &mut out,
1506        );
1507        assert_eq!(out, "\"a,b\"");
1508        out.clear();
1509        encode(
1510            &Value::Str("a\"b".into()),
1511            &enc("json", HolePosition::Value, None, None, true),
1512            &mut out,
1513        );
1514        assert_eq!(out, "a\"b");
1515    }
1516
1517    #[test]
1518    fn formats_apply_before_encoding() {
1519        assert_eq!(formatted_text(ValueRef::U64(5), None, Some("03")), "005");
1520        assert_eq!(formatted_text(ValueRef::U64(255), None, Some("x")), "ff");
1521        assert_eq!(
1522            formatted_text(ValueRef::Str("ab"), None, Some(">4")),
1523            "  ab"
1524        );
1525        assert_eq!(
1526            formatted_text(ValueRef::F64(0.295), None, Some(".2")),
1527            "0.29"
1528        );
1529        assert_eq!(
1530            formatted_text(ValueRef::U64(7), None, Some(" .3 ")),
1531            "7.000"
1532        );
1533    }
1534
1535    /// 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}