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 text,
172 expr,
173 decl_type,
174 format,
175 raw,
176 span: self.span,
177 })
178 }
179
180 fn until_close(&mut self) -> Result<String, String> {
183 let close = self.opts.close.clone();
184 let mut depth = 0i32;
185 let mut quote: Option<char> = None;
186 let mut buf = String::new();
187 while self.pos < self.chars.len() {
188 let c = self.chars[self.pos];
189 if let Some(q) = quote {
190 buf.push(c);
191 self.pos += 1;
192 if c == '\\' && self.pos < self.chars.len() {
193 buf.push(self.chars[self.pos]);
194 self.pos += 1;
195 } else if c == q {
196 quote = None;
197 }
198 continue;
199 }
200 if depth == 0 && self.starts_with(&close) {
201 self.take(&close);
202 return Ok(buf);
203 }
204 match c {
205 '"' | '\'' => quote = Some(c),
206 '(' | '[' | '{' => depth += 1,
207 ')' | ']' | '}' => depth -= 1,
208 _ => {}
209 }
210 buf.push(c);
211 self.pos += 1;
212 }
213 Err(self.err(&format!("unterminated hole; expected `{close}`")))
214 }
215
216 fn projection(&mut self) -> Result<TilePiece, String> {
218 self.take(&self.opts.sigil.clone());
219 self.take("for");
220 let header = self.header_until_brace(HeaderKind::For)?;
221 let (source_text, sep) = split_sep(&header);
222 let source = for_source_from_text(source_text.trim(), self.span, true)
223 .map_err(|e| self.err(&format!("projection: {e}")))?;
224 let body = self.block()?;
225 Ok(TilePiece::Projection {
226 source,
227 sep,
228 body,
229 span: self.span,
230 })
231 }
232
233 fn branch(&mut self) -> Result<TilePiece, String> {
235 self.take(&self.opts.sigil.clone());
236 self.take("if");
237 let header = self.header_until_brace(HeaderKind::If)?;
238 let cond = parse_hole_expr(header.trim())
239 .map_err(|e| self.err(&format!("branch condition `{}`: {e}", header.trim())))?;
240 let then = self.block()?;
241 let save = self.pos;
242 self.skip_ws();
243 let else_kw = format!("{}else", self.opts.sigil);
244 let otherwise = if self.starts_with(&else_kw) {
245 self.take(&else_kw);
246 self.skip_ws();
247 Some(self.block()?)
248 } else {
249 self.pos = save;
250 None
251 };
252 Ok(TilePiece::Branch {
253 cond,
254 then,
255 otherwise,
256 span: self.span,
257 })
258 }
259
260 fn header_until_brace(&mut self, kind: HeaderKind) -> Result<String, String> {
265 let mut depth = 0i32;
266 let mut quote: Option<char> = None;
267 let mut buf = String::new();
268 while self.pos < self.chars.len() {
269 let c = self.chars[self.pos];
270 if let Some(q) = quote {
271 buf.push(c);
272 self.pos += 1;
273 if c == q {
274 quote = None;
275 }
276 continue;
277 }
278 if depth == 0
282 && !self.opts.open.starts_with('{')
283 && self.starts_with(&self.opts.open.clone())
284 {
285 return Err(self.err(
286 "directive has no `{` block; a hole cannot appear in a directive header",
287 ));
288 }
289 if depth == 0 && c == '{' {
290 const HEADER_PUNCT: &str = "<>=!+-*/%.,()[]&|?:";
299 let placeholder = placeholder_len(&self.chars, self.pos).is_some_and(|len| {
300 let before = buf.chars().last().is_some_and(|b| HEADER_PUNCT.contains(b));
301 let after = self
302 .chars
303 .get(self.pos + len)
304 .is_some_and(|a| HEADER_PUNCT.contains(*a));
305 if before || after {
306 return true;
307 }
308 let rest: String = self.chars[self.pos + len..].iter().collect();
309 let rest = rest.trim_start();
310 if rest.is_empty()
311 || rest.starts_with(&self.opts.sigil)
312 || rest.starts_with(&self.opts.open)
313 {
314 return false;
315 }
316 if rest.starts_with(|c: char| HEADER_PUNCT.contains(c) && !"()[]".contains(c)) {
317 return true;
318 }
319 !header_complete(kind, &buf)
324 });
325 if !placeholder {
326 return Ok(buf);
327 }
328 }
329 match c {
330 '"' | '\'' => quote = Some(c),
331 '(' | '[' => depth += 1,
332 ')' | ']' => depth -= 1,
333 _ => {}
334 }
335 buf.push(c);
336 self.pos += 1;
337 }
338 Err(self.err("directive has no `{` block"))
339 }
340
341 fn block(&mut self) -> Result<Vec<TilePiece>, String> {
343 if self.pos >= self.chars.len() || self.chars[self.pos] != '{' {
344 return Err(self.err("expected `{`"));
345 }
346 self.pos += 1;
347 let mut body = self.pieces(true)?;
348 if self.pos >= self.chars.len() || self.chars[self.pos] != '}' {
349 return Err(self.err("unterminated block; expected `}`"));
350 }
351 self.pos += 1;
352 trim_block(&mut body);
353 Ok(body)
354 }
355
356 fn skip_ws(&mut self) {
357 while self.pos < self.chars.len() && self.chars[self.pos].is_whitespace() {
358 self.pos += 1;
359 }
360 }
361}
362
363fn trim_block(body: &mut Vec<TilePiece>) {
367 if let Some(TilePiece::Static(s)) = body.first_mut() {
368 let t = s.trim_start().to_string();
369 *s = t;
370 }
371 if let Some(TilePiece::Static(s)) = body.last_mut() {
372 let t = s.trim_end().to_string();
373 *s = t;
374 }
375 body.retain(|p| !matches!(p, TilePiece::Static(s) if s.is_empty()));
376}
377
378#[derive(Clone, Copy)]
380enum HeaderKind {
381 For,
382 If,
383}
384
385fn header_complete(kind: HeaderKind, header: &str) -> bool {
389 let h = header.trim();
390 if h.is_empty() {
391 return false;
392 }
393 match kind {
394 HeaderKind::If => parse_hole_expr(h).is_ok(),
395 HeaderKind::For => {
396 let dangling = h.ends_with(|c: char| "<>=!+-*/%&|,(".contains(c))
397 || ["where", "order", "in", "&&", "||"]
398 .iter()
399 .any(|kw| h.ends_with(kw));
400 if dangling {
401 return false;
402 }
403 let (source, _) = split_sep(h);
404 for_source_from_text(source.trim(), Span { line: 0, col: 0 }, true).is_ok()
405 }
406 }
407}
408
409fn split_sep(header: &str) -> (String, Option<String>) {
413 let unescaped = header.replace("\\\"", "\"");
414 let t = unescaped.trim_end();
415 if let Some(q) = t.strip_suffix('"')
416 && let Some(open_quote) = q.rfind('"')
417 && q[..open_quote].trim_end().ends_with(" sep")
418 {
419 let sep = q[open_quote + 1..].to_string();
420 let head = q[..open_quote].trim_end();
421 let head = head[..head.len() - 3].trim_end();
422 return (head.to_string(), Some(sep));
423 }
424 (t.to_string(), None)
425}
426
427pub fn parse_hole_expr(text: &str) -> Result<Expr, String> {
428 let tokens = lex(text)?;
429 parse_expression(tokens)
430}
431
432fn rfind_top_level(s: &str, needle: char) -> Option<usize> {
434 let mut depth = 0i32;
435 let mut quote: Option<char> = None;
436 let mut found = None;
437 for (i, c) in s.char_indices() {
438 if let Some(q) = quote {
439 if c == q {
440 quote = None;
441 }
442 continue;
443 }
444 match c {
445 '"' | '\'' => quote = Some(c),
446 '(' | '[' | '{' => depth += 1,
447 ')' | ']' | '}' => depth -= 1,
448 _ if depth == 0 && c == needle => found = Some(i),
449 _ => {}
450 }
451 }
452 found
453}
454
455fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
457 let mut i = pos + 1;
458 let first = *chars.get(i)?;
459 if !(first.is_ascii_alphabetic() || first == '_') {
460 return None;
461 }
462 while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
463 i += 1;
464 }
465 (chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
466}
467
468pub fn render_template(pieces: &[TilePiece], opts: &TileOptions) -> String {
471 let mut out = String::new();
472 for piece in pieces {
473 match piece {
474 TilePiece::Static(s) => {
475 out.push_str(&s.replace(&opts.open, &format!("{}{}", opts.open, opts.open)))
476 }
477 TilePiece::Hole(h) => {
478 out.push_str(&opts.open);
479 out.push_str(&h.text);
480 out.push_str(&opts.close);
481 }
482 TilePiece::Projection {
483 source, sep, body, ..
484 } => {
485 out.push_str(&opts.sigil);
486 out.push_str("for ");
487 out.push_str(&source.text);
488 if let Some(s) = sep {
489 out.push_str(&format!(" sep \"{s}\""));
490 }
491 out.push_str(" { ");
492 out.push_str(&render_template(body, opts));
493 out.push_str(" }");
494 }
495 TilePiece::Branch {
496 cond,
497 then,
498 otherwise,
499 ..
500 } => {
501 out.push_str(&opts.sigil);
502 out.push_str("if ");
503 out.push_str(&super::pprint::pp_expr(cond));
504 out.push_str(" { ");
505 out.push_str(&render_template(then, opts));
506 out.push_str(" }");
507 if let Some(o) = otherwise {
508 out.push(' ');
509 out.push_str(&opts.sigil);
510 out.push_str("else { ");
511 out.push_str(&render_template(o, opts));
512 out.push_str(" }");
513 }
514 }
515 }
516 }
517 out
518}