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