Skip to main content

polydat_grammar/
parser.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Recursive descent parser for the Polydat DSL.
5//!
6//! Parses a token stream (from the lexer) into an AST.
7//! Infix arithmetic expressions (`+`, `-`, `*`, `/`, `%`, `^`) are
8//! handled by a Pratt (precedence-climbing) parser that produces
9//! `Expr::BinOp` nodes, later desugared by the compiler into
10//! function calls.
11//!
12//! ## String interpolation
13//!
14//! Per SRD 10 §"String Interpolation", string literals containing
15//! `{ … }` placeholders are desugared to a `printf` call over
16//! the placeholder bodies. The bodies are parsed as full GK
17//! expressions via [`parse_expression`] — same entry the rest of
18//! the language uses — so anything that can appear on a binding
19//! right-hand side can appear inside a placeholder.
20//!
21//! Examples:
22//!
23//! - `"hello"` — no placeholders → `Expr::StringLit("hello")`.
24//! - `"{name}"` — bare identifier → `printf("{}", name)`.
25//! - `"x={a + b}"` — infix expression → `printf("x={}", a + b)`.
26//! - `"{format_u64(hash(cycle), 10)}@example.com"` — nested call
27//!   → `printf("{}@example.com", format_u64(hash(cycle), 10))`.
28//! - `"{row.id}"` — field access → `printf("{}", row.id)`.
29//! - `"{{literal braces}}"` — escaped → stays a `StringLit` (printf
30//!   emits `{` / `}` from `{{` / `}}` at format time).
31//! - `"x={:05}"` — printf format spec, not a Polydat expression → stays
32//!   a `StringLit`; the user is calling printf by hand.
33//! - `"missing close {abc"` — unterminated placeholder → stays a
34//!   `StringLit`.
35//!
36//! The desugaring is pure syntactic sugar. The resulting `printf`
37//! call goes through the standard binding/assembly path: each
38//! placeholder expression compiles to a node, the printf node
39//! ingests their outputs as wires, and at evaluation time
40//! `Value::to_display_string()` renders each input into its slot.
41//! No special runtime support is needed beyond `printf`.
42
43use crate::ast::*;
44use crate::lexer::{Span, Token, TokenKind};
45
46/// Parser state.
47struct Parser {
48    tokens: Vec<Token>,
49    pos: usize,
50    /// The delimiters and sigil a tile is read under when it names
51    /// none of its own: the host's, when one supplied them.
52    tile_defaults: TileOptions,
53}
54
55impl Parser {
56    fn new(tokens: Vec<Token>) -> Self {
57        Self {
58            tokens,
59            pos: 0,
60            tile_defaults: TileOptions::default(),
61        }
62    }
63
64    fn peek(&self) -> &TokenKind {
65        &self.tokens[self.pos].kind
66    }
67
68    fn span(&self) -> Span {
69        self.tokens[self.pos].span
70    }
71
72    fn advance(&mut self) -> &Token {
73        let tok = &self.tokens[self.pos];
74        if self.pos < self.tokens.len() - 1 {
75            self.pos += 1;
76        }
77        tok
78    }
79
80    fn expect(&mut self, expected: &TokenKind) -> Result<&Token, String> {
81        if self.peek() == expected {
82            Ok(self.advance())
83        } else {
84            Err(format!(
85                "expected {:?}, got {:?} at line {}, col {}",
86                expected,
87                self.peek(),
88                self.span().line,
89                self.span().col
90            ))
91        }
92    }
93
94    fn expect_ident(&mut self) -> Result<String, String> {
95        match self.peek().clone() {
96            TokenKind::Ident(name) => {
97                self.advance();
98                Ok(name)
99            }
100            // `input` is a soft keyword: at statement start it's a
101            // declaration, elsewhere (module-signature param names,
102            // call-site named args, body references like `hash(input)`)
103            // it is a plain identifier. This mirrors the convention
104            // in `nbrs/stdlib/modeling.polydat` where `input:` is the
105            // canonical parameter name for cycle-driven modules.
106            TokenKind::Input => {
107                self.advance();
108                Ok("input".to_string())
109            }
110            // SRD 71: `cursor` is a soft keyword. At statement start
111            // it opens a `cursor q = …` decl; in identifier position
112            // (param names, binding LHS, body references) it's the
113            // workload-level cursor parameter.
114            TokenKind::Cursor => {
115                self.advance();
116                Ok("cursor".to_string())
117            }
118            // SRD 71: `over` is a soft keyword used only by the
119            // cursor-decl syntax. In identifier position it's a
120            // plain identifier — pre-SRD-71 workloads that happened
121            // to name a wire `over` keep working.
122            TokenKind::Over => {
123                self.advance();
124                Ok("over".to_string())
125            }
126            _ => Err(format!(
127                "expected identifier, got {:?} at line {}, col {}",
128                self.peek(),
129                self.span().line,
130                self.span().col
131            )),
132        }
133    }
134
135    fn at_eof(&self) -> bool {
136        matches!(self.peek(), TokenKind::Eof)
137    }
138}
139
140/// Parse a token stream into a PolydatFile AST.
141pub fn parse(tokens: Vec<Token>) -> Result<PolydatFile, String> {
142    parse_with_tile_defaults(tokens, &TileOptions::default())
143}
144
145/// [`parse`] reading every tile that names no delimiters or sigil of
146/// its own under `tile_defaults`.
147///
148/// A tile body is raw text until it is read, and what it means depends
149/// on the delimiters it is read under, so a host that supplies its own
150/// supplies them here rather than re-reading afterwards: the text is
151/// admitted once, under the reading that was intended. A tile that
152/// names any delimiter of its own keeps all of them.
153pub fn parse_with_tile_defaults(
154    tokens: Vec<Token>,
155    tile_defaults: &TileOptions,
156) -> Result<PolydatFile, String> {
157    let mut parser = Parser::new(tokens);
158    parser.tile_defaults = tile_defaults.clone();
159    let mut statements = Vec::new();
160
161    while !parser.at_eof() {
162        parse_statement_into(&mut parser, &mut statements)?;
163    }
164
165    Ok(PolydatFile { statements })
166}
167
168/// Parse a token stream as a single Polydat expression.
169///
170/// Used by string-interpolation desugaring to compile placeholder
171/// bodies (`{ … }` inside string literals) the same way any
172/// other binding right-hand side is compiled. Identifiers,
173/// nested function calls, infix arithmetic, and field access
174/// all work uniformly because this is the same `parse_expr`
175/// entry the compiler uses elsewhere.
176///
177/// ```text
178/// // "{format_u64(hash(cycle), 10)}" → printf("{}", format_u64(hash(cycle), 10))
179/// // "{a + b}"                       → printf("{}", a + b)
180/// ```
181///
182/// Returns an error if the tokens don't form a single complete
183/// expression, or if there are trailing tokens after the
184/// expression ends.
185pub fn parse_expression(tokens: Vec<Token>) -> Result<Expr, String> {
186    let mut parser = Parser::new(tokens);
187    let expr = parse_expr(&mut parser)?;
188    if !parser.at_eof() {
189        let span = parser.span();
190        return Err(format!(
191            "expected end of expression at line {}, col {}, got {:?}",
192            span.line,
193            span.col,
194            parser.peek()
195        ));
196    }
197    Ok(expr)
198}
199
200/// Parse one statement and append the resulting AST node(s) to
201/// `out`. Most statement kinds map 1-to-1, but the tuple form of
202/// `input (a: u64, b: f64)` desugars into N `InputDecl` statements
203/// at parse time — hence the `Vec` sink rather than a single
204/// return value.
205fn parse_statement_into(p: &mut Parser, out: &mut Vec<Statement>) -> Result<(), String> {
206    match p.peek() {
207        TokenKind::Pragma => out.push(parse_pragma(p)?),
208        TokenKind::Input => parse_input_decl(p, out)?,
209        TokenKind::Extern => out.push(parse_extern_port(p)?),
210        TokenKind::Cursor => out.push(parse_cursor_decl(p)?),
211        TokenKind::For(_) => out.push(parse_for_statement(p)?),
212        TokenKind::Tile => out.push(parse_tile(p)?),
213        TokenKind::Const | TokenKind::Shared | TokenKind::Volatile => {
214            out.push(parse_modified_binding(p)?);
215        }
216        TokenKind::LParen => out.push(parse_destructuring_binding(p)?),
217        TokenKind::Ident(_) => {
218            // Lookahead to distinguish:
219            //   name := expr              → cycle binding
220            //   name(p: type) -> ... := { → module def
221            if is_module_def(p) {
222                out.push(parse_module_def(p)?);
223            } else if is_polytile_binding(p) {
224                out.push(parse_polytile_binding(p)?);
225            } else {
226                out.push(parse_cycle_binding(p)?);
227            }
228        }
229        _ => {
230            return Err(format!(
231                "unexpected token {:?} at line {}, col {}",
232                p.peek(),
233                p.span().line,
234                p.span().col
235            ));
236        }
237    }
238    Ok(())
239}
240
241/// `pragma <name>` — first-class module directive. The pragma name
242/// is a bare identifier; arguments are not currently supported (the
243/// recognised set in SRD 15 has none, and adding them later is
244/// non-breaking). See SRD 15 §"Module-Level Pragmas".
245/// `for <source> { statements }` — SRD 113 §2.
246///
247/// The lexer already captured the source text. A bare identifier names
248/// a bound producer; anything else is comprehension text handed to the
249/// comprehension parser, so the traversal grammar has one owner.
250fn parse_for_statement(p: &mut Parser) -> Result<Statement, String> {
251    let span = p.span();
252    let text = match p.peek().clone() {
253        TokenKind::For(text) => text,
254        other => {
255            return Err(format!(
256                "expected `for`, got {other:?} at line {}, col {}",
257                span.line, span.col
258            ));
259        }
260    };
261    p.advance();
262    let source = for_source_from_text(&text, span, true)?;
263    if !matches!(p.peek(), TokenKind::LBrace) {
264        return Err(format!(
265            "`for {text}` at line {}, col {} needs a `{{` block on the same line; \
266             to bind a producer instead, write `name := for {text}`",
267            span.line, span.col
268        ));
269    }
270    p.advance();
271    let mut body = Vec::new();
272    while !matches!(p.peek(), TokenKind::RBrace | TokenKind::Eof) {
273        parse_statement_into(p, &mut body)?;
274    }
275    p.expect(&TokenKind::RBrace)?;
276    Ok(Statement::For(ForStmt { source, body, span }))
277}
278
279/// `tile name [: encoding] [(options)] := body` — SRD 114 §2.1.
280///
281/// The lexer captured block and heredoc bodies raw into a `TileBody`
282/// token; a string-literal body arrives as an ordinary string token.
283fn parse_tile(p: &mut Parser) -> Result<Statement, String> {
284    let span = p.span();
285    p.expect(&TokenKind::Tile)?;
286    let name = p.expect_ident()?;
287    let encoding = if matches!(p.peek(), TokenKind::Colon) {
288        p.advance();
289        Some(p.expect_ident()?)
290    } else {
291        None
292    };
293    let mut options = p.tile_defaults.clone();
294    if matches!(p.peek(), TokenKind::LParen) {
295        p.advance();
296        while !matches!(p.peek(), TokenKind::RParen | TokenKind::Eof) {
297            let key = p.expect_ident()?;
298            match key.as_str() {
299                "delims" => {
300                    options.open = expect_string(p, "delims open")?;
301                    options.close = expect_string(p, "delims close")?;
302                    if options.open.is_empty() || options.close.is_empty() {
303                        return Err(format!(
304                            "tile '{name}' at line {}, col {}: delimiters must not be empty",
305                            span.line, span.col
306                        ));
307                    }
308                }
309                "sigil" => {
310                    options.sigil = expect_string(p, "sigil")?;
311                    if options.sigil.is_empty() {
312                        return Err(format!(
313                            "tile '{name}' at line {}, col {}: sigil must not be empty",
314                            span.line, span.col
315                        ));
316                    }
317                }
318                "strict" => options.strict = true,
319                "instring" => options.in_string = true,
320                other => {
321                    return Err(format!(
322                        "tile '{name}' at line {}, col {}: unknown option '{other}'; options are delims, sigil, strict, instring",
323                        span.line, span.col
324                    ));
325                }
326            }
327            if matches!(p.peek(), TokenKind::Comma) {
328                p.advance();
329            }
330        }
331        p.expect(&TokenKind::RParen)?;
332    }
333    // A tile binds a wire, so `:=` precedes every body form; the lexer
334    // captures a block or heredoc body right after it.
335    if !matches!(p.peek(), TokenKind::ColonEq) {
336        return Err(format!(
337            "tile '{name}' at line {}, col {}: expected `:=` before the body; a tile binds a wire, \
338             as in `tile {name} : json := {{ ... }}` or `tile {name} := \"...\"`, got {:?}",
339            span.line,
340            span.col,
341            p.peek()
342        ));
343    }
344    p.advance();
345    let (body_kind, body) = match p.peek().clone() {
346        TokenKind::TileBody(text, kind) => {
347            p.advance();
348            (kind, text)
349        }
350        TokenKind::StringLit(s) => {
351            p.advance();
352            (TileBodyKind::Literal, s)
353        }
354        other => {
355            return Err(format!(
356                "tile '{name}' at line {}, col {}: expected a body (a `{{ }}` or `[ ]` block, `<<< >>>` heredoc, or string), got {other:?}",
357                span.line, span.col
358            ));
359        }
360    };
361    if let Some(enc) = &encoding
362        && !matches!(enc.as_str(), "json" | "text" | "csv")
363    {
364        return Err(format!(
365            "tile '{name}' at line {}, col {}: unknown encoding '{enc}'; encodings are json, text, csv",
366            span.line, span.col
367        ));
368    }
369    let pieces = super::tile::parse_template(&body, &options, span)
370        .map_err(|e| format!("tile '{name}': {e}"))?;
371    Ok(Statement::Tile(TileDef {
372        name,
373        encoding,
374        options,
375        body_kind,
376        pieces,
377        span,
378    }))
379}
380
381/// `name := polytile(...)` or `name := polytile_json(...)`: a tile
382/// whose body arrives as an argument (SRD 114 §5.6).
383fn is_polytile_binding(p: &Parser) -> bool {
384    p.pos + 3 < p.tokens.len()
385        && matches!(&p.tokens[p.pos].kind, TokenKind::Ident(_))
386        && matches!(&p.tokens[p.pos + 1].kind, TokenKind::ColonEq)
387        && matches!(&p.tokens[p.pos + 2].kind, TokenKind::Ident(f) if f == "polytile" || f == "polytile_json")
388        && matches!(&p.tokens[p.pos + 3].kind, TokenKind::LParen)
389}
390
391/// `name := polytile("<encoding>", "<template>" [, open: "..", close: "..", sigil: ".."])`
392/// `name := polytile_json("<structural json>" [, options])`
393///
394/// The body is a string literal or a heredoc, taken raw: it is
395/// compiled as a template, never evaluated, so interpolation inside it
396/// belongs to the tile. This is the form a host that only holds strings
397/// emits as a program transform.
398fn parse_polytile_binding(p: &mut Parser) -> Result<Statement, String> {
399    let span = p.span();
400    let name = p.expect_ident()?;
401    p.expect(&TokenKind::ColonEq)?;
402    let func = p.expect_ident()?;
403    p.expect(&TokenKind::LParen)?;
404    let at = |what: &str| {
405        format!(
406            "{func} for '{name}' at line {}, col {}: {what}",
407            span.line, span.col
408        )
409    };
410    let encoding = if func == "polytile" {
411        let e = expect_string(p, "the encoding").map_err(|m| at(&m))?;
412        if !matches!(p.peek(), TokenKind::Comma) {
413            return Err(at("expected `,` and then the template"));
414        }
415        p.advance();
416        Some(e)
417    } else {
418        None
419    };
420    let body = expect_string(p, "the template body (a string or a `<<< >>>` heredoc)")
421        .map_err(|m| at(&m))?;
422    let mut options = p.tile_defaults.clone();
423    while matches!(p.peek(), TokenKind::Comma) {
424        p.advance();
425        if matches!(p.peek(), TokenKind::RParen) {
426            break;
427        }
428        let key = p.expect_ident()?;
429        p.expect(&TokenKind::Colon)
430            .map_err(|_| at(&format!("option `{key}` needs `: \"value\"`")))?;
431        match key.as_str() {
432            "open" => options.open = expect_string(p, "open").map_err(|m| at(&m))?,
433            "close" => options.close = expect_string(p, "close").map_err(|m| at(&m))?,
434            "sigil" => options.sigil = expect_string(p, "sigil").map_err(|m| at(&m))?,
435            "strict" => {
436                let v = p.expect_ident().map_err(|m| at(&m))?;
437                options.strict = v == "true";
438            }
439            "instring" => {
440                let v = p.expect_ident().map_err(|m| at(&m))?;
441                options.in_string = v == "true";
442            }
443            other => {
444                return Err(at(&format!(
445                    "unknown option '{other}'; options are open, close, sigil, strict, instring"
446                )));
447            }
448        }
449    }
450    p.expect(&TokenKind::RParen).map_err(|m| at(&m))?;
451    if options.open.is_empty() || options.close.is_empty() || options.sigil.is_empty() {
452        return Err(at("delimiters and sigil must not be empty"));
453    }
454    let tile = match encoding {
455        Some(enc) => super::tile_structural::tile_from_text(&name, &enc, &body, &options, span)?,
456        None => super::tile_structural::tile_from_json_text(&name, &body, &options, span)?,
457    };
458    Ok(Statement::Tile(tile))
459}
460
461fn expect_string(p: &mut Parser, what: &str) -> Result<String, String> {
462    match p.peek().clone() {
463        TokenKind::StringLit(s) => {
464            p.advance();
465            Ok(s)
466        }
467        other => Err(format!(
468            "expected a string for {what}, got {other:?} at line {}, col {}",
469            p.span().line,
470            p.span().col
471        )),
472    }
473}
474
475/// Classify and parse the text after `for`.
476pub fn for_source_from_text(
477    text: &str,
478    span: Span,
479    allow_producer: bool,
480) -> Result<ForSource, String> {
481    if text.is_empty() {
482        return Err(format!(
483            "`for` at line {}, col {} has no comprehension",
484            span.line, span.col
485        ));
486    }
487    let is_ident = text.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
488        && text
489            .chars()
490            .next()
491            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
492    if is_ident {
493        if allow_producer {
494            return Ok(ForSource::producer(text, span));
495        }
496        return Err(format!(
497            "`for {text}` at line {}, col {}: a producer expression needs comprehension text such as `k in 1..10`, \
498             or a derivation such as `{text} where {{k}} > 1` or `{text} order halton/5`",
499            span.line, span.col
500        ));
501    }
502    // A derivation: `<producer> [where <pred>] [order <spec>]`. The
503    // head is a bare identifier and the text has no `in` clause.
504    {
505        use crate::comprehension::parse::{split_at_order, split_at_where};
506        let (head, order) = split_at_order(text);
507        let (base, filter) = split_at_where(&head);
508        let base = base.trim();
509        let base_is_ident = !base.is_empty()
510            && base.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
511            && base
512                .chars()
513                .next()
514                .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
515        if base_is_ident && (filter.is_some() || order.is_some()) {
516            return Ok(ForSource::derived(base, filter, order, span));
517        }
518    }
519    let algebra = crate::comprehension::spec::parse_comprehension_algebra(text)
520        .map_err(|e| format!("`for {text}` at line {}, col {}: {e}", span.line, span.col))?;
521    // Built through the constructor, which refuses a tree the text
522    // cannot write back. Text this parser accepts should always be
523    // writable, so a refusal here is a round-trip gap between the two
524    // halves of the comprehension grammar rather than an author error,
525    // and says so.
526    ForSource::comprehension(algebra, span).ok_or_else(|| {
527        format!(
528            "`for {text}` at line {}, col {}: parsed, but the comprehension it denotes has no text \
529             form, so it could not be written back; this is a gap between the comprehension \
530             parser and its renderer, not an error in the program",
531            span.line, span.col
532        )
533    })
534}
535
536fn parse_pragma(p: &mut Parser) -> Result<Statement, String> {
537    let span = p.span();
538    p.expect(&TokenKind::Pragma)?;
539    let name = p.expect_ident()?;
540    Ok(Statement::Pragma { name, span })
541}
542
543/// Lookahead: is this a module def? Pattern: ident ( ident : ident ...
544fn is_module_def(p: &Parser) -> bool {
545    // Need at least: ident ( <param-name> : type
546    // The param name accepts plain idents AND the soft keyword
547    // `input` (canonical for cycle-driven modules — see
548    // `nbrs/stdlib/modeling.polydat`).
549    if p.pos + 4 >= p.tokens.len() {
550        return false;
551    }
552    let third_is_param_name = matches!(
553        &p.tokens[p.pos + 2].kind,
554        TokenKind::Ident(_) | TokenKind::Input,
555    );
556    matches!(&p.tokens[p.pos].kind, TokenKind::Ident(_))
557        && matches!(&p.tokens[p.pos + 1].kind, TokenKind::LParen)
558        && third_is_param_name
559        && matches!(&p.tokens[p.pos + 3].kind, TokenKind::Colon)
560}
561
562/// `name(param: type, ...) -> (output: type, ...) := { body }`
563fn parse_module_def(p: &mut Parser) -> Result<Statement, String> {
564    let span = p.span();
565    let name = p.expect_ident()?;
566
567    // Parse params: (name: type, ...)
568    p.expect(&TokenKind::LParen)?;
569    let mut params = Vec::new();
570    while !matches!(p.peek(), TokenKind::RParen) {
571        let pname = p.expect_ident()?;
572        p.expect(&TokenKind::Colon)?;
573        let ptype = p.expect_ident()?;
574        params.push(TypedParam {
575            name: pname,
576            typ: ptype,
577        });
578        if matches!(p.peek(), TokenKind::Comma) {
579            p.advance();
580        }
581    }
582    p.expect(&TokenKind::RParen)?;
583
584    // Parse -> (output: type, ...)
585    p.expect(&TokenKind::Arrow)?;
586    p.expect(&TokenKind::LParen)?;
587    let mut outputs = Vec::new();
588    while !matches!(p.peek(), TokenKind::RParen) {
589        let oname = p.expect_ident()?;
590        p.expect(&TokenKind::Colon)?;
591        let otype = p.expect_ident()?;
592        outputs.push(TypedParam {
593            name: oname,
594            typ: otype,
595        });
596        if matches!(p.peek(), TokenKind::Comma) {
597            p.advance();
598        }
599    }
600    p.expect(&TokenKind::RParen)?;
601
602    // Parse := { body }
603    p.expect(&TokenKind::ColonEq)?;
604    p.expect(&TokenKind::LBrace)?;
605
606    let mut body = Vec::new();
607    while !matches!(p.peek(), TokenKind::RBrace | TokenKind::Eof) {
608        parse_statement_into(p, &mut body)?;
609    }
610    p.expect(&TokenKind::RBrace)?;
611
612    Ok(Statement::ModuleDef(ModuleDef {
613        name,
614        params,
615        outputs,
616        body,
617        span,
618    }))
619}
620
621/// `extern name: type = default`
622fn parse_extern_port(p: &mut Parser) -> Result<Statement, String> {
623    let span = p.span();
624    p.advance(); // consume 'extern'
625
626    let name = p.expect_ident()?;
627    p.expect(&TokenKind::Colon)?;
628    let typ = p.expect_ident()?;
629
630    // Optional default: = expr
631    let default = if matches!(p.peek(), TokenKind::Eq) {
632        p.advance(); // consume '='
633        Some(parse_expr(p)?)
634    } else {
635        None
636    };
637
638    Ok(Statement::ExternPort(ExternPort {
639        name,
640        typ,
641        default,
642        span,
643    }))
644}
645
646/// Parse an `input` declaration. Two surface forms, both emit one
647/// [`Statement::InputDecl`] per declared slot:
648///
649/// - `input <name>[: <type>]` — bare single
650/// - `input (<name>[: <type>][, ...])` — tuple form mirroring the
651///   module-signature param-list shape (see
652///   a host-provided cycle module). Desugars to N InputDecls.
653///
654/// Empty tuple `input ()` is rejected — declare zero inputs by
655/// simply omitting the `input` line.
656fn parse_input_decl(p: &mut Parser, out: &mut Vec<Statement>) -> Result<(), String> {
657    let keyword_span = p.span();
658    p.advance(); // consume 'input'
659
660    if matches!(p.peek(), TokenKind::LParen) {
661        // Tuple form: input (a: u64, b: f64, ...)
662        p.advance(); // consume '('
663        if matches!(p.peek(), TokenKind::RParen) {
664            return Err(format!(
665                "`input ()` is empty; omit the line entirely to declare zero inputs \
666                 (at line {}, col {})",
667                keyword_span.line, keyword_span.col,
668            ));
669        }
670        loop {
671            let span = p.span();
672            let name = p.expect_ident()?;
673            let ty = if matches!(p.peek(), TokenKind::Colon) {
674                p.advance();
675                Some(p.expect_ident()?)
676            } else {
677                None
678            };
679            out.push(Statement::InputDecl(InputDecl { name, ty, span }));
680            if matches!(p.peek(), TokenKind::Comma) {
681                p.advance();
682            } else {
683                break;
684            }
685        }
686        p.expect(&TokenKind::RParen)?;
687    } else {
688        // Bare form: input name[: type]
689        let span = p.span();
690        let name = p.expect_ident()?;
691        let ty = if matches!(p.peek(), TokenKind::Colon) {
692            p.advance();
693            Some(p.expect_ident()?)
694        } else {
695            None
696        };
697        out.push(Statement::InputDecl(InputDecl { name, ty, span }));
698    }
699    Ok(())
700}
701
702/// `cursor name = Cursor()` or `cursor name = expr [over partition_source]`
703///
704/// The trailing `over <expr>` clause (SRD 71) names a partition
705/// source the cursor narrows by — see [`CursorDecl::over`].
706fn parse_cursor_decl(p: &mut Parser) -> Result<Statement, String> {
707    let span = p.span();
708    p.advance(); // consume 'cursor'
709    let name = p.expect_ident()?;
710    p.expect(&TokenKind::Eq)?;
711    let constructor = parse_expr(p)?;
712    // Optional `over <expr>` clause — partition narrowing source.
713    let over = if matches!(p.peek(), TokenKind::Over) {
714        p.advance();
715        Some(parse_expr(p)?)
716    } else {
717        None
718    };
719    Ok(Statement::Cursor(CursorDecl {
720        name,
721        constructor,
722        over,
723        span,
724    }))
725}
726
727/// `<modifier>* name := expr` where each modifier ∈ {const,
728/// shared, volatile, ...}. Modifiers may appear in any order;
729/// duplicates and the contradictory `const` + `volatile` combo
730/// are rejected (see [`BindingModifier::from_iter`]).
731fn parse_modified_binding(p: &mut Parser) -> Result<Statement, String> {
732    let start_span = p.span();
733    let mut collected: Vec<WireModifier> = Vec::new();
734
735    loop {
736        let m = match p.peek() {
737            TokenKind::Const => WireModifier::Const,
738            TokenKind::Shared => WireModifier::Shared,
739            TokenKind::Volatile => WireModifier::Volatile,
740            _ => break,
741        };
742        if collected.contains(&m) {
743            return Err(format!(
744                "duplicate `{m:?}` modifier at line {}, col {}",
745                p.span().line,
746                p.span().col,
747            ));
748        }
749        collected.push(m);
750        p.advance();
751    }
752
753    let modifier = BindingModifier::try_from_iter(collected)
754        .map_err(|e| format!("{e} at line {}, col {}", start_span.line, start_span.col,))?;
755
756    match p.peek() {
757        // Soft keywords (`input`, `cursor`, `over`) and regular
758        // identifiers are all accepted as binding names. The
759        // soft-keyword recognition lives in `expect_ident`; this
760        // guard just dispatches to the binding-with-modifier
761        // path when the upcoming token can serve as an ident.
762        TokenKind::Ident(_) | TokenKind::Input | TokenKind::Cursor | TokenKind::Over => {
763            parse_cycle_binding_with_modifier(p, modifier)
764        }
765        _ => Err(format!(
766            "expected binding name after modifiers at line {}, col {}",
767            p.span().line,
768            p.span().col
769        )),
770    }
771}
772
773/// `name := expr`
774fn parse_cycle_binding(p: &mut Parser) -> Result<Statement, String> {
775    parse_cycle_binding_with_modifier(p, BindingModifier::NONE)
776}
777
778fn parse_cycle_binding_with_modifier(
779    p: &mut Parser,
780    modifier: BindingModifier,
781) -> Result<Statement, String> {
782    let span = p.span();
783    let name = p.expect_ident()?;
784    // Optional type annotation — `shared name: f64 := expr`. Pins the
785    // shared CELL's type for life (scope_model.md §"Type stability");
786    // literal inference (`1` vs `1.0`) stops being load-bearing. Only
787    // `shared` bindings carry a cell whose type the annotation can pin,
788    // so it is rejected elsewhere rather than silently ignored.
789    let type_annotation = if matches!(p.peek(), TokenKind::Colon) {
790        p.advance(); // consume ':'
791        let typ = p.expect_ident()?;
792        if !modifier.is_shared() {
793            return Err(format!(
794                "type annotation `{name}: {typ}` is only supported on `shared`                  bindings (it pins the shared cell's type). For a plain typed                  slot use `extern {name}: {typ} = …` at line {}, col {}",
795                span.line, span.col,
796            ));
797        }
798        Some(typ)
799    } else {
800        None
801    };
802    p.expect(&TokenKind::ColonEq)?;
803    let value = parse_expr(p)?;
804
805    Ok(Statement::Binding(Binding {
806        targets: vec![name],
807        value,
808        modifier,
809        type_annotation,
810        span,
811    }))
812}
813
814/// `(a, b, c) := expr`
815fn parse_destructuring_binding(p: &mut Parser) -> Result<Statement, String> {
816    let span = p.span();
817    p.advance(); // consume '('
818    let mut targets = Vec::new();
819    loop {
820        targets.push(p.expect_ident()?);
821        if matches!(p.peek(), TokenKind::Comma) {
822            p.advance();
823        } else {
824            break;
825        }
826    }
827    p.expect(&TokenKind::RParen)?;
828    p.expect(&TokenKind::ColonEq)?;
829    let value = parse_expr(p)?;
830
831    Ok(Statement::Binding(Binding {
832        targets,
833        value,
834        modifier: BindingModifier::NONE,
835        type_annotation: None,
836        span,
837    }))
838}
839
840/// Parse an expression with operator precedence (Pratt parsing).
841///
842/// Handles infix arithmetic operators (`+`, `-`, `*`, `/`, `%`, `^`)
843/// with correct precedence and associativity. Atoms are literals,
844/// identifiers, function calls, parenthesized groups, and unary negation.
845fn parse_expr(p: &mut Parser) -> Result<Expr, String> {
846    parse_expr_bp(p, 0)
847}
848
849/// Pratt parser core: parse expression with minimum binding power.
850///
851/// Precedence levels (lowest to highest):
852///   Level 0a: `||` (logical Or)      — bp (1, 2)
853///   Level 0b: `&&` (logical And)     — bp (3, 4)
854///   Level 1: `==` `!=`               — bp (5, 6)
855///   Level 2: `<` `>` `<=` `>=`       — bp (7, 8)
856///   Level 3: `|`  (BitOr)            — bp (9, 10)
857///   Level 4: `^`  (BitXor)           — bp (11, 12)
858///   Level 5: `&`  (BitAnd)           — bp (13, 14)
859///   Level 6: `<<` `>>` (Shl/Shr)     — bp (15, 16)
860///   Level 7: `+` `-` (Add/Sub)       — bp (17, 18)
861///   Level 8: `*` `/` `%`             — bp (19, 20)
862///   Level 9: `**` (Pow, right)       — bp (22, 21)
863///   Level 10: `-` `!` (unary, in parse_atom)
864///
865/// SRD-84 Part 1: `||` / `&&` sit *below* comparison (the lowest
866/// bands) so `a > b && c > d` parses as `(a > b) && (c > d)`, and
867/// `||` binds looser than `&&` (C/Rust convention). Comparison ops
868/// sit below arithmetic/bitwise so `a + b < c * d` parses as
869/// `(a + b) < (c * d)`; equality below relational so `a < b == c`
870/// parses as `(a < b) == c`.
871fn parse_expr_bp(p: &mut Parser, min_bp: u8) -> Result<Expr, String> {
872    let mut lhs = parse_atom(p)?;
873    // SRD-84 Part 1b — `as <type>` postfix cast binds tightly to the
874    // atom (Rust convention): `a + b as u64` is `a + (b as u64)`;
875    // parenthesise to cast a whole sub-expression.
876    lhs = parse_postfix_as(p, lhs)?;
877
878    loop {
879        let op = match p.peek() {
880            TokenKind::PipePipe => Some((BinOpKind::Or, 1, 2)),
881            TokenKind::AmpAmp => Some((BinOpKind::And, 3, 4)),
882            TokenKind::EqEq => Some((BinOpKind::Eq, 5, 6)),
883            TokenKind::BangEq => Some((BinOpKind::Ne, 5, 6)),
884            TokenKind::Lt => Some((BinOpKind::Lt, 7, 8)),
885            TokenKind::Gt => Some((BinOpKind::Gt, 7, 8)),
886            TokenKind::LtEq => Some((BinOpKind::Le, 7, 8)),
887            TokenKind::GtEq => Some((BinOpKind::Ge, 7, 8)),
888            TokenKind::Pipe => Some((BinOpKind::BitOr, 9, 10)),
889            TokenKind::Caret => Some((BinOpKind::BitXor, 11, 12)),
890            TokenKind::Ampersand => Some((BinOpKind::BitAnd, 13, 14)),
891            TokenKind::ShiftLeft => Some((BinOpKind::Shl, 15, 16)),
892            TokenKind::ShiftRight => Some((BinOpKind::Shr, 15, 16)),
893            TokenKind::Plus => Some((BinOpKind::Add, 17, 18)),
894            TokenKind::Minus => Some((BinOpKind::Sub, 17, 18)),
895            TokenKind::Star => Some((BinOpKind::Mul, 19, 20)),
896            TokenKind::Slash => Some((BinOpKind::Div, 19, 20)),
897            TokenKind::Percent => Some((BinOpKind::Mod, 19, 20)),
898            TokenKind::StarStar => Some((BinOpKind::Pow, 22, 21)), // right-associative
899            _ => None,
900        };
901
902        let Some((op_kind, l_bp, r_bp)) = op else {
903            break;
904        };
905        if l_bp < min_bp {
906            break;
907        }
908
909        p.advance(); // consume operator token
910        let rhs = parse_expr_bp(p, r_bp)?;
911        lhs = Expr::BinOp(Box::new(lhs), op_kind, Box::new(rhs));
912    }
913
914    Ok(lhs)
915}
916
917/// SRD-84 Part 1b — parse trailing `as <type>` casts on an expression.
918/// `as` is a *soft* keyword (contextual): only the postfix `as <type>`
919/// form is a cast; `as` is otherwise a normal identifier.
920fn parse_postfix_as(p: &mut Parser, mut expr: Expr) -> Result<Expr, String> {
921    while matches!(p.peek(), TokenKind::Ident(s) if s.as_str() == "as") {
922        let span = p.span();
923        p.advance(); // consume `as`
924        let ty_name = match p.peek() {
925            TokenKind::Ident(name) => name.clone(),
926            other => return Err(format!("expected a type name after `as`, found {other:?}")),
927        };
928        p.advance(); // consume the type name
929        let port_type = crate::PortType::from_keyword(&ty_name)
930            .ok_or_else(|| format!("unknown type `{ty_name}` in `... as {ty_name}` cast"))?;
931        expr = Expr::Cast(Box::new(expr), port_type, span);
932    }
933    Ok(expr)
934}
935
936/// Parse an atomic expression: literal, identifier, function call,
937/// parenthesized group, or unary negation.
938/// Parses the block form of conditional selection:
939/// `if <cond> { <then> } else { <else> }`, including `else if` chains.
940///
941/// This is **surface sugar only**. It desugars here, at parse time, into the
942/// existing `if(cond, then, else)` call intrinsic, exactly as `a + b` is sugar
943/// for `u64_add(a, b)`. Everything downstream is therefore inherited rather than
944/// duplicated: branch-type dispatch (Str > F64 > U64), automatic u64→f64
945/// widening of the narrower branch, and the compiled `select_*` node selection
946/// all live in `binding.rs`'s desugar and behave identically for both spellings.
947///
948/// Two semantic points that follow from Polydat being a dataflow kernel language
949/// rather than an imperative one, and which the block syntax deliberately does
950/// not pretend otherwise about:
951///
952/// * **Both branches always evaluate.** There is no short-circuit; `select_*`
953///   picks between two values that have both already been computed. A branch is
954///   not a guard, so it cannot be used to avoid a division by zero or an
955///   out-of-range read on the untaken side.
956/// * **`else` is mandatory.** Every Polydat expression yields a value and there
957///   is no unit type, so a one-armed `if` would have nothing to produce when the
958///   condition is false.
959fn parse_if_block(p: &mut Parser, span: Span) -> Result<Expr, String> {
960    let cond = parse_expr(p)?;
961
962    if !matches!(p.peek(), TokenKind::LBrace) {
963        return Err(format!(
964            "expected `{{` to open the then-branch of an `if` expression, got {:?} at line {}, col {}. \
965             Block form is `if <cond> {{ <then> }} else {{ <else> }}`; the call form `if(cond, a, b)` \
966             is also accepted.",
967            p.peek(),
968            p.span().line,
969            p.span().col
970        ));
971    }
972    p.advance();
973    let then_expr = parse_expr(p)?;
974    p.expect(&TokenKind::RBrace)?;
975
976    match p.peek().clone() {
977        TokenKind::Ident(word) if word == "else" => {
978            p.advance();
979        }
980        other => {
981            return Err(format!(
982                "expected `else` after the then-branch of an `if` expression, got {:?} at line {}, col {}. \
983                 `else` is required: a Polydat expression always produces a value, so there is no \
984                 result for the false path without it.",
985                other,
986                p.span().line,
987                p.span().col
988            ));
989        }
990    }
991
992    // `else if ...` chains by recursing: the else-branch is itself an if-expression.
993    let else_expr = match p.peek().clone() {
994        TokenKind::Ident(word) if word == "if" => {
995            let else_span = p.span();
996            p.advance();
997            parse_if_block(p, else_span)?
998        }
999        TokenKind::LBrace => {
1000            p.advance();
1001            let e = parse_expr(p)?;
1002            p.expect(&TokenKind::RBrace)?;
1003            e
1004        }
1005        other => {
1006            return Err(format!(
1007                "expected `{{` or `if` after `else`, got {:?} at line {}, col {}",
1008                other,
1009                p.span().line,
1010                p.span().col
1011            ));
1012        }
1013    };
1014
1015    // Desugar to the call intrinsic; `binding.rs` handles it from here.
1016    Ok(Expr::Call(CallExpr {
1017        func: "if".into(),
1018        args: vec![
1019            Arg::Positional(cond),
1020            Arg::Positional(then_expr),
1021            Arg::Positional(else_expr),
1022        ],
1023        span,
1024    }))
1025}
1026
1027fn parse_atom(p: &mut Parser) -> Result<Expr, String> {
1028    let span = p.span();
1029
1030    match p.peek().clone() {
1031        TokenKind::Minus => {
1032            // Unary negation: `-expr`
1033            p.advance();
1034            let inner = parse_atom(p)?;
1035            Ok(Expr::UnaryNeg(Box::new(inner), span))
1036        }
1037        TokenKind::Bang => {
1038            // Unary bitwise NOT: `!expr`
1039            p.advance();
1040            let inner = parse_atom(p)?;
1041            Ok(Expr::UnaryBitNot(Box::new(inner), span))
1042        }
1043        TokenKind::LParen => {
1044            // Parenthesized grouping (not a function call — that is
1045            // handled inside the Ident branch below).
1046            p.advance(); // consume '('
1047            let inner = parse_expr(p)?;
1048            p.expect(&TokenKind::RParen)?;
1049            Ok(inner)
1050        }
1051        TokenKind::StringLit(s) => {
1052            p.advance();
1053            Ok(parse_interpolated_string(s, span))
1054        }
1055        TokenKind::IntLit(v) => {
1056            p.advance();
1057            Ok(Expr::IntLit(v, span))
1058        }
1059        TokenKind::FloatLit(v) => {
1060            p.advance();
1061            Ok(Expr::FloatLit(v, span))
1062        }
1063        TokenKind::LBracket => parse_array_lit(p),
1064        TokenKind::For(text) => {
1065            p.advance();
1066            let source = for_source_from_text(&text, span, false)?;
1067            Ok(Expr::For(Box::new(source)))
1068        }
1069        TokenKind::Ident(name) => {
1070            p.advance();
1071            // `if` is a SOFT keyword, like `over` and `input`: it stays a plain
1072            // identifier to the lexer, and only the shape that follows decides how
1073            // it parses. `if(` is the long-standing call form and is left entirely
1074            // alone; anything else is the block form below. Keeping it soft is what
1075            // lets both spellings coexist without a lexer change or a migration.
1076            if name == "if" && !matches!(p.peek(), TokenKind::LParen) {
1077                parse_if_block(p, span)
1078            } else if matches!(p.peek(), TokenKind::LParen) {
1079                // Function call: name(args...)
1080                parse_call(p, name, span)
1081            } else if matches!(p.peek(), TokenKind::Dot) {
1082                parse_field_chain(p, name, span)
1083            } else {
1084                Ok(Expr::Ident(name, span))
1085            }
1086        }
1087        // `input` is a soft keyword: usable as a plain identifier in
1088        // expressions (e.g. `hash(input)` inside a module body where
1089        // `input` is the parameter name).
1090        TokenKind::Input => {
1091            p.advance();
1092            let name = "input".to_string();
1093            if matches!(p.peek(), TokenKind::Dot) {
1094                parse_field_chain(p, name, span)
1095            } else {
1096                Ok(Expr::Ident(name, span))
1097            }
1098        }
1099        // `cursor` is also a soft keyword in expression position:
1100        // SRD 71's `over cursor.partitions` form names the
1101        // workload's `cursor` parameter. The statement-level
1102        // `cursor q = …` decl is handled by `parse_statement_into`
1103        // before expression parsing kicks in.
1104        TokenKind::Cursor => {
1105            p.advance();
1106            let name = "cursor".to_string();
1107            if matches!(p.peek(), TokenKind::Dot) {
1108                parse_field_chain(p, name, span)
1109            } else {
1110                Ok(Expr::Ident(name, span))
1111            }
1112        }
1113        // `over` is a soft keyword used only by the cursor-decl
1114        // syntax; in expression position it's a plain identifier.
1115        // (Useful if a workload reuses the name `over` for a
1116        // wire — backward compat with anything pre-SRD-71.)
1117        TokenKind::Over => {
1118            p.advance();
1119            let name = "over".to_string();
1120            if matches!(p.peek(), TokenKind::Dot) {
1121                parse_field_chain(p, name, span)
1122            } else {
1123                Ok(Expr::Ident(name, span))
1124            }
1125        }
1126        _ => Err(format!(
1127            "expected expression, got {:?} at line {}, col {}",
1128            p.peek(),
1129            span.line,
1130            span.col
1131        )),
1132    }
1133}
1134
1135/// Desugar a string literal that contains `{ … }` placeholders
1136/// into a `printf` call.
1137///
1138/// SRD 10 §"String Interpolation": `{name}` references resolve
1139/// to other bindings or workload parameters; the compiler
1140/// splits the template into a format string and the placeholder
1141/// expressions, then wires them into a `Printf` node that
1142/// formats at evaluation time. This is pure syntactic sugar —
1143/// no special runtime support beyond the standard node path.
1144///
1145/// Implementation: each placeholder body is lexed and parsed as
1146/// a full Polydat expression via the same `parse_expression` entry
1147/// the rest of the language uses, so nesting, function calls,
1148/// arithmetic, and field access all work uniformly:
1149///
1150/// | Input                                            | Result                                     |
1151/// |--------------------------------------------------|--------------------------------------------|
1152/// | `"hello"`                                        | `Expr::StringLit("hello")`                 |
1153/// | `"hello {name}"`                                 | `printf("hello {}", name)`                 |
1154/// | `"{a}-{b}"`                                      | `printf("{}-{}", a, b)`                    |
1155/// | `"{format_u64(hash(cycle), 10)}@example.com"`    | `printf("{}@example.com", format_u64(hash(cycle), 10))` |
1156/// | `"x={a + b}"`                                    | `printf("x={}", a + b)`                    |
1157/// | `"{x:05}"`                                       | `Expr::StringLit("{x:05}")` (format spec — left to printf) |
1158/// | `"{{literal}}"`                                  | `Expr::StringLit("{{literal}}")` (escaped braces) |
1159///
1160/// The placeholder scan is brace- and string-aware: `}` inside
1161/// a quoted string or inside nested parentheses doesn't
1162/// terminate the placeholder. `{{` and `}}` keep printf's escape
1163/// semantics for emitting literal braces in output. A
1164/// placeholder body that fails to parse as a complete
1165/// expression makes the whole literal stay as `StringLit` — the
1166/// user's intent was likely a printf format spec written by
1167/// hand, or an unbalanced brace, neither of which we should
1168/// interpret further.
1169fn parse_interpolated_string(s: String, span: Span) -> Expr {
1170    let segments = match scan_interpolation_segments(&s) {
1171        Some(segs) => segs,
1172        None => return Expr::StringLit(s, span), // unbalanced — leave alone
1173    };
1174
1175    if !segments
1176        .iter()
1177        .any(|seg| matches!(seg, Segment::Placeholder(_)))
1178    {
1179        return Expr::StringLit(s, span);
1180    }
1181
1182    // Build the printf format string and gather the placeholder
1183    // expressions, parsing each via the standard expression
1184    // parser so nested calls / arithmetic / field access all
1185    // work uniformly.
1186    let mut format_str = String::with_capacity(s.len());
1187    let mut placeholder_exprs: Vec<Expr> = Vec::new();
1188    for seg in segments {
1189        match seg {
1190            Segment::Literal(text) => format_str.push_str(&text),
1191            Segment::Placeholder(body) => {
1192                let expr = match parse_placeholder_body(&body, span) {
1193                    Ok(e) => e,
1194                    // Unparseable body → bail out, keep the
1195                    // string literal untouched. The user may
1196                    // have written a printf format spec or
1197                    // some other non-GK content.
1198                    Err(_) => return Expr::StringLit(s, span),
1199                };
1200                placeholder_exprs.push(expr);
1201                format_str.push_str("{}");
1202            }
1203        }
1204    }
1205
1206    let mut args: Vec<Arg> = Vec::with_capacity(placeholder_exprs.len() + 1);
1207    args.push(Arg::Positional(Expr::StringLit(format_str, span)));
1208    for e in placeholder_exprs {
1209        args.push(Arg::Positional(e));
1210    }
1211    Expr::Call(CallExpr {
1212        func: "printf".into(),
1213        args,
1214        span,
1215    })
1216}
1217
1218/// One piece of an interpolated string after segmentation.
1219enum Segment {
1220    /// Literal text to copy into the format string. Includes
1221    /// printf's own `{{` / `}}` escapes verbatim — printf's
1222    /// `parse_format` pass turns them into single-brace output.
1223    Literal(String),
1224    /// A `{ … }` placeholder body, with the surrounding braces
1225    /// stripped. Will be lexed + parsed as a Polydat expression.
1226    Placeholder(String),
1227}
1228
1229/// Walk the input, splitting at each `{` that opens a
1230/// placeholder (i.e. not part of `{{`). Brace and string
1231/// awareness: nested `(`/`[`/`{` increase depth, the matching
1232/// closer decreases it, and `}` only terminates a placeholder
1233/// when at depth zero and not inside a `"…"` string literal.
1234///
1235/// Returns `None` if a placeholder is unterminated — the caller
1236/// treats the whole input as a non-interpolated literal.
1237fn scan_interpolation_segments(s: &str) -> Option<Vec<Segment>> {
1238    let chars: Vec<char> = s.chars().collect();
1239    let mut segments: Vec<Segment> = Vec::new();
1240    let mut literal = String::new();
1241    let mut i = 0;
1242    while i < chars.len() {
1243        let c = chars[i];
1244        // Escaped braces: keep verbatim in the literal so printf
1245        // emits a single-brace output.
1246        if c == '{' && i + 1 < chars.len() && chars[i + 1] == '{' {
1247            literal.push_str("{{");
1248            i += 2;
1249            continue;
1250        }
1251        if c == '}' && i + 1 < chars.len() && chars[i + 1] == '}' {
1252            literal.push_str("}}");
1253            i += 2;
1254            continue;
1255        }
1256        if c == '{' {
1257            if !literal.is_empty() {
1258                segments.push(Segment::Literal(std::mem::take(&mut literal)));
1259            }
1260            let body_start = i + 1;
1261            let body_end = find_placeholder_end(&chars, body_start)?;
1262            let body: String = chars[body_start..body_end].iter().collect();
1263            segments.push(Segment::Placeholder(body));
1264            i = body_end + 1; // skip the `}`
1265            continue;
1266        }
1267        literal.push(c);
1268        i += 1;
1269    }
1270    if !literal.is_empty() {
1271        segments.push(Segment::Literal(literal));
1272    }
1273    Some(segments)
1274}
1275
1276/// Find the index of the `}` that closes the placeholder
1277/// starting at `start`. Tracks paren/bracket/brace depth and
1278/// double-quoted string state so unbalanced sub-expressions
1279/// inside a placeholder body don't terminate it prematurely.
1280fn find_placeholder_end(chars: &[char], start: usize) -> Option<usize> {
1281    let mut depth: i32 = 0;
1282    let mut in_string = false;
1283    let mut i = start;
1284    while i < chars.len() {
1285        let c = chars[i];
1286        if in_string {
1287            if c == '\\' && i + 1 < chars.len() {
1288                // Skip the escape sequence (eg \" or \\). One
1289                // char of lookahead is enough — we only need to
1290                // avoid mistaking the next char for a string
1291                // terminator.
1292                i += 2;
1293                continue;
1294            }
1295            if c == '"' {
1296                in_string = false;
1297            }
1298            i += 1;
1299            continue;
1300        }
1301        match c {
1302            '"' => in_string = true,
1303            '(' | '[' | '{' => depth += 1,
1304            ')' | ']' => depth -= 1,
1305            '}' => {
1306                if depth == 0 {
1307                    return Some(i);
1308                }
1309                depth -= 1;
1310            }
1311            _ => {}
1312        }
1313        i += 1;
1314    }
1315    None
1316}
1317
1318/// Lex and parse a placeholder body as a single Polydat expression.
1319fn parse_placeholder_body(body: &str, _span: Span) -> Result<Expr, String> {
1320    let body = body.trim();
1321    if body.is_empty() {
1322        return Err("empty placeholder".into());
1323    }
1324    let tokens = crate::lexer::lex(body)?;
1325    parse_expression(tokens)
1326}
1327
1328/// Parse a dotted field-access chain (`a.b`, `q.cursor.idx`) —
1329/// the base name has already been consumed and the parser sits
1330/// on the first `.`. Intermediate levels flatten into the
1331/// source using the established `__` wire convention, so
1332/// `q.cursor.idx` yields `FieldAccess { source: "q__cursor",
1333/// field: "idx" }` — the same shape one-level access lowers to,
1334/// reading the wire `q__cursor__idx`.
1335fn parse_field_chain(p: &mut Parser, base: String, span: Span) -> Result<Expr, String> {
1336    p.advance(); // consume the first '.'
1337    let mut source = base;
1338    let mut field = p.expect_ident()?;
1339    while matches!(p.peek(), TokenKind::Dot) {
1340        p.advance();
1341        source = format!("{source}__{field}");
1342        field = p.expect_ident()?;
1343    }
1344    Ok(Expr::FieldAccess {
1345        source,
1346        field,
1347        span,
1348    })
1349}
1350
1351/// Parse `name(args...)` — the name has already been consumed.
1352fn parse_call(p: &mut Parser, func: String, span: Span) -> Result<Expr, String> {
1353    p.advance(); // consume '('
1354    let mut args = Vec::new();
1355
1356    if !matches!(p.peek(), TokenKind::RParen) {
1357        loop {
1358            args.push(parse_arg(p)?);
1359            if matches!(p.peek(), TokenKind::Comma) {
1360                p.advance();
1361            } else {
1362                break;
1363            }
1364        }
1365    }
1366
1367    p.expect(&TokenKind::RParen)?;
1368    Ok(Expr::Call(CallExpr { func, args, span }))
1369}
1370
1371/// Parse a single argument: either `name: expr` (named) or `expr` (positional).
1372///
1373/// `name` accepts both plain identifiers and the soft keyword
1374/// `input` — the latter is the canonical parameter name in
1375/// host-provided cycle-driven modules.
1376fn parse_arg(p: &mut Parser) -> Result<Arg, String> {
1377    let arg_name: Option<String> = match p.peek() {
1378        TokenKind::Ident(name) => Some(name.clone()),
1379        TokenKind::Input => Some("input".to_string()),
1380        _ => None,
1381    };
1382    if let Some(name) = arg_name
1383        && p.pos + 1 < p.tokens.len()
1384        && matches!(p.tokens[p.pos + 1].kind, TokenKind::Colon)
1385    {
1386        p.advance(); // consume ident/keyword
1387        p.advance(); // consume ':'
1388        let value = parse_expr(p)?;
1389        return Ok(Arg::Named(name, value));
1390    }
1391    let expr = parse_expr(p)?;
1392    Ok(Arg::Positional(expr))
1393}
1394
1395/// Parse `[expr, expr, ...]`
1396fn parse_array_lit(p: &mut Parser) -> Result<Expr, String> {
1397    let span = p.span();
1398    p.advance(); // consume '['
1399    let mut elements = Vec::new();
1400
1401    if !matches!(p.peek(), TokenKind::RBracket) {
1402        loop {
1403            elements.push(parse_expr(p)?);
1404            if matches!(p.peek(), TokenKind::Comma) {
1405                p.advance();
1406            } else {
1407                break;
1408            }
1409        }
1410    }
1411
1412    p.expect(&TokenKind::RBracket)?;
1413    Ok(Expr::ArrayLit(elements, span))
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418    use super::*;
1419    use crate::lexer::lex;
1420
1421    fn parse_str(s: &str) -> PolydatFile {
1422        let tokens = lex(s).unwrap();
1423        parse(tokens).unwrap()
1424    }
1425
1426    fn parse_str_err(s: &str) -> String {
1427        let tokens = lex(s).unwrap();
1428        match parse(tokens) {
1429            Ok(_) => panic!("expected parse error from: {s:?}"),
1430            Err(e) => e,
1431        }
1432    }
1433
1434    fn cycle_modifier_of(f: &PolydatFile) -> BindingModifier {
1435        match &f.statements[0] {
1436            Statement::Binding(b) => b.modifier,
1437            other => panic!("expected cycle binding, got {other:?}"),
1438        }
1439    }
1440
1441    #[test]
1442    fn parse_volatile_modifier() {
1443        let f = parse_str("volatile x := 42");
1444        let m = cycle_modifier_of(&f);
1445        assert!(m.is_volatile() && !m.is_const() && !m.is_shared());
1446    }
1447
1448    #[test]
1449    fn parse_modifiers_in_any_order_yields_same_set() {
1450        let m1 = cycle_modifier_of(&parse_str("const shared x := 42"));
1451        let m2 = cycle_modifier_of(&parse_str("shared const x := 42"));
1452        assert_eq!(
1453            m1, m2,
1454            "ordering shouldn't matter: `const shared` and `shared const` collapse to the same set"
1455        );
1456        assert!(m1.is_const() && m1.is_shared());
1457    }
1458
1459    #[test]
1460    fn parse_shared_volatile_combination() {
1461        let m = cycle_modifier_of(&parse_str("shared volatile x := 42"));
1462        assert!(m.is_shared() && m.is_volatile() && !m.is_const());
1463    }
1464
1465    #[test]
1466    fn parse_rejects_const_volatile_combo() {
1467        let err = parse_str_err("const volatile x := 42");
1468        assert!(
1469            err.contains("const") && err.contains("volatile"),
1470            "error should name the conflicting keywords: {err}"
1471        );
1472    }
1473
1474    #[test]
1475    fn parse_rejects_volatile_const_combo_same_as_const_volatile() {
1476        // Order-independent rejection.
1477        let err = parse_str_err("volatile const x := 42");
1478        assert!(err.contains("const") && err.contains("volatile"));
1479    }
1480
1481    #[test]
1482    fn parse_rejects_duplicate_modifier() {
1483        let err = parse_str_err("const const x := 42");
1484        assert!(
1485            err.contains("duplicate"),
1486            "error should call out duplicate: {err}"
1487        );
1488    }
1489
1490    #[test]
1491    fn parse_volatile_const_binding() {
1492        // `volatile const x := 42` — const binding with volatile
1493        // modifier. The grammar accepts modifier stacking; the
1494        // `const + volatile` combination is rejected by
1495        // `BindingModifier::from_iter` as semantically
1496        // contradictory, but `volatile` alone (no const) is fine
1497        // and the parser must accept the lexical sequence.
1498        let f = parse_str("volatile x := 42");
1499        match &f.statements[0] {
1500            Statement::Binding(b) => {
1501                assert!(b.modifier.is_volatile());
1502                assert!(!b.modifier.is_const());
1503            }
1504            other => panic!("expected binding, got {other:?}"),
1505        }
1506    }
1507
1508    #[test]
1509    fn parse_input_bare() {
1510        let f = parse_str("input cycle: u64");
1511        assert_eq!(f.statements.len(), 1);
1512        match &f.statements[0] {
1513            Statement::InputDecl(d) => {
1514                assert_eq!(d.name, "cycle");
1515                assert_eq!(d.ty.as_deref(), Some("u64"));
1516            }
1517            other => panic!("expected InputDecl, got {other:?}"),
1518        }
1519    }
1520
1521    #[test]
1522    fn parse_input_bare_untyped() {
1523        let f = parse_str("input cycle");
1524        match &f.statements[0] {
1525            Statement::InputDecl(d) => {
1526                assert_eq!(d.name, "cycle");
1527                assert!(d.ty.is_none(), "no type annotation");
1528            }
1529            other => panic!("expected InputDecl, got {other:?}"),
1530        }
1531    }
1532
1533    #[test]
1534    fn parse_input_tuple_form() {
1535        // Tuple form desugars to N InputDecl statements, mirroring
1536        // the module-signature param-list shape.
1537        let f = parse_str("input (cycle: u64, q: f64)");
1538        assert_eq!(f.statements.len(), 2);
1539        match &f.statements[0] {
1540            Statement::InputDecl(d) => {
1541                assert_eq!(d.name, "cycle");
1542                assert_eq!(d.ty.as_deref(), Some("u64"));
1543            }
1544            other => panic!("expected InputDecl, got {other:?}"),
1545        }
1546        match &f.statements[1] {
1547            Statement::InputDecl(d) => {
1548                assert_eq!(d.name, "q");
1549                assert_eq!(d.ty.as_deref(), Some("f64"));
1550            }
1551            other => panic!("expected InputDecl, got {other:?}"),
1552        }
1553    }
1554
1555    #[test]
1556    fn parse_input_tuple_empty_rejected() {
1557        // `input ()` is malformed — to declare zero inputs, omit the line.
1558        let tokens = crate::lexer::lex("input ()").unwrap();
1559        let err = parse(tokens).unwrap_err();
1560        assert!(
1561            err.contains("empty"),
1562            "error should mention empty tuple: {err}"
1563        );
1564    }
1565
1566    #[test]
1567    fn parse_const_binding() {
1568        let f = parse_str("const lut := dist_normal(72.0, 5.0)");
1569        assert_eq!(f.statements.len(), 1);
1570        match &f.statements[0] {
1571            Statement::Binding(b) => {
1572                assert_eq!(b.targets, vec!["lut"]);
1573                assert!(b.modifier.is_const());
1574                match &b.value {
1575                    Expr::Call(c) => {
1576                        assert_eq!(c.func, "dist_normal");
1577                        assert_eq!(c.args.len(), 2);
1578                    }
1579                    _ => panic!("expected call"),
1580                }
1581            }
1582            _ => panic!("expected const binding"),
1583        }
1584    }
1585
1586    #[test]
1587    fn parse_cycle_binding() {
1588        let f = parse_str("seed := hash(cycle)");
1589        match &f.statements[0] {
1590            Statement::Binding(b) => {
1591                assert_eq!(b.targets, vec!["seed"]);
1592                match &b.value {
1593                    Expr::Call(c) => {
1594                        assert_eq!(c.func, "hash");
1595                        assert_eq!(c.args.len(), 1);
1596                    }
1597                    _ => panic!("expected call"),
1598                }
1599            }
1600            _ => panic!("expected cycle binding"),
1601        }
1602    }
1603
1604    #[test]
1605    fn parse_destructuring() {
1606        let f = parse_str("(tenant, device, reading) := mixed_radix(cycle, 100, 1000, 0)");
1607        match &f.statements[0] {
1608            Statement::Binding(b) => {
1609                assert_eq!(b.targets, vec!["tenant", "device", "reading"]);
1610                match &b.value {
1611                    Expr::Call(c) => {
1612                        assert_eq!(c.func, "mixed_radix");
1613                        assert_eq!(c.args.len(), 4);
1614                    }
1615                    _ => panic!("expected call"),
1616                }
1617            }
1618            _ => panic!("expected cycle binding"),
1619        }
1620    }
1621
1622    #[test]
1623    fn parse_named_args() {
1624        let f = parse_str("const lut := dist_normal(mean: 72.0, stddev: 5.0)");
1625        match &f.statements[0] {
1626            Statement::Binding(b) => match &b.value {
1627                Expr::Call(c) => {
1628                    assert!(matches!(&c.args[0], Arg::Named(n, _) if n == "mean"));
1629                    assert!(matches!(&c.args[1], Arg::Named(n, _) if n == "stddev"));
1630                }
1631                _ => panic!("expected call"),
1632            },
1633            _ => panic!("expected const binding"),
1634        }
1635    }
1636
1637    #[test]
1638    fn parse_string_lit_plain() {
1639        // Bare strings without `{name}` placeholders stay as
1640        // `Expr::StringLit`.
1641        let f = parse_str(r#"id := "static text""#);
1642        match &f.statements[0] {
1643            Statement::Binding(b) => match &b.value {
1644                Expr::StringLit(s, _) => assert_eq!(s, "static text"),
1645                _ => panic!("expected string lit"),
1646            },
1647            _ => panic!("expected binding"),
1648        }
1649    }
1650
1651    #[test]
1652    fn parse_string_lit_interpolated() {
1653        // Strings containing `{ident}` placeholders compile to a
1654        // `printf(fmt, idents...)` call so the named idents flow
1655        // as wires from the surrounding scope.
1656        let f = parse_str(r#"id := "{code}-{seq}""#);
1657        match &f.statements[0] {
1658            Statement::Binding(b) => match &b.value {
1659                Expr::Call(c) => {
1660                    assert_eq!(c.func, "printf");
1661                    assert_eq!(c.args.len(), 3);
1662                    match &c.args[0] {
1663                        Arg::Positional(Expr::StringLit(s, _)) => assert_eq!(s, "{}-{}"),
1664                        _ => panic!("expected format string as first arg"),
1665                    }
1666                    match &c.args[1] {
1667                        Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "code"),
1668                        _ => panic!("expected ident `code`"),
1669                    }
1670                    match &c.args[2] {
1671                        Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "seq"),
1672                        _ => panic!("expected ident `seq`"),
1673                    }
1674                }
1675                other => panic!("expected printf call, got {other:?}"),
1676            },
1677            _ => panic!("expected binding"),
1678        }
1679    }
1680
1681    #[test]
1682    fn parse_string_lit_format_spec_left_alone() {
1683        // printf format specs (`{:05}`, `{:x}`, `{:.3}`) and
1684        // empty positional placeholders (`{}`) aren't valid GK
1685        // expressions, so the literal is preserved untouched
1686        // for printf's own parser.
1687        let f = parse_str(r#"id := "x={:05}""#);
1688        match &f.statements[0] {
1689            Statement::Binding(b) => match &b.value {
1690                Expr::StringLit(s, _) => assert_eq!(s, "x={:05}"),
1691                _ => panic!("expected literal"),
1692            },
1693            _ => panic!("expected binding"),
1694        }
1695    }
1696
1697    #[test]
1698    fn parse_string_lit_nested_call() {
1699        // SRD 10 example: function calls inside placeholders
1700        // parse as full expressions and become printf args.
1701        let f = parse_str(r#"email := "{format_u64(hash(cycle), 10)}@example.com""#);
1702        let call = match &f.statements[0] {
1703            Statement::Binding(b) => match &b.value {
1704                Expr::Call(c) => c,
1705                other => panic!("expected printf call, got {other:?}"),
1706            },
1707            _ => panic!("expected binding"),
1708        };
1709        assert_eq!(call.func, "printf");
1710        assert_eq!(call.args.len(), 2);
1711        match &call.args[0] {
1712            Arg::Positional(Expr::StringLit(s, _)) => assert_eq!(s, "{}@example.com"),
1713            other => panic!("expected format string, got {other:?}"),
1714        }
1715        match &call.args[1] {
1716            Arg::Positional(Expr::Call(inner)) => {
1717                assert_eq!(inner.func, "format_u64");
1718                assert_eq!(inner.args.len(), 2);
1719                match &inner.args[0] {
1720                    Arg::Positional(Expr::Call(h)) => assert_eq!(h.func, "hash"),
1721                    other => panic!("expected hash(...) call, got {other:?}"),
1722                }
1723                match &inner.args[1] {
1724                    Arg::Positional(Expr::IntLit(10, _)) => {}
1725                    other => panic!("expected literal 10, got {other:?}"),
1726                }
1727            }
1728            other => panic!("expected format_u64 call, got {other:?}"),
1729        }
1730    }
1731
1732    #[test]
1733    fn parse_string_lit_arithmetic_in_placeholder() {
1734        // Infix arithmetic inside placeholders parses via the
1735        // standard Pratt expression path.
1736        let f = parse_str(r#"id := "x={a + b * 2}""#);
1737        let call = match &f.statements[0] {
1738            Statement::Binding(b) => match &b.value {
1739                Expr::Call(c) => c,
1740                other => panic!("expected call, got {other:?}"),
1741            },
1742            _ => panic!("expected binding"),
1743        };
1744        assert_eq!(call.func, "printf");
1745        match &call.args[1] {
1746            Arg::Positional(Expr::BinOp(_, BinOpKind::Add, _)) => {}
1747            other => panic!("expected addition, got {other:?}"),
1748        }
1749    }
1750
1751    #[test]
1752    fn parse_string_lit_field_access() {
1753        // Field access (`base.ordinal`) inside placeholders.
1754        let f = parse_str(r#"k := "row {row.id}""#);
1755        let call = match &f.statements[0] {
1756            Statement::Binding(b) => match &b.value {
1757                Expr::Call(c) => c,
1758                other => panic!("expected call, got {other:?}"),
1759            },
1760            _ => panic!("expected binding"),
1761        };
1762        assert_eq!(call.func, "printf");
1763        match &call.args[1] {
1764            Arg::Positional(Expr::FieldAccess { source, field, .. }) => {
1765                assert_eq!(source, "row");
1766                assert_eq!(field, "id");
1767            }
1768            other => panic!("expected field access, got {other:?}"),
1769        }
1770    }
1771
1772    #[test]
1773    fn parse_string_lit_escaped_braces() {
1774        // Doubled braces (`{{`, `}}`) keep printf's escape
1775        // semantics — they emit literal `{` / `}` at format time
1776        // and don't open a placeholder.
1777        let f = parse_str(r#"k := "{{not a placeholder}} but {real}""#);
1778        let call = match &f.statements[0] {
1779            Statement::Binding(b) => match &b.value {
1780                Expr::Call(c) => c,
1781                other => panic!("expected call, got {other:?}"),
1782            },
1783            _ => panic!("expected binding"),
1784        };
1785        match &call.args[0] {
1786            Arg::Positional(Expr::StringLit(s, _)) => {
1787                assert_eq!(s, "{{not a placeholder}} but {}");
1788            }
1789            other => panic!("expected fmt string, got {other:?}"),
1790        }
1791        match &call.args[1] {
1792            Arg::Positional(Expr::Ident(n, _)) => assert_eq!(n, "real"),
1793            other => panic!("expected ident `real`, got {other:?}"),
1794        }
1795    }
1796
1797    #[test]
1798    fn parse_string_lit_unterminated_falls_back() {
1799        // An unterminated `{` makes the whole string stay literal.
1800        let f = parse_str(r#"k := "missing close {abc""#);
1801        match &f.statements[0] {
1802            Statement::Binding(b) => match &b.value {
1803                Expr::StringLit(s, _) => assert_eq!(s, "missing close {abc"),
1804                other => panic!("expected literal, got {other:?}"),
1805            },
1806            _ => panic!("expected binding"),
1807        }
1808    }
1809
1810    #[test]
1811    fn parse_string_lit_parens_in_placeholder() {
1812        // Function-call parens inside a placeholder don't
1813        // confuse the brace scanner; the matching `}` is found
1814        // at depth zero.
1815        let f = parse_str(r#"k := "{abs(x - y)}""#);
1816        let call = match &f.statements[0] {
1817            Statement::Binding(b) => match &b.value {
1818                Expr::Call(c) => c,
1819                other => panic!("expected call, got {other:?}"),
1820            },
1821            _ => panic!("expected binding"),
1822        };
1823        assert_eq!(call.func, "printf");
1824        match &call.args[1] {
1825            Arg::Positional(Expr::Call(inner)) => assert_eq!(inner.func, "abs"),
1826            other => panic!("expected abs call, got {other:?}"),
1827        }
1828    }
1829
1830    #[test]
1831    fn parse_array_lit() {
1832        let f = parse_str("const weights := [60.0, 20.0, 15.0, 5.0]");
1833        match &f.statements[0] {
1834            Statement::Binding(b) => match &b.value {
1835                Expr::ArrayLit(elems, _) => assert_eq!(elems.len(), 4),
1836                _ => panic!("expected array lit"),
1837            },
1838            _ => panic!("expected const binding"),
1839        }
1840    }
1841
1842    #[test]
1843    fn parse_nested_call() {
1844        let f = parse_str("x := hash(interleave(a, b))");
1845        match &f.statements[0] {
1846            Statement::Binding(b) => match &b.value {
1847                Expr::Call(c) => {
1848                    assert_eq!(c.func, "hash");
1849                    assert_eq!(c.args.len(), 1);
1850                    match &c.args[0] {
1851                        Arg::Positional(Expr::Call(inner)) => {
1852                            assert_eq!(inner.func, "interleave");
1853                            assert_eq!(inner.args.len(), 2);
1854                        }
1855                        _ => panic!("expected nested call"),
1856                    }
1857                }
1858                _ => panic!("expected call"),
1859            },
1860            _ => panic!("expected binding"),
1861        }
1862    }
1863
1864    #[test]
1865    fn parse_full_program() {
1866        let src = r#"
1867            // Const bindings (compile-time fold or scope-init pull)
1868            const temp_lut := dist_normal(mean: 72.0, stddev: 5.0)
1869            const weights := [60.0, 20.0, 15.0]
1870
1871            // Cycle bindings (per-cycle eval)
1872            input cycle: u64
1873            (tenant, device) := mixed_radix(cycle, 100, 0)
1874            tenant_h := hash(tenant)
1875            code := mod(tenant_h, 10000)
1876            device_id := "{code}-{seq}"
1877        "#;
1878        let f = parse_str(src);
1879        assert_eq!(f.statements.len(), 7);
1880    }
1881
1882    #[test]
1883    fn parse_mixed_positional_named() {
1884        let f = parse_str("const lut := dist_normal(72.0, 5.0, resolution: 2000)");
1885        match &f.statements[0] {
1886            Statement::Binding(b) => match &b.value {
1887                Expr::Call(c) => {
1888                    assert!(matches!(&c.args[0], Arg::Positional(_)));
1889                    assert!(matches!(&c.args[1], Arg::Positional(_)));
1890                    assert!(matches!(&c.args[2], Arg::Named(n, _) if n == "resolution"));
1891                }
1892                _ => panic!("expected call"),
1893            },
1894            _ => panic!("expected const binding"),
1895        }
1896    }
1897
1898    #[test]
1899    fn parse_simple_addition() {
1900        let f = parse_str("y := a + b");
1901        match &f.statements[0] {
1902            Statement::Binding(b) => match &b.value {
1903                Expr::BinOp(lhs, BinOpKind::Add, rhs) => {
1904                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
1905                    assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "b"));
1906                }
1907                _ => panic!("expected BinOp Add, got {:?}", b.value),
1908            },
1909            _ => panic!("expected cycle binding"),
1910        }
1911    }
1912
1913    #[test]
1914    fn parse_precedence_mul_over_add() {
1915        // `a + b * c` should parse as `a + (b * c)`
1916        let f = parse_str("y := a + b * c");
1917        match &f.statements[0] {
1918            Statement::Binding(b) => match &b.value {
1919                Expr::BinOp(lhs, BinOpKind::Add, rhs) => {
1920                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
1921                    match &**rhs {
1922                        Expr::BinOp(rl, BinOpKind::Mul, rr) => {
1923                            assert!(matches!(**rl, Expr::Ident(ref s, _) if s == "b"));
1924                            assert!(matches!(**rr, Expr::Ident(ref s, _) if s == "c"));
1925                        }
1926                        _ => panic!("expected inner Mul"),
1927                    }
1928                }
1929                _ => panic!("expected outer Add"),
1930            },
1931            _ => panic!("expected cycle binding"),
1932        }
1933    }
1934
1935    #[test]
1936    fn parse_parenthesized_grouping() {
1937        // `(a + b) * c` — parens override precedence
1938        let f = parse_str("y := (a + b) * c");
1939        match &f.statements[0] {
1940            Statement::Binding(b) => {
1941                match &b.value {
1942                    Expr::BinOp(lhs, BinOpKind::Mul, rhs) => {
1943                        match &**lhs {
1944                            Expr::BinOp(_, BinOpKind::Add, _) => {} // correct
1945                            _ => panic!("expected inner Add in lhs"),
1946                        }
1947                        assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "c"));
1948                    }
1949                    _ => panic!("expected outer Mul"),
1950                }
1951            }
1952            _ => panic!("expected cycle binding"),
1953        }
1954    }
1955
1956    /// Helper: unwrap a binding's value as the `if` call the block form desugars to.
1957    fn if_call(src: &str) -> CallExpr {
1958        let f = parse_str(src);
1959        match &f.statements[0] {
1960            Statement::Binding(b) => match &b.value {
1961                Expr::Call(c) => {
1962                    assert_eq!(
1963                        c.func, "if",
1964                        "block form must desugar to the `if` intrinsic"
1965                    );
1966                    assert_eq!(c.args.len(), 3, "if intrinsic takes (cond, then, else)");
1967                    c.clone()
1968                }
1969                other => panic!("expected Call, got {:?}", other),
1970            },
1971            _ => panic!("expected binding"),
1972        }
1973    }
1974
1975    #[test]
1976    fn if_block_desugars_to_the_call_intrinsic() {
1977        // The whole point: the block form is sugar, not a second construct. It must
1978        // produce exactly what the long-standing call form produces, so branch-type
1979        // dispatch and widening in binding.rs apply to it unchanged.
1980        let block = if_call("y := if c { a } else { b }");
1981        let call = if_call("y := if(c, a, b)");
1982        for (i, (bl, ca)) in block.args.iter().zip(call.args.iter()).enumerate() {
1983            match (bl, ca) {
1984                (Arg::Positional(Expr::Ident(x, _)), Arg::Positional(Expr::Ident(y, _))) => {
1985                    assert_eq!(x, y, "arg {} differs between block and call form", i);
1986                }
1987                _ => panic!("expected plain idents in both forms"),
1988            }
1989        }
1990    }
1991
1992    #[test]
1993    fn if_block_accepts_expressions_in_condition_and_branches() {
1994        let c = if_call("y := if segments > 0 { total / segments } else { 0 }");
1995        assert!(
1996            matches!(
1997                &c.args[0],
1998                Arg::Positional(Expr::BinOp(_, BinOpKind::Gt, _))
1999            ),
2000            "condition should parse as a full expression"
2001        );
2002        assert!(
2003            matches!(
2004                &c.args[1],
2005                Arg::Positional(Expr::BinOp(_, BinOpKind::Div, _))
2006            ),
2007            "then-branch should parse as a full expression"
2008        );
2009    }
2010
2011    #[test]
2012    fn if_block_chains_else_if() {
2013        // `else if` nests as the else-branch, so the chain is right-associative.
2014        let c = if_call("y := if a { 1 } else if b { 2 } else { 3 }");
2015        match &c.args[2] {
2016            Arg::Positional(Expr::Call(inner)) => {
2017                assert_eq!(inner.func, "if");
2018                assert!(matches!(
2019                    &inner.args[1],
2020                    Arg::Positional(Expr::IntLit(2, _))
2021                ));
2022                assert!(matches!(
2023                    &inner.args[2],
2024                    Arg::Positional(Expr::IntLit(3, _))
2025                ));
2026            }
2027            other => panic!("expected nested if in else position, got {:?}", other),
2028        }
2029    }
2030
2031    #[test]
2032    fn if_block_nests_inside_other_expressions() {
2033        // It is an expression, so it composes like one.
2034        let f = parse_str("y := 1 + if c { 2 } else { 3 }");
2035        match &f.statements[0] {
2036            Statement::Binding(b) => match &b.value {
2037                Expr::BinOp(_, BinOpKind::Add, rhs) => {
2038                    assert!(matches!(**rhs, Expr::Call(ref c) if c.func == "if"));
2039                }
2040                other => panic!("expected Add with an if on the rhs, got {:?}", other),
2041            },
2042            _ => panic!("expected binding"),
2043        }
2044    }
2045
2046    #[test]
2047    fn if_call_form_still_parses_as_a_call() {
2048        // `if` stays a soft keyword: `if(` must not be captured by the block form.
2049        let c = if_call("y := if(c, a, b)");
2050        assert_eq!(c.args.len(), 3);
2051    }
2052
2053    #[test]
2054    fn if_block_requires_else() {
2055        // Every Polydat expression yields a value, so a one-armed if has no result
2056        // on the false path. The error must say that rather than failing cryptically.
2057        let err = parse_str_err("y := if c { a }");
2058        assert!(
2059            err.contains("else"),
2060            "error should name the missing else: {}",
2061            err
2062        );
2063    }
2064
2065    #[test]
2066    fn if_block_reports_a_missing_brace_helpfully() {
2067        let err = parse_str_err("y := if c a else b");
2068        assert!(
2069            err.contains("if <cond>"),
2070            "error should show the block form: {}",
2071            err
2072        );
2073    }
2074
2075    #[test]
2076    fn parse_unary_negation() {
2077        let f = parse_str("y := -x");
2078        match &f.statements[0] {
2079            Statement::Binding(b) => match &b.value {
2080                Expr::UnaryNeg(inner, _) => {
2081                    assert!(matches!(**inner, Expr::Ident(ref s, _) if s == "x"));
2082                }
2083                _ => panic!("expected UnaryNeg"),
2084            },
2085            _ => panic!("expected cycle binding"),
2086        }
2087    }
2088
2089    #[test]
2090    fn parse_func_call_with_infix_arg() {
2091        // `sin(cycle * 0.25)` — infix inside function args
2092        let f = parse_str("y := sin(cycle * 0.25)");
2093        match &f.statements[0] {
2094            Statement::Binding(b) => match &b.value {
2095                Expr::Call(c) => {
2096                    assert_eq!(c.func, "sin");
2097                    assert_eq!(c.args.len(), 1);
2098                    match &c.args[0] {
2099                        Arg::Positional(Expr::BinOp(_, BinOpKind::Mul, _)) => {}
2100                        _ => panic!("expected Mul inside sin() arg"),
2101                    }
2102                }
2103                _ => panic!("expected call"),
2104            },
2105            _ => panic!("expected cycle binding"),
2106        }
2107    }
2108
2109    #[test]
2110    fn parse_power_right_associative() {
2111        // `a ** b ** c` should parse as `a ** (b ** c)` (right-associative)
2112        let f = parse_str("y := a ** b ** c");
2113        match &f.statements[0] {
2114            Statement::Binding(b) => match &b.value {
2115                Expr::BinOp(lhs, BinOpKind::Pow, rhs) => {
2116                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
2117                    match &**rhs {
2118                        Expr::BinOp(rl, BinOpKind::Pow, rr) => {
2119                            assert!(matches!(**rl, Expr::Ident(ref s, _) if s == "b"));
2120                            assert!(matches!(**rr, Expr::Ident(ref s, _) if s == "c"));
2121                        }
2122                        _ => panic!("expected inner Pow"),
2123                    }
2124                }
2125                _ => panic!("expected outer Pow"),
2126            },
2127            _ => panic!("expected cycle binding"),
2128        }
2129    }
2130
2131    #[test]
2132    fn parse_negate_function_call() {
2133        // `-sin(x)` — unary negation of a function call
2134        let f = parse_str("y := -sin(x)");
2135        match &f.statements[0] {
2136            Statement::Binding(b) => match &b.value {
2137                Expr::UnaryNeg(inner, _) => match &**inner {
2138                    Expr::Call(c) => assert_eq!(c.func, "sin"),
2139                    _ => panic!("expected Call inside UnaryNeg"),
2140                },
2141                _ => panic!("expected UnaryNeg"),
2142            },
2143            _ => panic!("expected cycle binding"),
2144        }
2145    }
2146
2147    #[test]
2148    fn parse_all_operators() {
2149        // Ensure all operators parse without error.
2150        let f = parse_str("y := a + b - c * d / e % f ** g");
2151        match &f.statements[0] {
2152            Statement::Binding(_) => {} // just checking it parses
2153            _ => panic!("expected cycle binding"),
2154        }
2155    }
2156
2157    #[test]
2158    fn parse_star_star_power() {
2159        // `x ** 2.0` parses as BinOp(x, Pow, 2.0)
2160        let f = parse_str("y := x ** 2.0");
2161        match &f.statements[0] {
2162            Statement::Binding(b) => match &b.value {
2163                Expr::BinOp(lhs, BinOpKind::Pow, rhs) => {
2164                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "x"));
2165                    assert!(matches!(**rhs, Expr::FloatLit(v, _) if v == 2.0));
2166                }
2167                _ => panic!("expected BinOp Pow, got {:?}", b.value),
2168            },
2169            _ => panic!("expected cycle binding"),
2170        }
2171    }
2172
2173    #[test]
2174    fn parse_caret_is_xor() {
2175        // `a ^ b` parses as BinOp(a, BitXor, b)
2176        let f = parse_str("y := a ^ b");
2177        match &f.statements[0] {
2178            Statement::Binding(b) => match &b.value {
2179                Expr::BinOp(lhs, BinOpKind::BitXor, rhs) => {
2180                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
2181                    assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "b"));
2182                }
2183                _ => panic!("expected BinOp BitXor, got {:?}", b.value),
2184            },
2185            _ => panic!("expected cycle binding"),
2186        }
2187    }
2188
2189    #[test]
2190    fn parse_bitand_binds_tighter_than_bitor() {
2191        // `a & b | c` should parse as `(a & b) | c`
2192        let f = parse_str("y := a & b | c");
2193        match &f.statements[0] {
2194            Statement::Binding(b) => {
2195                match &b.value {
2196                    Expr::BinOp(lhs, BinOpKind::BitOr, rhs) => {
2197                        match &**lhs {
2198                            Expr::BinOp(_, BinOpKind::BitAnd, _) => {} // correct
2199                            _ => panic!("expected inner BitAnd in lhs"),
2200                        }
2201                        assert!(matches!(**rhs, Expr::Ident(ref s, _) if s == "c"));
2202                    }
2203                    _ => panic!("expected outer BitOr"),
2204                }
2205            }
2206            _ => panic!("expected cycle binding"),
2207        }
2208    }
2209
2210    #[test]
2211    fn parse_shift_left() {
2212        // `a << 4` parses as BinOp(a, Shl, 4)
2213        let f = parse_str("y := a << 4");
2214        match &f.statements[0] {
2215            Statement::Binding(b) => match &b.value {
2216                Expr::BinOp(lhs, BinOpKind::Shl, rhs) => {
2217                    assert!(matches!(**lhs, Expr::Ident(ref s, _) if s == "a"));
2218                    assert!(matches!(**rhs, Expr::IntLit(4, _)));
2219                }
2220                _ => panic!("expected BinOp Shl, got {:?}", b.value),
2221            },
2222            _ => panic!("expected cycle binding"),
2223        }
2224    }
2225
2226    #[test]
2227    fn parse_cursor_without_over_clause() {
2228        let f = parse_str("cursor q = range(0, 100)");
2229        match &f.statements[0] {
2230            Statement::Cursor(c) => {
2231                assert_eq!(c.name, "q");
2232                assert!(c.over.is_none(), "no `over` → over is None");
2233            }
2234            other => panic!("expected Cursor, got {other:?}"),
2235        }
2236    }
2237
2238    #[test]
2239    fn parse_cursor_with_over_iter_var() {
2240        let f = parse_str("cursor q = range(0, 100) over p");
2241        match &f.statements[0] {
2242            Statement::Cursor(c) => {
2243                assert_eq!(c.name, "q");
2244                match &c.over {
2245                    Some(Expr::Ident(name, _)) => assert_eq!(name, "p"),
2246                    other => panic!("expected Some(Ident('p')), got {other:?}"),
2247                }
2248            }
2249            other => panic!("expected Cursor, got {other:?}"),
2250        }
2251    }
2252
2253    #[test]
2254    fn parse_cursor_with_over_dotted_param_projection() {
2255        let f = parse_str("cursor q = range(0, 100) over cursor.partitions");
2256        match &f.statements[0] {
2257            Statement::Cursor(c) => {
2258                assert!(c.over.is_some(), "should have over clause");
2259                // `cursor.partitions` parses as a field access.
2260                match &c.over {
2261                    Some(Expr::FieldAccess { .. }) => {} // OK
2262                    Some(other) => panic!("expected FieldAccess, got {other:?}"),
2263                    None => panic!("expected Some"),
2264                }
2265            }
2266            other => panic!("expected Cursor, got {other:?}"),
2267        }
2268    }
2269
2270    #[test]
2271    fn parse_cursor_over_does_not_swallow_following_statement() {
2272        let f = parse_str("cursor q = range(0, 100) over p\nother := 42");
2273        assert_eq!(f.statements.len(), 2);
2274    }
2275
2276    #[test]
2277    fn parse_chained_field_access_flattens_intermediate_levels() {
2278        // SRD 71 scalar projections: `q.cursor.idx` reads the
2279        // wire `q__cursor__idx` — intermediate dot levels
2280        // flatten into the FieldAccess source using the same
2281        // `__` convention one-level access lowers to.
2282        let f = parse_str("i := q.cursor.idx");
2283        match &f.statements[0] {
2284            Statement::Binding(b) => match &b.value {
2285                Expr::FieldAccess { source, field, .. } => {
2286                    assert_eq!(source, "q__cursor");
2287                    assert_eq!(field, "idx");
2288                }
2289                other => panic!("expected FieldAccess, got {other:?}"),
2290            },
2291            other => panic!("expected binding, got {other:?}"),
2292        }
2293        // Deeper chains keep flattening.
2294        let f = parse_str("x := a.b.c.d");
2295        match &f.statements[0] {
2296            Statement::Binding(b) => match &b.value {
2297                Expr::FieldAccess { source, field, .. } => {
2298                    assert_eq!(source, "a__b__c");
2299                    assert_eq!(field, "d");
2300                }
2301                other => panic!("expected FieldAccess, got {other:?}"),
2302            },
2303            other => panic!("expected binding, got {other:?}"),
2304        }
2305    }
2306
2307    #[test]
2308    fn parse_unary_bitnot() {
2309        // `!x` parses as UnaryBitNot(x)
2310        let f = parse_str("y := !x");
2311        match &f.statements[0] {
2312            Statement::Binding(b) => match &b.value {
2313                Expr::UnaryBitNot(inner, _) => {
2314                    assert!(matches!(**inner, Expr::Ident(ref s, _) if s == "x"));
2315                }
2316                _ => panic!("expected UnaryBitNot, got {:?}", b.value),
2317            },
2318            _ => panic!("expected cycle binding"),
2319        }
2320    }
2321}