1use crate::PortType;
10
11use super::ast::{Expr, TileHole, TileOptions, TilePiece};
12use super::lexer::{Span, lex};
13use super::parser::{for_source_from_text, parse_expression};
14
15pub fn parse_template(
18 text: &str,
19 opts: &TileOptions,
20 span: Span,
21) -> Result<Vec<TilePiece>, String> {
22 let mut p = TemplateParser {
23 chars: text.chars().collect(),
24 pos: 0,
25 opts,
26 span,
27 };
28 let pieces = p.pieces(false)?;
29 if p.pos < p.chars.len() {
30 return Err(p.err("unexpected `}` closing a block that was never opened"));
31 }
32 Ok(pieces)
33}
34
35struct TemplateParser<'a> {
36 chars: Vec<char>,
37 pos: usize,
38 opts: &'a TileOptions,
39 span: Span,
40}
41
42impl TemplateParser<'_> {
43 fn err(&self, msg: &str) -> String {
44 format!(
45 "tile at line {}, col {}: {msg} (template offset {})",
46 self.span.line, self.span.col, self.pos
47 )
48 }
49
50 fn starts_with(&self, s: &str) -> bool {
51 let sc: Vec<char> = s.chars().collect();
52 self.chars[self.pos..].starts_with(&sc)
53 }
54
55 fn take(&mut self, s: &str) {
56 self.pos += s.chars().count();
57 }
58
59 fn rest(&self) -> String {
60 self.chars[self.pos..].iter().collect()
61 }
62
63 fn pieces(&mut self, in_block: bool) -> Result<Vec<TilePiece>, String> {
66 let open = self.opts.open.clone();
67 let doubled = format!("{open}{open}");
68 let sigil = self.opts.sigil.clone();
69 let mut out: Vec<TilePiece> = Vec::new();
70 let mut static_buf = String::new();
71 let mut static_depth = 0i32;
74 let flush = |buf: &mut String, out: &mut Vec<TilePiece>| {
75 if !buf.is_empty() {
76 out.push(TilePiece::Static(std::mem::take(buf)));
77 }
78 };
79 while self.pos < self.chars.len() {
80 if self.starts_with(&doubled) {
81 self.take(&doubled);
82 static_buf.push_str(&open);
83 continue;
84 }
85 if self.starts_with(&open) {
86 flush(&mut static_buf, &mut out);
87 out.push(TilePiece::Hole(self.hole()?));
88 continue;
89 }
90 if self.starts_with(&sigil) {
91 let after: String = self.rest().chars().skip(sigil.chars().count()).collect();
92 if after.starts_with("for") && after[3..].starts_with(char::is_whitespace) {
93 flush(&mut static_buf, &mut out);
94 out.push(self.projection()?);
95 continue;
96 }
97 if after.starts_with("if") && after[2..].starts_with(char::is_whitespace) {
98 flush(&mut static_buf, &mut out);
99 out.push(self.branch()?);
100 continue;
101 }
102 }
103 let c = self.chars[self.pos];
104 if c == '{' {
105 static_depth += 1;
106 } else if c == '}' {
107 if in_block && static_depth == 0 {
108 break;
109 }
110 static_depth -= 1;
111 }
112 static_buf.push(c);
113 self.pos += 1;
114 }
115 flush(&mut static_buf, &mut out);
116 Ok(out)
117 }
118
119 fn hole(&mut self) -> Result<TileHole, String> {
121 let start = self.pos;
122 self.take(&self.opts.open.clone());
123 let inner = self.until_close()?;
124 let text = inner.trim().to_string();
125 if text.is_empty() {
126 return Err(self.err("empty hole"));
127 }
128 let (mut body, raw) = match text.strip_suffix('!') {
129 Some(b) => (b.trim().to_string(), true),
130 None => (text.clone(), false),
131 };
132 let mut format = None;
133 if let Some(idx) = rfind_top_level(&body, '|')
134 && !body[..idx].ends_with('|')
135 && body[idx + 1..]
136 .trim()
137 .chars()
138 .all(|c| c.is_ascii_alphanumeric() || ".<>^-+#0_".contains(c))
139 && !body[idx + 1..].trim().is_empty()
140 {
141 format = Some(body[idx + 1..].trim().to_string());
142 body = body[..idx].trim().to_string();
143 }
144 let mut decl_type = None;
145 if let Some(idx) = rfind_top_level(&body, ':')
146 && let Some(kw) = body.get(idx + 1..).map(str::trim)
147 && PortType::from_keyword(kw).is_some()
148 {
149 decl_type = Some(kw.to_string());
150 body = body[..idx].trim().to_string();
151 }
152 if body.is_empty() {
153 return Err(self.err(&format!("hole `{text}` has no expression")));
154 }
155 let expr = parse_hole_expr(&body).map_err(|e| {
156 if let Some(idx) = rfind_top_level(&body, ':')
159 && let Some(word) = body.get(idx + 1..).map(str::trim)
160 && !word.is_empty()
161 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
162 {
163 return self.err(&format!(
164 "hole `{text}`: unknown type '{word}'; types are the port-type keywords (u64, i64, f64, str, bool, json, bytes, ...)"
165 ));
166 }
167 self.err(&format!("hole `{text}`: {e}"))
168 })?;
169 let _ = start;
170 Ok(TileHole {
171 expr,
172 decl_type,
173 format,
174 raw,
175 span: self.span,
176 })
177 }
178
179 fn until_close(&mut self) -> Result<String, String> {
182 let close = self.opts.close.clone();
183 let mut depth = 0i32;
184 let mut quote: Option<char> = None;
185 let mut buf = String::new();
186 while self.pos < self.chars.len() {
187 let c = self.chars[self.pos];
188 if let Some(q) = quote {
189 buf.push(c);
190 self.pos += 1;
191 if c == '\\' && self.pos < self.chars.len() {
192 buf.push(self.chars[self.pos]);
193 self.pos += 1;
194 } else if c == q {
195 quote = None;
196 }
197 continue;
198 }
199 if depth == 0 && self.starts_with(&close) {
200 self.take(&close);
201 return Ok(buf);
202 }
203 match c {
204 '"' | '\'' => quote = Some(c),
205 '(' | '[' | '{' => depth += 1,
206 ')' | ']' | '}' => depth -= 1,
207 _ => {}
208 }
209 buf.push(c);
210 self.pos += 1;
211 }
212 Err(self.err(&format!("unterminated hole; expected `{close}`")))
213 }
214
215 fn projection(&mut self) -> Result<TilePiece, String> {
217 self.take(&self.opts.sigil.clone());
218 self.take("for");
219 let header = self.header_until_brace(HeaderKind::For)?;
220 let (source_text, sep) = split_sep(&header);
221 let source = for_source_from_text(source_text.trim(), self.span, true)
222 .map_err(|e| self.err(&format!("projection: {e}")))?;
223 let body = self.block()?;
224 Ok(TilePiece::Projection {
225 source,
226 sep,
227 body,
228 span: self.span,
229 })
230 }
231
232 fn branch(&mut self) -> Result<TilePiece, String> {
234 self.take(&self.opts.sigil.clone());
235 self.take("if");
236 let header = self.header_until_brace(HeaderKind::If)?;
237 let cond = parse_hole_expr(header.trim())
238 .map_err(|e| self.err(&format!("branch condition `{}`: {e}", header.trim())))?;
239 let then = self.block()?;
240 let save = self.pos;
241 self.skip_ws();
242 let else_kw = format!("{}else", self.opts.sigil);
243 let otherwise = if self.starts_with(&else_kw) {
244 self.take(&else_kw);
245 self.skip_ws();
246 Some(self.block()?)
247 } else {
248 self.pos = save;
249 None
250 };
251 Ok(TilePiece::Branch {
252 cond,
253 then,
254 otherwise,
255 span: self.span,
256 })
257 }
258
259 fn header_until_brace(&mut self, kind: HeaderKind) -> Result<String, String> {
264 let mut depth = 0i32;
265 let mut quote: Option<char> = None;
266 let mut buf = String::new();
267 while self.pos < self.chars.len() {
268 let c = self.chars[self.pos];
269 if let Some(q) = quote {
270 buf.push(c);
271 self.pos += 1;
272 if c == q {
273 quote = None;
274 }
275 continue;
276 }
277 if depth == 0
281 && !self.opts.open.starts_with('{')
282 && self.starts_with(&self.opts.open.clone())
283 {
284 return Err(self.err(
285 "directive has no `{` block; a hole cannot appear in a directive header",
286 ));
287 }
288 if depth == 0 && c == '{' {
289 const HEADER_PUNCT: &str = "<>=!+-*/%.,()[]&|?:";
298 let placeholder = placeholder_len(&self.chars, self.pos).is_some_and(|len| {
299 let before = buf.chars().last().is_some_and(|b| HEADER_PUNCT.contains(b));
300 let after = self
301 .chars
302 .get(self.pos + len)
303 .is_some_and(|a| HEADER_PUNCT.contains(*a));
304 if before || after {
305 return true;
306 }
307 let rest: String = self.chars[self.pos + len..].iter().collect();
308 let rest = rest.trim_start();
309 if rest.is_empty()
310 || rest.starts_with(&self.opts.sigil)
311 || rest.starts_with(&self.opts.open)
312 {
313 return false;
314 }
315 if rest.starts_with(|c: char| HEADER_PUNCT.contains(c) && !"()[]".contains(c)) {
316 return true;
317 }
318 !header_complete(kind, &buf)
323 });
324 if !placeholder {
325 return Ok(buf);
326 }
327 }
328 match c {
329 '"' | '\'' => quote = Some(c),
330 '(' | '[' => depth += 1,
331 ')' | ']' => depth -= 1,
332 _ => {}
333 }
334 buf.push(c);
335 self.pos += 1;
336 }
337 Err(self.err("directive has no `{` block"))
338 }
339
340 fn block(&mut self) -> Result<Vec<TilePiece>, String> {
342 if self.pos >= self.chars.len() || self.chars[self.pos] != '{' {
343 return Err(self.err("expected `{`"));
344 }
345 self.pos += 1;
346 let mut body = self.pieces(true)?;
347 if self.pos >= self.chars.len() || self.chars[self.pos] != '}' {
348 return Err(self.err("unterminated block; expected `}`"));
349 }
350 self.pos += 1;
351 trim_block(&mut body);
352 Ok(body)
353 }
354
355 fn skip_ws(&mut self) {
356 while self.pos < self.chars.len() && self.chars[self.pos].is_whitespace() {
357 self.pos += 1;
358 }
359 }
360}
361
362fn trim_block(body: &mut Vec<TilePiece>) {
366 if let Some(TilePiece::Static(s)) = body.first_mut() {
367 let t = s.trim_start().to_string();
368 *s = t;
369 }
370 if let Some(TilePiece::Static(s)) = body.last_mut() {
371 let t = s.trim_end().to_string();
372 *s = t;
373 }
374 body.retain(|p| !matches!(p, TilePiece::Static(s) if s.is_empty()));
375}
376
377#[derive(Clone, Copy)]
379enum HeaderKind {
380 For,
381 If,
382}
383
384fn header_complete(kind: HeaderKind, header: &str) -> bool {
388 let h = header.trim();
389 if h.is_empty() {
390 return false;
391 }
392 match kind {
393 HeaderKind::If => parse_hole_expr(h).is_ok(),
394 HeaderKind::For => {
395 let dangling = h.ends_with(|c: char| "<>=!+-*/%&|,(".contains(c))
396 || ["where", "order", "in", "&&", "||"]
397 .iter()
398 .any(|kw| h.ends_with(kw));
399 if dangling {
400 return false;
401 }
402 let (source, _) = split_sep(h);
403 for_source_from_text(source.trim(), Span { line: 0, col: 0 }, true).is_ok()
404 }
405 }
406}
407
408fn split_sep(header: &str) -> (String, Option<String>) {
412 let unescaped = header.replace("\\\"", "\"");
413 let t = unescaped.trim_end();
414 if let Some(q) = t.strip_suffix('"')
415 && let Some(open_quote) = q.rfind('"')
416 && q[..open_quote].trim_end().ends_with(" sep")
417 {
418 let sep = q[open_quote + 1..].to_string();
419 let head = q[..open_quote].trim_end();
420 let head = head[..head.len() - 3].trim_end();
421 return (head.to_string(), Some(sep));
422 }
423 (t.to_string(), None)
424}
425
426pub fn parse_hole_expr(text: &str) -> Result<Expr, String> {
427 let tokens = lex(text)?;
428 parse_expression(tokens)
429}
430
431fn rfind_top_level(s: &str, needle: char) -> Option<usize> {
433 let mut depth = 0i32;
434 let mut quote: Option<char> = None;
435 let mut found = None;
436 for (i, c) in s.char_indices() {
437 if let Some(q) = quote {
438 if c == q {
439 quote = None;
440 }
441 continue;
442 }
443 match c {
444 '"' | '\'' => quote = Some(c),
445 '(' | '[' | '{' => depth += 1,
446 ')' | ']' | '}' => depth -= 1,
447 _ if depth == 0 && c == needle => found = Some(i),
448 _ => {}
449 }
450 }
451 found
452}
453
454fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
456 let mut i = pos + 1;
457 let first = *chars.get(i)?;
458 if !(first.is_ascii_alphabetic() || first == '_') {
459 return None;
460 }
461 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
462 i += 1;
463 }
464 (chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
465}
466
467pub fn render_template(pieces: &[TilePiece], opts: &TileOptions) -> String {
470 let mut out = String::new();
471 for piece in pieces {
472 match piece {
473 TilePiece::Static(s) => {
474 out.push_str(&s.replace(&opts.open, &format!("{}{}", opts.open, opts.open)))
475 }
476 TilePiece::Hole(h) => {
477 out.push_str(&opts.open);
478 out.push_str(&h.to_text());
479 out.push_str(&opts.close);
480 }
481 TilePiece::Projection {
482 source, sep, body, ..
483 } => {
484 out.push_str(&opts.sigil);
485 out.push_str("for ");
486 out.push_str(&source.to_text());
487 if let Some(s) = sep {
488 out.push_str(&format!(" sep \"{s}\""));
489 }
490 out.push_str(" { ");
491 out.push_str(&render_template(body, opts));
492 out.push_str(" }");
493 }
494 TilePiece::Branch {
495 cond,
496 then,
497 otherwise,
498 ..
499 } => {
500 out.push_str(&opts.sigil);
501 out.push_str("if ");
502 out.push_str(&super::pprint::pp_expr(cond));
503 out.push_str(" { ");
504 out.push_str(&render_template(then, opts));
505 out.push_str(" }");
506 if let Some(o) = otherwise {
507 out.push(' ');
508 out.push_str(&opts.sigil);
509 out.push_str("else { ");
510 out.push_str(&render_template(o, opts));
511 out.push_str(" }");
512 }
513 }
514 }
515 }
516 out
517}