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