1use 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 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 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 self.all_names.push(tile.name.clone());
125 if lowering.inputs.is_empty() {
126 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 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 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 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
241struct TileLowering {
243 tile_name: String,
244 encoding: String,
245 options: TileOptions,
246 inputs: Vec<String>,
248 children: Vec<ChildSpec>,
249 bodies: Vec<std::sync::Arc<super::traversal::BodySource>>,
253 hole_counter: usize,
254 in_string: bool,
257 strict: bool,
259 span: super::lexer::Span,
260}
261
262struct HoleTyping {
266 wire: PortType,
267 declared: Option<PortType>,
268 effective: PortType,
269 adapter: Option<(PortType, PortType)>,
270 expectation: &'static str,
271}
272
273struct BodyContext {
275 elements: Vec<(String, PortType)>,
276 bindings: Vec<String>,
277 cascade: Vec<(String, usize, String)>,
279 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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
1079fn 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
1123fn 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
1144fn 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
1164fn 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
1184fn 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
1206fn 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}