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