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 (polytile.md §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::{IndexedTuples, RuntimeTuple, evaluate_indexed};
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 (polytile.md §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, so construction compiles
283    /// no body for an engine that never renders it.
284    pub bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
285    /// The body programs for the interpreter, which construction
286    /// needs: the canonical kernels are built over them, and
287    /// memoisation runs against those.
288    pub children: Vec<Arc<PolydatProgram>>,
289    /// One kernel over each body program, the canonical kernel the
290    /// comprehension evaluator installs tuple values into.
291    canonicals: Vec<Arc<PolydatKernel>>,
292    /// Per body, its projection's tuples when the comprehension is the
293    /// same every render: no generator clause and no placeholder in
294    /// its sources. Evaluated once at construction.
295    memo: Vec<Option<Arc<[RuntimeTuple]>>>,
296}
297
298impl std::fmt::Debug for TileProgram {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        f.debug_struct("TileProgram")
301            .field("spec", &self.spec)
302            .field("ops", &self.ops)
303            .field("children", &self.children.len())
304            .finish_non_exhaustive()
305    }
306}
307
308/// Give every body hole an ordinal within its body, so a body kernel's
309/// entry can resolve the hole's output index once and keep it by
310/// position (polytile.md §7.1).
311fn number_child_holes(ops: &mut [RtOp]) {
312    fn walk(ops: &mut [RtOp], next: &mut usize) {
313        for op in ops.iter_mut() {
314            match op {
315                RtOp::Hole(RtSource::Child(_, k), _) => {
316                    *k = *next;
317                    *next += 1;
318                }
319                RtOp::Branch {
320                    cond,
321                    then,
322                    otherwise,
323                } => {
324                    if let RtSource::Child(_, k) = cond {
325                        *k = *next;
326                        *next += 1;
327                    }
328                    walk(then, next);
329                    walk(otherwise, next);
330                }
331                RtOp::Repeat { body, .. } => {
332                    let mut inner = 0;
333                    walk(body, &mut inner);
334                }
335                _ => {}
336            }
337        }
338    }
339    let mut top = 0;
340    walk(ops, &mut top);
341}
342
343/// A projection's tuples for one render: the memo's, or the ones the
344/// comprehension evaluated to this render, each computed from its
345/// position as the render reaches it.
346enum Projection<'a> {
347    Memo(&'a [RuntimeTuple]),
348    Indexed(IndexedTuples),
349}
350
351impl Projection<'_> {
352    fn len(&self) -> u64 {
353        match self {
354            Projection::Memo(tuples) => tuples.len() as u64,
355            Projection::Indexed(tuples) => tuples.len(),
356        }
357    }
358
359    /// The tuple at `i`, below [`Self::len`]; an evaluated one is
360    /// computed into `buf`.
361    fn tuple<'b>(&'b self, i: u64, buf: &'b mut RuntimeTuple) -> &'b RuntimeTuple {
362        match self {
363            Projection::Memo(tuples) => &tuples[i as usize],
364            Projection::Indexed(tuples) => {
365                *buf = tuples.get(i).unwrap_or_default();
366                buf
367            }
368        }
369    }
370}
371
372/// Evaluate every projection whose tuples cannot change between
373/// renders, once.
374fn memoize(
375    ops: &[RtOp],
376    canonicals: &[Arc<PolydatKernel>],
377    memo: &mut [Option<Arc<[RuntimeTuple]>>],
378) {
379    for op in ops {
380        match op {
381            RtOp::Repeat {
382                stream,
383                child,
384                body,
385                generators,
386                ..
387            } => {
388                // The memo holds the tuples a render iterates, computed
389                // once; an order over a large product computes only
390                // its selected tuples.
391                if generators.is_empty()
392                    && !stream.text.contains('{')
393                    && let Ok(tuples) = evaluate_indexed(&stream.ast, &*canonicals[*child])
394                {
395                    memo[*child] = Some(tuples.to_vec().into());
396                }
397                memoize(body, canonicals, memo);
398            }
399            RtOp::Branch {
400                then, otherwise, ..
401            } => {
402                memoize(then, canonicals, memo);
403                memoize(otherwise, canonicals, memo);
404            }
405            _ => {}
406        }
407    }
408}
409
410impl TileProgram {
411    /// A program from a skeleton and the bodies its projections run.
412    ///
413    /// The compiler's path: it lowered the bodies, so it hands them
414    /// over as they are. Each body carries the settings the parent
415    /// compiled under, and its program per engine is built when a
416    /// render on that engine first asks for it.
417    pub fn from_parts(
418        spec: TileSpec,
419        bodies: Vec<Arc<crate::dsl::traversal::BodySource>>,
420    ) -> Result<Self, String> {
421        // Construction needs the interpreter's program: the canonical
422        // kernel the comprehension evaluator installs tuples into is
423        // built over it, and memoisation runs there. Every other
424        // engine's is built on the first render that asks for it.
425        let mut children: Vec<Arc<PolydatProgram>> = Vec::with_capacity(bodies.len());
426        for (i, body) in bodies.iter().enumerate() {
427            let program = body
428                .program_on(crate::Engine::Interpreter(crate::JitMode::Auto))
429                .map_err(|e| format!("projection body {i} failed to compile: {e}"))?;
430            children.push(
431                program
432                    .as_interpreter()
433                    .ok_or_else(|| format!("projection body {i} is not an interpreter program"))?,
434            );
435        }
436        let canonicals: Vec<Arc<PolydatKernel>> = children
437            .iter()
438            .map(|p| Arc::new(PolydatKernel::from_program(p.clone())))
439            .collect();
440        let mut ops = lower_ops(&spec.ops);
441        number_child_holes(&mut ops);
442        let mut memo = vec![None; children.len()];
443        memoize(&ops, &canonicals, &mut memo);
444        Ok(TileProgram {
445            spec,
446            ops,
447            bodies,
448            children,
449            canonicals,
450            memo,
451        })
452    }
453
454    /// A program from a serialized skeleton, whose projection bodies
455    /// are rebuilt from the source text it carries under the default
456    /// settings.
457    ///
458    /// The route for a skeleton that reaches the runtime as text — a
459    /// host that stored one, a test that wrote one. The compiler's own
460    /// path is [`Self::from_parts`], which hands the bodies over
461    /// rather than describing them. A standalone skeleton is a tree of
462    /// its own: its bodies share a fresh compile ledger and resource
463    /// scope. [`Self::from_json_for`] loads one for a kernel's tree.
464    pub fn from_json(json: &str) -> Self {
465        Self::from_json_in(
466            json,
467            crate::kernel::CompileLedger::new(),
468            crate::resource::ResourceScope::new(),
469        )
470    }
471
472    /// A program from a serialized skeleton, loaded for `kernel`, the
473    /// kernel that renders it: its projection bodies compile in the
474    /// kernel's tree as an inline tile's bodies do, recording their
475    /// programs on the kernel's [`CompileLedger`](crate::kernel::CompileLedger)
476    /// and reaching the host resources through the kernel's
477    /// [`ResourceScope`](crate::resource::ResourceScope)
478    /// (polytile.md §7.1).
479    pub fn from_json_for(json: &str, kernel: &dyn crate::Kernel) -> Self {
480        Self::from_json_in(json, kernel.ledger().clone(), kernel.resources().clone())
481    }
482
483    /// A program from a serialized skeleton whose bodies belong to the
484    /// tree of `ledger` and `resources`.
485    fn from_json_in(
486        json: &str,
487        ledger: Arc<crate::kernel::CompileLedger>,
488        resources: crate::resource::ResourceScope,
489    ) -> Self {
490        let spec: TileSpec = serde_json::from_str(json)
491            .unwrap_or_else(|e| panic!("tile_render: malformed skeleton payload: {e}"));
492        let name = spec.name.clone();
493        let bodies: Vec<Arc<crate::dsl::traversal::BodySource>> = spec
494            .children
495            .iter()
496            .enumerate()
497            .map(|(i, c)| {
498                Arc::new(
499                    crate::dsl::traversal::BodySource::from_source_in(
500                        &c.source,
501                        &format!("tile '{name}' :: projection body {i}"),
502                        ledger.clone(),
503                        resources.clone(),
504                    )
505                    .unwrap_or_else(|e| {
506                        panic!(
507                            "tile '{name}': projection body failed to parse: {e}\n{}",
508                            c.source
509                        )
510                    }),
511                )
512            })
513            .collect();
514        Self::from_parts(spec, bodies).unwrap_or_else(|e| panic!("tile '{name}': {e}"))
515    }
516
517    /// The body program of projection `child` for a render on
518    /// `engine` — the engine the kernel doing the rendering runs on,
519    /// which is the rule a `for` body follows (polytile.md §7.2). An
520    /// engine that refuses the body falls back to the interpreter's
521    /// program and says so once.
522    fn body_program_on(&self, child: usize, engine: crate::Engine) -> Arc<dyn KernelProgram> {
523        if matches!(engine, crate::Engine::Interpreter(_)) {
524            return self.children[child].clone();
525        }
526        self.bodies[child].program_on(engine).unwrap_or_else(|e| {
527            crate::library::support::audit::debug(&format!(
528                "tile '{}': projection body {child} renders on the interpreter: {e}",
529                self.spec.name
530            ));
531            self.children[child].clone()
532        })
533    }
534
535    /// True when any op re-runs a projection body.
536    pub fn has_projections(&self) -> bool {
537        fn walk(ops: &[RtOp]) -> bool {
538            ops.iter().any(|op| match op {
539                RtOp::Repeat { .. } => true,
540                RtOp::Branch {
541                    then, otherwise, ..
542                } => walk(then) || walk(otherwise),
543                _ => false,
544            })
545        }
546        walk(&self.ops)
547    }
548
549    /// Render with the node's wire inputs, the hole values, on the
550    /// interpreter, over `bodies`, the rendering state's own kernels
551    /// for the projection bodies.
552    pub fn render(&self, inputs: &[Value], bodies: &mut BodyKernels) -> String {
553        let refs: Vec<ValueRef<'_>> = inputs.iter().map(ValueRef::from).collect();
554        let mut out = String::new();
555        self.render_into(
556            &refs,
557            crate::Engine::Interpreter(crate::JitMode::Auto),
558            bodies,
559            &mut out,
560        );
561        out
562    }
563
564    /// Render into any text sink from borrowed views of the hole
565    /// values: a `String` at P1, a step's own scratch in a compiled
566    /// closure. Every hole is encoded here, from the view straight into
567    /// the sink, and a projection's body runs on `engine`, the engine
568    /// of the kernel rendering, in a kernel the rendering state owns
569    /// (`bodies`) and reuses across renders.
570    pub fn render_into<W: std::fmt::Write>(
571        &self,
572        inputs: &[ValueRef<'_>],
573        engine: crate::Engine,
574        bodies: &mut BodyKernels,
575        out: &mut W,
576    ) {
577        self.render_ops(&self.ops, inputs, engine, bodies, None, out);
578    }
579
580    fn render_ops<W: std::fmt::Write>(
581        &self,
582        ops: &[RtOp],
583        inputs: &[ValueRef<'_>],
584        engine: crate::Engine,
585        bodies: &mut BodyKernels,
586        mut child: Option<&mut BodyEntry>,
587        out: &mut W,
588    ) {
589        for op in ops {
590            match op {
591                // `Copy`: a memcpy from the static interner (polytile.md §6,
592                // compiled_handles.md §3). The bytes were interned at build.
593                RtOp::Copy(s) => out.put(s),
594                RtOp::Hole(source, enc) => match source {
595                    RtSource::Wire(i) => {
596                        encode_ref(inputs.get(*i).copied().unwrap_or(ValueRef::None), enc, out)
597                    }
598                    RtSource::Child(name, k) => {
599                        if let Some(entry) = child.as_deref_mut()
600                            && let Some(i) = entry.hole(*k, name)
601                        {
602                            let v = entry.kernel.pull_at(i);
603                            encode_ref(ValueRef::from(&v), enc, out)
604                        }
605                    }
606                },
607                RtOp::Branch {
608                    cond,
609                    then,
610                    otherwise,
611                } => {
612                    let c = self.truthy(cond, inputs, child.as_deref_mut());
613                    let branch = if c { then } else { otherwise };
614                    self.render_ops(branch, inputs, engine, bodies, child.as_deref_mut(), out);
615                }
616                RtOp::Repeat {
617                    stream,
618                    child: child_idx,
619                    sep,
620                    body,
621                    generators,
622                } => {
623                    // The tuples: memoized when the comprehension is the
624                    // same every render, otherwise evaluated now with the
625                    // same evaluator the `for` construct opens a traversal
626                    // with, which applies order strategies, samples
627                    // continuous sources, and runs predicates over the
628                    // tuple. Generators are bound first, so the parent
629                    // kernel it sees is empty and the canonical kernel is
630                    // the body program.
631                    let memoized = self.memo[*child_idx].clone();
632                    let tuples = match &memoized {
633                        Some(t) => Projection::Memo(&t[..]),
634                        None => {
635                            let mut streamer = (**stream).clone();
636                            if !generators.is_empty() {
637                                streamer.ast = bind_generators(&streamer.ast, generators, inputs);
638                            }
639                            Projection::Indexed(
640                                evaluate_indexed(&streamer.ast, &*self.canonicals[*child_idx])
641                                    .unwrap_or_else(|e| {
642                                        panic!(
643                                            "tile '{}': projection `for {}` failed at render: {e}",
644                                            self.spec.name, streamer.text
645                                        )
646                                    }),
647                            )
648                        }
649                    };
650                    let child_spec = &self.spec.children[*child_idx];
651                    // The body runs on the engine the kernel rendering
652                    // it runs on, which is the rule a `for` body
653                    // already followed. The kernel over its program is
654                    // owned by the rendering state and reused across
655                    // its renders.
656                    let program = self.body_program_on(*child_idx, engine);
657                    let mut first = true;
658                    let fail = |name: &str, e: crate::kernel::WriteError| -> ! {
659                        panic!(
660                            "tile '{}': projection body input `{name}`: {e}",
661                            self.spec.name
662                        )
663                    };
664                    bodies.with(&program, engine, |entry, bodies| {
665                        let mut buf = RuntimeTuple::new();
666                        for index in 0..tuples.len() {
667                            let tuple = tuples.tuple(index, &mut buf);
668                            if !first {
669                                out.put(sep);
670                            }
671                            first = false;
672                            {
673                                // The body's inputs by index: the names are
674                                // resolved on the first tuple and kept.
675                                let BodyEntry {
676                                    kernel,
677                                    elements,
678                                    cascade,
679                                    ..
680                                } = &mut *entry;
681                                kernel.set_inputs(&[index]);
682                                let elements = elements.get_or_insert_with(|| {
683                                    tuple.iter().map(|(n, _)| kernel.input_index(n)).collect()
684                                });
685                                for (k, (name, v)) in tuple.iter().enumerate() {
686                                    if let Some(i) = elements.get(k).copied().flatten() {
687                                        kernel
688                                            .set_input_at(i, v.clone())
689                                            .unwrap_or_else(|e| fail(name, e));
690                                    }
691                                }
692                                let cascade = cascade.get_or_insert_with(|| {
693                                    child_spec
694                                        .cascade
695                                        .iter()
696                                        .map(|(n, _, _)| kernel.input_index(n))
697                                        .collect()
698                                });
699                                for (k, (name, input_idx, ty)) in
700                                    child_spec.cascade.iter().enumerate()
701                                {
702                                    if let Some(i) = cascade.get(k).copied().flatten()
703                                        && let Some(v) = inputs.get(*input_idx)
704                                    {
705                                        kernel
706                                            .set_input_at(i, typed_for(&owned(*v), ty))
707                                            .unwrap_or_else(|e| fail(name, e));
708                                    }
709                                }
710                                // Each element is a binding of its own, so
711                                // the body's consts are evaluated for it.
712                                if !kernel.const_inits().is_empty()
713                                    && let Err(e) = kernel.init()
714                                {
715                                    panic!("tile '{}': projection body: {e}", self.spec.name);
716                                }
717                            }
718                            self.render_ops(body, inputs, engine, bodies, Some(entry), out);
719                        }
720                    });
721                }
722            }
723        }
724    }
725
726    /// A branch condition's truth, as the `cond` encoding decides it.
727    fn truthy(
728        &self,
729        source: &RtSource,
730        inputs: &[ValueRef<'_>],
731        child: Option<&mut BodyEntry>,
732    ) -> bool {
733        match source {
734            RtSource::Wire(i) => truthy_of(inputs.get(*i).copied().unwrap_or(ValueRef::None)),
735            RtSource::Child(name, k) => match child {
736                Some(entry) => match entry.hole(*k, name) {
737                    Some(i) => truthy_of(ValueRef::from(&entry.kernel.pull_at(i))),
738                    None => false,
739                },
740                None => false,
741            },
742        }
743    }
744}
745
746/// A borrowed view as an owned value, for the paths that bind values
747/// into a body program or a comprehension.
748fn owned(v: ValueRef<'_>) -> Value {
749    match v {
750        ValueRef::U64(n) => Value::U64(n),
751        ValueRef::I64(n) => Value::I64(n),
752        ValueRef::F64(f) => Value::F64(f),
753        ValueRef::Bool(b) => Value::Bool(b),
754        ValueRef::Str(s) => Value::Str(Arc::from(s)),
755        ValueRef::Bytes(b) => Value::Bytes(Arc::from(b)),
756        ValueRef::Json(j) => Value::Json(Arc::new(j.clone())),
757        ValueRef::None => Value::None,
758        ValueRef::Other(v) => v.clone(),
759    }
760}
761
762/// A cascaded value as the body's extern expects it. Values arrive on
763/// the render node's inputs as they are, so this is the value itself;
764/// text is parsed only when a `Str` reaches a non-string extern.
765fn typed_for(v: &Value, ty: &str) -> Value {
766    match (v, PortType::from_keyword(ty)) {
767        (Value::Str(_), Some(t)) if t != PortType::Str => retype(v, ty),
768        _ => v.clone(),
769    }
770}
771
772/// Replace each generator-call clause with the literal values its wire
773/// carries at this render: a list value (a stream, a vector, a JSON
774/// array) contributes its items, a scalar contributes itself. Text that
775/// spells a JSON array is read as one.
776fn bind_generators(
777    c: &crate::iteration::comprehension::Comprehension,
778    generators: &[(String, usize, String)],
779    inputs: &[ValueRef<'_>],
780) -> crate::iteration::comprehension::Comprehension {
781    use crate::iteration::comprehension::Comprehension as K;
782    use crate::iteration::comprehension::source::{LiteralValue, Source};
783    match c {
784        K::Clause {
785            name,
786            source: Source::Generator { .. },
787        } => {
788            let Some((_, idx, ty)) = generators.iter().find(|(n, _, _)| n == name) else {
789                return c.clone();
790            };
791            let raw = inputs.get(*idx).map(|v| owned(*v)).unwrap_or(Value::None);
792            let items: Vec<Value> =
793                match crate::iteration::comprehension::source_values::iteration_interior(&raw) {
794                    Some(interior) => interior,
795                    None => match &raw {
796                        Value::Str(text) => {
797                            match serde_json::from_str::<serde_json::Value>(text.trim()) {
798                                Ok(serde_json::Value::Array(items)) => items
799                                    .iter()
800                                    .map(|j| {
801                                        retype(
802                                            &Value::Str(j.to_string().trim_matches('"').into()),
803                                            ty,
804                                        )
805                                    })
806                                    .collect(),
807                                _ => vec![typed_for(&raw, ty)],
808                            }
809                        }
810                        _ => vec![raw.clone()],
811                    },
812                };
813            // An element declared `json` takes every item as the JSON
814            // value it is, its kind kept, so the body's extern receives
815            // what it declares; another declared type takes the scalar.
816            let json_items = ty == "json";
817            let values = items
818                .iter()
819                .map(|v| {
820                    if json_items {
821                        return LiteralValue::Json(json_of(v));
822                    }
823                    match v {
824                        Value::U64(n) => LiteralValue::unsigned(*n),
825                        Value::I64(n) => LiteralValue::Int(*n),
826                        Value::F64(f) => LiteralValue::Float(*f),
827                        Value::Bool(b) => LiteralValue::Bool(*b),
828                        // JSON scalars carry their own kind.
829                        Value::Json(j) => match j.as_ref() {
830                            serde_json::Value::Number(n) if n.is_u64() => {
831                                LiteralValue::unsigned(n.as_u64().unwrap_or(0))
832                            }
833                            serde_json::Value::Number(n) if n.is_i64() => {
834                                LiteralValue::Int(n.as_i64().unwrap_or(0))
835                            }
836                            serde_json::Value::Number(n) => {
837                                LiteralValue::Float(n.as_f64().unwrap_or(0.0))
838                            }
839                            serde_json::Value::Bool(b) => LiteralValue::Bool(*b),
840                            serde_json::Value::String(s) => LiteralValue::String(s.clone()),
841                            other => LiteralValue::String(other.to_string()),
842                        },
843                        other => LiteralValue::String(other.to_display_string()),
844                    }
845                })
846                .collect();
847            K::Clause {
848                name: name.clone(),
849                source: Source::Literal { values },
850            }
851        }
852        K::Clause { .. } => c.clone(),
853        K::Cartesian { children } => K::Cartesian {
854            children: children
855                .iter()
856                .map(|ch| bind_generators(ch, generators, inputs))
857                .collect(),
858        },
859        K::Zip { children, mode } => K::Zip {
860            children: children
861                .iter()
862                .map(|ch| bind_generators(ch, generators, inputs))
863                .collect(),
864            mode: *mode,
865        },
866        K::Union { children } => K::Union {
867            children: children
868                .iter()
869                .map(|ch| bind_generators(ch, generators, inputs))
870                .collect(),
871        },
872        K::Filter { child, predicate } => K::Filter {
873            child: Box::new(bind_generators(child, generators, inputs)),
874            predicate: predicate.clone(),
875        },
876        K::Order {
877            child,
878            strategy,
879            truncation,
880            seed,
881        } => K::Order {
882            child: Box::new(bind_generators(child, generators, inputs)),
883            strategy: *strategy,
884            truncation: *truncation,
885            seed: *seed,
886        },
887    }
888}
889
890/// Recover a typed value from the display text a cascaded wire arrives
891/// as, using the child extern's declared type.
892/// A generator item as a JSON value: a JSON item as it is, a scalar as
893/// the JSON of its kind.
894fn json_of(v: &Value) -> serde_json::Value {
895    match v {
896        Value::Json(j) => j.as_ref().clone(),
897        Value::U64(n) => serde_json::Value::from(*n),
898        Value::I64(n) => serde_json::Value::from(*n),
899        Value::F64(f) => serde_json::Number::from_f64(*f)
900            .map(serde_json::Value::Number)
901            .unwrap_or(serde_json::Value::Null),
902        Value::Bool(b) => serde_json::Value::Bool(*b),
903        Value::Str(s) => serde_json::Value::String(s.to_string()),
904        Value::None => serde_json::Value::Null,
905        other => serde_json::Value::String(other.to_display_string()),
906    }
907}
908
909fn retype(v: &Value, ty: &str) -> Value {
910    let text = v.to_display_string();
911    match PortType::from_keyword(ty) {
912        Some(PortType::U64) => text.parse().map(Value::U64).unwrap_or(Value::None),
913        Some(PortType::F64) => text.parse().map(Value::F64).unwrap_or(Value::None),
914        Some(PortType::Bool) => Value::Bool(matches!(text.trim(), "true" | "1")),
915        Some(PortType::Str) | None => Value::Str(text.into()),
916        Some(_) => v.clone(),
917    }
918}
919
920/// A cached body kernel: the program it was created from, the kernel,
921/// and the body's names resolved to indices once (polytile.md §7.1), so
922/// a tuple is bound and its holes read with no lookup per tuple.
923struct BodyEntry {
924    program: Arc<dyn KernelProgram>,
925    kernel: Box<dyn Kernel>,
926    /// The tuple elements' input indices, by position in the tuple;
927    /// `None` for an element the body does not declare.
928    elements: Option<Vec<Option<usize>>>,
929    /// The cascade's input indices, by position in the cascade.
930    cascade: Option<Vec<Option<usize>>>,
931    /// The body holes' output indices, by ordinal.
932    holes: Vec<Option<Option<usize>>>,
933}
934
935impl BodyEntry {
936    /// The output index of body hole `k`, named `name`, resolved once.
937    fn hole(&mut self, k: usize, name: &str) -> Option<usize> {
938        if self.holes.len() <= k {
939            self.holes.resize(k + 1, None);
940        }
941        if self.holes[k].is_none() {
942            self.holes[k] = Some(self.kernel.output_index(name));
943        }
944        self.holes[k].flatten()
945    }
946}
947
948/// The kernels one rendering state keeps over its projection bodies:
949/// one per body program and engine, created on the first render that
950/// reaches the body and reused by every render after, so a projection
951/// creates nothing per tuple. A tile render node owns one of these in
952/// its scratch (axiom S3): the storage belongs to the state that
953/// renders, never to the node, which every state of the program
954/// shares. A clone is empty, since a clone of a state is a new state.
955#[derive(Default)]
956pub struct BodyKernels {
957    entries: HashMap<(usize, crate::Engine), BodyEntry>,
958    /// Kernels created so far, for the tests.
959    created: u64,
960}
961
962impl Clone for BodyKernels {
963    fn clone(&self) -> Self {
964        Self::default()
965    }
966}
967
968// SAFETY: the kernels are reached only through `&mut self` (`with`),
969// which the owning state holds exclusively; every `&self` method
970// (`created`, `clone`, `Debug`) reads a count and touches no kernel. A
971// set inside a program shared across threads is therefore never used
972// from more than one thread, and a state created from that program
973// starts with an empty set of its own.
974unsafe impl Sync for BodyKernels {}
975
976impl std::fmt::Debug for BodyKernels {
977    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
978        f.debug_struct("BodyKernels")
979            .field("entries", &self.entries.len())
980            .field("created", &self.created)
981            .finish()
982    }
983}
984
985/// The engine a body kernel is kept under: the interpreter's body
986/// program is one program whatever the enclosing kernel's cone mode.
987fn body_engine_key(engine: crate::Engine) -> crate::Engine {
988    match engine {
989        crate::Engine::Interpreter(_) => crate::Engine::Interpreter(crate::JitMode::Auto),
990        other => other,
991    }
992}
993
994impl BodyKernels {
995    /// Kernels this state has created so far.
996    pub fn created(&self) -> u64 {
997        self.created
998    }
999
1000    /// The count and a clone, for the unit test of both.
1001    #[cfg(test)]
1002    fn clone_for_test(&self) -> (u64, BodyKernels) {
1003        (self.created, self.clone())
1004    }
1005
1006    /// Run `f` over the kernel for `program` on `engine`, created on
1007    /// first use. The entry is taken out for the call, so a nested
1008    /// projection's body finds the set free for its own kernels.
1009    fn with(
1010        &mut self,
1011        program: &Arc<dyn KernelProgram>,
1012        engine: crate::Engine,
1013        f: impl FnOnce(&mut BodyEntry, &mut BodyKernels),
1014    ) {
1015        let engine = body_engine_key(engine);
1016        // The entry pins its program so the address cannot be reused by
1017        // a later program while a kernel built for this one is kept.
1018        let key = (Arc::as_ptr(program) as *const () as usize, engine);
1019        let mut entry = self
1020            .entries
1021            .remove(&key)
1022            .filter(|e| Arc::ptr_eq(&e.program, program))
1023            .unwrap_or_else(|| {
1024                self.created += 1;
1025                BodyEntry {
1026                    program: program.clone(),
1027                    kernel: program.clone().create_uninitialized(),
1028                    elements: None,
1029                    cascade: None,
1030                    holes: Vec::new(),
1031                }
1032            });
1033        f(&mut entry, self);
1034        self.entries.insert(key, entry);
1035    }
1036}
1037
1038/// The tile render node's state: its projection bodies' kernels.
1039pub(crate) mod render_state {
1040    use super::{BodyKernels, TileRender};
1041    use crate::ast::{ScratchBuf, ScratchElem, Value};
1042
1043    pub(crate) fn layout(_node: &TileRender) -> Vec<ScratchElem> {
1044        vec![ScratchElem::Kernels]
1045    }
1046
1047    pub(crate) fn eval(
1048        node: &TileRender,
1049        scratch: &mut [ScratchBuf],
1050        inputs: &[Value],
1051        outputs: &mut [Value],
1052    ) {
1053        let bodies = bodies_of(&mut scratch[0]);
1054        outputs[0] = Value::Str(node.program.render(inputs, bodies).into());
1055    }
1056
1057    /// The body kernel set a scratch entry holds.
1058    pub(crate) fn bodies_of(entry: &mut ScratchBuf) -> &mut BodyKernels {
1059        match entry {
1060            ScratchBuf::Kernels(b) => b,
1061            other => panic!("a tile render's scratch holds {other:?}, not its body kernels"),
1062        }
1063    }
1064}
1065
1066/// A text sink that cannot fail: a `String`, or the `BytesSink` over a
1067/// step's string scratch in the compiled closure. `fmt::Write`'s
1068/// results are ignored because neither sink reports an error.
1069pub(crate) trait Sink: std::fmt::Write {
1070    fn put(&mut self, s: &str) {
1071        let _ = self.write_str(s);
1072    }
1073    fn put_char(&mut self, c: char) {
1074        let _ = self.write_char(c);
1075    }
1076}
1077
1078impl<W: std::fmt::Write> Sink for W {}
1079
1080/// Encode one value per a hole's encoding, into any text sink.
1081pub fn encode<W: std::fmt::Write>(value: &Value, enc: &HoleEncoding, out: &mut W) {
1082    encode_ref(ValueRef::from(value), enc, out)
1083}
1084
1085/// Encode a borrowed view of a value (compiled_handles.md §6): the compiled
1086/// closure calls this on its slot without owning a `Value`, and a
1087/// string hole is encoded from its producer's scratch in place.
1088pub fn encode_ref<W: std::fmt::Write>(value: ValueRef<'_>, enc: &HoleEncoding, out: &mut W) {
1089    if enc.cond {
1090        out.put_char(if truthy_of(value) { '1' } else { '0' });
1091        return;
1092    }
1093    let ty = enc.ty.as_deref();
1094    // A number with no format, or a float under a `.N` precision,
1095    // writes its digits straight into the sink (polytile.md §7.1):
1096    // digits, a sign, and a point need no escaping in any encoding or
1097    // position, and a numeric type is written bare in a JSON value
1098    // position, so the text is the same as the general path's, without
1099    // the `String` the general path builds. The float writer is
1100    // byte-identical to `format!` (`support::float_text`, proved by
1101    // `tests/float_text.rs`).
1102    if is_numeric_keyword(ty.unwrap_or("u64")) {
1103        match (enc.format.as_deref(), value) {
1104            (None, ValueRef::U64(n)) => {
1105                put_u64(n, out);
1106                return;
1107            }
1108            (None, ValueRef::I64(n)) => {
1109                if n < 0 {
1110                    out.put_char('-');
1111                }
1112                put_u64(n.unsigned_abs(), out);
1113                return;
1114            }
1115            (None, ValueRef::F64(f)) => {
1116                let _ = float_text::write_shortest(f, out);
1117                return;
1118            }
1119            (Some(fmt), ValueRef::F64(_) | ValueRef::U64(_)) => {
1120                if let (Some(prec), Some(f)) = (precision_of(fmt), as_f64(value)) {
1121                    let _ = float_text::write_fixed(f, prec, out);
1122                    return;
1123                }
1124            }
1125            _ => {}
1126        }
1127    }
1128    let text = formatted_text(value, ty, enc.format.as_deref());
1129    if enc.raw {
1130        out.put(&text);
1131        return;
1132    }
1133    match (enc.encoding.as_str(), enc.position) {
1134        ("json", HolePosition::InString) => push_json_escaped(&text, out),
1135        ("json", HolePosition::Value) => {
1136            let kind = ty.unwrap_or_else(|| value.port_type().to_keyword());
1137            match (kind, value) {
1138                (_, ValueRef::None) => out.put("null"),
1139                ("bool", _) => out.put(if truthy_of(value) { "true" } else { "false" }),
1140                ("json", ValueRef::Json(j)) => {
1141                    let _ = write!(out, "{j}");
1142                }
1143                ("str", _) | ("String", _) | ("string", _) => {
1144                    out.put_char('"');
1145                    push_json_escaped(&text, out);
1146                    out.put_char('"');
1147                }
1148                (k, _) if is_numeric_keyword(k) => out.put(&text),
1149                (_, ValueRef::Json(j)) => {
1150                    let _ = write!(out, "{j}");
1151                }
1152                (_, ValueRef::Bool(b)) => out.put(if b { "true" } else { "false" }),
1153                (_, ValueRef::U64(_)) | (_, ValueRef::F64(_)) => out.put(&text),
1154                _ => {
1155                    out.put_char('"');
1156                    push_json_escaped(&text, out);
1157                    out.put_char('"');
1158                }
1159            }
1160        }
1161        ("csv", _) => {
1162            if text.contains([',', '"', '\n']) {
1163                out.put_char('"');
1164                for (i, piece) in text.split('"').enumerate() {
1165                    if i > 0 {
1166                        out.put("\"\"");
1167                    }
1168                    out.put(piece);
1169                }
1170                out.put_char('"');
1171            } else {
1172                out.put(&text);
1173            }
1174        }
1175        _ => out.put(&text),
1176    }
1177}
1178
1179/// The decimal digits of `n`, written without an allocation.
1180fn put_u64<W: std::fmt::Write>(mut n: u64, out: &mut W) {
1181    if n == 0 {
1182        out.put_char('0');
1183        return;
1184    }
1185    let mut buf = [0u8; 20];
1186    let mut i = buf.len();
1187    while n > 0 {
1188        i -= 1;
1189        buf[i] = b'0' + (n % 10) as u8;
1190        n /= 10;
1191    }
1192    // SAFETY-free: the buffer holds ASCII digits only.
1193    out.put(std::str::from_utf8(&buf[i..]).expect("ascii digits"));
1194}
1195
1196fn truthy_of(v: ValueRef<'_>) -> bool {
1197    match v {
1198        ValueRef::Bool(b) => b,
1199        ValueRef::U64(n) => n != 0,
1200        ValueRef::F64(f) => f != 0.0,
1201        ValueRef::Str(s) => !s.is_empty() && s != "0" && s != "false",
1202        ValueRef::None => false,
1203        _ => true,
1204    }
1205}
1206
1207fn is_numeric_keyword(k: &str) -> bool {
1208    matches!(
1209        k,
1210        "u64"
1211            | "i64"
1212            | "f64"
1213            | "f32"
1214            | "u32"
1215            | "i32"
1216            | "u16"
1217            | "i16"
1218            | "u8"
1219            | "i8"
1220            | "u128"
1221            | "i128"
1222            | "f16"
1223    )
1224}
1225
1226/// Display text for a value under an optional printf-style format:
1227/// `.N` precision for floats, `0N` zero-padded width, `N` width, `>N`
1228/// and `<N` alignment, `x`/`X` hex for integers.
1229fn formatted_text<'a>(
1230    value: ValueRef<'a>,
1231    ty: Option<&str>,
1232    format: Option<&str>,
1233) -> std::borrow::Cow<'a, str> {
1234    use std::borrow::Cow;
1235    // A string with no format is borrowed as it is; everything else is
1236    // owned text. The base text is produced only where a format needs
1237    // it: a precision or a hex format writes the number once, itself.
1238    let base = |value: ValueRef<'a>| -> Cow<'a, str> {
1239        match (ty, value) {
1240            (Some("bool"), v) => Cow::Owned(truthy_of(v).to_string()),
1241            // Text quotes nothing: a JSON string in a text position is
1242            // its text, as a `str` hole is.
1243            (_, ValueRef::Json(serde_json::Value::String(s))) => Cow::Owned(s.clone()),
1244            (_, ValueRef::Json(j)) => Cow::Owned(j.to_string()),
1245            (_, v) => v.display(),
1246        }
1247    };
1248    let Some(fmt) = format else {
1249        return base(value);
1250    };
1251    let fmt = fmt.trim();
1252    if let Some(prec) = precision_of(fmt) {
1253        if let Some(f) = as_f64(value) {
1254            return Cow::Owned(float_text::fixed_string(f, prec));
1255        }
1256        return base(value);
1257    }
1258    if fmt == "x" || fmt == "X" {
1259        if let ValueRef::U64(n) = value {
1260            return Cow::Owned(if fmt == "x" {
1261                format!("{n:x}")
1262            } else {
1263                format!("{n:X}")
1264            });
1265        }
1266        return base(value);
1267    }
1268    let base = base(value);
1269    if let Some(w) = fmt.strip_prefix('0').and_then(|w| w.parse::<usize>().ok()) {
1270        return Cow::Owned(format!("{base:0>w$}"));
1271    }
1272    if let Some(w) = fmt.strip_prefix('>').and_then(|w| w.parse::<usize>().ok()) {
1273        return Cow::Owned(format!("{base:>w$}"));
1274    }
1275    if let Some(w) = fmt.strip_prefix('<').and_then(|w| w.parse::<usize>().ok()) {
1276        return Cow::Owned(format!("{base:<w$}"));
1277    }
1278    if let Ok(w) = fmt.parse::<usize>() {
1279        return Cow::Owned(format!("{base:>w$}"));
1280    }
1281    base
1282}
1283
1284fn as_f64(v: ValueRef<'_>) -> Option<f64> {
1285    match v {
1286        ValueRef::F64(f) => Some(f),
1287        ValueRef::U64(n) => Some(n as f64),
1288        _ => None,
1289    }
1290}
1291
1292/// The `N` of a `.N` precision format, after trimming.
1293fn precision_of(fmt: &str) -> Option<usize> {
1294    fmt.trim()
1295        .strip_prefix('.')
1296        .and_then(|p| p.parse::<usize>().ok())
1297}
1298
1299fn push_json_escaped<W: std::fmt::Write>(s: &str, out: &mut W) {
1300    for c in s.chars() {
1301        match c {
1302            '"' => out.put("\\\""),
1303            '\\' => out.put("\\\\"),
1304            '\n' => out.put("\\n"),
1305            '\r' => out.put("\\r"),
1306            '\t' => out.put("\\t"),
1307            c if (c as u32) < 0x20 => {
1308                let _ = write!(out, "\\u{:04x}", c as u32);
1309            }
1310            c => out.put_char(c),
1311        }
1312    }
1313}
1314
1315/// Encode one value under a hole's spec
1316/// (`encoding|position|type|format|flags`).
1317///
1318/// A library node a program may call. The compiler emitted one of
1319/// these per hole until encoding moved into the renderer, which
1320/// encodes each hole where it stands in the skeleton; nothing
1321/// generates this node now.
1322#[crate::polydat_node(category = Formatting)]
1323fn tile_encode(
1324    value: Value,
1325    spec: Const<&str>,
1326    #[poly_const(HoleEncoding::from_spec, from = spec)] enc: &HoleEncoding,
1327) -> String {
1328    let mut out = String::new();
1329    encode(&value, enc, &mut out);
1330    out
1331}
1332
1333/// The closure-tier form of `tile_render` (polytile.md §7.2): every hole
1334/// value is read from its slot as a borrowed view, by the wire type the
1335/// kernel fixed, and the document is rendered straight into the step's
1336/// own string scratch through a `BytesSink`; nothing is decoded into
1337/// an owned `Value` on the way. A wire
1338/// wider than one slot, or of a kind without a view, is read as a value
1339/// through the typed decoder.
1340fn tile_render_compiled(
1341    node: &TileRender,
1342    wire_types: &[PortType],
1343    engine: crate::Engine,
1344) -> crate::ast::CompiledSlotKit {
1345    // Native code bakes this address, so it must outlive every kernel
1346    // compiled from the program: one reference count of the node's own
1347    // `Arc` is given up here and never taken back. Tile programs are
1348    // not shared through a process-wide table keyed by their bytes
1349    // (polytile.md §7.1, "Lifetime").
1350    let program: &'static TileProgram = unsafe { &*std::sync::Arc::into_raw(node.program.clone()) };
1351    // Per wire: its first slot and its type; a one-slot carrier or a
1352    // `Ref2` kind is viewed in place, a two-slot immediate is decoded.
1353    let mut reads: Vec<(usize, PortType)> = Vec::with_capacity(wire_types.len());
1354    let mut offset = 0usize;
1355    for &ty in wire_types {
1356        reads.push((offset, ty));
1357        offset += ty.slot_width().max(1);
1358    }
1359    crate::ast::CompiledSlotKit {
1360        scratch: vec![
1361            crate::ast::ScratchElem::Str,
1362            crate::ast::ScratchElem::Kernels,
1363        ],
1364        op: Box::new(
1365            move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [crate::ast::ScratchBuf]| {
1366                // Owned values only for the two-slot immediates; they keep
1367                // their positions, so the views are built once they are
1368                // all in place.
1369                let owned_values: Vec<Value> = reads
1370                    .iter()
1371                    .filter(|(_, ty)| ty.slot_color() == crate::ast::SlotColor::Imm2)
1372                    .map(|&(offset, ty)| crate::compile::marshal::decode_output(inputs, offset, ty))
1373                    .collect();
1374                let mut next_owned = 0usize;
1375                let refs: Vec<ValueRef<'_>> = reads
1376                    .iter()
1377                    .map(|&(offset, ty)| {
1378                        if ty.slot_color() == crate::ast::SlotColor::Imm2 {
1379                            let v = ValueRef::from(&owned_values[next_owned]);
1380                            next_owned += 1;
1381                            v
1382                        } else {
1383                            // SAFETY: a pair in the buffer was published by
1384                            // a producer whose storage is alive (S3, S4).
1385                            unsafe { crate::compile::marshal::arg_ref(ty, &inputs[offset..]) }
1386                        }
1387                    })
1388                    .collect();
1389                // The document is rendered straight into this step's own
1390                // scratch (axiom S3). A projection body runs on the
1391                // engine this kit was built for — the engine of the
1392                // kernel doing the rendering — which is the rule a
1393                // `for` body already follows. The closure had no engine
1394                // to ask until `compiled_slot` was given the one it is
1395                // building for; before that every compiled kernel
1396                // rendered its bodies on `Engine::default()`.
1397                let (text, bodies) = scratch.split_at_mut(1);
1398                let crate::ast::ScratchBuf::Str(buf) = &mut text[0] else {
1399                    unreachable!("the render step owns a string entry");
1400                };
1401                let bodies = render_state::bodies_of(&mut bodies[0]);
1402                buf.clear();
1403                let mut w = BytesSink(buf);
1404                program.render_into(&refs, engine, bodies, &mut w);
1405                let (p, l) = scratch[0].ptr_len();
1406                outputs[0] = p;
1407                outputs[1] = l;
1408            },
1409        ),
1410    }
1411}
1412
1413/// A text sink over the bytes of a step's string scratch: what a
1414/// compiled render writes into.
1415pub(crate) struct BytesSink<'a>(pub(crate) &'a mut Vec<u8>);
1416
1417impl std::fmt::Write for BytesSink<'_> {
1418    fn write_str(&mut self, s: &str) -> std::fmt::Result {
1419        self.0.extend_from_slice(s.as_bytes());
1420        Ok(())
1421    }
1422}
1423
1424/// Render a compiled tile skeleton over its encoded hole texts.
1425///
1426/// The compiler emits this for a `tile` statement and hands it the
1427/// skeleton it built, projection bodies and all, as a value rather than
1428/// as text, so construction has no payload to parse and the bodies keep
1429/// what the compiler knew about them: their source directory, library
1430/// paths, strict flag, pragmas, and the modules the program had
1431/// resolved.
1432#[crate::polydat_node(
1433    category = Formatting,
1434    variadic_min = 0,
1435    compiled_slot = tile_render_compiled,
1436    state = render_state
1437)]
1438fn tile_render(program: Const<Arc<TileProgram>>, values: &[Value]) -> String {
1439    // A render without a state's scratch (a node evaluated on its
1440    // own): body kernels of the call's own.
1441    program.render(values, &mut BodyKernels::default())
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446    use super::*;
1447
1448    /// A rendering state's body kernels are created on the first
1449    /// render that reaches a projection and reused by every render
1450    /// after; a clone of the set is a new, empty set.
1451    #[test]
1452    fn body_kernels_are_created_once_per_state_and_reused() {
1453        let src =
1454            "input cycle: u64\ntile t : text := \"@for k in 0..3 sep \\\",\\\" {${k + cycle}}\"\n";
1455        let mut k = crate::dsl::compile_polydat_interpreter(src).unwrap();
1456        let program = k.program();
1457        let node = (0..program.node_count())
1458            .find(|&i| program.node_meta(i).name == "tile_render")
1459            .expect("the tile's render node");
1460        let bodies_of = |k: &mut PolydatKernel| match &k.state().core.node_scratch[node][0] {
1461            crate::ast::ScratchBuf::Kernels(b) => b.clone_for_test(),
1462            other => panic!("{other:?}"),
1463        };
1464        assert_eq!(bodies_of(&mut k).0, 0, "nothing before the first render");
1465        k.set_inputs(&[10]);
1466        assert_eq!(k.pull_ref("t").as_str(), "10,11,12");
1467        assert_eq!(bodies_of(&mut k).0, 1, "one kernel for the body");
1468        for c in 0..5u64 {
1469            k.set_inputs(&[c]);
1470            let _ = k.pull_ref("t");
1471        }
1472        let (created, clone) = bodies_of(&mut k);
1473        assert_eq!(created, 1, "reused across renders");
1474        assert_eq!(clone.created(), 0, "a clone is a new state's empty set");
1475    }
1476
1477    fn enc(
1478        encoding: &str,
1479        position: HolePosition,
1480        ty: Option<&str>,
1481        format: Option<&str>,
1482        raw: bool,
1483    ) -> HoleEncoding {
1484        HoleEncoding {
1485            encoding: encoding.into(),
1486            position,
1487            ty: ty.map(str::to_string),
1488            format: format.map(str::to_string),
1489            raw,
1490            cond: false,
1491        }
1492    }
1493
1494    #[test]
1495    fn json_value_and_string_positions_encode_by_type() {
1496        let mut out = String::new();
1497        encode(
1498            &Value::Str("a\"b".into()),
1499            &enc("json", HolePosition::Value, Some("str"), None, false),
1500            &mut out,
1501        );
1502        assert_eq!(out, "\"a\\\"b\"");
1503        out.clear();
1504        encode(
1505            &Value::U64(7),
1506            &enc("json", HolePosition::Value, None, None, false),
1507            &mut out,
1508        );
1509        assert_eq!(out, "7");
1510        out.clear();
1511        encode(
1512            &Value::Str("x\ny".into()),
1513            &enc("json", HolePosition::InString, None, None, false),
1514            &mut out,
1515        );
1516        assert_eq!(out, "x\\ny");
1517        out.clear();
1518        encode(
1519            &Value::F64(2.0 / 3.0),
1520            &enc("json", HolePosition::Value, None, Some(".2"), false),
1521            &mut out,
1522        );
1523        assert_eq!(out, "0.67");
1524        out.clear();
1525        encode(
1526            &Value::None,
1527            &enc("json", HolePosition::Value, None, None, false),
1528            &mut out,
1529        );
1530        assert_eq!(out, "null");
1531    }
1532
1533    #[test]
1534    fn spec_round_trips() {
1535        let e = enc(
1536            "json",
1537            HolePosition::InString,
1538            Some("u64"),
1539            Some(".2"),
1540            true,
1541        );
1542        assert_eq!(HoleEncoding::from_spec(&e.to_spec()), e);
1543        let c = HoleEncoding {
1544            cond: true,
1545            ..enc("text", HolePosition::Text, None, None, false)
1546        };
1547        assert_eq!(HoleEncoding::from_spec(&c.to_spec()), c);
1548    }
1549
1550    #[test]
1551    fn csv_quotes_when_needed_and_raw_skips_escaping() {
1552        let mut out = String::new();
1553        encode(
1554            &Value::Str("a,b".into()),
1555            &enc("csv", HolePosition::Text, None, None, false),
1556            &mut out,
1557        );
1558        assert_eq!(out, "\"a,b\"");
1559        out.clear();
1560        encode(
1561            &Value::Str("a\"b".into()),
1562            &enc("json", HolePosition::Value, None, None, true),
1563            &mut out,
1564        );
1565        assert_eq!(out, "a\"b");
1566    }
1567
1568    #[test]
1569    fn formats_apply_before_encoding() {
1570        assert_eq!(formatted_text(ValueRef::U64(5), None, Some("03")), "005");
1571        assert_eq!(formatted_text(ValueRef::U64(255), None, Some("x")), "ff");
1572        assert_eq!(
1573            formatted_text(ValueRef::Str("ab"), None, Some(">4")),
1574            "  ab"
1575        );
1576        assert_eq!(
1577            formatted_text(ValueRef::F64(0.295), None, Some(".2")),
1578            "0.29"
1579        );
1580        assert_eq!(
1581            formatted_text(ValueRef::U64(7), None, Some(" .3 ")),
1582            "7.000"
1583        );
1584    }
1585
1586    /// A float hole's bytes are `format!`'s, on the direct path and on
1587    /// the general one, in every encoding and position.
1588    #[test]
1589    fn float_holes_write_rust_text() {
1590        let cases: [(f64, Option<&str>, &str); 8] = [
1591            (100.0, None, "100.0"),
1592            (0.1, None, "0.1"),
1593            (5e-5, None, "5e-5"),
1594            (1e16, None, "1e16"),
1595            (-0.0, None, "-0.0"),
1596            (2.0 / 3.0, Some(".2"), "0.67"),
1597            (0.295, Some(".2"), "0.29"),
1598            (2.5, Some(".0"), "2"),
1599        ];
1600        for (f, fmt, want) in cases {
1601            for (encoding, position) in [
1602                ("text", HolePosition::Text),
1603                ("json", HolePosition::Value),
1604                ("json", HolePosition::InString),
1605                ("csv", HolePosition::Text),
1606            ] {
1607                for ty in [None, Some("f64")] {
1608                    let mut out = String::new();
1609                    encode(
1610                        &Value::F64(f),
1611                        &enc(encoding, position, ty, fmt, false),
1612                        &mut out,
1613                    );
1614                    assert_eq!(out, want, "{f:?} {fmt:?} {encoding} {position:?} {ty:?}");
1615                }
1616            }
1617            // A non-numeric declared type takes the general path; the
1618            // text is the same, quoted where the position quotes.
1619            let mut out = String::new();
1620            encode(
1621                &Value::F64(f),
1622                &enc("json", HolePosition::Value, Some("str"), fmt, false),
1623                &mut out,
1624            );
1625            assert_eq!(out, format!("\"{want}\""));
1626        }
1627    }
1628
1629    /// A tile's ops say whether rendering it re-runs a projection body
1630    /// (`TileProgram::has_projections`): a body inside a branch arm
1631    /// counts, and the answer agrees with the compiled body programs.
1632    #[test]
1633    fn a_tile_reports_whether_any_op_re_runs_a_projection() {
1634        fn wire(index: usize) -> HoleSource {
1635            HoleSource::Wire {
1636                index,
1637                spec: "text|text".into(),
1638            }
1639        }
1640        fn program(ops: Vec<TileOp>, children: Vec<ChildSpec>) -> TileProgram {
1641            TileProgram::from_json(
1642                &TileSpec {
1643                    name: "t".into(),
1644                    encoding: "text".into(),
1645                    ops,
1646                    children,
1647                }
1648                .to_json(),
1649            )
1650        }
1651        let body = || ChildSpec {
1652            source: "input cycle: u64\nextern k: u64\nout := u64_add(k, 0)\n".to_string(),
1653            cascade: Vec::new(),
1654        };
1655        let repeat = |child: usize| TileOp::Repeat {
1656            stream: StreamerValue::parse_text("k in 0..3").unwrap().to_json(),
1657            child,
1658            sep: ",".into(),
1659            body: vec![TileOp::Hole(HoleSource::Child {
1660                name: "out".into(),
1661                spec: "text|text".into(),
1662            })],
1663            generators: Vec::new(),
1664        };
1665
1666        // Statics, holes, and a branch over them: nothing re-runs.
1667        let flat = program(
1668            vec![
1669                TileOp::Static("a=".into()),
1670                TileOp::Hole(wire(0)),
1671                TileOp::Branch {
1672                    cond: wire(1),
1673                    then: vec![TileOp::Static("yes".into())],
1674                    otherwise: vec![TileOp::Hole(wire(0))],
1675                },
1676            ],
1677            Vec::new(),
1678        );
1679        assert!(!flat.has_projections());
1680        assert!(flat.children.is_empty());
1681
1682        // A projection at the top level.
1683        let top = program(vec![TileOp::Static("[".into()), repeat(0)], vec![body()]);
1684        assert!(top.has_projections());
1685        assert_eq!(top.children.len(), 1);
1686
1687        // A projection inside a branch arm: the walk recurses, so the
1688        // arm that holds it is found whichever arm it is.
1689        for (then, otherwise) in [
1690            (vec![repeat(0)], vec![TileOp::Static("none".into())]),
1691            (vec![TileOp::Static("none".into())], vec![repeat(0)]),
1692        ] {
1693            let branched = program(
1694                vec![TileOp::Branch {
1695                    cond: wire(0),
1696                    then,
1697                    otherwise,
1698                }],
1699                vec![body()],
1700            );
1701            assert!(branched.has_projections());
1702        }
1703    }
1704}