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();
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 self.all_names.push(tile.name.clone());
123 if lowering.inputs.is_empty() {
124 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 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 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 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
239struct TileLowering {
241 tile_name: String,
242 encoding: String,
243 options: TileOptions,
244 inputs: Vec<String>,
246 children: Vec<ChildSpec>,
247 bodies: Vec<std::sync::Arc<super::traversal::BodySource>>,
251 hole_counter: usize,
252 in_string: bool,
255 strict: bool,
257 span: super::lexer::Span,
258}
259
260struct HoleTyping {
264 wire: PortType,
265 declared: Option<PortType>,
266 effective: PortType,
267 adapter: Option<(PortType, PortType)>,
268 expectation: &'static str,
269}
270
271struct BodyContext {
273 elements: Vec<(String, PortType)>,
274 bindings: Vec<String>,
275 cascade: Vec<(String, usize, String)>,
277 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[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
1077fn 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
1121fn 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
1142fn 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
1162fn 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
1182fn 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
1204fn 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}