Skip to main content

polydat_core/dsl/
tile_lower.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Lowering a `tile` statement to a program (SRD 114 §6).
5//!
6//! Each hole becomes a binding of its expression in the enclosing
7//! scope (or the wire itself); the render node carries the hole's
8//! encoding spec (the tile's encoding, the hole's position, its
9//! declared type, its format, and its raw flag) and encodes the value
10//! where the hole stands. There is no encoder node: a `tile_encode`
11//! per hole is what this emitted before encoding moved into the
12//! renderer. Static runs fold into single instructions. Projection
13//! bodies lower the same way in a child program; the render node
14//! compiles that body once at setup and re-runs it per tuple. The tile
15//! itself becomes a `tile_render` call over the hole wires and any
16//! wires a body imports.
17
18use std::collections::BTreeSet;
19
20use crate::ast::PortType;
21use crate::compile::assembly::PolydatAssembler;
22use crate::iteration::comprehension::StreamerValue;
23use crate::library::tile_render::{
24    ChildSpec, HoleEncoding, HolePosition, HoleSource, TileOp, TileSpec,
25};
26
27use super::ast::{Expr, ForSource, ForSourceKind, TileDef, TileOptions, TilePiece};
28use super::compile::Compiler;
29use super::refs::collect_expr_refs;
30use super::tile::render_template;
31
32impl Compiler {
33    /// Lower a tile into encode bindings and a `tile_render` binding
34    /// named after the tile. Records the tile so later tiles can splice
35    /// it.
36    pub(super) fn compile_tile(
37        &mut self,
38        asm: &mut PolydatAssembler,
39        tile: &TileDef,
40    ) -> Result<(), String> {
41        let encoding = tile.encoding.clone().unwrap_or_else(|| "text".to_string());
42        let mut lowering = TileLowering {
43            tile_name: tile.name.clone(),
44            encoding: encoding.clone(),
45            options: tile.options.clone(),
46            inputs: Vec::new(),
47            children: Vec::new(),
48            bodies: Vec::new(),
49            hole_counter: 0,
50            in_string: tile.options.in_string,
51            strict: self.strict || tile.options.strict,
52            span: tile.span,
53        };
54        let pieces = self.splice_tiles(&tile.pieces, &tile.name, &tile.encoding, 0)?;
55        if encoding == "json" && !tile.options.in_string && !tile.name.starts_with("__") {
56            Self::validate_json_skeleton_impl(&tile.name, &pieces)?;
57        }
58        let ops = lowering.lower_pieces(self, asm, &pieces, None)?;
59        let spec = TileSpec {
60            name: tile.name.clone(),
61            encoding,
62            ops,
63            children: lowering.children,
64        };
65        let mut shape = SkeletonShape::default();
66        shape.count(&spec.ops);
67        self.pending_events
68            .push(super::events::CompileEvent::TileCompiled {
69                tile: tile.name.clone(),
70                encoding: spec.encoding.clone(),
71                statics: shape.statics,
72                static_bytes: shape.static_bytes,
73                holes: shape.holes,
74                branches: shape.branches,
75                projections: shape.projections,
76                bodies: spec.children.iter().map(|c| c.source.clone()).collect(),
77            });
78        // The skeleton goes to the node as the value it is. It used to
79        // be serialized to JSON, written into the program as a string
80        // literal, and parsed back at node construction — which lost
81        // everything about a projection body that JSON cannot carry,
82        // the source directory, library paths, strict flag, pragmas
83        // and resolved modules among them, and made a malformed
84        // payload a panic at construction rather than a compile error.
85        let bodies = lowering.bodies.clone();
86        let program = std::sync::Arc::new(
87            crate::library::tile_render::TileProgram::from_parts(spec, bodies)
88                .map_err(|e| format!("tile '{}': {e}", tile.name))?,
89        );
90        let wires: Vec<crate::compile::assembly::WireRef> = lowering
91            .inputs
92            .iter()
93            .map(|name| {
94                if self.input_names.contains(name) {
95                    crate::compile::assembly::WireRef::input(name)
96                } else {
97                    crate::compile::assembly::WireRef::node(name)
98                }
99            })
100            .collect();
101        let wire_types: Vec<crate::ast::PortType> = lowering
102            .inputs
103            .iter()
104            .map(|n| {
105                asm.output_type(n)
106                    .or_else(|| asm.input_type(n))
107                    .unwrap_or(crate::ast::PortType::Str)
108            })
109            .collect();
110        let node = super::factory::build_node(
111            "tile_render",
112            &wires,
113            &wire_types,
114            &[super::factory::ConstArg::Opaque(program)],
115        )
116        .map_err(|e| format!("tile '{}': {e}", tile.name))?;
117        asm.add_node(&tile.name, node, wires);
118        // The tile's name joins the binding targets, which the output
119        // pass declares in the order they were bound. Registering the
120        // output here instead would put the tile ahead of every
121        // binding that pass has not reached yet.
122        self.all_names.push(tile.name.clone());
123        if lowering.inputs.is_empty() {
124            // No holes: the tile is a constant, and says so.
125            asm.mark_const_output(&tile.name);
126            asm.set_output_modifier(&tile.name, super::ast::BindingModifier::CONST);
127        }
128        self.tiles.push(tile.clone());
129        Ok(())
130    }
131
132    /// SRD 114 §5.2: a `json` skeleton must be valid JSON once every
133    /// hole is a placeholder. Every hole stands in as `0`, which is a
134    /// value in value position and text inside a string or a key; a
135    /// projection body appears once; a branch shows its first arm. A
136    /// document that fails to parse is rejected here, with serde's
137    /// position, rather than rendering malformed output per cycle.
138    /// Author tiles only: the compiler's own body tiles are fragments.
139    fn validate_json_skeleton_impl(name: &str, pieces: &[TilePiece]) -> Result<(), String> {
140        fn placeholder(pieces: &[TilePiece], out: &mut String) {
141            for piece in pieces {
142                match piece {
143                    TilePiece::Static(s) => out.push_str(s),
144                    TilePiece::Hole(_) => out.push('0'),
145                    TilePiece::Projection { body, .. } => placeholder(body, out),
146                    TilePiece::Branch { then, .. } => placeholder(then, out),
147                }
148            }
149        }
150        let mut doc = String::new();
151        placeholder(pieces, &mut doc);
152        match serde_json::from_str::<serde_json::Value>(&doc) {
153            Ok(_) => Ok(()),
154            Err(e) => Err(format!(
155                "tile '{name}': the json body is not valid JSON once every hole is a placeholder: {e}; \
156                 with holes as `0`, one repetition per projection, and each branch's first arm, the skeleton reads: {}",
157                doc.trim()
158            )),
159        }
160    }
161
162    /// Replace splice holes, `${name}` where `name` is an earlier tile,
163    /// with that tile's pieces, recursively.
164    fn splice_tiles(
165        &self,
166        pieces: &[TilePiece],
167        current: &str,
168        encoding: &Option<String>,
169        depth: usize,
170    ) -> Result<Vec<TilePiece>, String> {
171        if depth > 16 {
172            return Err(format!(
173                "tile '{current}': splice nesting too deep; is there a cycle?"
174            ));
175        }
176        let mut out = Vec::with_capacity(pieces.len());
177        for piece in pieces {
178            match piece {
179                TilePiece::Hole(h) => {
180                    if let Expr::Ident(name, _) = &h.expr
181                        && h.decl_type.is_none()
182                        && h.format.is_none()
183                        && let Some(other) = self.tiles.iter().rev().find(|t| &t.name == name)
184                    {
185                        if name == current {
186                            return Err(format!("tile '{current}' splices itself"));
187                        }
188                        // Pieces are spliced only into a tile of the same
189                        // encoding, where they mean the same thing. Across
190                        // encodings the inner tile is an ordinary wire:
191                        // its rendered text enters through the hole and
192                        // is encoded (or, with `!`, inlined) as any string.
193                        if other.encoding.as_deref().unwrap_or("text")
194                            == encoding.as_deref().unwrap_or("text")
195                        {
196                            out.extend(self.splice_tiles(
197                                &other.pieces,
198                                name,
199                                encoding,
200                                depth + 1,
201                            )?);
202                            continue;
203                        }
204                    }
205                    out.push(piece.clone());
206                }
207                TilePiece::Projection {
208                    source,
209                    sep,
210                    body,
211                    span,
212                } => out.push(TilePiece::Projection {
213                    source: source.clone(),
214                    sep: sep.clone(),
215                    body: self.splice_tiles(body, current, encoding, depth + 1)?,
216                    span: *span,
217                }),
218                TilePiece::Branch {
219                    cond,
220                    then,
221                    otherwise,
222                    span,
223                } => out.push(TilePiece::Branch {
224                    cond: cond.clone(),
225                    then: self.splice_tiles(then, current, encoding, depth + 1)?,
226                    otherwise: match otherwise {
227                        Some(o) => Some(self.splice_tiles(o, current, encoding, depth + 1)?),
228                        None => None,
229                    },
230                    span: *span,
231                }),
232                other => out.push(other.clone()),
233            }
234        }
235        Ok(out)
236    }
237}
238
239/// State for one tile's lowering.
240struct TileLowering {
241    tile_name: String,
242    encoding: String,
243    options: TileOptions,
244    /// Wire names passed to `tile_render`, in input order.
245    inputs: Vec<String>,
246    children: Vec<ChildSpec>,
247    /// Each projection body as the carrier a `for` body uses, built
248    /// here under the parent's settings and handed to the node with
249    /// the skeleton rather than re-derived from text at the far end.
250    bodies: Vec<std::sync::Arc<super::traversal::BodySource>>,
251    hole_counter: usize,
252    /// Whether the static text so far leaves a `json` skeleton inside a
253    /// string literal.
254    in_string: bool,
255    /// Reject implicit adapters at holes (SRD 114 §4.4).
256    strict: bool,
257    span: super::lexer::Span,
258}
259
260/// How one hole was typed (SRD 114 §4): the wire's compile-time type,
261/// the declared type if any, the type the encoder sees, and the
262/// adapter between them when the declaration asks for one.
263struct HoleTyping {
264    wire: PortType,
265    declared: Option<PortType>,
266    effective: PortType,
267    adapter: Option<(PortType, PortType)>,
268    expectation: &'static str,
269}
270
271/// A projection body under construction.
272struct BodyContext {
273    elements: Vec<(String, PortType)>,
274    bindings: Vec<String>,
275    /// Outer names the body references: `(name, node input index, type keyword)`.
276    cascade: Vec<(String, usize, String)>,
277    /// Producers re-bound inside the body program for nested projections.
278    producers: Vec<String>,
279    counter: usize,
280}
281
282impl TileLowering {
283    fn lower_pieces(
284        &mut self,
285        compiler: &mut Compiler,
286        asm: &mut PolydatAssembler,
287        pieces: &[TilePiece],
288        mut body: Option<&mut BodyContext>,
289    ) -> Result<Vec<TileOp>, String> {
290        let mut ops: Vec<TileOp> = Vec::new();
291        let mut static_buf = String::new();
292        let flush = |buf: &mut String, ops: &mut Vec<TileOp>| {
293            if !buf.is_empty() {
294                ops.push(TileOp::Static(std::mem::take(buf)));
295            }
296        };
297        for piece in pieces {
298            match piece {
299                TilePiece::Static(s) => {
300                    self.track_string_state(s);
301                    static_buf.push_str(s);
302                }
303                TilePiece::Hole(h) => {
304                    flush(&mut static_buf, &mut ops);
305                    let enc = HoleEncoding {
306                        encoding: self.encoding.clone(),
307                        position: self.position(),
308                        ty: h.decl_type.clone(),
309                        format: h.format.clone(),
310                        raw: h.raw,
311                        cond: false,
312                    };
313                    let source = match body.as_deref_mut() {
314                        Some(ctx) => {
315                            self.body_hole(compiler, asm, &h.to_text(), &h.expr, enc, ctx)?
316                        }
317                        None => self.wire_hole(compiler, asm, &h.to_text(), &h.expr, enc)?,
318                    };
319                    ops.push(TileOp::Hole(source));
320                }
321                TilePiece::Branch {
322                    cond,
323                    then,
324                    otherwise,
325                    ..
326                } => {
327                    flush(&mut static_buf, &mut ops);
328                    let enc = HoleEncoding {
329                        encoding: self.encoding.clone(),
330                        position: HolePosition::Text,
331                        ty: None,
332                        format: None,
333                        raw: true,
334                        cond: true,
335                    };
336                    let text = format!("if {}", super::pprint::pp_expr(cond));
337                    let source = match body.as_deref_mut() {
338                        Some(ctx) => self.body_hole(compiler, asm, &text, cond, enc, ctx)?,
339                        None => self.wire_hole(compiler, asm, &text, cond, enc)?,
340                    };
341                    let saved = self.in_string;
342                    let then_ops = self.lower_pieces(compiler, asm, then, body.as_deref_mut())?;
343                    self.in_string = saved;
344                    let else_ops = match otherwise {
345                        Some(o) => self.lower_pieces(compiler, asm, o, body.as_deref_mut())?,
346                        None => Vec::new(),
347                    };
348                    self.in_string = saved;
349                    ops.push(TileOp::Branch {
350                        cond: source,
351                        then: then_ops,
352                        otherwise: else_ops,
353                    });
354                }
355                TilePiece::Projection {
356                    source,
357                    sep,
358                    body: proj_body,
359                    ..
360                } => {
361                    flush(&mut static_buf, &mut ops);
362                    if let Some(ctx) = body.as_deref_mut() {
363                        let op =
364                            self.nested_projection(compiler, asm, source, sep, proj_body, ctx)?;
365                        ops.push(op);
366                        continue;
367                    }
368                    // The same resolution the `for` construct uses, so
369                    // inline text, bound producers, and derivations
370                    // (`base where ... order ...`) all project.
371                    let (comprehension, warnings) = super::traversal::resolve_source_with(
372                        source,
373                        &compiler.producers_seen,
374                        compiler.validation_mode(),
375                        &compiler.source_scope(),
376                    )
377                    .map_err(|e| format!("tile '{}': projection: {e}", self.tile_name))?;
378                    compiler
379                        .pending_events
380                        .extend(super::traversal::warning_events(source, &warnings));
381                    self.check_bounded(&comprehension, &source.to_text())?;
382                    self.check_predicates(&comprehension, &source.to_text())?;
383                    let stream =
384                        StreamerValue::new(source.to_text(), comprehension.clone()).to_json();
385                    let mut probe = |expr: &str| self.generator_type(compiler, asm, expr, None);
386                    let elements = super::traversal::element_types(&comprehension, &mut probe)
387                        .map_err(|e| {
388                            format!(
389                                "tile '{}': projection `for {}`: {e}",
390                                self.tile_name,
391                                source.to_text()
392                            )
393                        })?;
394                    // Generator-call sources are expressions over the
395                    // enclosing scope: each compiles to a wire here and
396                    // its value reaches the render node as an input, so
397                    // the projection needs no kernel of its own.
398                    let mut generators = Vec::new();
399                    for (name, expr_text) in generator_clauses(&comprehension) {
400                        if expr_text.contains('{') {
401                            return Err(format!(
402                                "tile '{}': projection `for {}`: generator `{expr_text}` uses a `{{name}}` placeholder; \
403                                 in a tile the generator is an expression over the scope's wires, so name the wire directly",
404                                self.tile_name,
405                                source.to_text()
406                            ));
407                        }
408                        let expr = super::tile::parse_hole_expr(&expr_text).map_err(|e| {
409                            format!(
410                                "tile '{}': projection `for {}`: generator `{expr_text}`: {e}",
411                                self.tile_name,
412                                source.to_text()
413                            )
414                        })?;
415                        let wire = self.next_name("g");
416                        compiler
417                            .compile_binding(asm, std::slice::from_ref(&wire), &expr)
418                            .map_err(|e| {
419                                format!(
420                                    "tile '{}': projection `for {}`: generator `{expr_text}`: {e}",
421                                    self.tile_name,
422                                    source.to_text()
423                                )
424                            })?;
425                        let idx = self.push_input(wire);
426                        let ty = elements
427                            .iter()
428                            .find(|(n, _)| *n == name)
429                            .map(|(_, t)| t.to_keyword())
430                            .unwrap_or("str");
431                        generators.push((name, idx, ty.to_string()));
432                    }
433                    let mut ctx = BodyContext {
434                        elements,
435                        bindings: Vec::new(),
436                        cascade: Vec::new(),
437                        producers: Vec::new(),
438                        counter: 0,
439                    };
440                    let saved = self.in_string;
441                    let body_ops = self.lower_pieces(compiler, asm, proj_body, Some(&mut ctx))?;
442                    self.in_string = saved;
443                    // The body's own input is the tuple index; the
444                    // program's `cycle` reaches it as a cascaded extern
445                    // like any other outer wire.
446                    let mut src = String::from("input __tuple: u64\n");
447                    for (name, ty) in &ctx.elements {
448                        src.push_str(&format!("extern {name}: {}\n", ty.to_keyword()));
449                    }
450                    for (name, _, ty) in &ctx.cascade {
451                        src.push_str(&format!("extern {name}: {ty}\n"));
452                    }
453                    for b in &ctx.bindings {
454                        src.push_str(b);
455                        src.push('\n');
456                    }
457                    // The body as the carrier a `for` body uses, under
458                    // the settings the parent compiles with: its
459                    // source directory and library paths, its strict
460                    // flag, its pragmas, and the modules it has
461                    // resolved, which a body written as text inside a
462                    // JSON payload could not be given. It is compiled
463                    // once here, so a bad reference is a compile error
464                    // of the enclosing program rather than a failure
465                    // inside `tile_render`'s construction, and the
466                    // program that compile produced is the one the
467                    // renderer uses.
468                    let body = compiler
469                        .body_source_for(
470                            &src,
471                            &format!("tile '{}' :: projection body", self.tile_name),
472                        )
473                        .map_err(|e| format!("tile '{}': projection body: {e}", self.tile_name))?;
474                    body.program_on(crate::Engine::Interpreter(crate::JitMode::Auto))
475                        .map_err(|e| format!("tile '{}': projection body: {e}", self.tile_name))?;
476                    self.bodies.push(std::sync::Arc::new(body));
477                    self.children.push(ChildSpec {
478                        source: src,
479                        cascade: ctx.cascade,
480                    });
481                    let child = self.children.len() - 1;
482                    let default_sep = match self.encoding.as_str() {
483                        "json" | "csv" => ",",
484                        _ => "",
485                    };
486                    ops.push(TileOp::Repeat {
487                        stream,
488                        child,
489                        sep: sep.clone().unwrap_or_else(|| default_sep.to_string()),
490                        body: body_ops,
491                        generators,
492                    });
493                }
494            }
495        }
496        flush(&mut static_buf, &mut ops);
497        Ok(ops)
498    }
499
500    /// The compile-time type of a hole expression (SRD 114 §4.2): the
501    /// same inference every binding gets, with projection elements and
502    /// cascaded outer wires resolved from the body context.
503    fn infer_type(
504        &self,
505        compiler: &Compiler,
506        asm: &PolydatAssembler,
507        expr: &Expr,
508        ctx: Option<&BodyContext>,
509    ) -> PortType {
510        if let Expr::Ident(name, _) = expr
511            && let Some(ctx) = ctx
512        {
513            if let Some((_, t)) = ctx.elements.iter().find(|(n, _)| n == name) {
514                return *t;
515            }
516            if let Some((_, _, t)) = ctx.cascade.iter().find(|(n, _, _)| n == name) {
517                return PortType::from_keyword(t).unwrap_or(PortType::U64);
518            }
519        }
520        super::binding::infer_expr_type(expr, asm, &compiler.input_names)
521    }
522
523    /// The element type of a generator-call source. In a tile the
524    /// generator is an expression over the enclosing scope, so it is
525    /// typed by the same inference as a hole; the isolated probe the
526    /// `for` construct uses is the fallback when it does not parse.
527    fn generator_type(
528        &self,
529        compiler: &Compiler,
530        asm: &PolydatAssembler,
531        expr: &str,
532        ctx: Option<&BodyContext>,
533    ) -> Result<PortType, String> {
534        match super::tile::parse_hole_expr(expr) {
535            Ok(e) => Ok(self.infer_type(compiler, asm, &e, ctx)),
536            Err(_) => compiler.probe_element_type(expr),
537        }
538    }
539
540    /// Type one hole (SRD 114 §4): the declared type wins and must be
541    /// reachable from the wire type through the assembler's own adapter
542    /// catalog; otherwise the wire type stands. Strict mode rejects the
543    /// adapter a declaration would need.
544    fn type_hole(
545        &self,
546        text: &str,
547        wire: PortType,
548        declared: Option<&str>,
549        position: HolePosition,
550        cond: bool,
551    ) -> Result<HoleTyping, String> {
552        let tile = &self.tile_name;
553        let expectation = if cond {
554            "a truth value"
555        } else {
556            match (self.encoding.as_str(), position) {
557                ("json", HolePosition::Value) => "any JSON value",
558                ("json", HolePosition::InString) => "text inside a JSON string",
559                ("csv", _) => "a CSV field",
560                _ => "text",
561            }
562        };
563        if matches!(
564            wire,
565            PortType::Ext
566                | PortType::Handle
567                | PortType::Reg128
568                | PortType::RegI8x16
569                | PortType::RegI16x8
570                | PortType::RegI32x4
571                | PortType::RegI64x2
572                | PortType::RegF16x8
573                | PortType::RegF32x4
574                | PortType::RegF64x2
575        ) {
576            return Err(format!(
577                "tile '{tile}': hole `{text}`: a {} wire has no text form and cannot fill a hole",
578                wire.to_keyword()
579            ));
580        }
581        let declared_ty = match declared {
582            Some(kw) => Some(
583                PortType::from_keyword(kw)
584                    .ok_or_else(|| format!("tile '{tile}': hole `{text}`: unknown type '{kw}'"))?,
585            ),
586            None => None,
587        };
588        let mut adapter = None;
589        if let Some(to) = declared_ty
590            && to != wire
591        {
592            if crate::compile::assembly::auto_adapter(wire, to).is_none() {
593                return Err(format!(
594                    "tile '{tile}': hole `{text}`: no conversion from the wire type {} to the declared type {}; \
595                     write the conversion explicitly in the expression",
596                    wire.to_keyword(),
597                    to.to_keyword()
598                ));
599            }
600            if self.strict {
601                return Err(format!(
602                    "tile '{tile}': hole `{text}`: strict mode rejects the implicit {} -> {} adapter the declaration needs; \
603                     write the conversion explicitly in the expression",
604                    wire.to_keyword(),
605                    to.to_keyword()
606                ));
607            }
608            adapter = Some((wire, to));
609        }
610        Ok(HoleTyping {
611            wire,
612            declared: declared_ty,
613            effective: declared_ty.unwrap_or(wire),
614            adapter,
615            expectation,
616        })
617    }
618
619    /// Record how a hole was typed for `explain tiles` (SRD 114 §4.4).
620    fn record(&self, compiler: &mut Compiler, text: &str, typing: &HoleTyping, enc: &HoleEncoding) {
621        let kw = typing.effective.to_keyword();
622        let mut encoder = if enc.cond {
623            "truth value as 1 or 0".to_string()
624        } else if enc.raw {
625            "raw text, no escaping".to_string()
626        } else {
627            match (enc.encoding.as_str(), enc.position) {
628                ("json", HolePosition::Value) => match typing.effective {
629                    PortType::Str | PortType::Bytes => {
630                        "json string, quoted and escaped".to_string()
631                    }
632                    PortType::Bool => "json boolean".to_string(),
633                    PortType::Json => "json value, serialized".to_string(),
634                    t if is_numeric(t) => "json number".to_string(),
635                    _ => "json string, quoted and escaped".to_string(),
636                },
637                ("json", HolePosition::InString) => "json escaped text".to_string(),
638                ("csv", _) => "csv field, quoted when needed".to_string(),
639                _ => "display text".to_string(),
640            }
641        };
642        if let Some(f) = &enc.format {
643            encoder.push_str(&format!(", format {f}"));
644        }
645        compiler
646            .pending_events
647            .push(super::events::CompileEvent::TileHoleTyped {
648                tile: self.tile_name.clone(),
649                hole: text.to_string(),
650                wire_type: typing.wire.to_keyword().to_string(),
651                declared: typing.declared.map(|t| t.to_keyword().to_string()),
652                expectation: format!("{} ({kw})", typing.expectation),
653                encoder,
654                adapter: typing
655                    .adapter
656                    .map(|(f, t)| format!("{} -> {}", f.to_keyword(), t.to_keyword())),
657            });
658    }
659
660    /// A hole in the tile's own scope: the expression as a binding of
661    /// the enclosing program, `__tile_<name>_hN := expr`, with the
662    /// declaration's adapter node between when one is needed; a hole
663    /// that names a wire uses the wire itself. The render node takes
664    /// the value and encodes it at the hole (SRD 117 step 1).
665    fn wire_hole(
666        &mut self,
667        compiler: &mut Compiler,
668        asm: &mut PolydatAssembler,
669        text: &str,
670        expr: &Expr,
671        mut enc: HoleEncoding,
672    ) -> Result<HoleSource, String> {
673        let wire = self.infer_type(compiler, asm, expr, None);
674        let typing = self.type_hole(text, wire, enc.ty.as_deref(), enc.position, enc.cond)?;
675        enc.ty = Some(typing.effective.to_keyword().to_string());
676        let name = self.next_name("h");
677        let err = |e: String| format!("tile '{}': hole `{text}`: {e}", self.tile_name);
678        let value = match typing.adapter {
679            Some((from, to)) => {
680                let vname = format!("{name}_v");
681                compiler
682                    .compile_binding(asm, std::slice::from_ref(&vname), expr)
683                    .map_err(err)?;
684                let aname = format!("{name}_a");
685                let node = crate::compile::assembly::auto_adapter(from, to)
686                    .expect("adapter checked by type_hole");
687                asm.add_node(
688                    &aname,
689                    node,
690                    vec![crate::compile::assembly::WireRef::node(&vname)],
691                );
692                compiler.all_names.push(aname.clone());
693                Expr::Ident(aname, self.span)
694            }
695            None => expr.clone(),
696        };
697        let input = match &value {
698            Expr::Ident(wire, _) if asm.output_type(wire.as_str()).is_some() => wire.clone(),
699            _ => {
700                compiler
701                    .compile_binding(asm, std::slice::from_ref(&name), &value)
702                    .map_err(err)?;
703                name
704            }
705        };
706        self.record(compiler, text, &typing, &enc);
707        let index = self.push_input(input);
708        Ok(HoleSource::Wire {
709            index,
710            spec: enc.to_spec(),
711        })
712    }
713
714    /// A hole inside a projection body: a binding of the child program
715    /// whose value the render node encodes at the hole. Outer names it
716    /// references cascade in as render node inputs, typed by the
717    /// parent's wire.
718    fn body_hole(
719        &mut self,
720        compiler: &mut Compiler,
721        asm: &mut PolydatAssembler,
722        text: &str,
723        expr: &Expr,
724        mut enc: HoleEncoding,
725        ctx: &mut BodyContext,
726    ) -> Result<HoleSource, String> {
727        let mut refs = BTreeSet::new();
728        collect_expr_refs(expr, &mut refs);
729        for r in refs {
730            self.cascade_ref(asm, r, ctx);
731        }
732        let wire = self.infer_type(compiler, asm, expr, Some(ctx));
733        let typing = self.type_hole(text, wire, enc.ty.as_deref(), enc.position, enc.cond)?;
734        enc.ty = Some(typing.effective.to_keyword().to_string());
735        let value = match typing.adapter {
736            // The child is source text, so the adapter is the `as`
737            // fusion, which inserts the same catalog adapter the parent
738            // scope would.
739            Some((_, to)) => Expr::Cast(Box::new(expr.clone()), to, self.span),
740            None => expr.clone(),
741        };
742        let name = format!("__b{}", ctx.counter);
743        ctx.counter += 1;
744        ctx.bindings
745            .push(format!("{name} := {}", super::pprint::pp_expr(&value)));
746        self.record(compiler, text, &typing, &enc);
747        Ok(HoleSource::Child {
748            name,
749            spec: enc.to_spec(),
750        })
751    }
752
753    /// Make an outer name the body references available inside the body
754    /// program as a cascaded extern typed by the parent's wire. Names
755    /// the body defines itself, and names the parent does not define,
756    /// are left alone; the latter surface as unknown wires when the
757    /// body compiles.
758    fn cascade_ref(&mut self, asm: &PolydatAssembler, r: String, ctx: &mut BodyContext) {
759        if ctx.elements.iter().any(|(n, _)| *n == r)
760            || ctx.cascade.iter().any(|(n, _, _)| *n == r)
761            || ctx.producers.contains(&r)
762        {
763            return;
764        }
765        // An extern carries its declared type; a coordinate input is u64.
766        let ty = asm.node_output_type(&r).or_else(|| {
767            if asm.input_names().iter().any(|n| *n == r) {
768                Some(asm.input_type(&r).unwrap_or(PortType::U64))
769            } else {
770                None
771            }
772        });
773        if let Some(ty) = ty {
774            let idx = self.push_input(r.clone());
775            ctx.cascade.push((r, idx, ty.to_keyword().to_string()));
776        }
777    }
778
779    /// SRD 114 §5.4: a projection's comprehension must have bounded
780    /// cardinality.
781    fn check_bounded(
782        &self,
783        c: &crate::iteration::comprehension::Comprehension,
784        text: &str,
785    ) -> Result<(), String> {
786        use crate::iteration::comprehension::Comprehension as K;
787        use crate::iteration::comprehension::cardinality::CardinalityClass as C;
788        use crate::iteration::comprehension::strategy::StrategyName as S;
789        // Sampling a continuous source needs a space-filling strategy;
790        // say so here rather than when the first render fails.
791        if let K::Order {
792            child, strategy, ..
793        } = c
794            && crate::iteration::comprehension::runtime::has_continuous_axis(child)
795            && !matches!(
796                strategy,
797                S::Halton | S::Sobol | S::Lhs | S::Shuffle | S::Extrema
798            )
799        {
800            return Err(format!(
801                "tile '{}': projection `for {text}` orders a continuous source with `{strategy:?}`; \
802                 a continuous source needs a sampling strategy: halton, sobol, lhs, shuffle, or extrema",
803                self.tile_name
804            ));
805        }
806        match c.metadata().cardinality {
807            C::Bounded(_) | C::BoundedAtMost(_) => Ok(()),
808            // A generator or parameter list with no cardinality hint
809            // reports unbounded because its count is unknown, not
810            // infinite. Its finiteness is the source's own contract, as
811            // it is for the `for` construct, so it projects.
812            C::Unbounded if has_unhinted_source(c) => Ok(()),
813            C::Unbounded => Err(format!(
814                "tile '{}': projection `for {text}` has unbounded cardinality; a projection renders once per tuple and needs a finite source",
815                self.tile_name
816            )),
817            // A continuous interval has no finite tuple set of its own;
818            // an order strategy with a count samples one.
819            C::Continuous { .. } | C::ContinuousAtMost { .. } | C::Hybrid(_) => Err(format!(
820                "tile '{}': projection `for {text}` ranges over a continuous source, which has no finite tuple set; \
821                 add `order <strategy>/<count>` (halton, sobol, lhs, or shuffle) to sample that many points",
822                self.tile_name
823            )),
824        }
825    }
826
827    /// A filter predicate may name the comprehension's own elements;
828    /// a `{name}` for a wire outside it has no value at render time.
829    fn check_predicates(
830        &self,
831        c: &crate::iteration::comprehension::Comprehension,
832        text: &str,
833    ) -> Result<(), String> {
834        let elements = c.coordinate_names();
835        for pred in filter_predicates(c) {
836            for name in placeholder_names(&pred) {
837                if !elements.contains(&name) {
838                    return Err(format!(
839                        "tile '{}': projection `for {text}`: predicate placeholder `{{{name}}}` names a wire outside the comprehension; \
840                         a projection's predicate sees only its elements",
841                        self.tile_name
842                    ));
843                }
844            }
845        }
846        Ok(())
847    }
848
849    /// A projection inside a projection body (SRD 114 §5.4). The body
850    /// program is Polydat source, so the nested projection becomes a
851    /// `tile` statement inside it, compiled by this same lowering one
852    /// level down; the outer body reads the nested tile's text through a
853    /// raw hole. Producers the nested source names are re-bound inside
854    /// the body program from their original text.
855    fn nested_projection(
856        &mut self,
857        compiler: &mut Compiler,
858        asm: &mut PolydatAssembler,
859        source: &ForSource,
860        sep: &Option<String>,
861        body: &[TilePiece],
862        ctx: &mut BodyContext,
863    ) -> Result<TileOp, String> {
864        // Validate the nested source here, in the enclosing scope, so a
865        // bad source is this tile's error and its element names are known.
866        let (comprehension, warnings) = super::traversal::resolve_source_with(
867            source,
868            &compiler.producers_seen,
869            compiler.validation_mode(),
870            &compiler.source_scope(),
871        )
872        .map_err(|e| format!("tile '{}': nested projection: {e}", self.tile_name))?;
873        compiler
874            .pending_events
875            .extend(super::traversal::warning_events(source, &warnings));
876        self.check_bounded(&comprehension, &source.to_text())?;
877        self.check_predicates(&comprehension, &source.to_text())?;
878        let mut probe = |expr: &str| self.generator_type(compiler, asm, expr, Some(&*ctx));
879        let nested_elements =
880            super::traversal::element_types(&comprehension, &mut probe).map_err(|e| {
881                format!(
882                    "tile '{}': nested projection `for {}`: {e}",
883                    self.tile_name,
884                    source.to_text()
885                )
886            })?;
887        // Generator expressions of the nested source read the scope's
888        // wires; they must reach the body program too.
889        for (_, expr_text) in generator_clauses(&comprehension) {
890            if let Ok(expr) = super::tile::parse_hole_expr(&expr_text) {
891                let mut refs = BTreeSet::new();
892                collect_expr_refs(&expr, &mut refs);
893                for r in refs {
894                    self.cascade_ref(asm, r, ctx);
895                }
896            }
897        }
898        // Producers the source names, directly or as a derivation base,
899        // are re-bound in the body program from their original text.
900        let producer = match &source.kind {
901            ForSourceKind::Producer(n) | ForSourceKind::Derived { base: n, .. } => Some(n.clone()),
902            ForSourceKind::Comprehension(_) => None,
903        };
904        if let Some(name) = producer
905            && !ctx.producers.contains(&name)
906        {
907            let p = compiler
908                .producers_seen
909                .iter()
910                .rev()
911                .find(|p| p.name == name)
912                .cloned()
913                .ok_or_else(|| {
914                    format!(
915                        "tile '{}': producer '{name}' is not bound before this tile",
916                        self.tile_name
917                    )
918                })?;
919            for r in placeholder_names(&p.source_text) {
920                self.cascade_ref(asm, r, ctx);
921            }
922            ctx.producers.push(name.clone());
923            ctx.bindings
924                .push(format!("{name} := for {}", p.source_text));
925        }
926        for r in placeholder_names(&source.to_text()) {
927            self.cascade_ref(asm, r, ctx);
928        }
929        // Every outer wire the nested body reads must reach the body
930        // program; the nested tile then cascades it one level further.
931        let mut refs = BTreeSet::new();
932        collect_piece_refs(body, &compiler.producers_seen, &mut refs);
933        for r in refs {
934            if nested_elements.iter().any(|(n, _)| *n == r) {
935                continue;
936            }
937            self.cascade_ref(asm, r, ctx);
938        }
939        let piece = TilePiece::Projection {
940            source: source.clone(),
941            sep: sep.clone(),
942            body: body.to_vec(),
943            span: self.span,
944        };
945        let text = render_template(std::slice::from_ref(&piece), &self.options);
946        let name = format!("__b{}", ctx.counter);
947        ctx.counter += 1;
948        let defaults = TileOptions::default();
949        let mut opts = Vec::new();
950        if self.options.open != defaults.open || self.options.close != defaults.close {
951            opts.push(format!(
952                "delims \"{}\" \"{}\"",
953                escape_polydat_string(&self.options.open),
954                escape_polydat_string(&self.options.close)
955            ));
956        }
957        if self.options.sigil != defaults.sigil {
958            opts.push(format!(
959                "sigil \"{}\"",
960                escape_polydat_string(&self.options.sigil)
961            ));
962        }
963        if self.strict {
964            opts.push("strict".to_string());
965        }
966        // Inside a JSON string the nested tile's holes must escape as
967        // text from its first byte; the option carries the position.
968        if self.in_string {
969            opts.push("instring".to_string());
970        }
971        let opts = if opts.is_empty() {
972            String::new()
973        } else {
974            format!(" ({})", opts.join(", "))
975        };
976        ctx.bindings.push(format!(
977            "tile {name} : {}{opts} := \"{}\"",
978            self.encoding,
979            escape_polydat_string(&text)
980        ));
981        // The nested tile's text is complete: it is copied as it is.
982        let raw = HoleEncoding {
983            encoding: self.encoding.clone(),
984            position: HolePosition::Text,
985            ty: None,
986            format: None,
987            raw: true,
988            cond: false,
989        };
990        Ok(TileOp::Hole(HoleSource::Child {
991            name,
992            spec: raw.to_spec(),
993        }))
994    }
995
996    fn push_input(&mut self, name: String) -> usize {
997        if let Some(i) = self.inputs.iter().position(|n| *n == name) {
998            return i;
999        }
1000        self.inputs.push(name);
1001        self.inputs.len() - 1
1002    }
1003
1004    fn next_name(&mut self, kind: &str) -> String {
1005        let n = self.hole_counter;
1006        self.hole_counter += 1;
1007        format!("__tile_{}_{kind}{n}", self.tile_name)
1008    }
1009
1010    fn position(&self) -> HolePosition {
1011        if self.encoding != "json" {
1012            HolePosition::Text
1013        } else if self.in_string {
1014            HolePosition::InString
1015        } else {
1016            HolePosition::Value
1017        }
1018    }
1019
1020    /// Track whether static JSON text ends inside a string literal.
1021    fn track_string_state(&mut self, s: &str) {
1022        if self.encoding != "json" {
1023            return;
1024        }
1025        let mut escaped = false;
1026        for c in s.chars() {
1027            if self.in_string {
1028                if escaped {
1029                    escaped = false;
1030                } else if c == '\\' {
1031                    escaped = true;
1032                } else if c == '"' {
1033                    self.in_string = false;
1034                }
1035            } else if c == '"' {
1036                self.in_string = true;
1037            }
1038        }
1039    }
1040}
1041
1042/// Counts over a skeleton for `explain tiles`.
1043#[derive(Default)]
1044struct SkeletonShape {
1045    statics: usize,
1046    static_bytes: usize,
1047    holes: usize,
1048    branches: usize,
1049    projections: usize,
1050}
1051
1052impl SkeletonShape {
1053    fn count(&mut self, ops: &[TileOp]) {
1054        for op in ops {
1055            match op {
1056                TileOp::Static(s) => {
1057                    self.statics += 1;
1058                    self.static_bytes += s.len();
1059                }
1060                TileOp::Hole(_) => self.holes += 1,
1061                TileOp::Branch {
1062                    then, otherwise, ..
1063                } => {
1064                    self.branches += 1;
1065                    self.count(then);
1066                    self.count(otherwise);
1067                }
1068                TileOp::Repeat { body, .. } => {
1069                    self.projections += 1;
1070                    self.count(body);
1071                }
1072            }
1073        }
1074    }
1075}
1076
1077/// Wire names a run of pieces references: hole expressions, branch
1078/// conditions, `{name}` placeholders in nested projection sources, and
1079/// the wires their generator expressions read.
1080fn collect_piece_refs(
1081    pieces: &[TilePiece],
1082    producers: &[super::traversal::Producer],
1083    out: &mut BTreeSet<String>,
1084) {
1085    for p in pieces {
1086        match p {
1087            TilePiece::Static(_) => {}
1088            TilePiece::Hole(h) => collect_expr_refs(&h.expr, out),
1089            TilePiece::Branch {
1090                cond,
1091                then,
1092                otherwise,
1093                ..
1094            } => {
1095                collect_expr_refs(cond, out);
1096                collect_piece_refs(then, producers, out);
1097                if let Some(o) = otherwise {
1098                    collect_piece_refs(o, producers, out);
1099                }
1100            }
1101            TilePiece::Projection { source, body, .. } => {
1102                out.extend(placeholder_names(&source.to_text()));
1103                if let ForSourceKind::Producer(n) | ForSourceKind::Derived { base: n, .. } =
1104                    &source.kind
1105                {
1106                    out.insert(n.clone());
1107                }
1108                if let Ok(c) = super::traversal::resolve_source(source, producers) {
1109                    for (_, expr_text) in generator_clauses(&c) {
1110                        if let Ok(expr) = super::tile::parse_hole_expr(&expr_text) {
1111                            collect_expr_refs(&expr, out);
1112                        }
1113                    }
1114                }
1115                collect_piece_refs(body, producers, out);
1116            }
1117        }
1118    }
1119}
1120
1121/// `(element, expression)` for each generator-call clause.
1122fn generator_clauses(c: &crate::iteration::comprehension::Comprehension) -> Vec<(String, String)> {
1123    use crate::iteration::comprehension::Comprehension as K;
1124    use crate::iteration::comprehension::source::Source;
1125    let mut out = Vec::new();
1126    match c {
1127        K::Clause {
1128            name,
1129            source: Source::Generator { expr, .. },
1130        } => out.push((name.clone(), expr.clone())),
1131        K::Clause { .. } => {}
1132        K::Cartesian { children } | K::Zip { children, .. } | K::Union { children } => {
1133            for ch in children {
1134                out.extend(generator_clauses(ch));
1135            }
1136        }
1137        K::Filter { child, .. } | K::Order { child, .. } => out.extend(generator_clauses(child)),
1138    }
1139    out
1140}
1141
1142/// Every filter predicate in a comprehension.
1143fn filter_predicates(c: &crate::iteration::comprehension::Comprehension) -> Vec<String> {
1144    use crate::iteration::comprehension::Comprehension as K;
1145    let mut out = Vec::new();
1146    match c {
1147        K::Clause { .. } => {}
1148        K::Cartesian { children } | K::Zip { children, .. } | K::Union { children } => {
1149            for ch in children {
1150                out.extend(filter_predicates(ch));
1151            }
1152        }
1153        K::Filter { child, predicate } => {
1154            out.push(predicate.clone());
1155            out.extend(filter_predicates(child));
1156        }
1157        K::Order { child, .. } => out.extend(filter_predicates(child)),
1158    }
1159    out
1160}
1161
1162/// Whether a comprehension draws on a generator call or a workload
1163/// parameter list that carries no cardinality hint.
1164fn has_unhinted_source(c: &crate::iteration::comprehension::Comprehension) -> bool {
1165    use crate::iteration::comprehension::Comprehension as K;
1166    use crate::iteration::comprehension::source::Source;
1167    match c {
1168        K::Clause { source, .. } => matches!(
1169            source,
1170            Source::Generator {
1171                cardinality_hint: None,
1172                ..
1173            } | Source::WorkloadParamList { len_hint: None, .. }
1174        ),
1175        K::Cartesian { children } | K::Zip { children, .. } | K::Union { children } => {
1176            children.iter().any(has_unhinted_source)
1177        }
1178        K::Filter { child, .. } | K::Order { child, .. } => has_unhinted_source(child),
1179    }
1180}
1181
1182/// `{name}` placeholders in comprehension text.
1183fn placeholder_names(text: &str) -> Vec<String> {
1184    let mut out = Vec::new();
1185    let chars: Vec<char> = text.chars().collect();
1186    let mut i = 0;
1187    while i < chars.len() {
1188        if chars[i] == '{' {
1189            let start = i + 1;
1190            let mut j = start;
1191            while j < chars.len() && (chars[j].is_ascii_alphanumeric() || chars[j] == '_') {
1192                j += 1;
1193            }
1194            if j > start && chars.get(j) == Some(&'}') {
1195                out.push(chars[start..j].iter().collect());
1196                i = j;
1197            }
1198        }
1199        i += 1;
1200    }
1201    out
1202}
1203
1204/// Escape text for a Polydat string literal.
1205fn escape_polydat_string(s: &str) -> String {
1206    s.replace('\\', "\\\\")
1207        .replace('"', "\\\"")
1208        .replace('\n', "\\n")
1209        .replace('\t', "\\t")
1210}
1211
1212fn is_numeric(t: PortType) -> bool {
1213    matches!(
1214        t,
1215        PortType::U64
1216            | PortType::I64
1217            | PortType::F64
1218            | PortType::U32
1219            | PortType::I32
1220            | PortType::F32
1221            | PortType::U16
1222            | PortType::I16
1223            | PortType::U8
1224            | PortType::I8
1225            | PortType::F16
1226            | PortType::U128
1227            | PortType::I128
1228    )
1229}