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 => self.next_token()?,
401                    Token::LParen if leading_var.is_some() => {
402                        let name = leading_var.unwrap();
403                        bail!(
404                            "{}: `{}` starts with an uppercase letter and is therefore a variable; \
405                             predicate names must start with a lowercase letter. Did you mean `{}`?",
406                            self.sc.get_error_context(),
407                            name,
408                            lowercase_first(&name)
409                        );
410                    }
411                    _ => bail!("parse_terms: expected comparison operator, got {}", self.token),
412                };
413                let right_base_term = self.parse_base_term()?;
414                let term = match op {
415                    Token::Eq => ast::Term::Eq(left_base_term, right_base_term),
416                    Token::BangEq => ast::Term::Ineq(left_base_term, right_base_term),
417                    Token::Lt => ast::Term::Atom(alloc!(
418                        self,
419                        ast::Atom {
420                            sym: lt_sym(self.arena),
421                            args: alloc_slice!(self, &[left_base_term, right_base_term]),
422                        }
423                    )),
424                    Token::Le => ast::Term::Atom(self.arena.alloc(ast::Atom {
425                        sym: le_sym(self.arena),
426                        args: alloc_slice!(self, &[left_base_term, right_base_term]),
427                    })),
428                    Token::Gt => ast::Term::Atom(alloc!(
429                        self,
430                        ast::Atom {
431                            sym: gt_sym(self.arena),
432                            args: alloc_slice!(self, &[left_base_term, right_base_term]),
433                        }
434                    )),
435                    Token::Ge => ast::Term::Atom(self.arena.alloc(ast::Atom {
436                        sym: ge_sym(self.arena),
437                        args: alloc_slice!(self, &[left_base_term, right_base_term]),
438                    })),
439                    _ => unreachable!(),
440                };
441                Ok(alloc!(self, term))
442            }
443            Token::Ident { .. } => {
444                let atom = self.parse_atom()?;
445                if let Some(interval) = self.try_parse_interval()? {
446                    Ok(alloc!(self, ast::Term::TemporalAtom(atom, interval)))
447                } else {
448                    Ok(alloc!(self, ast::Term::Atom(atom)))
449                }
450            }
451            _ => bail!("parse_term: unexpected token {:?}", self.token),
452        }
453    }
454
455    // bracket_atoms ::= `[` [ atom {`,` atom } ] `]`
456    fn parse_bracket_atoms(&mut self, atoms: &mut Vec<&'arena ast::Atom<'arena>>) -> Result<()> {
457        self.expect(Token::LBracket)?;
458        self.parse_atoms(atoms)?;
459        self.expect(Token::RBracket)?;
460        Ok(())
461    }
462
463    // `atoms ::= [ atom {`,` atom } [`,`] ]
464    fn parse_atoms(&mut self, atoms: &mut Vec<&'arena ast::Atom<'arena>>) -> Result<()> {
465        if let Token::Ident { .. } = self.token {
466            atoms.push(self.parse_atom()?);
467            loop {
468                if Token::Comma != self.token {
469                    break;
470                }
471                self.next_token()?;
472                if !matches!(self.token, Token::Ident { .. }) {
473                    break; // trailing comma before `]`
474                }
475                atoms.push(self.parse_atom()?);
476            }
477        }
478        Ok(())
479    }
480
481    // atom ::= qualified_name `(` args `)`
482    // qualified_name ::= ident { `.` ident }
483    pub fn parse_atom(&mut self) -> Result<&'arena ast::Atom<'arena>> {
484        let mut name_buf = match &self.token {
485            Token::Ident { name } => {
486                if is_variable(name) {
487                    bail!(
488                        "{}: `{}` is not a valid predicate name: predicate names must start \
489                         with a lowercase letter (identifiers starting with an uppercase letter \
490                         are variables). Did you mean `{}`?",
491                        self.sc.get_error_context(),
492                        name,
493                        lowercase_first(name)
494                    );
495                }
496                name.clone()
497            }
498            _ => bail!("parse_atom: expected identifer got {}", self.token),
499        };
500
501        self.next_token()?;
502
503        // Handle qualified names: ident.ident.ident(...)
504        while self.token == Token::Dot {
505            // Peek ahead: if the next token is an Ident followed by something
506            // that continues the atom (Dot or LParen), consume the dot+ident.
507            // We need to speculatively consume the Dot.
508            self.next_token()?;
509            match &self.token {
510                Token::Ident { name: next_name } => {
511                    name_buf.push('.');
512                    name_buf.push_str(next_name);
513                    self.next_token()?;
514                }
515                _ => {
516                    // The dot was actually a clause terminator or something else.
517                    // We can't put the dot back, so this is an error in the
518                    // qualified-name context. However, this path shouldn't be
519                    // reached in practice because the parser calls parse_atom
520                    // only when it knows an atom follows.
521                    bail!(
522                        "parse_atom: expected identifier after `.` in qualified name, got {}",
523                        self.token
524                    );
525                }
526            }
527        }
528
529        let name = self.arena.alloc_str(&name_buf);
530
531        self.expect(Token::LParen)?;
532        let mut args = vec![];
533        if Token::RParen != self.token {
534            self.parse_base_terms(&mut args)?;
535        }
536        self.expect(Token::RParen)?;
537        let args = alloc_slice!(self, &args);
538        Ok(alloc!(
539            self,
540            ast::Atom {
541                sym: self.arena.predicate_sym(name, None),
542                args
543            }
544        ))
545    }
546
547    fn parse_transforms(
548        &mut self,
549        transforms: &mut Vec<&'arena ast::TransformStmt<'arena>>,
550    ) -> Result<()> {
551        // Transform clauses are separated by `,` (matching mangle-go) or `;`
552        // (legacy mangle-rs). Both accepted; one-of required between clauses.
553        if Token::Do == self.token {
554            self.next_token()?;
555            let expr = self.parse_base_term()?;
556            transforms.push(alloc!(
557                self,
558                ast::TransformStmt {
559                    var: None,
560                    app: expr
561                }
562            ));
563            self.expect_transform_sep()?;
564        }
565        loop {
566            if Token::Let != self.token {
567                break;
568            }
569            self.next_token()?;
570            if let Token::Ident { name } = &self.token {
571                let name = alloc_str!(self, name.as_str());
572                self.next_token()?;
573                self.expect(Token::Eq)?;
574                let expr = self.parse_base_term()?;
575                transforms.push(alloc!(
576                    self,
577                    ast::TransformStmt {
578                        var: Some(name),
579                        app: expr
580                    }
581                ))
582            }
583            if let Token::Dot = self.token {
584                break;
585            }
586            self.expect_transform_sep()?;
587        }
588        Ok(())
589    }
590
591    fn expect_transform_sep(&mut self) -> Result<()> {
592        match self.token {
593            Token::Comma | Token::Semi => self.next_token(),
594            _ => bail!(
595                "{}: expected `,` or `;` got {}",
596                self.sc.get_error_context(),
597                self.token
598            ),
599        }
600    }
601
602    // -----------------------------------------------------------------------
603    // Temporal interval parsing: @[bound] or @[bound, bound]
604    // -----------------------------------------------------------------------
605
606    /// Try to parse `@[...]` if the current token is `@`. Returns None otherwise.
607    fn try_parse_interval(&mut self) -> Result<Option<ast::Interval>> {
608        if self.token != Token::At {
609            return Ok(None);
610        }
611        self.next_token()?; // consume @
612        self.expect(Token::LBracket)?;
613        let start = self.parse_temporal_bound(true)?;
614        let end = if self.token == Token::Comma {
615            self.next_token()?;
616            self.parse_temporal_bound(false)?
617        } else {
618            // Point interval: @[T] means @[T, T]
619            start
620        };
621        self.expect(Token::RBracket)?;
622        Ok(Some(ast::Interval { start, end }))
623    }
624
625    /// Parse a single temporal bound: timestamp, variable, or `_` (infinity).
626    fn parse_temporal_bound(&mut self, is_start: bool) -> Result<ast::TemporalBound> {
627        match &self.token {
628            Token::Timestamp { nanos } => {
629                let nanos = *nanos;
630                self.next_token()?;
631                Ok(ast::TemporalBound::Timestamp(nanos))
632            }
633            Token::Ident { name } if name == "_" => {
634                self.next_token()?;
635                if is_start {
636                    Ok(ast::TemporalBound::NegInf)
637                } else {
638                    Ok(ast::TemporalBound::PosInf)
639                }
640            }
641            Token::Ident { name } if is_variable(name) => {
642                let var_idx = self.arena.variable_sym(name);
643                self.next_token()?;
644                Ok(ast::TemporalBound::Variable(var_idx))
645            }
646            _ => bail!("parse_temporal_bound: expected timestamp, variable, or '_', got {:?}", self.token),
647        }
648    }
649
650    // -----------------------------------------------------------------------
651
652    // base_term ::= var
653    //             | fun`(`[base_term {',' base_term}`)`
654    //             | string_constant
655    //             | bytes_constant
656    //             | number_constant
657    //             | float_constant
658    //             | name_constant
659    pub fn parse_base_term(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
660        match &self.token {
661            Token::LBracket => return self.parse_list_or_map(),
662            Token::LBrace => return self.parse_struct(),
663            _ => {}
664        }
665
666        let mut is_type = false;
667        let mut base_term = match &self.token {
668            Token::Ident { name } if name == "_" => {
669                let unique = format!("_Anon{}", self.anon_counter);
670                self.anon_counter += 1;
671                ast::BaseTerm::Variable(self.arena.variable_sym(&unique))
672            }
673            Token::Ident { name } if is_variable(name) => {
674                ast::BaseTerm::Variable(self.arena.variable_sym(name))
675            }
676            Token::Ident { name } if is_fn(name) => {
677                let name = self.arena.alloc_str(name);
678                // Arguments parsed below.
679                ast::BaseTerm::ApplyFn(self.arena.function_sym(name, None), &[])
680            }
681            Token::DotIdent { name } => {
682                let name = self.arena.alloc_str(name);
683                is_type = true;
684                // Arguments parsed below.
685                ast::BaseTerm::ApplyFn(self.arena.function_sym(name, None), &[])
686            }
687            Token::String { decoded } => {
688                let value = self.arena.alloc_str(decoded.as_str());
689                ast::BaseTerm::Const(ast::Const::String(value))
690            }
691            Token::Bytes { decoded } => {
692                let value = self.arena.alloc_slice_copy(decoded);
693                ast::BaseTerm::Const(ast::Const::Bytes(value))
694            }
695            Token::Int { decoded } => ast::BaseTerm::Const(ast::Const::Number(*decoded)),
696            Token::Float { decoded } => ast::BaseTerm::Const(ast::Const::Float(*decoded)),
697            Token::Timestamp { nanos } => ast::BaseTerm::Const(ast::Const::Time(*nanos)),
698            Token::Duration { nanos } => ast::BaseTerm::Const(ast::Const::Duration(*nanos)),
699            Token::Name { name } => {
700                let name = self.arena.intern(name);
701                ast::BaseTerm::Const(ast::Const::Name(name))
702            }
703            _ => bail!("parse_base_term: unexpected token {:?}", self.token),
704        };
705        self.next_token()?;
706        if let ast::BaseTerm::ApplyFn(fn_sym, _) = base_term {
707            let mut fn_args = vec![];
708            if is_type {
709                self.parse_langle_base_terms(&mut fn_args)?;
710            } else {
711                self.parse_paren_base_terms(&mut fn_args)?;
712            }
713            let fn_args = self.arena.alloc_slice_copy(&fn_args);
714            base_term = ast::BaseTerm::ApplyFn(fn_sym, fn_args);
715        }
716        let base_term = alloc!(self, base_term);
717        Ok(base_term)
718    }
719
720    fn parse_list_or_map(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
721        self.expect(Token::LBracket)?;
722        if Token::RBracket == self.token {
723            self.next_token()?;
724            return Ok(alloc!(
725                self,
726                ast::BaseTerm::ApplyFn(fn_list_sym(self.arena), &[])
727            ));
728        }
729        let first = self.parse_base_term()?;
730        let expr = if Token::Colon != self.token {
731            let mut items = vec![first];
732            if Token::Comma == self.token {
733                self.next_token()?;
734                if Token::RBracket != self.token {
735                    self.parse_base_terms(&mut items)?;
736                }
737            }
738            // Otherwise it's a single-element list `[x]`; fall through to
739            // the closing `]` check below.
740            ast::BaseTerm::ApplyFn(fn_list_sym(self.arena), alloc_slice!(self, &items))
741        } else {
742            self.expect(Token::Colon)?; // is a map
743            let first_val = self.parse_base_term()?;
744            let mut items = vec![first, first_val];
745            loop {
746                if Token::Comma != self.token {
747                    break;
748                }
749                self.next_token()?;
750                if Token::RBracket == self.token {
751                    break; // trailing comma
752                }
753                items.push(self.parse_base_term()?);
754                self.expect(Token::Colon)?;
755                items.push(self.parse_base_term()?);
756            }
757            ast::BaseTerm::ApplyFn(fn_map_sym(self.arena), alloc_slice!(self, &items))
758        };
759        self.expect(Token::RBracket)?;
760        Ok(alloc!(self, expr))
761    }
762
763    fn parse_struct(&mut self) -> Result<&'arena ast::BaseTerm<'arena>> {
764        self.expect(Token::LBrace)?;
765        if Token::RBrace == self.token {
766            self.next_token()?;
767            return Ok(alloc!(
768                self,
769                ast::BaseTerm::ApplyFn(fn_struct_sym(self.arena), &[])
770            ));
771        }
772        let mut items = vec![];
773        let name = self.parse_base_term()?;
774        if let ast::BaseTerm::Const(ast::Const::Name { .. }) = name {
775            items.push(name)
776        } else {
777            bail!("parse_base_term: expected name in struct expression {{ ... }} got {name:?}",);
778        }
779        self.expect(Token::Colon)?;
780        items.push(self.parse_base_term()?);
781        loop {
782            if Token::Comma != self.token {
783                break;
784            }
785            self.next_token()?;
786            if Token::RBrace == self.token {
787                break; // trailing comma
788            }
789            let name = self.parse_base_term()?;
790            if let ast::BaseTerm::Const(ast::Const::Name { .. }) = name {
791                items.push(name)
792            } else {
793                bail!("parse_base_term: expected name in struct expression {{ ... }} got {name:?}");
794            }
795            self.expect(Token::Colon)?;
796            items.push(self.parse_base_term()?);
797        }
798        self.expect(Token::RBrace)?;
799        Ok(alloc!(
800            self,
801            ast::BaseTerm::ApplyFn(fn_struct_sym(self.arena), alloc_slice!(self, &items))
802        ))
803    }
804
805    /// langle_members ::= `<` [member { `,` member } [`,`]] `>`
806    /// member        ::= base_term [`:` base_term]
807    ///                 | `opt` base_term `:` base_term
808    ///
809    /// When a member contains a colon, both base_terms are pushed (flattened).
810    /// This makes `.Struct</x : /number>` parse identically to `fn:Struct(/x, /number)`.
811    ///
812    /// An `opt` member collapses to a single `fn:opt(name, type)` base term, so
813    /// `.Struct</x : /number, opt /y : /string>` is equivalent to
814    /// `fn:Struct(/x, /number, fn:opt(/y, /string))`.
815    fn parse_langle_base_terms(
816        &mut self,
817        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
818    ) -> Result<()> {
819        self.expect(Token::Lt)?;
820        if Token::Gt == self.token {
821            self.next_token()?;
822            return Ok(());
823        }
824        self.parse_langle_member(base_terms)?;
825        while Token::Comma == self.token {
826            self.next_token()?;
827            if !base_term_start(&self.token) && !self.is_opt_keyword() {
828                break; // trailing comma
829            }
830            self.parse_langle_member(base_terms)?;
831        }
832        self.expect(Token::Gt)?;
833        Ok(())
834    }
835
836    fn is_opt_keyword(&self) -> bool {
837        matches!(&self.token, Token::Ident { name } if name == "opt")
838    }
839
840    fn parse_langle_member(
841        &mut self,
842        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
843    ) -> Result<()> {
844        if self.is_opt_keyword() {
845            self.next_token()?;
846            let name = self.parse_base_term()?;
847            self.expect(Token::Colon)?;
848            let ty = self.parse_base_term()?;
849            let opt = alloc!(
850                self,
851                ast::BaseTerm::ApplyFn(fn_opt_sym(self.arena), alloc_slice!(self, &[name, ty]))
852            );
853            base_terms.push(opt);
854            return Ok(());
855        }
856        base_terms.push(self.parse_base_term()?);
857        if Token::Colon == self.token {
858            self.next_token()?;
859            base_terms.push(self.parse_base_term()?);
860        }
861        Ok(())
862    }
863
864    /// paren_base_terms ::=  `(` [base_terms] `)`
865    fn parse_paren_base_terms(
866        &mut self,
867        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
868    ) -> Result<()> {
869        self.expect(Token::LParen)?;
870        if Token::RParen != self.token {
871            self.parse_base_terms(base_terms)?;
872        }
873        self.expect(Token::RParen)?;
874        Ok(())
875    }
876
877    /// base_terms ::= base_term { `,` base_term } [`,`]
878    fn parse_base_terms(
879        &mut self,
880        base_terms: &mut Vec<&'arena ast::BaseTerm<'arena>>,
881    ) -> Result<()> {
882        base_terms.push(self.parse_base_term()?);
883        while let Token::Comma = self.token {
884            self.next_token()?;
885            if !base_term_start(&self.token) {
886                break; // trailing comma
887            }
888            base_terms.push(self.parse_base_term()?);
889        }
890
891        Ok(())
892    }
893}
894
895fn is_variable(name: &str) -> bool {
896    name.chars().next().unwrap().is_ascii_uppercase()
897}
898
899fn lowercase_first(name: &str) -> String {
900    let mut chars = name.chars();
901    match chars.next() {
902        Some(c) => c.to_ascii_lowercase().to_string() + chars.as_str(),
903        None => String::new(),
904    }
905}
906
907fn is_fn(name: &str) -> bool {
908    name.starts_with("fn:")
909}
910
911fn base_term_start(t: &Token) -> bool {
912    match t {
913        Token::Name { .. }
914        | Token::Int { .. }
915        | Token::Float { .. }
916        | Token::String { .. }
917        | Token::Bytes { .. }
918        | Token::Timestamp { .. }
919        | Token::Duration { .. }
920        | Token::LBracket
921        | Token::LBrace
922        | Token::DotIdent { .. } => true,
923        Token::Ident { name } => is_variable(name) || is_fn(name) || name == "_",
924        _ => false,
925    }
926}
927
928#[cfg(test)]
929mod test {
930
931    use super::*;
932    use googletest::prelude::{eq, gtest, verify_that};
933
934    fn make_parser<'arena>(
935        arena: &'arena Arena,
936        input: &'arena str,
937    ) -> Parser<'arena, &'arena [u8]> {
938        let mut p = Parser::new(arena, input.as_bytes(), "test");
939        p.next_token().unwrap();
940        p
941    }
942
943    #[test]
944    fn test_empty_unit() -> Result<()> {
945        let arena = Arena::new_with_global_interner();
946        let mut p = make_parser(&arena, "");
947        match p.parse_unit()? {
948            &ast::Unit { decls: &[pkg], .. } => {
949                assert_eq!(pkg, &empty_package_decl(&arena));
950            }
951            z => panic!("unexpected: {:?}", z),
952        }
953        Ok(())
954    }
955
956    #[test]
957    fn test_package_use() -> Result<()> {
958        let arena = Arena::new_with_global_interner();
959        let input = "Package foo[bar()]! Use baz[bar()]!";
960
961        let mut p = make_parser(&arena, input);
962        let unit = p.parse_unit()?;
963        match unit.decls {
964            &[
965                &ast::Decl {
966                    atom:
967                        &ast::Atom {
968                            sym: got_package_sym,
969                            ..
970                        },
971                    descr:
972                        &[
973                            &ast::Atom {
974                                sym: got_name_sym1,
975                                args: &[ast::BaseTerm::Const(ast::Const::String("foo"))],
976                            },
977                            &ast::Atom {
978                                sym: got_bar_sym1,
979                                args: &[],
980                            },
981                        ],
982                    ..
983                },
984                &ast::Decl {
985                    atom:
986                        &ast::Atom {
987                            sym: got_use_sym, ..
988                        },
989                    descr:
990                        &[
991                            &ast::Atom {
992                                sym: got_name_sym2,
993                                args: &[ast::BaseTerm::Const(ast::Const::String("baz"))],
994                            },
995                            &ast::Atom {
996                                sym: got_bar_sym2,
997                                args: &[],
998                            },
999                        ],
1000                    ..
1001                },
1002            ] => {
1003                assert_eq!(got_use_sym, use_sym(&arena));
1004                assert_eq!(got_package_sym, package_sym(&arena));
1005                assert_eq!(got_name_sym1, name_sym(&arena));
1006                assert_eq!(got_name_sym2, name_sym(&arena));
1007                assert_eq!(got_bar_sym1, arena.predicate_sym("bar", None));
1008                assert_eq!(got_bar_sym2, arena.predicate_sym("bar", None));
1009            }
1010            z => panic!("unexpected {z:?}"),
1011        }
1012        Ok(())
1013    }
1014
1015    #[test]
1016    fn test_decl() -> Result<()> {
1017        let arena = Arena::new_with_global_interner();
1018        let input = "Decl foo(X, Y).";
1019        let mut p = make_parser(&arena, input);
1020        match p.parse_decl()? {
1021            &ast::Decl {
1022                atom:
1023                    &ast::Atom {
1024                        sym: got_foo_sym,
1025                        args:
1026                            &[
1027                                &ast::BaseTerm::Variable(x_sym),
1028                                &ast::BaseTerm::Variable(y_sym),
1029                            ],
1030                    },
1031                ..
1032            } => {
1033                assert_eq!(got_foo_sym, arena.predicate_sym("foo", None));
1034                assert_eq!(x_sym, arena.variable_sym("X"));
1035                assert_eq!(y_sym, arena.variable_sym("Y"))
1036            }
1037            decl => panic!("got {:?}", decl),
1038        };
1039        Ok(())
1040    }
1041
1042    #[test]
1043    fn test_base_term() -> googletest::Result<()> {
1044        let arena = Arena::new_with_global_interner();
1045        let input = "X 3 1.5 'foo' /foo fn:list() fn:list(/a) fn:list(/a, 3)"; //.as_bytes();
1046        let mut p = make_parser(&arena, input);
1047        let mut got_base_terms = vec![];
1048        loop {
1049            if Token::Eof == p.token {
1050                break;
1051            }
1052            // TODO: "err_to_test_failure".
1053            let base_term = p.parse_base_term().unwrap();
1054            got_base_terms.push(base_term);
1055        }
1056        let expected = vec![
1057            arena.variable("X"),
1058            arena.const_(ast::Const::Number(3)),
1059            arena.const_(ast::Const::Float(1.5)),
1060            arena.const_(ast::Const::String("foo")),
1061            arena.const_(arena.name("/foo")),
1062            arena.apply_fn(fn_list_sym(&arena), &[]),
1063            arena.apply_fn(fn_list_sym(&arena), &[arena.const_(arena.name("/a"))]),
1064            arena.apply_fn(
1065                fn_list_sym(&arena),
1066                &[
1067                    arena.const_(arena.name("/a")),
1068                    arena.const_(ast::Const::Number(3)),
1069                ],
1070            ),
1071        ];
1072        verify_that!(got_base_terms, eq(&expected))
1073    }
1074
1075    #[test]
1076    fn test_term() -> googletest::Result<()> {
1077        let arena = Arena::new_with_global_interner();
1078        let input = "foo(/bar) !bar() X = Z X != 3 3 < 1 3 <= 1";
1079        let mut p = make_parser(&arena, input);
1080        let mut got_terms = vec![];
1081        loop {
1082            if Token::Eof == p.token {
1083                break;
1084            }
1085            // TODO: "err_to_test_failure".
1086            got_terms.push(p.parse_term().unwrap());
1087        }
1088        let expected = [
1089            &ast::Term::Atom(arena.atom(
1090                arena.predicate_sym("foo", None),
1091                &[arena.const_(arena.name("/bar"))],
1092            )),
1093            &ast::Term::NegAtom(arena.atom(arena.predicate_sym("bar", None), &[])),
1094            &ast::Term::Eq(arena.variable("X"), arena.variable("Z")),
1095            &ast::Term::Ineq(
1096                arena.variable("X"),
1097                arena.alloc(ast::BaseTerm::Const(ast::Const::Number(3))),
1098            ),
1099            &ast::Term::Atom(arena.atom(
1100                arena.predicate_sym(":lt", Some(2)),
1101                &[
1102                    arena.const_(ast::Const::Number(3)),
1103                    arena.const_(ast::Const::Number(1)),
1104                ],
1105            )),
1106            &ast::Term::Atom(arena.atom(
1107                arena.predicate_sym(":le", Some(2)),
1108                &[
1109                    arena.const_(ast::Const::Number(3)),
1110                    arena.const_(ast::Const::Number(1)),
1111                ],
1112            )),
1113        ];
1114        verify_that!(got_terms, eq(&expected))
1115    }
1116
1117    #[gtest]
1118    fn test_structured_data_and_types() -> googletest::Result<()> {
1119        let arena = Arena::new_with_global_interner();
1120        let input =
1121            "[] [1,2,3] [1: 'one', 2: 'two'] {} {/foo: /bar} {/name: \"alice\", /age: 30} .List<.Option</name>, /string>";
1122        let mut p = make_parser(&arena, input);
1123        let mut got_base_terms = vec![];
1124        loop {
1125            if Token::Eof == p.token {
1126                break;
1127            }
1128            // TODO: "err_to_test_failure".
1129            let base_term = p.parse_base_term().unwrap();
1130            got_base_terms.push(base_term);
1131        }
1132        let expected = vec![
1133            arena.apply_fn(fn_list_sym(&arena), &[]),
1134            arena.apply_fn(
1135                fn_list_sym(&arena),
1136                &[
1137                    arena.const_(ast::Const::Number(1)),
1138                    arena.const_(ast::Const::Number(2)),
1139                    arena.const_(ast::Const::Number(3)),
1140                ],
1141            ),
1142            arena.apply_fn(
1143                fn_map_sym(&arena),
1144                &[
1145                    arena.const_(ast::Const::Number(1)),
1146                    arena.const_(ast::Const::String("one")),
1147                    arena.const_(ast::Const::Number(2)),
1148                    arena.const_(ast::Const::String("two")),
1149                ],
1150            ),
1151            arena.apply_fn(fn_struct_sym(&arena), &[]),
1152            arena.apply_fn(
1153                fn_struct_sym(&arena),
1154                &[
1155                    arena.const_(arena.name("/foo")),
1156                    arena.const_(arena.name("/bar")),
1157                ],
1158            ),
1159            arena.apply_fn(
1160                fn_struct_sym(&arena),
1161                &[
1162                    arena.const_(arena.name("/name")),
1163                    arena.const_(ast::Const::String("alice")),
1164                    arena.const_(arena.name("/age")),
1165                    arena.const_(ast::Const::Number(30)),
1166                ],
1167            ),
1168            arena.apply_fn(
1169                fn_list_type_sym(&arena),
1170                &[
1171                    arena.apply_fn(
1172                        fn_option_type_sym(&arena),
1173                        &[arena.const_(arena.name("/name"))],
1174                    ),
1175                    arena.const_(arena.name("/string")),
1176                ],
1177            ),
1178        ];
1179        verify_that!(got_base_terms, eq(&expected))
1180    }
1181
1182    #[gtest]
1183    fn test_trailing_commas() -> googletest::Result<()> {
1184        let arena = Arena::new_with_global_interner();
1185        let input = "[1, 2, 3,] [1: 'one', 2: 'two',] {/a: 1, /b: 2,}";
1186        let mut p = make_parser(&arena, input);
1187        let mut got_base_terms = vec![];
1188        loop {
1189            if Token::Eof == p.token {
1190                break;
1191            }
1192            let base_term = p.parse_base_term().unwrap();
1193            got_base_terms.push(base_term);
1194        }
1195        let expected = vec![
1196            arena.apply_fn(
1197                fn_list_sym(&arena),
1198                &[
1199                    arena.const_(ast::Const::Number(1)),
1200                    arena.const_(ast::Const::Number(2)),
1201                    arena.const_(ast::Const::Number(3)),
1202                ],
1203            ),
1204            arena.apply_fn(
1205                fn_map_sym(&arena),
1206                &[
1207                    arena.const_(ast::Const::Number(1)),
1208                    arena.const_(ast::Const::String("one")),
1209                    arena.const_(ast::Const::Number(2)),
1210                    arena.const_(ast::Const::String("two")),
1211                ],
1212            ),
1213            arena.apply_fn(
1214                fn_struct_sym(&arena),
1215                &[
1216                    arena.const_(arena.name("/a")),
1217                    arena.const_(ast::Const::Number(1)),
1218                    arena.const_(arena.name("/b")),
1219                    arena.const_(ast::Const::Number(2)),
1220                ],
1221            ),
1222        ];
1223        verify_that!(got_base_terms, eq(&expected))
1224    }
1225
1226    #[test]
1227    fn test_single_element_list() -> googletest::Result<()> {
1228        let arena = Arena::new_with_global_interner();
1229        let mut p = make_parser(&arena, "[42]");
1230        let got = p.parse_base_term().unwrap();
1231        let expected = arena.apply_fn(
1232            fn_list_sym(&arena),
1233            &[arena.const_(ast::Const::Number(42))],
1234        );
1235        verify_that!(got, eq(expected))
1236    }
1237
1238    #[test]
1239    fn test_descr_trailing_comma() -> googletest::Result<()> {
1240        // `Decl foo descr [ bar(), ]` — trailing comma after an atom list.
1241        let arena = Arena::new_with_global_interner();
1242        let mut p = make_parser(&arena, "Decl foo(X) descr [ bar(), ].");
1243        p.parse_decl().expect("descr list with trailing comma parses");
1244        Ok(())
1245    }
1246
1247    #[test]
1248    fn test_opt_in_struct_type() -> googletest::Result<()> {
1249        // `opt NAME : TYPE` inside .Struct<...> collapses to a single
1250        // fn:opt(NAME, TYPE) base term so it's distinguishable from a
1251        // required field (which is two flattened args).
1252        let arena = Arena::new_with_global_interner();
1253        let mut p = make_parser(&arena, ".Struct</x : /number, opt /y : /string>");
1254        let got = p.parse_base_term().unwrap();
1255        let opt_inner = arena.apply_fn(
1256            fn_opt_sym(&arena),
1257            &[arena.const_(arena.name("/y")), arena.const_(arena.name("/string"))],
1258        );
1259        let expected = arena.apply_fn(
1260            arena.function_sym("fn:Struct", None),
1261            &[
1262                arena.const_(arena.name("/x")),
1263                arena.const_(arena.name("/number")),
1264                opt_inner,
1265            ],
1266        );
1267        verify_that!(got, eq(expected))
1268    }
1269
1270    #[test]
1271    fn test_transform_sep_comma_and_semi() -> googletest::Result<()> {
1272        // Transform clauses may be separated by `,` (mangle-go) or `;`
1273        // (legacy mangle-rs). Both must parse identically.
1274        let arena = Arena::new_with_global_interner();
1275        let src_comma = "q(K, S) :- p(K, V) |> do fn:group_by(K), let S = fn:sum(V).";
1276        let src_semi  = "q(K, S) :- p(K, V) |> do fn:group_by(K); let S = fn:sum(V).";
1277        let a = make_parser(&arena, src_comma).parse_clause().unwrap();
1278        let b = make_parser(&arena, src_semi).parse_clause().unwrap();
1279        verify_that!(a.transform.len(), eq(2))?;
1280        verify_that!(b.transform.len(), eq(2))
1281    }
1282
1283    #[test]
1284    fn test_clause() -> Result<()> {
1285        let arena = Arena::new_with_global_interner();
1286        let mut p = make_parser(&arena, "foo(X).");
1287        let clause = p.parse_clause()?;
1288        match clause {
1289            &ast::Clause {
1290                head:
1291                    &ast::Atom {
1292                        args: &[ast::BaseTerm::Variable(x_sym)],
1293                        ..
1294                    },
1295                premises: &[],
1296                transform: &[],
1297                ..
1298            } => {
1299                assert_eq!(*x_sym, arena.variable_sym("X"));
1300                assert_eq!(clause.head.sym, arena.predicate_sym("foo", None));
1301            }
1302            _ => panic!("unexpected: {:?}", clause),
1303        }
1304        let mut p = make_parser(&arena, "foo(X) :- !bar(X).");
1305        let clause = p.parse_clause()?;
1306        match clause {
1307            &ast::Clause {
1308                head:
1309                    &ast::Atom {
1310                        sym: foo_sym,
1311                        args: _,
1312                    },
1313                premises:
1314                    &[
1315                        &ast::Term::NegAtom(&ast::Atom {
1316                            sym: bar_sym,
1317                            args: _,
1318                        }),
1319                    ],
1320                transform: &[],
1321                ..
1322            } => {
1323                assert_eq!(foo_sym, arena.predicate_sym("foo", None));
1324                assert_eq!(bar_sym, arena.predicate_sym("bar", None));
1325            }
1326            _ => panic!("unexpected: {:?}", clause),
1327        };
1328        let mut p = make_parser(
1329            &arena,
1330            "foo(Z) ⟸ bar(Y) |> do fn:group_by(); let X = fn:count(Y).",
1331        );
1332
1333        let clause = p.parse_clause()?;
1334        match clause {
1335            &ast::Clause {
1336                head: &ast::Atom { .. },
1337                premises: &[&ast::Term::Atom(ast::Atom { .. })],
1338                transform:
1339                    &[
1340                        &ast::TransformStmt {
1341                            var: None,
1342                            app: ast::BaseTerm::ApplyFn(first_sym, _),
1343                        },
1344                        &ast::TransformStmt {
1345                            var: Some("X"),
1346                            app: ast::BaseTerm::ApplyFn(second_sym, _),
1347                        },
1348                    ],
1349                ..
1350            } => {
1351                assert_eq!(clause.head.sym, arena.predicate_sym("foo", None));
1352                assert_eq!(clause.transform.len(), 2);
1353                assert_eq!(*first_sym, arena.function_sym("fn:group_by", None));
1354                assert_eq!(*second_sym, arena.function_sym("fn:count", None));
1355            }
1356            _ => panic!("unexpected: {:?}", clause),
1357        }
1358
1359        Ok(())
1360    }
1361
1362    #[test]
1363    fn test_anonymous_variable_single() -> Result<()> {
1364        let arena = Arena::new_with_global_interner();
1365        let mut p = make_parser(&arena, "foo(_, X).");
1366        let clause = p.parse_clause()?;
1367        // The `_` should parse as a variable with a generated name `_Anon0`
1368        match clause.head.args {
1369            &[&ast::BaseTerm::Variable(anon), &ast::BaseTerm::Variable(x)] => {
1370                assert_eq!(anon, arena.variable_sym("_Anon0"));
1371                assert_eq!(x, arena.variable_sym("X"));
1372            }
1373            _ => panic!("unexpected args: {:?}", clause.head.args),
1374        }
1375        Ok(())
1376    }
1377
1378    #[test]
1379    fn test_anonymous_variable_multiple_distinct() -> Result<()> {
1380        let arena = Arena::new_with_global_interner();
1381        let mut p = make_parser(&arena, "foo(_, _, _).");
1382        let clause = p.parse_clause()?;
1383        // Each `_` should produce a distinct variable name
1384        match clause.head.args {
1385            &[
1386                &ast::BaseTerm::Variable(a0),
1387                &ast::BaseTerm::Variable(a1),
1388                &ast::BaseTerm::Variable(a2),
1389            ] => {
1390                assert_eq!(a0, arena.variable_sym("_Anon0"));
1391                assert_eq!(a1, arena.variable_sym("_Anon1"));
1392                assert_eq!(a2, arena.variable_sym("_Anon2"));
1393                // All three must be distinct
1394                assert_ne!(a0, a1);
1395                assert_ne!(a1, a2);
1396            }
1397            _ => panic!("unexpected args: {:?}", clause.head.args),
1398        }
1399        Ok(())
1400    }
1401
1402    #[test]
1403    fn test_anonymous_variable_in_rule_body() -> Result<()> {
1404        let arena = Arena::new_with_global_interner();
1405        let mut p = make_parser(&arena, "result(X) :- foo(X, _).");
1406        let clause = p.parse_clause()?;
1407        assert_eq!(clause.head.sym, arena.predicate_sym("result", None));
1408        match clause.premises {
1409            &[&ast::Term::Atom(&ast::Atom { args, .. })] => match args {
1410                &[&ast::BaseTerm::Variable(x), &ast::BaseTerm::Variable(anon)] => {
1411                    assert_eq!(x, arena.variable_sym("X"));
1412                    assert_eq!(anon, arena.variable_sym("_Anon0"));
1413                }
1414                _ => panic!("unexpected args: {:?}", args),
1415            },
1416            _ => panic!("unexpected premises: {:?}", clause.premises),
1417        }
1418        Ok(())
1419    }
1420
1421    #[test]
1422    fn test_anonymous_variable_with_negation() -> Result<()> {
1423        let arena = Arena::new_with_global_interner();
1424        let mut p = make_parser(&arena, "orphan(X) :- node(X, _), !has_parent(X).");
1425        let clause = p.parse_clause()?;
1426        assert_eq!(clause.head.sym, arena.predicate_sym("orphan", None));
1427        assert_eq!(clause.premises.len(), 2);
1428        // First premise: node(X, _)
1429        match clause.premises[0] {
1430            &ast::Term::Atom(&ast::Atom { args, .. }) => match args {
1431                &[&ast::BaseTerm::Variable(_), &ast::BaseTerm::Variable(anon)] => {
1432                    assert_eq!(anon, arena.variable_sym("_Anon0"));
1433                }
1434                _ => panic!("unexpected args: {:?}", args),
1435            },
1436            _ => panic!("expected Atom, got {:?}", clause.premises[0]),
1437        }
1438        // Second premise: !has_parent(X)
1439        match clause.premises[1] {
1440            &ast::Term::NegAtom(&ast::Atom { sym, .. }) => {
1441                assert_eq!(sym, arena.predicate_sym("has_parent", None));
1442            }
1443            _ => panic!("expected NegAtom, got {:?}", clause.premises[1]),
1444        }
1445        Ok(())
1446    }
1447
1448    #[test]
1449    fn test_uppercase_predicate_name_rejected_at_head() {
1450        let arena = Arena::new_with_global_interner();
1451        let mut p = make_parser(&arena, "Foo(1, 2).");
1452        let err = p.parse_clause().unwrap_err().to_string();
1453        assert!(
1454            err.contains("`Foo` is not a valid predicate name")
1455                && err.contains("Did you mean `foo`?"),
1456            "unexpected error: {err}"
1457        );
1458    }
1459
1460    #[test]
1461    fn test_uppercase_predicate_name_rejected_in_body() {
1462        let arena = Arena::new_with_global_interner();
1463        let mut p = make_parser(&arena, "bar(X) :- Foo(X, Y).");
1464        let err = p.parse_clause().unwrap_err().to_string();
1465        assert!(
1466            err.contains("`Foo` starts with an uppercase letter")
1467                && err.contains("Did you mean `foo`?"),
1468            "unexpected error: {err}"
1469        );
1470    }
1471
1472    // -----------------------------------------------------------------------
1473    // Temporal parsing tests (ported from Go temporal_integration_test.go)
1474    // -----------------------------------------------------------------------
1475
1476    /// Go: TestIntegration_TemporalFactParsing - simple temporal fact
1477    #[test]
1478    fn test_temporal_fact_with_interval() -> Result<()> {
1479        let arena = Arena::new_with_global_interner();
1480        let mut p = make_parser(&arena, "foo(/bar)@[2024-01-15, 2024-06-30].");
1481        let clause = p.parse_clause()?;
1482        assert!(clause.head_time.is_some(), "expected temporal annotation");
1483        let interval = clause.head_time.unwrap();
1484        match interval.start {
1485            ast::TemporalBound::Timestamp(_) => {}
1486            _ => panic!("expected Timestamp start, got {:?}", interval.start),
1487        }
1488        match interval.end {
1489            ast::TemporalBound::Timestamp(_) => {}
1490            _ => panic!("expected Timestamp end, got {:?}", interval.end),
1491        }
1492        Ok(())
1493    }
1494
1495    /// Go: TestIntegration_TemporalFactParsing - point interval fact
1496    #[test]
1497    fn test_temporal_fact_point_interval() -> Result<()> {
1498        let arena = Arena::new_with_global_interner();
1499        let mut p = make_parser(&arena, "event(/login)@[2024-03-15].");
1500        let clause = p.parse_clause()?;
1501        assert!(clause.head_time.is_some(), "expected temporal annotation");
1502        let interval = clause.head_time.unwrap();
1503        // Point interval: start == end
1504        assert_eq!(interval.start, interval.end);
1505        Ok(())
1506    }
1507
1508    /// Go: TestIntegration_TemporalFactParsing - non-temporal fact
1509    #[test]
1510    fn test_non_temporal_fact() -> Result<()> {
1511        let arena = Arena::new_with_global_interner();
1512        let mut p = make_parser(&arena, "regular(/fact).");
1513        let clause = p.parse_clause()?;
1514        assert!(clause.head_time.is_none(), "non-temporal fact should have no annotation");
1515        Ok(())
1516    }
1517
1518    /// Go: TestIntegration_TemporalDeclarations - temporal predicate declaration
1519    #[test]
1520    fn test_temporal_declaration() -> Result<()> {
1521        let arena = Arena::new_with_global_interner();
1522        let mut p = make_parser(&arena, "Decl employee(X) temporal bound [/name].");
1523        let unit = p.parse_unit()?;
1524        // decls[0] is the implicit empty Package decl
1525        assert_eq!(unit.decls.len(), 2);
1526        assert!(unit.decls[1].is_temporal, "expected temporal declaration");
1527        Ok(())
1528    }
1529
1530    /// Go: TestIntegration_TemporalDeclarations - non-temporal predicate declaration
1531    #[test]
1532    fn test_non_temporal_declaration() -> Result<()> {
1533        let arena = Arena::new_with_global_interner();
1534        let mut p = make_parser(&arena, "Decl config(X) bound [/string].");
1535        let unit = p.parse_unit()?;
1536        assert_eq!(unit.decls.len(), 2);
1537        assert!(!unit.decls[1].is_temporal, "expected non-temporal declaration");
1538        Ok(())
1539    }
1540
1541    /// Go: TestIntegration_TemporalDeclarations - temporal with documentation
1542    #[test]
1543    fn test_temporal_declaration_with_descr() -> Result<()> {
1544        let arena = Arena::new_with_global_interner();
1545        let input = r#"Decl status(X, Y) temporal
1546            descr [doc("Employee status over time")]
1547            bound [/name, /string]."#;
1548        let mut p = make_parser(&arena, input);
1549        let unit = p.parse_unit()?;
1550        assert_eq!(unit.decls.len(), 2);
1551        assert!(unit.decls[1].is_temporal, "expected temporal declaration");
1552        Ok(())
1553    }
1554
1555    /// Go: TestIntegration_BackwardCompatibility - non-temporal programs still work
1556    #[test]
1557    fn test_backward_compat_no_temporal() -> Result<()> {
1558        // Each program must be a valid unit. Test that no clauses get temporal annotations.
1559        let programs = [
1560            "edge(/a, /b). path(X, Y) :- edge(X, Y).",
1561            "all(/a). excluded(/a). included(X) :- all(X), !excluded(X).",
1562            "age(/alice, 30). adult(Name) :- age(Name, Age), Age >= 18 .",
1563        ];
1564        for prog in &programs {
1565            let arena = Arena::new_with_global_interner();
1566            let mut p = make_parser(&arena, prog);
1567            let unit = p.parse_unit()?;
1568            for clause in unit.clauses {
1569                assert!(clause.head_time.is_none(), "clause should not have temporal annotation in: {prog}");
1570            }
1571        }
1572        Ok(())
1573    }
1574
1575    /// Temporal rule with variable interval in head and body
1576    #[test]
1577    fn test_temporal_rule_with_variable_interval() -> Result<()> {
1578        let arena = Arena::new_with_global_interner();
1579        let mut p = make_parser(&arena, "reachable(X, Y)@[T] :- link(X, Y)@[T].");
1580        let clause = p.parse_clause()?;
1581        // Head has temporal annotation
1582        assert!(clause.head_time.is_some());
1583        let interval = clause.head_time.unwrap();
1584        match interval.start {
1585            ast::TemporalBound::Variable(_) => {}
1586            _ => panic!("expected Variable start, got {:?}", interval.start),
1587        }
1588        // Point interval: start == end
1589        assert_eq!(interval.start, interval.end);
1590        // Body premise is a TemporalAtom
1591        assert_eq!(clause.premises.len(), 1);
1592        match clause.premises[0] {
1593            ast::Term::TemporalAtom(_, _) => {}
1594            _ => panic!("expected TemporalAtom, got {:?}", clause.premises[0]),
1595        }
1596        Ok(())
1597    }
1598
1599    /// Temporal rule with interval range [S, E] variables
1600    #[test]
1601    fn test_temporal_rule_with_interval_range() -> Result<()> {
1602        let arena = Arena::new_with_global_interner();
1603        let mut p = make_parser(&arena, "reachable(X, Y)@[S, E] :- link(X, Y)@[S, E].");
1604        let clause = p.parse_clause()?;
1605        let interval = clause.head_time.unwrap();
1606        match interval.start {
1607            ast::TemporalBound::Variable(v) => {
1608                assert_eq!(arena.lookup_name(v.0).unwrap(), "S");
1609            }
1610            _ => panic!("expected Variable start"),
1611        }
1612        match interval.end {
1613            ast::TemporalBound::Variable(v) => {
1614                assert_eq!(arena.lookup_name(v.0).unwrap(), "E");
1615            }
1616            _ => panic!("expected Variable end"),
1617        }
1618        Ok(())
1619    }
1620
1621    /// Wildcard bounds: @[_, _] means eternal interval
1622    #[test]
1623    fn test_temporal_wildcard_bounds() -> Result<()> {
1624        let arena = Arena::new_with_global_interner();
1625        let mut p = make_parser(&arena, "always(/true)@[_, _].");
1626        let clause = p.parse_clause()?;
1627        let interval = clause.head_time.unwrap();
1628        assert_eq!(interval.start, ast::TemporalBound::NegInf);
1629        assert_eq!(interval.end, ast::TemporalBound::PosInf);
1630        Ok(())
1631    }
1632
1633    /// Colon syntax in angle brackets: `.Struct</x : /number, /y : /string>`
1634    /// should parse to `ApplyFn("fn:Struct", [/x, /number, /y, /string])`.
1635    #[gtest]
1636    fn test_colon_syntax_in_angle_brackets() -> googletest::Result<()> {
1637        let arena = Arena::new_with_global_interner();
1638        let input = ".Struct</x : /number, /y : /string>";
1639        let mut p = make_parser(&arena, input);
1640        let got = p.parse_base_term().unwrap();
1641
1642        // DotIdent `.Struct` produces PascalCase `fn:Struct` (type constructor).
1643        let struct_type_sym = arena.function_sym("fn:Struct", None);
1644        let expected = arena.apply_fn(
1645            struct_type_sym,
1646            &[
1647                arena.const_(arena.name("/x")),
1648                arena.const_(arena.name("/number")),
1649                arena.const_(arena.name("/y")),
1650                arena.const_(arena.name("/string")),
1651            ],
1652        );
1653        verify_that!(got, eq(expected))
1654    }
1655
1656    /// Colon syntax for TaggedUnion.
1657    #[gtest]
1658    fn test_tagged_union_colon_syntax() -> googletest::Result<()> {
1659        let arena = Arena::new_with_global_interner();
1660        let input = ".TaggedUnion</kind, /move : .Struct</x : /number>, /quit : .Struct<>>";
1661        let mut p = make_parser(&arena, input);
1662        let got = p.parse_base_term().unwrap();
1663
1664        let tu_sym = arena.function_sym("fn:TaggedUnion", None);
1665        let struct_sym = arena.function_sym("fn:Struct", None);
1666        let expected = arena.apply_fn(
1667            tu_sym,
1668            &[
1669                arena.const_(arena.name("/kind")),
1670                arena.const_(arena.name("/move")),
1671                arena.apply_fn(
1672                    struct_sym,
1673                    &[
1674                        arena.const_(arena.name("/x")),
1675                        arena.const_(arena.name("/number")),
1676                    ],
1677                ),
1678                arena.const_(arena.name("/quit")),
1679                arena.apply_fn(struct_sym, &[]),
1680            ],
1681        );
1682        verify_that!(got, eq(expected))
1683    }
1684
1685    /// Mixed: some members with colon, some without.
1686    #[gtest]
1687    fn test_mixed_colon_syntax() -> googletest::Result<()> {
1688        let arena = Arena::new_with_global_interner();
1689        // `.List</number>` — no colons.
1690        let input = ".List</number>";
1691        let mut p = make_parser(&arena, input);
1692        let got = p.parse_base_term().unwrap();
1693
1694        let list_sym = arena.function_sym("fn:List", None);
1695        let expected = arena.apply_fn(list_sym, &[arena.const_(arena.name("/number"))]);
1696        verify_that!(got, eq(expected))
1697    }
1698
1699    /// Paren syntax `fn:Struct(...)` still works unchanged.
1700    /// Note: `fn:Struct` via `fn:` prefix (Ident) produces the same symbol
1701    /// as `.Struct` via DotIdent — both become `fn:Struct`.
1702    #[gtest]
1703    fn test_paren_syntax_unchanged() -> googletest::Result<()> {
1704        let arena = Arena::new_with_global_interner();
1705        let input = "fn:Struct(/x, /number, /y, /string)";
1706        let mut p = make_parser(&arena, input);
1707        let got = p.parse_base_term().unwrap();
1708
1709        let struct_type_sym = arena.function_sym("fn:Struct", None);
1710        let expected = arena.apply_fn(
1711            struct_type_sym,
1712            &[
1713                arena.const_(arena.name("/x")),
1714                arena.const_(arena.name("/number")),
1715                arena.const_(arena.name("/y")),
1716                arena.const_(arena.name("/string")),
1717            ],
1718        );
1719        verify_that!(got, eq(expected))
1720    }
1721
1722    /// Trailing comma in colon syntax.
1723    #[gtest]
1724    fn test_colon_syntax_trailing_comma() -> googletest::Result<()> {
1725        let arena = Arena::new_with_global_interner();
1726        let input = ".Struct</x : /number,>";
1727        let mut p = make_parser(&arena, input);
1728        let got = p.parse_base_term().unwrap();
1729
1730        let struct_type_sym = arena.function_sym("fn:Struct", None);
1731        let expected = arena.apply_fn(
1732            struct_type_sym,
1733            &[
1734                arena.const_(arena.name("/x")),
1735                arena.const_(arena.name("/number")),
1736            ],
1737        );
1738        verify_that!(got, eq(expected))
1739    }
1740}