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