Skip to main content

mangle_parse/
lib.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::{Result, anyhow, bail};
16use ast::{Arena, Constraints};
17use mangle_ast as ast;
18use std::io;
19
20mod error;
21mod quote;
22mod scan;
23mod token;
24
25pub use error::{ErrorContext, ParseError};
26use token::Token;
27
28pub struct Parser<'arena, R>
29where
30    R: io::Read,
31{
32    sc: scan::Scanner<R>,
33    token: crate::token::Token,
34    arena: &'arena Arena,
35    anon_counter: usize,
36}
37
38fn package_sym(arena: &Arena) -> ast::PredicateIndex {
39    arena.predicate_sym("Package", Some(0))
40}
41
42fn name_sym(arena: &Arena) -> ast::PredicateIndex {
43    arena.predicate_sym("name", Some(1))
44}
45
46fn use_sym(arena: &Arena) -> ast::PredicateIndex {
47    arena.predicate_sym("Use", Some(0))
48}
49
50fn lt_sym(arena: &Arena) -> ast::PredicateIndex {
51    arena.predicate_sym(":lt", Some(2))
52}
53
54fn le_sym(arena: &Arena) -> ast::PredicateIndex {
55    arena.predicate_sym(":le", Some(2))
56}
57
58fn gt_sym(arena: &Arena) -> ast::PredicateIndex {
59    arena.predicate_sym(":gt", Some(2))
60}
61
62fn ge_sym(arena: &Arena) -> ast::PredicateIndex {
63    arena.predicate_sym(":ge", Some(2))
64}
65
66fn fn_list_sym(arena: &Arena) -> ast::FunctionIndex {
67    arena.function_sym("fn:list", None)
68}
69
70fn fn_map_sym(arena: &Arena) -> ast::FunctionIndex {
71    arena.function_sym("fn:map", None)
72}
73
74fn fn_struct_sym(arena: &Arena) -> ast::FunctionIndex {
75    arena.function_sym("fn:struct", None)
76}
77
78fn fn_list_type_sym(arena: &Arena) -> ast::FunctionIndex {
79    arena.function_sym("fn:List", None)
80}
81
82fn fn_option_type_sym(arena: &Arena) -> ast::FunctionIndex {
83    arena.function_sym("fn:Option", None)
84}
85
86fn fn_opt_sym(arena: &Arena) -> ast::FunctionIndex {
87    arena.function_sym("fn:opt", None)
88}
89
90fn empty_package_decl(arena: &Arena) -> ast::Decl<'_> {
91    ast::Decl {
92        atom: arena.alloc(ast::Atom {
93            sym: package_sym(arena),
94            args: &[],
95        }),
96        is_temporal: false,
97        descr: arena.alloc_slice_copy(&[arena.alloc(ast::Atom {
98            sym: name_sym(arena),
99            args: arena
100                .alloc_slice_copy(&[arena.alloc(ast::BaseTerm::Const(ast::Const::String("")))]),
101        })]),
102        bounds: None,
103        constraints: None,
104    }
105}
106
107macro_rules! alloc {
108    ($self:expr, $e:expr) => {
109        &*$self.arena.alloc($e)
110    };
111}
112
113macro_rules! alloc_str {
114    ($self:expr, $e:expr) => {
115        &*$self.arena.alloc_str($e)
116    };
117}
118
119macro_rules! alloc_slice {
120    ($self:expr, $e:expr) => {
121        &*$self.arena.alloc_slice_copy($e)
122    };
123}
124
125impl<'arena, R> Parser<'arena, R>
126where
127    R: io::Read,
128{
129    pub fn new<P: ToString>(arena: &'arena Arena, reader: R, path: P) -> Self
130    where
131        R: io::Read,
132    {
133        Self {
134            sc: scan::Scanner::new(reader, path),
135            token: token::Token::Illegal,
136            arena,
137            anon_counter: 0,
138        }
139    }
140
141    pub fn next_token(&mut self) -> Result<()> {
142        self.token = self.sc.next_token()?;
143        Ok(())
144    }
145
146    // Check that token is the expected one and advance.
147    fn expect(&mut self, expected: Token) -> Result<()> {
148        if expected != self.token {
149            let error = ParseError::Unexpected(
150                self.sc.get_error_context(),
151                expected.clone(),
152                self.token.clone(),
153            );
154            return Err(anyhow!(error));
155        }
156        self.next_token()
157    }
158
159    pub fn parse_unit(&mut self) -> Result<&'arena ast::Unit<'arena>> {
160        let package = if matches!(self.token.clone(), Token::Package) {
161            self.parse_package_decl()?
162        } else {
163            self.arena.alloc(empty_package_decl(self.arena))
164        };
165        let mut decls = vec![package];
166        while let Token::Use = self.token {
167            decls.push(self.parse_use_decl()?);
168        }
169        let mut clauses = vec![];
170        loop {
171            match self.token {
172                Token::Eof => break,
173                Token::Decl => decls.push(self.parse_decl()?),
174                _ => clauses.push(self.parse_clause()?),
175            }
176        }
177        let decls: &'arena [&'arena ast::Decl<'arena>] = self.arena.alloc_slice_copy(&decls);
178        let clauses: &'arena [&'arena ast::Clause<'arena>] = self.arena.alloc_slice_copy(&clauses);
179        let unit: &'arena ast::Unit<'arena> = &*self.arena.alloc(ast::Unit { clauses, decls });
180        Ok(unit)
181    }
182
183    /// package_decl ::= `package` name (`[` `]`)? `!`
184    pub fn parse_package_decl(&mut self) -> Result<&'arena ast::Decl<'arena>> {
185        self.expect(Token::Package)?;
186        let package_name: &'arena str = if let Token::Ident { name } = &self.token {
187            self.arena.alloc_str(name.as_str())
188        } else {
189            bail!("expected identifer got {}", self.token);
190        };
191
192        let name_atom: &'arena ast::Atom<'arena> = self.arena.alloc(ast::Atom {
193            sym: name_sym(self.arena),
194            args: self.arena.alloc_slice_copy(&[self
195                .arena
196                .alloc(ast::BaseTerm::Const(ast::Const::String(package_name)))]),
197        });
198        let mut descr_atoms: Vec<&'arena ast::Atom<'arena>> = vec![name_atom];
199        self.next_token()?;
200        if Token::LBracket == self.token {
201            self.parse_bracket_atoms(&mut descr_atoms)?;
202        }
203        let descr = alloc_slice!(self, &descr_atoms);
204
205        self.expect(Token::Bang)?;
206
207        let package_atom = alloc!(
208            self,
209            ast::Atom {
210                sym: package_sym(self.arena),
211                args: &[]
212            }
213        );
214
215        //let descr_atoms = ;
216        let decl: &'arena ast::Decl = alloc!(
217            self,
218            ast::Decl {
219                atom: package_atom,
220                bounds: None,
221                descr,
222                constraints: None,
223                is_temporal: false,
224            }
225        );
226        Ok(decl)
227    }
228
229    fn parse_use_decl(&mut self) -> Result<&'arena ast::Decl<'arena>> {
230        self.expect(Token::Use)?;
231        let use_atom = alloc!(
232            self,
233            ast::Atom {
234                sym: use_sym(self.arena),
235                args: &[]
236            }
237        );
238
239        let name = match &self.token {
240            Token::Ident { name } => name.as_str(),
241            _ => bail!("parse_use_decl: expected identifer got {}", self.token),
242        };
243
244        let name: &'arena str = alloc_str!(self, name);
245        let name = alloc!(self, ast::BaseTerm::Const(ast::Const::String(name)));
246        let args = alloc_slice!(self, &[name]);
247
248        let mut descr_atoms: Vec<&ast::Atom> = vec![self.arena.alloc(ast::Atom {
249            sym: name_sym(self.arena),
250            args,
251        })];
252        self.next_token()?;
253        if Token::LBracket == self.token {
254            self.parse_bracket_atoms(&mut descr_atoms)?;
255        }
256        self.expect(Token::Bang)?;
257
258        let descr_atoms = alloc_slice!(self, &descr_atoms);
259        Ok(alloc!(
260            self,
261            ast::Decl {
262                atom: use_atom,
263                descr: descr_atoms,
264                bounds: None,
265                constraints: None,
266                is_temporal: false,
267            }
268        ))
269    }
270
271    fn parse_decl(&mut self) -> Result<&'arena ast::Decl<'arena>> {
272        self.expect(Token::Decl)?;
273        let atom = self.parse_atom()?;
274        // Check for `temporal` keyword
275        let is_temporal = match &self.token {
276            Token::Ident { name } if name == "temporal" => {
277                self.next_token()?;
278                true
279            }
280            _ => false,
281        };
282        let mut descr_atoms = vec![];
283        if Token::Descr == self.token {
284            self.next_token()?;
285            self.parse_bracket_atoms(&mut descr_atoms)?;
286        }
287        let mut bound_decls = vec![];
288        loop {
289            if Token::Bound != self.token {
290                break;
291            }
292            bound_decls.push(self.parse_bounds_decl()?);
293        }
294        let bounds = if bound_decls.is_empty() {
295            None
296        } else {
297            Some(alloc_slice!(self, &bound_decls))
298        };
299        let constraints = if Token::Inclusion == self.token {
300            Some(self.parse_inclusion_constraint()?)
301        } else {
302            None
303        };
304        self.expect(Token::Dot)?;
305        Ok(alloc!(
306            self,
307            ast::Decl {
308                atom,
309                descr: alloc_slice!(self, &descr_atoms),
310                bounds,
311                constraints,
312                is_temporal,
313            }
314        ))
315    }
316
317    /// bound_decl ::= `bound` `[` base_term {`,` base_term} `]`
318    fn parse_bounds_decl(&mut self) -> Result<&'arena ast::BoundDecl<'arena>> {
319        self.expect(Token::Bound)?;
320        self.expect(Token::LBracket)?;
321        let mut base_terms = vec![];
322        self.parse_base_terms(&mut base_terms)?;
323        self.expect(Token::RBracket)?;
324        let base_terms = alloc_slice!(self, &base_terms);
325        let bound_decl = alloc!(self, ast::BoundDecl { base_terms });
326        Ok(bound_decl)
327    }
328
329    fn parse_inclusion_constraint(&mut self) -> Result<&'arena ast::Constraints<'arena>> {
330        self.expect(Token::Inclusion)?;
331        let mut consequences = vec![];
332        self.parse_bracket_atoms(&mut consequences)?;
333        let consequences = alloc_slice!(self, &consequences);
334        Ok(alloc!(
335            self,
336            Constraints {
337                consequences,
338                alternatives: &[]
339            }
340        ))
341    }
342
343    pub fn parse_clause(&mut self) -> Result<&'arena ast::Clause<'arena>> {
344        let head = self.parse_atom()?;
345        let head_time = self.try_parse_interval()?;
346        let mut premises = vec![];
347        let mut transform = vec![];
348        match self.token {
349            Token::ColonDash | Token::LongLeftDoubleArrow => {
350                self.next_token()?;
351                self.parse_terms(&mut premises)?;
352                if let Token::PipeGt = self.token {
353                    self.next_token()?;
354                    self.parse_transforms(&mut transform)?;
355                }
356            }
357            _ => {}
358        }
359        self.expect(Token::Dot)?;
360        let premises = alloc_slice!(self, &premises);
361        let transform = alloc_slice!(self, &transform);
362        Ok(alloc!(
363            self,
364            ast::Clause {
365                head,
366                head_time,
367                premises,
368                transform,
369            }
370        ))
371    }
372
373    /// terms ::= term { , term }
374    fn parse_terms(&mut self, terms: &mut Vec<&'arena ast::Term<'arena>>) -> Result<()> {
375        terms.push(self.parse_term()?);
376        loop {
377            if Token::Comma != self.token {
378                return Ok(());
379            }
380            self.next_token()?;
381            terms.push(self.parse_term()?);
382        }
383    }
384
385    pub fn parse_term(&mut self) -> Result<&'arena ast::Term<'arena>> {
386        match &self.token {
387            Token::Bang => {
388                self.next_token()?;
389                let atom = self.parse_atom()?;
390                Ok(alloc!(self, ast::Term::NegAtom(atom)))
391            }
392            t if base_term_start(t) => {
393                let leading_var: Option<String> = match &self.token {
394                    Token::Ident { name } if is_variable(name) => Some(name.clone()),
395                    _ => None,
396                };
397                let left_base_term = self.parse_base_term()?;
398                let op = self.token.clone();
399                match op {
400                    Token::Eq | Token::BangEq | Token::Lt | Token::Le | Token::Gt | Token::Ge => {
401                        self.next_token()?
402                    }
403                    Token::LParen if leading_var.is_some() => {
404                        let name = leading_var.unwrap();
405                        bail!(
406                            "{}: `{}` starts with an uppercase letter and is therefore a variable; \
407                             predicate names must start with a lowercase letter. Did you mean `{}`?",
408                            self.sc.get_error_context(),
409                            name,
410                            lowercase_first(&name)
411                        );
412                    }
413                    _ => bail!(
414                        "parse_terms: expected comparison operator, got {}",
415                        self.token
416                    ),
417                };
418                let right_base_term = self.parse_base_term()?;
419                let term = match op {
420                    Token::Eq => ast::Term::Eq(left_base_term, right_base_term),
421                    Token::BangEq => ast::Term::Ineq(left_base_term, right_base_term),
422                    Token::Lt => ast::Term::Atom(alloc!(
423                        self,
424                        ast::Atom {
425                            sym: lt_sym(self.arena),
426                            args: alloc_slice!(self, &[left_base_term, right_base_term]),
427                        }
428                    )),
429                    Token::Le => ast::Term::Atom(self.arena.alloc(ast::Atom {
430                        sym: le_sym(self.arena),
431                        args: alloc_slice!(self, &[left_base_term, right_base_term]),
432                    })),
433                    Token::Gt => ast::Term::Atom(alloc!(
434                        self,
435                        ast::Atom {
436                            sym: gt_sym(self.arena),
437                            args: alloc_slice!(self, &[left_base_term, right_base_term]),
438                        }
439                    )),
440                    Token::Ge => ast::Term::Atom(self.arena.alloc(ast::Atom {
441                        sym: ge_sym(self.arena),
442                        args: alloc_slice!(self, &[left_base_term, right_base_term]),
443                    })),
444                    _ => unreachable!(),
445                };
446                Ok(alloc!(self, term))
447            }
448            Token::Ident { .. } => {
449                let atom = self.parse_atom()?;
450                if let Some(interval) = self.try_parse_interval()? {
451                    Ok(alloc!(self, ast::Term::TemporalAtom(atom, interval)))
452                } else {
453                    Ok(alloc!(self, ast::Term::Atom(atom)))
454                }
455            }
456            _ => bail!("parse_term: unexpected token {:?}", self.token),
457        }
458    }
459
460    // bracket_atoms ::= `[` [ atom {`,` atom } ] `]`
461    fn parse_bracket_atoms(&mut self, atoms: &mut Vec<&'arena ast::Atom<'arena>>) -> Result<()> {
462        self.expect(Token::LBracket)?;
463        self.parse_atoms(atoms)?;
464        self.expect(Token::RBracket)?;
465        Ok(())
466    }
467
468    // `atoms ::= [ atom {`,` atom } [`,`] ]
469    fn parse_atoms(&mut self, atoms: &mut Vec<&'arena ast::Atom<'arena>>) -> Result<()> {
470        if let Token::Ident { .. } = self.token {
471            atoms.push(self.parse_atom()?);
472            loop {
473                if Token::Comma != self.token {
474                    break;
475                }
476                self.next_token()?;
477                if !matches!(self.token, Token::Ident { .. }) {
478                    break; // trailing comma before `]`
479                }
480                atoms.push(self.parse_atom()?);
481            }
482        }
483        Ok(())
484    }
485
486    // atom ::= qualified_name `(` args `)`
487    // qualified_name ::= ident { `.` ident }
488    pub fn parse_atom(&mut self) -> Result<&'arena ast::Atom<'arena>> {
489        let mut name_buf = match &self.token {
490            Token::Ident { name } => {
491                if is_variable(name) {
492                    bail!(
493                        "{}: `{}` is not a valid predicate name: predicate names must start \
494                         with a lowercase letter (identifiers starting with an uppercase letter \
495                         are variables). Did you mean `{}`?",
496                        self.sc.get_error_context(),
497                        name,
498                        lowercase_first(name)
499                    );
500                }
501                name.clone()
502            }
503            _ => bail!("parse_atom: expected identifer got {}", self.token),
504        };
505
506        self.next_token()?;
507
508        // Handle qualified names: ident.ident.ident(...)
509        while self.token == Token::Dot {
510            // Peek ahead: if the next token is an Ident followed by something
511            // that continues the atom (Dot or LParen), consume the dot+ident.
512            // We need to speculatively consume the Dot.
513            self.next_token()?;
514            match &self.token {
515                Token::Ident { name: next_name } => {
516                    name_buf.push('.');
517                    name_buf.push_str(next_name);
518                    self.next_token()?;
519                }
520                _ => {
521                    // The dot was actually a clause terminator or something else.
522                    // We can't put the dot back, so this is an error in the
523                    // qualified-name context. However, this path shouldn't be
524                    // reached in practice because the parser calls parse_atom
525                    // only when it knows an atom follows.
526                    bail!(
527                        "parse_atom: expected identifier after `.` in qualified name, got {}",
528                        self.token
529                    );
530                }
531            }
532        }
533
534        let name = self.arena.alloc_str(&name_buf);
535
536        self.expect(Token::LParen)?;
537        let mut args = vec![];
538        if Token::RParen != self.token {
539            self.parse_base_terms(&mut args)?;
540        }
541        self.expect(Token::RParen)?;
542        let args = alloc_slice!(self, &args);
543        Ok(alloc!(
544            self,
545            ast::Atom {
546                sym: self.arena.predicate_sym(name, None),
547                args
548            }
549        ))
550    }
551
552    fn parse_transforms(
553        &mut self,
554        transforms: &mut Vec<&'arena ast::TransformStmt<'arena>>,
555    ) -> Result<()> {
556        // Transform clauses are separated by `,` (matching mangle-go) or `;`
557        // (legacy mangle-rs). Both accepted; one-of required between clauses.
558        if Token::Do == self.token {
559            self.next_token()?;
560            let expr = self.parse_base_term()?;
561            transforms.push(alloc!(
562                self,
563                ast::TransformStmt {
564                    var: None,
565                    app: expr
566                }
567            ));
568            self.expect_transform_sep()?;
569        }
570        loop {
571            if Token::Let != self.token {
572                break;
573            }
574            self.next_token()?;
575            if let Token::Ident { name } = &self.token {
576                let name = alloc_str!(self, name.as_str());
577                self.next_token()?;
578                self.expect(Token::Eq)?;
579                let expr = self.parse_base_term()?;
580                transforms.push(alloc!(
581                    self,
582                    ast::TransformStmt {
583                        var: Some(name),
584                        app: expr
585                    }
586                ))
587            }
588            if let Token::Dot = self.token {
589                break;
590            }
591            self.expect_transform_sep()?;
592        }
593        Ok(())
594    }
595
596    fn expect_transform_sep(&mut self) -> Result<()> {
597        match self.token {
598            Token::Comma | Token::Semi => self.next_token(),
599            _ => bail!(
600                "{}: expected `,` or `;` got {}",
601                self.sc.get_error_context(),
602                self.token
603            ),
604        }
605    }
606
607    // -----------------------------------------------------------------------
608    // Temporal interval parsing: @[bound] or @[bound, bound]
609    // -----------------------------------------------------------------------
610
611    /// Try to parse `@[...]` if the current token is `@`. Returns None otherwise.
612    fn try_parse_interval(&mut self) -> Result<Option<ast::Interval>> {
613        if self.token != Token::At {
614            return Ok(None);
615        }
616        self.next_token()?; // consume @
617        self.expect(Token::LBracket)?;
618        let start = self.parse_temporal_bound(true)?;
619        let end = if self.token == Token::Comma {
620            self.next_token()?;
621            self.parse_temporal_bound(false)?
622        } else {
623            // Point interval: @[T] means @[T, T]
624            start
625        };
626        self.expect(Token::RBracket)?;
627        Ok(Some(ast::Interval { start, end }))
628    }
629
630    /// Parse a single temporal bound: timestamp, variable, or `_` (infinity).
631    fn parse_temporal_bound(&mut self, is_start: bool) -> Result<ast::TemporalBound> {
632        match &self.token {
633            Token::Timestamp { nanos } => {
634                let nanos = *nanos;
635                self.next_token()?;
636                Ok(ast::TemporalBound::Timestamp(nanos))
637            }
638            Token::Ident { name } if name == "_" => {
639                self.next_token()?;
640                if is_start {
641                    Ok(ast::TemporalBound::NegInf)
642                } else {
643                    Ok(ast::TemporalBound::PosInf)
644                }
645            }
646            Token::Ident { name } if is_variable(name) => {
647                let var_idx = self.arena.variable_sym(name);
648                self.next_token()?;
649                Ok(ast::TemporalBound::Variable(var_idx))
650            }
651            _ => bail!(
652                "parse_temporal_bound: expected timestamp, variable, or '_', got {:?}",
653                self.token
654            ),
655        }
656    }
657
658    // -----------------------------------------------------------------------
659
660    // base_term ::= var
661    //             | fun`(`[base_term {',' base_term}`)`
662    //             | string_constant
663    //             | bytes_constant
664    //             | number_constant
665    //             | float_constant
666    //             | name_constant
667    pub fn parse_base_term(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
668        match &self.token {
669            Token::LBracket => return self.parse_list_or_map(),
670            Token::LBrace => return self.parse_struct(),
671            _ => {}
672        }
673
674        let mut is_type = false;
675        let mut base_term = match &self.token {
676            Token::Ident { name } if name == "_" => {
677                let unique = format!("_Anon{}", self.anon_counter);
678                self.anon_counter += 1;
679                ast::BaseTerm::Variable(self.arena.variable_sym(&unique))
680            }
681            Token::Ident { name } if is_variable(name) => {
682                ast::BaseTerm::Variable(self.arena.variable_sym(name))
683            }
684            Token::Ident { name } if is_fn(name) => {
685                let name = self.arena.alloc_str(name);
686                // Arguments parsed below.
687                ast::BaseTerm::ApplyFn(self.arena.function_sym(name, None), &[])
688            }
689            Token::DotIdent { name } => {
690                let name = self.arena.alloc_str(name);
691                is_type = true;
692                // Arguments parsed below.
693                ast::BaseTerm::ApplyFn(self.arena.function_sym(name, None), &[])
694            }
695            Token::String { decoded } => {
696                let value = self.arena.alloc_str(decoded.as_str());
697                ast::BaseTerm::Const(ast::Const::String(value))
698            }
699            Token::Bytes { decoded } => {
700                let value = self.arena.alloc_slice_copy(decoded);
701                ast::BaseTerm::Const(ast::Const::Bytes(value))
702            }
703            Token::Int { decoded } => ast::BaseTerm::Const(ast::Const::Number(*decoded)),
704            Token::Float { decoded } => ast::BaseTerm::Const(ast::Const::Float(*decoded)),
705            Token::Timestamp { nanos } => ast::BaseTerm::Const(ast::Const::Time(*nanos)),
706            Token::Duration { nanos } => ast::BaseTerm::Const(ast::Const::Duration(*nanos)),
707            Token::Name { name } => {
708                let name = self.arena.intern(name);
709                ast::BaseTerm::Const(ast::Const::Name(name))
710            }
711            _ => bail!("parse_base_term: unexpected token {:?}", self.token),
712        };
713        self.next_token()?;
714        if let ast::BaseTerm::ApplyFn(fn_sym, _) = base_term {
715            let mut fn_args = vec![];
716            if is_type {
717                self.parse_langle_base_terms(&mut fn_args)?;
718            } else {
719                self.parse_paren_base_terms(&mut fn_args)?;
720            }
721            let fn_args = self.arena.alloc_slice_copy(&fn_args);
722            base_term = ast::BaseTerm::ApplyFn(fn_sym, fn_args);
723        }
724        let base_term = alloc!(self, base_term);
725        Ok(base_term)
726    }
727
728    fn parse_list_or_map(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
729        self.expect(Token::LBracket)?;
730        if Token::RBracket == self.token {
731            self.next_token()?;
732            return Ok(alloc!(
733                self,
734                ast::BaseTerm::ApplyFn(fn_list_sym(self.arena), &[])
735            ));
736        }
737        let first = self.parse_base_term()?;
738        let expr = if Token::Colon != self.token {
739            let mut items = vec![first];
740            if Token::Comma == self.token {
741                self.next_token()?;
742                if Token::RBracket != self.token {
743                    self.parse_base_terms(&mut items)?;
744                }
745            }
746            // Otherwise it's a single-element list `[x]`; fall through to
747            // the closing `]` check below.
748            ast::BaseTerm::ApplyFn(fn_list_sym(self.arena), alloc_slice!(self, &items))
749        } else {
750            self.expect(Token::Colon)?; // is a map
751            let first_val = self.parse_base_term()?;
752            let mut items = vec![first, first_val];
753            loop {
754                if Token::Comma != self.token {
755                    break;
756                }
757                self.next_token()?;
758                if Token::RBracket == self.token {
759                    break; // trailing comma
760                }
761                items.push(self.parse_base_term()?);
762                self.expect(Token::Colon)?;
763                items.push(self.parse_base_term()?);
764            }
765            ast::BaseTerm::ApplyFn(fn_map_sym(self.arena), alloc_slice!(self, &items))
766        };
767        self.expect(Token::RBracket)?;
768        Ok(alloc!(self, expr))
769    }
770
771    fn parse_struct(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
772        self.expect(Token::LBrace)?;
773        if Token::RBrace == self.token {
774            self.next_token()?;
775            return Ok(alloc!(
776                self,
777                ast::BaseTerm::ApplyFn(fn_struct_sym(self.arena), &[])
778            ));
779        }
780        let mut items = vec![];
781        let name = self.parse_base_term()?;
782        if let ast::BaseTerm::Const(ast::Const::Name { .. }) = name {
783            items.push(name)
784        } else {
785            bail!("parse_base_term: expected name in struct expression {{ ... }} got {name:?}",);
786        }
787        self.expect(Token::Colon)?;
788        items.push(self.parse_base_term()?);
789        loop {
790            if Token::Comma != self.token {
791                break;
792            }
793            self.next_token()?;
794            if Token::RBrace == self.token {
795                break; // trailing comma
796            }
797            let name = self.parse_base_term()?;
798            if let ast::BaseTerm::Const(ast::Const::Name { .. }) = name {
799                items.push(name)
800            } else {
801                bail!("parse_base_term: expected name in struct expression {{ ... }} got {name:?}");
802            }
803            self.expect(Token::Colon)?;
804            items.push(self.parse_base_term()?);
805        }
806        self.expect(Token::RBrace)?;
807        Ok(alloc!(
808            self,
809            ast::BaseTerm::ApplyFn(fn_struct_sym(self.arena), alloc_slice!(self, &items))
810        ))
811    }
812
813    /// langle_members ::= `<` [member { `,` member } [`,`]] `>`
814    /// member        ::= base_term [`:` base_term]
815    ///                 | `opt` base_term `:` base_term
816    ///
817    /// When a member contains a colon, both base_terms are pushed (flattened).
818    /// This makes `.Struct</x : /number>` parse identically to `fn:Struct(/x, /number)`.
819    ///
820    /// An `opt` member collapses to a single `fn:opt(name, type)` base term, so
821    /// `.Struct</x : /number, opt /y : /string>` is equivalent to
822    /// `fn:Struct(/x, /number, fn:opt(/y, /string))`.
823    fn parse_langle_base_terms(
824        &mut self,
825        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
826    ) -> Result<()> {
827        self.expect(Token::Lt)?;
828        if Token::Gt == self.token {
829            self.next_token()?;
830            return Ok(());
831        }
832        self.parse_langle_member(base_terms)?;
833        while Token::Comma == self.token {
834            self.next_token()?;
835            if !base_term_start(&self.token) && !self.is_opt_keyword() {
836                break; // trailing comma
837            }
838            self.parse_langle_member(base_terms)?;
839        }
840        self.expect(Token::Gt)?;
841        Ok(())
842    }
843
844    fn is_opt_keyword(&self) -> bool {
845        matches!(&self.token, Token::Ident { name } if name == "opt")
846    }
847
848    fn parse_langle_member(
849        &mut self,
850        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
851    ) -> Result<()> {
852        if self.is_opt_keyword() {
853            self.next_token()?;
854            let name = self.parse_base_term()?;
855            self.expect(Token::Colon)?;
856            let ty = self.parse_base_term()?;
857            let opt = alloc!(
858                self,
859                ast::BaseTerm::ApplyFn(fn_opt_sym(self.arena), alloc_slice!(self, &[name, ty]))
860            );
861            base_terms.push(opt);
862            return Ok(());
863        }
864        base_terms.push(self.parse_base_term()?);
865        if Token::Colon == self.token {
866            self.next_token()?;
867            base_terms.push(self.parse_base_term()?);
868        }
869        Ok(())
870    }
871
872    /// paren_base_terms ::=  `(` [base_terms] `)`
873    fn parse_paren_base_terms(
874        &mut self,
875        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
876    ) -> Result<()> {
877        self.expect(Token::LParen)?;
878        if Token::RParen != self.token {
879            self.parse_base_terms(base_terms)?;
880        }
881        self.expect(Token::RParen)?;
882        Ok(())
883    }
884
885    /// base_terms ::= base_term { `,` base_term } [`,`]
886    fn parse_base_terms(
887        &mut self,
888        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
889    ) -> Result<()> {
890        base_terms.push(self.parse_base_term()?);
891        while let Token::Comma = self.token {
892            self.next_token()?;
893            if !base_term_start(&self.token) {
894                break; // trailing comma
895            }
896            base_terms.push(self.parse_base_term()?);
897        }
898
899        Ok(())
900    }
901}
902
903fn is_variable(name: &str) -> bool {
904    name.chars().next().unwrap().is_ascii_uppercase()
905}
906
907fn lowercase_first(name: &str) -> String {
908    let mut chars = name.chars();
909    match chars.next() {
910        Some(c) => c.to_ascii_lowercase().to_string() + chars.as_str(),
911        None => String::new(),
912    }
913}
914
915fn is_fn(name: &str) -> bool {
916    name.starts_with("fn:")
917}
918
919fn base_term_start(t: &Token) -> bool {
920    match t {
921        Token::Name { .. }
922        | Token::Int { .. }
923        | Token::Float { .. }
924        | Token::String { .. }
925        | Token::Bytes { .. }
926        | Token::Timestamp { .. }
927        | Token::Duration { .. }
928        | Token::LBracket
929        | Token::LBrace
930        | Token::DotIdent { .. } => true,
931        Token::Ident { name } => is_variable(name) || is_fn(name) || name == "_",
932        _ => false,
933    }
934}
935
936#[cfg(test)]
937mod test {
938
939    use super::*;
940    use googletest::prelude::{eq, gtest, verify_that};
941
942    fn make_parser<'arena>(
943        arena: &'arena Arena,
944        input: &'arena str,
945    ) -> Parser<'arena, &'arena [u8]> {
946        let mut p = Parser::new(arena, input.as_bytes(), "test");
947        p.next_token().unwrap();
948        p
949    }
950
951    #[test]
952    fn test_empty_unit() -> Result<()> {
953        let arena = Arena::new_with_global_interner();
954        let mut p = make_parser(&arena, "");
955        match p.parse_unit()? {
956            &ast::Unit { decls: &[pkg], .. } => {
957                assert_eq!(pkg, &empty_package_decl(&arena));
958            }
959            z => panic!("unexpected: {:?}", z),
960        }
961        Ok(())
962    }
963
964    #[test]
965    fn test_package_use() -> Result<()> {
966        let arena = Arena::new_with_global_interner();
967        let input = "Package foo[bar()]! Use baz[bar()]!";
968
969        let mut p = make_parser(&arena, input);
970        let unit = p.parse_unit()?;
971        match unit.decls {
972            &[
973                &ast::Decl {
974                    atom:
975                        &ast::Atom {
976                            sym: got_package_sym,
977                            ..
978                        },
979                    descr:
980                        &[
981                            &ast::Atom {
982                                sym: got_name_sym1,
983                                args: &[ast::BaseTerm::Const(ast::Const::String("foo"))],
984                            },
985                            &ast::Atom {
986                                sym: got_bar_sym1,
987                                args: &[],
988                            },
989                        ],
990                    ..
991                },
992                &ast::Decl {
993                    atom:
994                        &ast::Atom {
995                            sym: got_use_sym, ..
996                        },
997                    descr:
998                        &[
999                            &ast::Atom {
1000                                sym: got_name_sym2,
1001                                args: &[ast::BaseTerm::Const(ast::Const::String("baz"))],
1002                            },
1003                            &ast::Atom {
1004                                sym: got_bar_sym2,
1005                                args: &[],
1006                            },
1007                        ],
1008                    ..
1009                },
1010            ] => {
1011                assert_eq!(got_use_sym, use_sym(&arena));
1012                assert_eq!(got_package_sym, package_sym(&arena));
1013                assert_eq!(got_name_sym1, name_sym(&arena));
1014                assert_eq!(got_name_sym2, name_sym(&arena));
1015                assert_eq!(got_bar_sym1, arena.predicate_sym("bar", None));
1016                assert_eq!(got_bar_sym2, arena.predicate_sym("bar", None));
1017            }
1018            z => panic!("unexpected {z:?}"),
1019        }
1020        Ok(())
1021    }
1022
1023    #[test]
1024    fn test_decl() -> Result<()> {
1025        let arena = Arena::new_with_global_interner();
1026        let input = "Decl foo(X, Y).";
1027        let mut p = make_parser(&arena, input);
1028        match p.parse_decl()? {
1029            &ast::Decl {
1030                atom:
1031                    &ast::Atom {
1032                        sym: got_foo_sym,
1033                        args:
1034                            &[
1035                                &ast::BaseTerm::Variable(x_sym),
1036                                &ast::BaseTerm::Variable(y_sym),
1037                            ],
1038                    },
1039                ..
1040            } => {
1041                assert_eq!(got_foo_sym, arena.predicate_sym("foo", None));
1042                assert_eq!(x_sym, arena.variable_sym("X"));
1043                assert_eq!(y_sym, arena.variable_sym("Y"))
1044            }
1045            decl => panic!("got {:?}", decl),
1046        };
1047        Ok(())
1048    }
1049
1050    #[test]
1051    fn test_base_term() -> googletest::Result<()> {
1052        let arena = Arena::new_with_global_interner();
1053        let input = "X 3 1.5 'foo' /foo fn:list() fn:list(/a) fn:list(/a, 3)"; //.as_bytes();
1054        let mut p = make_parser(&arena, input);
1055        let mut got_base_terms = vec![];
1056        loop {
1057            if Token::Eof == p.token {
1058                break;
1059            }
1060            // TODO: "err_to_test_failure".
1061            let base_term = p.parse_base_term().unwrap();
1062            got_base_terms.push(base_term);
1063        }
1064        let expected = vec![
1065            arena.variable("X"),
1066            arena.const_(ast::Const::Number(3)),
1067            arena.const_(ast::Const::Float(1.5)),
1068            arena.const_(ast::Const::String("foo")),
1069            arena.const_(arena.name("/foo")),
1070            arena.apply_fn(fn_list_sym(&arena), &[]),
1071            arena.apply_fn(fn_list_sym(&arena), &[arena.const_(arena.name("/a"))]),
1072            arena.apply_fn(
1073                fn_list_sym(&arena),
1074                &[
1075                    arena.const_(arena.name("/a")),
1076                    arena.const_(ast::Const::Number(3)),
1077                ],
1078            ),
1079        ];
1080        verify_that!(got_base_terms, eq(&expected))
1081    }
1082
1083    #[test]
1084    fn test_term() -> googletest::Result<()> {
1085        let arena = Arena::new_with_global_interner();
1086        let input = "foo(/bar) !bar() X = Z X != 3 3 < 1 3 <= 1";
1087        let mut p = make_parser(&arena, input);
1088        let mut got_terms = vec![];
1089        loop {
1090            if Token::Eof == p.token {
1091                break;
1092            }
1093            // TODO: "err_to_test_failure".
1094            got_terms.push(p.parse_term().unwrap());
1095        }
1096        let expected = [
1097            &ast::Term::Atom(arena.atom(
1098                arena.predicate_sym("foo", None),
1099                &[arena.const_(arena.name("/bar"))],
1100            )),
1101            &ast::Term::NegAtom(arena.atom(arena.predicate_sym("bar", None), &[])),
1102            &ast::Term::Eq(arena.variable("X"), arena.variable("Z")),
1103            &ast::Term::Ineq(
1104                arena.variable("X"),
1105                arena.alloc(ast::BaseTerm::Const(ast::Const::Number(3))),
1106            ),
1107            &ast::Term::Atom(arena.atom(
1108                arena.predicate_sym(":lt", Some(2)),
1109                &[
1110                    arena.const_(ast::Const::Number(3)),
1111                    arena.const_(ast::Const::Number(1)),
1112                ],
1113            )),
1114            &ast::Term::Atom(arena.atom(
1115                arena.predicate_sym(":le", Some(2)),
1116                &[
1117                    arena.const_(ast::Const::Number(3)),
1118                    arena.const_(ast::Const::Number(1)),
1119                ],
1120            )),
1121        ];
1122        verify_that!(got_terms, eq(&expected))
1123    }
1124
1125    #[gtest]
1126    fn test_structured_data_and_types() -> googletest::Result<()> {
1127        let arena = Arena::new_with_global_interner();
1128        let input = "[] [1,2,3] [1: 'one', 2: 'two'] {} {/foo: /bar} {/name: \"alice\", /age: 30} .List<.Option</name>, /string>";
1129        let mut p = make_parser(&arena, input);
1130        let mut got_base_terms = vec![];
1131        loop {
1132            if Token::Eof == p.token {
1133                break;
1134            }
1135            // TODO: "err_to_test_failure".
1136            let base_term = p.parse_base_term().unwrap();
1137            got_base_terms.push(base_term);
1138        }
1139        let expected = vec![
1140            arena.apply_fn(fn_list_sym(&arena), &[]),
1141            arena.apply_fn(
1142                fn_list_sym(&arena),
1143                &[
1144                    arena.const_(ast::Const::Number(1)),
1145                    arena.const_(ast::Const::Number(2)),
1146                    arena.const_(ast::Const::Number(3)),
1147                ],
1148            ),
1149            arena.apply_fn(
1150                fn_map_sym(&arena),
1151                &[
1152                    arena.const_(ast::Const::Number(1)),
1153                    arena.const_(ast::Const::String("one")),
1154                    arena.const_(ast::Const::Number(2)),
1155                    arena.const_(ast::Const::String("two")),
1156                ],
1157            ),
1158            arena.apply_fn(fn_struct_sym(&arena), &[]),
1159            arena.apply_fn(
1160                fn_struct_sym(&arena),
1161                &[
1162                    arena.const_(arena.name("/foo")),
1163                    arena.const_(arena.name("/bar")),
1164                ],
1165            ),
1166            arena.apply_fn(
1167                fn_struct_sym(&arena),
1168                &[
1169                    arena.const_(arena.name("/name")),
1170                    arena.const_(ast::Const::String("alice")),
1171                    arena.const_(arena.name("/age")),
1172                    arena.const_(ast::Const::Number(30)),
1173                ],
1174            ),
1175            arena.apply_fn(
1176                fn_list_type_sym(&arena),
1177                &[
1178                    arena.apply_fn(
1179                        fn_option_type_sym(&arena),
1180                        &[arena.const_(arena.name("/name"))],
1181                    ),
1182                    arena.const_(arena.name("/string")),
1183                ],
1184            ),
1185        ];
1186        verify_that!(got_base_terms, eq(&expected))
1187    }
1188
1189    #[gtest]
1190    fn test_trailing_commas() -> googletest::Result<()> {
1191        let arena = Arena::new_with_global_interner();
1192        let input = "[1, 2, 3,] [1: 'one', 2: 'two',] {/a: 1, /b: 2,}";
1193        let mut p = make_parser(&arena, input);
1194        let mut got_base_terms = vec![];
1195        loop {
1196            if Token::Eof == p.token {
1197                break;
1198            }
1199            let base_term = p.parse_base_term().unwrap();
1200            got_base_terms.push(base_term);
1201        }
1202        let expected = vec![
1203            arena.apply_fn(
1204                fn_list_sym(&arena),
1205                &[
1206                    arena.const_(ast::Const::Number(1)),
1207                    arena.const_(ast::Const::Number(2)),
1208                    arena.const_(ast::Const::Number(3)),
1209                ],
1210            ),
1211            arena.apply_fn(
1212                fn_map_sym(&arena),
1213                &[
1214                    arena.const_(ast::Const::Number(1)),
1215                    arena.const_(ast::Const::String("one")),
1216                    arena.const_(ast::Const::Number(2)),
1217                    arena.const_(ast::Const::String("two")),
1218                ],
1219            ),
1220            arena.apply_fn(
1221                fn_struct_sym(&arena),
1222                &[
1223                    arena.const_(arena.name("/a")),
1224                    arena.const_(ast::Const::Number(1)),
1225                    arena.const_(arena.name("/b")),
1226                    arena.const_(ast::Const::Number(2)),
1227                ],
1228            ),
1229        ];
1230        verify_that!(got_base_terms, eq(&expected))
1231    }
1232
1233    #[test]
1234    fn test_single_element_list() -> googletest::Result<()> {
1235        let arena = Arena::new_with_global_interner();
1236        let mut p = make_parser(&arena, "[42]");
1237        let got = p.parse_base_term().unwrap();
1238        let expected = arena.apply_fn(fn_list_sym(&arena), &[arena.const_(ast::Const::Number(42))]);
1239        verify_that!(got, eq(expected))
1240    }
1241
1242    #[test]
1243    fn test_descr_trailing_comma() -> googletest::Result<()> {
1244        // `Decl foo descr [ bar(), ]` — trailing comma after an atom list.
1245        let arena = Arena::new_with_global_interner();
1246        let mut p = make_parser(&arena, "Decl foo(X) descr [ bar(), ].");
1247        p.parse_decl()
1248            .expect("descr list with trailing comma parses");
1249        Ok(())
1250    }
1251
1252    #[test]
1253    fn test_opt_in_struct_type() -> googletest::Result<()> {
1254        // `opt NAME : TYPE` inside .Struct<...> collapses to a single
1255        // fn:opt(NAME, TYPE) base term so it's distinguishable from a
1256        // required field (which is two flattened args).
1257        let arena = Arena::new_with_global_interner();
1258        let mut p = make_parser(&arena, ".Struct</x : /number, opt /y : /string>");
1259        let got = p.parse_base_term().unwrap();
1260        let opt_inner = arena.apply_fn(
1261            fn_opt_sym(&arena),
1262            &[
1263                arena.const_(arena.name("/y")),
1264                arena.const_(arena.name("/string")),
1265            ],
1266        );
1267        let expected = arena.apply_fn(
1268            arena.function_sym("fn:Struct", None),
1269            &[
1270                arena.const_(arena.name("/x")),
1271                arena.const_(arena.name("/number")),
1272                opt_inner,
1273            ],
1274        );
1275        verify_that!(got, eq(expected))
1276    }
1277
1278    #[test]
1279    fn test_transform_sep_comma_and_semi() -> googletest::Result<()> {
1280        // Transform clauses may be separated by `,` (mangle-go) or `;`
1281        // (legacy mangle-rs). Both must parse identically.
1282        let arena = Arena::new_with_global_interner();
1283        let src_comma = "q(K, S) :- p(K, V) |> do fn:group_by(K), let S = fn:sum(V).";
1284        let src_semi = "q(K, S) :- p(K, V) |> do fn:group_by(K); let S = fn:sum(V).";
1285        let a = make_parser(&arena, src_comma).parse_clause().unwrap();
1286        let b = make_parser(&arena, src_semi).parse_clause().unwrap();
1287        verify_that!(a.transform.len(), eq(2))?;
1288        verify_that!(b.transform.len(), eq(2))
1289    }
1290
1291    #[test]
1292    fn test_clause() -> Result<()> {
1293        let arena = Arena::new_with_global_interner();
1294        let mut p = make_parser(&arena, "foo(X).");
1295        let clause = p.parse_clause()?;
1296        match clause {
1297            &ast::Clause {
1298                head:
1299                    &ast::Atom {
1300                        args: &[ast::BaseTerm::Variable(x_sym)],
1301                        ..
1302                    },
1303                premises: &[],
1304                transform: &[],
1305                ..
1306            } => {
1307                assert_eq!(*x_sym, arena.variable_sym("X"));
1308                assert_eq!(clause.head.sym, arena.predicate_sym("foo", None));
1309            }
1310            _ => panic!("unexpected: {:?}", clause),
1311        }
1312        let mut p = make_parser(&arena, "foo(X) :- !bar(X).");
1313        let clause = p.parse_clause()?;
1314        match clause {
1315            &ast::Clause {
1316                head:
1317                    &ast::Atom {
1318                        sym: foo_sym,
1319                        args: _,
1320                    },
1321                premises:
1322                    &[
1323                        &ast::Term::NegAtom(&ast::Atom {
1324                            sym: bar_sym,
1325                            args: _,
1326                        }),
1327                    ],
1328                transform: &[],
1329                ..
1330            } => {
1331                assert_eq!(foo_sym, arena.predicate_sym("foo", None));
1332                assert_eq!(bar_sym, arena.predicate_sym("bar", None));
1333            }
1334            _ => panic!("unexpected: {:?}", clause),
1335        };
1336        let mut p = make_parser(
1337            &arena,
1338            "foo(Z) ⟸ bar(Y) |> do fn:group_by(); let X = fn:count(Y).",
1339        );
1340
1341        let clause = p.parse_clause()?;
1342        match clause {
1343            &ast::Clause {
1344                head: &ast::Atom { .. },
1345                premises: &[&ast::Term::Atom(ast::Atom { .. })],
1346                transform:
1347                    &[
1348                        &ast::TransformStmt {
1349                            var: None,
1350                            app: ast::BaseTerm::ApplyFn(first_sym, _),
1351                        },
1352                        &ast::TransformStmt {
1353                            var: Some("X"),
1354                            app: ast::BaseTerm::ApplyFn(second_sym, _),
1355                        },
1356                    ],
1357                ..
1358            } => {
1359                assert_eq!(clause.head.sym, arena.predicate_sym("foo", None));
1360                assert_eq!(clause.transform.len(), 2);
1361                assert_eq!(*first_sym, arena.function_sym("fn:group_by", None));
1362                assert_eq!(*second_sym, arena.function_sym("fn:count", None));
1363            }
1364            _ => panic!("unexpected: {:?}", clause),
1365        }
1366
1367        Ok(())
1368    }
1369
1370    #[test]
1371    fn test_anonymous_variable_single() -> Result<()> {
1372        let arena = Arena::new_with_global_interner();
1373        let mut p = make_parser(&arena, "foo(_, X).");
1374        let clause = p.parse_clause()?;
1375        // The `_` should parse as a variable with a generated name `_Anon0`
1376        match clause.head.args {
1377            &[&ast::BaseTerm::Variable(anon), &ast::BaseTerm::Variable(x)] => {
1378                assert_eq!(anon, arena.variable_sym("_Anon0"));
1379                assert_eq!(x, arena.variable_sym("X"));
1380            }
1381            _ => panic!("unexpected args: {:?}", clause.head.args),
1382        }
1383        Ok(())
1384    }
1385
1386    #[test]
1387    fn test_anonymous_variable_multiple_distinct() -> Result<()> {
1388        let arena = Arena::new_with_global_interner();
1389        let mut p = make_parser(&arena, "foo(_, _, _).");
1390        let clause = p.parse_clause()?;
1391        // Each `_` should produce a distinct variable name
1392        match clause.head.args {
1393            &[
1394                &ast::BaseTerm::Variable(a0),
1395                &ast::BaseTerm::Variable(a1),
1396                &ast::BaseTerm::Variable(a2),
1397            ] => {
1398                assert_eq!(a0, arena.variable_sym("_Anon0"));
1399                assert_eq!(a1, arena.variable_sym("_Anon1"));
1400                assert_eq!(a2, arena.variable_sym("_Anon2"));
1401                // All three must be distinct
1402                assert_ne!(a0, a1);
1403                assert_ne!(a1, a2);
1404            }
1405            _ => panic!("unexpected args: {:?}", clause.head.args),
1406        }
1407        Ok(())
1408    }
1409
1410    #[test]
1411    fn test_anonymous_variable_in_rule_body() -> Result<()> {
1412        let arena = Arena::new_with_global_interner();
1413        let mut p = make_parser(&arena, "result(X) :- foo(X, _).");
1414        let clause = p.parse_clause()?;
1415        assert_eq!(clause.head.sym, arena.predicate_sym("result", None));
1416        match clause.premises {
1417            &[&ast::Term::Atom(&ast::Atom { args, .. })] => match args {
1418                &[&ast::BaseTerm::Variable(x), &ast::BaseTerm::Variable(anon)] => {
1419                    assert_eq!(x, arena.variable_sym("X"));
1420                    assert_eq!(anon, arena.variable_sym("_Anon0"));
1421                }
1422                _ => panic!("unexpected args: {:?}", args),
1423            },
1424            _ => panic!("unexpected premises: {:?}", clause.premises),
1425        }
1426        Ok(())
1427    }
1428
1429    #[test]
1430    fn test_anonymous_variable_with_negation() -> Result<()> {
1431        let arena = Arena::new_with_global_interner();
1432        let mut p = make_parser(&arena, "orphan(X) :- node(X, _), !has_parent(X).");
1433        let clause = p.parse_clause()?;
1434        assert_eq!(clause.head.sym, arena.predicate_sym("orphan", None));
1435        assert_eq!(clause.premises.len(), 2);
1436        // First premise: node(X, _)
1437        match clause.premises[0] {
1438            &ast::Term::Atom(&ast::Atom { args, .. }) => match args {
1439                &[&ast::BaseTerm::Variable(_), &ast::BaseTerm::Variable(anon)] => {
1440                    assert_eq!(anon, arena.variable_sym("_Anon0"));
1441                }
1442                _ => panic!("unexpected args: {:?}", args),
1443            },
1444            _ => panic!("expected Atom, got {:?}", clause.premises[0]),
1445        }
1446        // Second premise: !has_parent(X)
1447        match clause.premises[1] {
1448            &ast::Term::NegAtom(&ast::Atom { sym, .. }) => {
1449                assert_eq!(sym, arena.predicate_sym("has_parent", None));
1450            }
1451            _ => panic!("expected NegAtom, got {:?}", clause.premises[1]),
1452        }
1453        Ok(())
1454    }
1455
1456    #[test]
1457    fn test_uppercase_predicate_name_rejected_at_head() {
1458        let arena = Arena::new_with_global_interner();
1459        let mut p = make_parser(&arena, "Foo(1, 2).");
1460        let err = p.parse_clause().unwrap_err().to_string();
1461        assert!(
1462            err.contains("`Foo` is not a valid predicate name")
1463                && err.contains("Did you mean `foo`?"),
1464            "unexpected error: {err}"
1465        );
1466    }
1467
1468    #[test]
1469    fn test_uppercase_predicate_name_rejected_in_body() {
1470        let arena = Arena::new_with_global_interner();
1471        let mut p = make_parser(&arena, "bar(X) :- Foo(X, Y).");
1472        let err = p.parse_clause().unwrap_err().to_string();
1473        assert!(
1474            err.contains("`Foo` starts with an uppercase letter")
1475                && err.contains("Did you mean `foo`?"),
1476            "unexpected error: {err}"
1477        );
1478    }
1479
1480    // -----------------------------------------------------------------------
1481    // Temporal parsing tests (ported from Go temporal_integration_test.go)
1482    // -----------------------------------------------------------------------
1483
1484    /// Go: TestIntegration_TemporalFactParsing - simple temporal fact
1485    #[test]
1486    fn test_temporal_fact_with_interval() -> Result<()> {
1487        let arena = Arena::new_with_global_interner();
1488        let mut p = make_parser(&arena, "foo(/bar)@[2024-01-15, 2024-06-30].");
1489        let clause = p.parse_clause()?;
1490        assert!(clause.head_time.is_some(), "expected temporal annotation");
1491        let interval = clause.head_time.unwrap();
1492        match interval.start {
1493            ast::TemporalBound::Timestamp(_) => {}
1494            _ => panic!("expected Timestamp start, got {:?}", interval.start),
1495        }
1496        match interval.end {
1497            ast::TemporalBound::Timestamp(_) => {}
1498            _ => panic!("expected Timestamp end, got {:?}", interval.end),
1499        }
1500        Ok(())
1501    }
1502
1503    /// Go: TestIntegration_TemporalFactParsing - point interval fact
1504    #[test]
1505    fn test_temporal_fact_point_interval() -> Result<()> {
1506        let arena = Arena::new_with_global_interner();
1507        let mut p = make_parser(&arena, "event(/login)@[2024-03-15].");
1508        let clause = p.parse_clause()?;
1509        assert!(clause.head_time.is_some(), "expected temporal annotation");
1510        let interval = clause.head_time.unwrap();
1511        // Point interval: start == end
1512        assert_eq!(interval.start, interval.end);
1513        Ok(())
1514    }
1515
1516    /// Go: TestIntegration_TemporalFactParsing - non-temporal fact
1517    #[test]
1518    fn test_non_temporal_fact() -> Result<()> {
1519        let arena = Arena::new_with_global_interner();
1520        let mut p = make_parser(&arena, "regular(/fact).");
1521        let clause = p.parse_clause()?;
1522        assert!(
1523            clause.head_time.is_none(),
1524            "non-temporal fact should have no annotation"
1525        );
1526        Ok(())
1527    }
1528
1529    /// Go: TestIntegration_TemporalDeclarations - temporal predicate declaration
1530    #[test]
1531    fn test_temporal_declaration() -> Result<()> {
1532        let arena = Arena::new_with_global_interner();
1533        let mut p = make_parser(&arena, "Decl employee(X) temporal bound [/name].");
1534        let unit = p.parse_unit()?;
1535        // decls[0] is the implicit empty Package decl
1536        assert_eq!(unit.decls.len(), 2);
1537        assert!(unit.decls[1].is_temporal, "expected temporal declaration");
1538        Ok(())
1539    }
1540
1541    /// Go: TestIntegration_TemporalDeclarations - non-temporal predicate declaration
1542    #[test]
1543    fn test_non_temporal_declaration() -> Result<()> {
1544        let arena = Arena::new_with_global_interner();
1545        let mut p = make_parser(&arena, "Decl config(X) bound [/string].");
1546        let unit = p.parse_unit()?;
1547        assert_eq!(unit.decls.len(), 2);
1548        assert!(
1549            !unit.decls[1].is_temporal,
1550            "expected non-temporal declaration"
1551        );
1552        Ok(())
1553    }
1554
1555    /// Go: TestIntegration_TemporalDeclarations - temporal with documentation
1556    #[test]
1557    fn test_temporal_declaration_with_descr() -> Result<()> {
1558        let arena = Arena::new_with_global_interner();
1559        let input = r#"Decl status(X, Y) temporal
1560            descr [doc("Employee status over time")]
1561            bound [/name, /string]."#;
1562        let mut p = make_parser(&arena, input);
1563        let unit = p.parse_unit()?;
1564        assert_eq!(unit.decls.len(), 2);
1565        assert!(unit.decls[1].is_temporal, "expected temporal declaration");
1566        Ok(())
1567    }
1568
1569    /// Go: TestIntegration_BackwardCompatibility - non-temporal programs still work
1570    #[test]
1571    fn test_backward_compat_no_temporal() -> Result<()> {
1572        // Each program must be a valid unit. Test that no clauses get temporal annotations.
1573        let programs = [
1574            "edge(/a, /b). path(X, Y) :- edge(X, Y).",
1575            "all(/a). excluded(/a). included(X) :- all(X), !excluded(X).",
1576            "age(/alice, 30). adult(Name) :- age(Name, Age), Age >= 18 .",
1577        ];
1578        for prog in &programs {
1579            let arena = Arena::new_with_global_interner();
1580            let mut p = make_parser(&arena, prog);
1581            let unit = p.parse_unit()?;
1582            for clause in unit.clauses {
1583                assert!(
1584                    clause.head_time.is_none(),
1585                    "clause should not have temporal annotation in: {prog}"
1586                );
1587            }
1588        }
1589        Ok(())
1590    }
1591
1592    /// Temporal rule with variable interval in head and body
1593    #[test]
1594    fn test_temporal_rule_with_variable_interval() -> Result<()> {
1595        let arena = Arena::new_with_global_interner();
1596        let mut p = make_parser(&arena, "reachable(X, Y)@[T] :- link(X, Y)@[T].");
1597        let clause = p.parse_clause()?;
1598        // Head has temporal annotation
1599        assert!(clause.head_time.is_some());
1600        let interval = clause.head_time.unwrap();
1601        match interval.start {
1602            ast::TemporalBound::Variable(_) => {}
1603            _ => panic!("expected Variable start, got {:?}", interval.start),
1604        }
1605        // Point interval: start == end
1606        assert_eq!(interval.start, interval.end);
1607        // Body premise is a TemporalAtom
1608        assert_eq!(clause.premises.len(), 1);
1609        match clause.premises[0] {
1610            ast::Term::TemporalAtom(_, _) => {}
1611            _ => panic!("expected TemporalAtom, got {:?}", clause.premises[0]),
1612        }
1613        Ok(())
1614    }
1615
1616    /// Temporal rule with interval range [S, E] variables
1617    #[test]
1618    fn test_temporal_rule_with_interval_range() -> Result<()> {
1619        let arena = Arena::new_with_global_interner();
1620        let mut p = make_parser(&arena, "reachable(X, Y)@[S, E] :- link(X, Y)@[S, E].");
1621        let clause = p.parse_clause()?;
1622        let interval = clause.head_time.unwrap();
1623        match interval.start {
1624            ast::TemporalBound::Variable(v) => {
1625                assert_eq!(arena.lookup_name(v.0).unwrap(), "S");
1626            }
1627            _ => panic!("expected Variable start"),
1628        }
1629        match interval.end {
1630            ast::TemporalBound::Variable(v) => {
1631                assert_eq!(arena.lookup_name(v.0).unwrap(), "E");
1632            }
1633            _ => panic!("expected Variable end"),
1634        }
1635        Ok(())
1636    }
1637
1638    /// Wildcard bounds: @[_, _] means eternal interval
1639    #[test]
1640    fn test_temporal_wildcard_bounds() -> Result<()> {
1641        let arena = Arena::new_with_global_interner();
1642        let mut p = make_parser(&arena, "always(/true)@[_, _].");
1643        let clause = p.parse_clause()?;
1644        let interval = clause.head_time.unwrap();
1645        assert_eq!(interval.start, ast::TemporalBound::NegInf);
1646        assert_eq!(interval.end, ast::TemporalBound::PosInf);
1647        Ok(())
1648    }
1649
1650    /// Colon syntax in angle brackets: `.Struct</x : /number, /y : /string>`
1651    /// should parse to `ApplyFn("fn:Struct", [/x, /number, /y, /string])`.
1652    #[gtest]
1653    fn test_colon_syntax_in_angle_brackets() -> googletest::Result<()> {
1654        let arena = Arena::new_with_global_interner();
1655        let input = ".Struct</x : /number, /y : /string>";
1656        let mut p = make_parser(&arena, input);
1657        let got = p.parse_base_term().unwrap();
1658
1659        // DotIdent `.Struct` produces PascalCase `fn:Struct` (type constructor).
1660        let struct_type_sym = arena.function_sym("fn:Struct", None);
1661        let expected = arena.apply_fn(
1662            struct_type_sym,
1663            &[
1664                arena.const_(arena.name("/x")),
1665                arena.const_(arena.name("/number")),
1666                arena.const_(arena.name("/y")),
1667                arena.const_(arena.name("/string")),
1668            ],
1669        );
1670        verify_that!(got, eq(expected))
1671    }
1672
1673    /// Colon syntax for TaggedUnion.
1674    #[gtest]
1675    fn test_tagged_union_colon_syntax() -> googletest::Result<()> {
1676        let arena = Arena::new_with_global_interner();
1677        let input = ".TaggedUnion</kind, /move : .Struct</x : /number>, /quit : .Struct<>>";
1678        let mut p = make_parser(&arena, input);
1679        let got = p.parse_base_term().unwrap();
1680
1681        let tu_sym = arena.function_sym("fn:TaggedUnion", None);
1682        let struct_sym = arena.function_sym("fn:Struct", None);
1683        let expected = arena.apply_fn(
1684            tu_sym,
1685            &[
1686                arena.const_(arena.name("/kind")),
1687                arena.const_(arena.name("/move")),
1688                arena.apply_fn(
1689                    struct_sym,
1690                    &[
1691                        arena.const_(arena.name("/x")),
1692                        arena.const_(arena.name("/number")),
1693                    ],
1694                ),
1695                arena.const_(arena.name("/quit")),
1696                arena.apply_fn(struct_sym, &[]),
1697            ],
1698        );
1699        verify_that!(got, eq(expected))
1700    }
1701
1702    /// Mixed: some members with colon, some without.
1703    #[gtest]
1704    fn test_mixed_colon_syntax() -> googletest::Result<()> {
1705        let arena = Arena::new_with_global_interner();
1706        // `.List</number>` — no colons.
1707        let input = ".List</number>";
1708        let mut p = make_parser(&arena, input);
1709        let got = p.parse_base_term().unwrap();
1710
1711        let list_sym = arena.function_sym("fn:List", None);
1712        let expected = arena.apply_fn(list_sym, &[arena.const_(arena.name("/number"))]);
1713        verify_that!(got, eq(expected))
1714    }
1715
1716    /// Paren syntax `fn:Struct(...)` still works unchanged.
1717    /// Note: `fn:Struct` via `fn:` prefix (Ident) produces the same symbol
1718    /// as `.Struct` via DotIdent — both become `fn:Struct`.
1719    #[gtest]
1720    fn test_paren_syntax_unchanged() -> googletest::Result<()> {
1721        let arena = Arena::new_with_global_interner();
1722        let input = "fn:Struct(/x, /number, /y, /string)";
1723        let mut p = make_parser(&arena, input);
1724        let got = p.parse_base_term().unwrap();
1725
1726        let struct_type_sym = arena.function_sym("fn:Struct", None);
1727        let expected = arena.apply_fn(
1728            struct_type_sym,
1729            &[
1730                arena.const_(arena.name("/x")),
1731                arena.const_(arena.name("/number")),
1732                arena.const_(arena.name("/y")),
1733                arena.const_(arena.name("/string")),
1734            ],
1735        );
1736        verify_that!(got, eq(expected))
1737    }
1738
1739    /// Trailing comma in colon syntax.
1740    #[gtest]
1741    fn test_colon_syntax_trailing_comma() -> googletest::Result<()> {
1742        let arena = Arena::new_with_global_interner();
1743        let input = ".Struct</x : /number,>";
1744        let mut p = make_parser(&arena, input);
1745        let got = p.parse_base_term().unwrap();
1746
1747        let struct_type_sym = arena.function_sym("fn:Struct", None);
1748        let expected = arena.apply_fn(
1749            struct_type_sym,
1750            &[
1751                arena.const_(arena.name("/x")),
1752                arena.const_(arena.name("/number")),
1753            ],
1754        );
1755        verify_that!(got, eq(expected))
1756    }
1757}