Skip to main content

arena_terms_parser/
lexer.rs

1//! Lexer for Prolog-like terms with operator definitions.
2//!
3//! This module defines the [`TermLexer`] type, which tokenizes Prolog-style term input
4//! and produces [`TermToken`] values used by the parser.
5//!
6//! The lexer is automatically generated by the **[`alex`]** tool (part of [`parlex-gen`])
7//! and incorporates pattern actions for building atoms, numbers, strings, dates, and
8//! structured terms. It supports nested parentheses, quoted names, and multi-line
9//! comments.
10//!
11//! In addition, the lexer integrates operator definitions via [`OperDefs`], allowing
12//! it to recognize operator symbols and return specialized tokens annotated with
13//! operator table indices. These operator-aware tokens are later used by the parser
14//! to resolve shift/reduce conflicts using precedence and associativity during term parsing.
15//!
16//! # Components
17//! - [`TermLexer`]: The main lexer implementation.
18//! - [`TermToken`]: Token type emitted by the lexer.
19//! - [`OperDefs`]: Table of operator definitions used for lookup.
20//!
21//! # Code Generation
22//! The actual DFA tables and lexer rules are generated at build time by
23//! **[`alex`]**, included from `OUT_DIR` as `lexer_data.rs`.
24//!
25//! [`TermLexer`]: struct.TermLexer
26//! [`TermToken`]: struct.TermToken
27//! [`OperDefs`]: crate::oper::OperDefs
28//! [`alex`]: https://crates.io/crates/parlex-gen
29
30use crate::encoding::Encoding;
31use crate::{TermToken, TokenID, Value};
32use lexer_data::{LexData, Mode, Rule};
33use parlex::{Lexer, LexerData, LexerDriver, LexerStats, ParlexError, Span};
34use std::marker::PhantomData;
35use try_next::TryNextWithContext;
36
37use arena_terms::{Arena, Fixity, OperDef, Term};
38use chrono::{DateTime, FixedOffset, Utc};
39use smartstring::alias::String;
40
41/// Includes the generated lexer definition produced by **`parlex-gen`**’s
42/// [`alex`](https://crates.io/crates/parlex-gen) tool.
43///
44/// The included file provides:
45/// - DFA tables and mode enumeration ([`Mode`]),
46/// - rule identifiers ([`Rule`]),
47/// - aggregate metadata ([`LexData`]) consumed by the runtime [`Lexer`].
48///
49/// The included file (`lexer_data.rs`) is generated at build time by the
50/// project’s `build.rs` script.
51pub mod lexer_data {
52    include!(concat!(env!("OUT_DIR"), "/lexer_data.rs"));
53}
54
55/// Parses a date/time string into a Unix epoch timestamp in milliseconds.
56///
57/// Accepts either:
58/// - an RFC 3339–formatted string (e.g., `"2024-03-15T10:30:00Z"`), or
59/// - a custom date-time format pattern when `fmt` is provided.
60///
61/// The parsed value is normalized to UTC before conversion.
62///
63/// # Parameters
64/// - `s`: The date-time string to parse.
65/// - `fmt`: Optional custom format string compatible with [`chrono::DateTime::parse_from_str`].
66///
67/// # Returns
68/// The corresponding timestamp in **milliseconds since the Unix epoch**.
69///
70/// # Errors
71/// Returns an error if the input cannot be parsed according to the expected format.
72///
73/// [`chrono::DateTime::parse_from_str`]: chrono::DateTime::parse_from_str
74fn parse_date_to_epoch(s: &str, fmt: Option<&str>) -> Result<i64, ParlexError> {
75    let dt_fixed: DateTime<FixedOffset> = match fmt {
76        None => DateTime::parse_from_rfc3339(s).map_err(|e| ParlexError::from_err(e, None))?,
77        Some(layout) => {
78            DateTime::parse_from_str(s, layout).map_err(|e| ParlexError::from_err(e, None))?
79        }
80    };
81    let dt_utc = dt_fixed.with_timezone(&Utc);
82    Ok(dt_utc.timestamp_millis())
83}
84
85/// Parses a signed 64-bit integer from a string using the specified numeric base.
86///
87/// Supports standard bases (2, 8, 10, 16, etc.) via [`i64::from_str_radix`].
88/// Returns `0` for empty strings.
89///
90/// # Parameters
91/// - `s`: The numeric string slice.
92/// - `base`: The integer base (radix), e.g., 10 for decimal or 16 for hexadecimal.
93///
94/// # Returns
95/// The parsed integer as an `i64`.
96///
97/// # Errors
98/// Returns an error if:
99/// - the string contains invalid digits for the given base, or
100/// - the parsed value exceeds the bounds of an `i64`.
101fn parse_i64(s: &str, base: u32) -> Result<i64, std::num::ParseIntError> {
102    if s.is_empty() {
103        return Ok(0);
104    }
105    let n = i64::from_str_radix(s, base)?;
106    Ok(n)
107}
108
109/// Stateful driver that handles rule matches from the generated DFA.
110///
111/// `TermLexerDriver` receives callbacks when a rule matches. It can:
112/// - **emit tokens** (e.g., identifiers, numbers, operators),
113/// - **adjust internal bookkeeping** (e.g., nested comment depth),
114/// - **switch modes** (e.g., on comment boundaries).
115///
116/// The driver is parameterized by an input type `I` that yields bytes and
117/// supports contextual access to [`Arena`].
118///
119/// # Internal State
120/// - [`comment_level`](#structfield.comment_level): current nesting depth of
121///   block comments; positive values mean we’re inside a comment.
122/// - [`_marker`](#structfield._marker): binds the generic `I` without storage.
123///
124/// # Associated Types (via `LexerDriver`)
125/// - `LexerData = LexData`
126/// - `Token = TermToken`
127/// - `Lexer = Lexer<I, Self, Arena>`
128/// - `Error = TermError`
129/// - `Context = Arena`
130///
131/// # Errors
132/// - `TermError::ParseInt` for invalid numeric literals,
133/// - `TermError::FromUtf8` for invalid UTF-8 when decoding identifiers,
134pub struct TermLexerDriver<I> {
135    /// Marker to bind the driver to the input type `I` without storing it.
136    _marker: PhantomData<I>,
137
138    /// Input encoding for transcoding buffer bytes to UTF-8 strings.
139    encoding: Encoding,
140
141    /// Secondary buffer.
142    pub buffer2: Vec<u8>,
143
144    /// Secondary span.
145    pub span2: Span,
146
147    /// Nesting depth of parentheses `(...)` in the input.
148    nest_count: isize,
149
150    /// Nesting depth of block comments `/* ... */`.
151    comment_nest_count: isize,
152
153    /// Nesting depth of curly braces `{...}`.
154    curly_nest_count: isize,
155
156    /// Nesting depth inside embedded script sections (`{ ... }`).
157    script_curly_nest_count: isize,
158
159    /// Counter tracking progress when lexing binary data sections.
160    bin_count: isize,
161
162    /// Temporary buffer holding the label of the current
163    /// binary section being processed.
164    bin_label: Vec<u8>,
165
166    /// Temporary buffer holding date/time parsing format
167    ///  used for string-to-epoch conversions.
168    date_format: String,
169}
170
171#[inline]
172fn yield_id<I>(lexer: &mut Lexer<I, TermLexerDriver<I>, Arena>, token_id: TokenID)
173where
174    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
175{
176    lexer.yield_token(TermToken {
177        token_id,
178        value: Value::None,
179        span: Some(lexer.span()),
180        op_tab_index: None,
181    });
182}
183
184#[inline]
185fn yield_term<I>(
186    lexer: &mut Lexer<I, TermLexerDriver<I>, Arena>,
187    token_id: TokenID,
188    term: Term,
189    span: Span,
190) where
191    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
192{
193    lexer.yield_token(TermToken {
194        token_id,
195        value: Value::Term(term),
196        span: Some(span),
197        op_tab_index: None,
198    });
199}
200
201#[inline]
202fn yield_optab<I>(
203    lexer: &mut Lexer<I, TermLexerDriver<I>, Arena>,
204    token_id: TokenID,
205    term: Term,
206    op_tab_index: Option<usize>,
207    span: Span,
208) where
209    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
210{
211    lexer.yield_token(TermToken {
212        token_id,
213        value: Value::Term(term),
214        span: Some(span),
215        op_tab_index,
216    });
217}
218
219impl<I> TermLexerDriver<I> {
220    #[inline]
221    fn take_bytes(&mut self, lexer: &mut Lexer<I, Self, Arena>) -> Vec<u8>
222    where
223        I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
224    {
225        lexer.accum_flag = false;
226        std::mem::take(&mut lexer.buffer)
227    }
228
229    #[inline]
230    fn take_str(&mut self, lexer: &mut Lexer<I, Self, Arena>) -> Result<String, ParlexError>
231    where
232        I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
233    {
234        let bytes = self.take_bytes(lexer);
235        let s = self.encoding.decode(&bytes)
236            .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
237        Ok(String::from(s))
238    }
239
240    #[inline]
241    fn take_bytes2(&mut self, lexer: &mut Lexer<I, Self, Arena>) -> Vec<u8>
242    where
243        I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
244    {
245        lexer.accum_flag = false;
246        std::mem::take(&mut self.buffer2)
247    }
248
249    #[inline]
250    fn take_str2(&mut self, lexer: &mut Lexer<I, Self, Arena>) -> Result<String, ParlexError>
251    where
252        I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
253    {
254        let bytes = self.take_bytes2(lexer);
255        let s = self.encoding.decode(&bytes)
256            .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
257        Ok(String::from(s))
258    }
259}
260
261impl<I> LexerDriver for TermLexerDriver<I>
262where
263    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
264{
265    /// Rule identifiers and metadata produced by the lexer generator.
266    type LexerData = LexData;
267
268    /// Concrete token type emitted by the driver.
269    type Token = TermToken;
270
271    /// Concrete lexer type parameterized by input, driver and context.
272    type Lexer = Lexer<I, Self, Self::Context>;
273
274    /// Externally supplied context available to actions (symbol table).
275    type Context = Arena;
276
277    /// Handles a single lexer rule match.
278    ///
279    /// Called by the lexer when a DFA rule in [`Lexer`] fires. The implementation
280    /// typically inspects `rule`, reads the matched span from `lexer`, and either:
281    ///
282    /// - emits a [`TermToken`] (e.g., numbers, operators),
283    /// - updates internal state (e.g., `comment_nest_count`),
284    /// - or returns an error if the match is invalid.
285    ///
286    /// Implementations may also use `context` (an [`Arena`]) to intern identifiers
287    /// and store terms in [`Value::Term`].
288    ///
289    /// # Errors
290    /// Propagates any lexical, parsing, UTF-8 decoding, or arena errors as
291    /// [`TermError`].
292    fn action(
293        &mut self,
294        lexer: &mut Self::Lexer,
295        arena: &mut Self::Context,
296        rule: <Self::LexerData as LexerData>::LexerRule,
297    ) -> Result<(), ParlexError> {
298        log::trace!(
299            "ACTION begin: mode {:?}, rule {:?}, buf {:?}, buf2 {:?}, label {:?}, accum {}",
300            lexer.mode(),
301            rule,
302            std::string::String::from_utf8_lossy(&lexer.buffer),
303            std::string::String::from_utf8_lossy(&self.buffer2),
304            std::string::String::from_utf8_lossy(&self.bin_label),
305            lexer.accum_flag,
306        );
307        match rule {
308            Rule::Empty => {
309                unreachable!()
310            }
311            Rule::LineComment => {}
312            Rule::CommentStart => {
313                if self.comment_nest_count == 0 {
314                    lexer.begin(Mode::Comment);
315                }
316                self.comment_nest_count += 1;
317            }
318            Rule::CommentEnd => {
319                self.comment_nest_count -= 1;
320                if self.comment_nest_count == 0 {
321                    lexer.begin(Mode::Expr);
322                }
323            }
324            Rule::CommentChar | Rule::ExprSpace | Rule::CommentAnyChar => {}
325            Rule::ExprNewLine | Rule::CommentNewLine => {
326                // New line
327            }
328            Rule::LeftParen => {
329                self.nest_count += 1;
330                yield_id(lexer, TokenID::LeftParen);
331            }
332            Rule::RightParen => {
333                self.nest_count -= 1;
334                yield_id(lexer, TokenID::RightParen);
335            }
336            Rule::LeftBrack => {
337                self.nest_count += 1;
338                yield_id(lexer, TokenID::LeftBrack);
339            }
340            Rule::RightBrack => {
341                self.nest_count -= 1;
342                yield_id(lexer, TokenID::RightBrack);
343            }
344            Rule::Comma => {
345                yield_id(lexer, TokenID::Comma);
346            }
347            Rule::Pipe => {
348                yield_id(lexer, TokenID::Pipe);
349            }
350            Rule::RightBrace => {
351                self.nest_count -= 1;
352                self.curly_nest_count -= 1;
353                if self.curly_nest_count >= 0 {
354                    lexer.begin(Mode::Str);
355                    yield_id(lexer, TokenID::RightParen);
356                    let op_tab_idx = arena.lookup_oper("++");
357                    yield_optab(
358                        lexer,
359                        TokenID::AtomOper,
360                        arena.atom("++"),
361                        op_tab_idx,
362                        lexer.span(),
363                    );
364                    lexer.clear();
365                    lexer.accum();
366                } else {
367                    return Err(ParlexError {
368                        message: format!("error on lexeme `}}`"),
369                        span: Some(lexer.span()),
370                    });
371                }
372            }
373            Rule::Func => {
374                self.nest_count += 1;
375                lexer.buffer.pop();
376                let s = self.take_str(lexer)?;
377                let atom = arena.atom(&s);
378                let op_tab_idx = arena.lookup_oper(&s);
379                let op_tab = arena.get_oper(op_tab_idx);
380                if op_tab.is_oper() {
381                    let (has_empty, has_non_empty) =
382                        [Fixity::Prefix, Fixity::Infix, Fixity::Postfix]
383                            .iter()
384                            .filter_map(|f| {
385                                op_tab
386                                    .get_op_def(*f)
387                                    .map(|x| x.args.len() <= OperDef::required_arity(*f))
388                            })
389                            .fold((false, false), |(e, ne), is_empty| {
390                                if is_empty { (true, ne) } else { (e, true) }
391                            });
392
393                    match (has_empty, has_non_empty) {
394                        (false, false) => unreachable!(),
395                        (true, false) => {
396                            yield_optab(lexer, TokenID::AtomOper, atom, op_tab_idx, lexer.span());
397                            yield_id(lexer, TokenID::LeftParen);
398                        }
399                        (false, true) => {
400                            yield_optab(lexer, TokenID::FuncOper, atom, op_tab_idx, lexer.span());
401                        }
402                        (true, true) => {
403                            return Err(ParlexError {
404                                message: format!("arguments conflict in op defs for {:?}", atom),
405                                span: Some(lexer.span()),
406                            });
407                        }
408                    }
409                } else {
410                    yield_optab(lexer, TokenID::Func, atom, op_tab_idx, lexer.span());
411                }
412            }
413            Rule::Var => {
414                let s = self.take_str(lexer)?;
415                yield_term(lexer, TokenID::Var, arena.var(s), lexer.span());
416            }
417            Rule::Atom => {
418                if lexer.buffer == b"." && self.nest_count == 0 {
419                    yield_id(lexer, TokenID::Dot);
420                    yield_id(lexer, TokenID::End);
421                } else {
422                    let s = self.take_str(lexer)?;
423                    let atom = arena.atom(&s);
424                    let op_tab_idx = arena.lookup_oper(&s);
425                    let op_tab = arena.get_oper(op_tab_idx);
426                    if op_tab.is_oper() {
427                        yield_optab(lexer, TokenID::AtomOper, atom, op_tab_idx, lexer.span());
428                    } else {
429                        yield_optab(lexer, TokenID::Atom, atom, op_tab_idx, lexer.span());
430                    }
431                }
432            }
433
434            Rule::DateEpoch => {
435                let mut s = self.take_str(lexer)?;
436                s.pop();
437                s.drain(0..5);
438                let s = s.trim();
439                let d =
440                    parse_i64(s, 10).map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
441                yield_term(lexer, TokenID::Date, arena.date(d), lexer.span());
442            }
443            Rule::Date => {
444                lexer.begin(Mode::Date);
445                lexer.clear();
446                self.buffer2.clear();
447                self.date_format.clear();
448                self.span2 = lexer.span();
449            }
450            Rule::Date1 => {
451                lexer.begin(Mode::Time);
452                self.date_format.push_str("%Y-%m-%d");
453                self.buffer2.extend(&lexer.buffer);
454                self.span2.merge(lexer.span_ref());
455            }
456            Rule::Date2 => {
457                lexer.begin(Mode::Time);
458                self.date_format.push_str("%m/%d/%Y");
459                self.buffer2.extend(&lexer.buffer);
460                self.span2.merge(lexer.span_ref());
461            }
462            Rule::Date3 => {
463                lexer.begin(Mode::Time);
464                self.date_format.push_str("%d-%b-%Y");
465                self.buffer2.extend(&lexer.buffer);
466                self.span2.merge(lexer.span_ref());
467            }
468            Rule::Time1 => {
469                lexer.begin(Mode::Zone);
470                self.date_format.push_str("T%H:%M:%S%.f");
471                self.buffer2.extend(&lexer.buffer);
472                self.span2.merge(lexer.span_ref());
473            }
474            Rule::Time2 => {
475                lexer.begin(Mode::Zone);
476                self.date_format.push_str("T%H:%M:%S");
477                self.buffer2.extend(&lexer.buffer);
478                self.buffer2.extend(b":00");
479                self.span2.merge(lexer.span_ref());
480            }
481            Rule::Time3 => {
482                lexer.begin(Mode::Zone);
483                self.date_format.push_str(" %H:%M:%S%.f");
484                self.buffer2.extend(&lexer.buffer);
485                self.span2.merge(lexer.span_ref());
486            }
487            Rule::Time4 => {
488                lexer.begin(Mode::Zone);
489                self.date_format.push_str(" %H:%M:%S");
490                self.buffer2.extend(&lexer.buffer);
491                self.buffer2.extend(b":00");
492                self.span2.merge(lexer.span_ref());
493            }
494            Rule::Time5 => {
495                lexer.begin(Mode::Zone);
496                self.date_format.push_str(" %I:%M:%S%.f %p");
497                self.buffer2.extend(&lexer.buffer);
498                self.span2.merge(lexer.span_ref());
499            }
500            Rule::Time6 => {
501                lexer.begin(Mode::Zone);
502                self.date_format.push_str(" %I:%M:%S %p");
503                self.buffer2.extend(&lexer.buffer[..lexer.buffer.len() - 3]);
504                self.buffer2.extend(b":00");
505                self.buffer2.extend(&lexer.buffer[lexer.buffer.len() - 3..]);
506                self.span2.merge(lexer.span_ref());
507            }
508            Rule::Zone1 => {
509                if lexer.mode() == Mode::Time {
510                    self.date_format.push_str(" %H:%M:%S");
511                    self.buffer2.extend(b" 00:00:00");
512                }
513                lexer.begin(Mode::Expr);
514                self.date_format.push_str("%:z");
515                self.buffer2.extend(b"+00:00");
516                let s = self.take_str2(lexer)?;
517                let d = parse_date_to_epoch(s.trim_end(), Some(self.date_format.as_str()))?;
518                self.span2.merge(lexer.span_ref());
519                yield_term(lexer, TokenID::Date, arena.date(d), self.span2);
520            }
521            Rule::Zone2 => {
522                self.span2.merge(lexer.span_ref());
523                if lexer.mode() == Mode::Time {
524                    self.date_format.push_str(" %H:%M:%S");
525                    self.buffer2.extend(b" 00:00:00");
526                }
527                lexer.begin(Mode::Expr);
528                if lexer.buffer[0] == b' ' {
529                    self.date_format.push(' ');
530                }
531                self.date_format.push_str("%:z");
532                lexer.buffer.pop();
533                self.buffer2.extend(&lexer.buffer);
534                let s = self.take_str2(lexer)?;
535                let d = parse_date_to_epoch(s.trim_end(), Some(self.date_format.as_str()))?;
536                yield_term(lexer, TokenID::Date, arena.date(d), self.span2);
537            }
538            Rule::TimeRightBrace => {
539                self.span2.merge(lexer.span_ref());
540                lexer.begin(Mode::Expr);
541                self.date_format.push_str(" %H:%M:%S%:z");
542                self.buffer2.extend(b" 00:00:00+00:00");
543                let s = self.take_str2(lexer)?;
544                let d = parse_date_to_epoch(&s, Some(self.date_format.as_str()))?;
545                yield_term(lexer, TokenID::Date, arena.date(d), self.span2);
546            }
547            Rule::ZoneRightBrace => {
548                self.span2.merge(lexer.span_ref());
549                lexer.begin(Mode::Expr);
550                self.date_format.push_str("%:z");
551                self.buffer2.extend(b"+00:00");
552                let s = self.take_str2(lexer)?;
553                let d = parse_date_to_epoch(&s, Some(self.date_format.as_str()))?;
554                yield_term(lexer, TokenID::Date, arena.date(d), self.span2);
555            }
556
557            Rule::Hex => {
558                lexer.begin(Mode::Hex);
559                self.buffer2.clear();
560                self.span2 = lexer.span();
561            }
562            Rule::HexSpace => {
563                self.span2.merge(lexer.span_ref());
564            }
565            Rule::HexNewLine => {
566                // New line
567                self.span2.merge(lexer.span_ref());
568            }
569            Rule::HexByte => {
570                let s = str::from_utf8(&lexer.buffer)
571                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
572                self.span2.merge(lexer.span_ref());
573                let b = u8::from_str_radix(s, 16)
574                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
575                self.buffer2.push(b);
576            }
577            Rule::HexRightBrace => {
578                lexer.buffer.pop();
579                let bytes = self.take_bytes2(lexer);
580                self.span2.merge(lexer.span_ref());
581                yield_term(lexer, TokenID::Bin, arena.bin(bytes), self.span2);
582                lexer.begin(Mode::Expr);
583            }
584            Rule::Bin => {
585                lexer.begin(Mode::Bin);
586                self.span2 = lexer.span();
587            }
588            Rule::Text => {
589                lexer.begin(Mode::Text);
590                self.span2 = lexer.span();
591            }
592            Rule::BinSpace | Rule::TextSpace => {
593                self.span2.merge(lexer.span_ref());
594            }
595            Rule::BinNewLine | Rule::TextNewLine => {
596                // New line
597                self.span2.merge(lexer.span_ref());
598            }
599            r @ (Rule::BinCount | Rule::TextCount) => {
600                let s = str::from_utf8(&lexer.buffer)
601                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
602                let mut s = String::from(s.trim());
603                self.span2.merge(lexer.span_ref());
604                if &s[s.len() - 1..] == "\n" {
605                    // New line
606                }
607                if &s[s.len() - 1..] == ":" {
608                    s.pop();
609                }
610                self.bin_count = s
611                    .parse()
612                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
613                if self.bin_count > 0 {
614                    if r == Rule::BinCount {
615                        lexer.begin(Mode::BinCount);
616                    } else {
617                        lexer.begin(Mode::TextCount);
618                    }
619                    lexer.clear();
620                    lexer.accum();
621                }
622            }
623            r @ (Rule::BinCountAnyChar | Rule::TextCountAnyChar) => {
624                self.span2.merge(lexer.span_ref());
625                self.bin_count -= 1;
626                if self.bin_count == 0 {
627                    self.buffer2.extend(&lexer.buffer);
628                    lexer.clear();
629                    if r == Rule::BinCountAnyChar {
630                        lexer.begin(Mode::Bin);
631                    } else {
632                        lexer.begin(Mode::Text);
633                    }
634                }
635            }
636            r @ (Rule::BinCountNLChar | Rule::TextCountNewLine) => {
637                // New line.
638                // - bin{N:...}: preserve raw bytes verbatim (including \r\n).
639                // - text{N:...}: normalize \r\n → \n to match legacy behavior
640                //   (legacy's `{NL}` rule appends a single "\n" regardless).
641                // Accumulation is on in these modes, so the matched newline is
642                // at the tail of `lexer.buffer`. Check the last two bytes.
643                self.span2.merge(lexer.span_ref());
644                if r == Rule::TextCountNewLine {
645                    let len = lexer.buffer.len();
646                    if len >= 2 && lexer.buffer[len - 2] == b'\r' {
647                        lexer.buffer.truncate(len - 2);
648                        lexer.buffer.push(b'\n');
649                    }
650                }
651                self.bin_count -= 1;
652                if self.bin_count == 0 {
653                    self.buffer2.extend(&lexer.buffer);
654                    lexer.clear();
655                    if r == Rule::BinCountNLChar {
656                        lexer.begin(Mode::Bin);
657                    } else {
658                        lexer.begin(Mode::Text);
659                    }
660                }
661            }
662            r @ (Rule::BinRightBrace | Rule::TextRightBrace) => {
663                self.span2.merge(lexer.span_ref());
664                if r == Rule::BinRightBrace {
665                    let bytes = self.take_bytes2(lexer);
666                    yield_term(lexer, TokenID::Bin, arena.bin(bytes), self.span2);
667                } else {
668                    let s = self.take_str2(lexer)?;
669                    yield_term(lexer, TokenID::Str, arena.str(s), self.span2);
670                }
671                lexer.begin(Mode::Expr);
672            }
673            r @ (Rule::BinLabelStart | Rule::TextLabelStart) => {
674                self.span2.merge(lexer.span_ref());
675
676                self.bin_label.clear();
677                let len = lexer.buffer.len();
678                if lexer.buffer[len - 1] == b'\n' {
679                    // New line
680                    self.bin_label.push(b'\n');
681                    lexer.buffer.pop();
682                    let len = lexer.buffer.len();
683                    if lexer.buffer[len - 1] == b'\r' {
684                        self.bin_label.insert(0, b'\r');
685                        lexer.buffer.pop();
686                    }
687                } else {
688                    let len = lexer.buffer.len();
689                    let b = lexer.buffer[len - 1];
690                    self.bin_label.push(b);
691                    lexer.buffer.pop();
692                }
693
694                let buf = std::mem::take(&mut lexer.buffer);
695                self.bin_label.extend(buf);
696
697                if r == Rule::BinLabelStart {
698                    lexer.begin(Mode::BinLabel);
699                } else {
700                    lexer.begin(Mode::TextLabel);
701                }
702            }
703            r @ (Rule::BinLabelEnd | Rule::TextLabelEnd) => {
704                self.span2.merge(lexer.span_ref());
705
706                if lexer.buffer[0] != b':' {
707                    // New line
708                }
709                if lexer.buffer == self.bin_label {
710                    if r == Rule::BinLabelEnd {
711                        lexer.begin(Mode::Bin);
712                    } else {
713                        lexer.begin(Mode::Text);
714                    }
715                } else {
716                    if r == Rule::TextLabelEnd && lexer.buffer[0] == b'\r' {
717                        lexer.buffer.remove(0);
718                    }
719                    self.buffer2.extend(&lexer.buffer);
720                }
721            }
722            r @ (Rule::BinLabelNLChar | Rule::TextLabelNewLine) => {
723                // New line
724                self.span2.merge(lexer.span_ref());
725
726                if r == Rule::TextLabelNewLine && lexer.buffer[0] == b'\r' {
727                    lexer.buffer.remove(0);
728                }
729                self.buffer2.extend(&lexer.buffer);
730            }
731            Rule::BinLabelAnyChar | Rule::TextLabelAnyChar => {
732                self.span2.merge(lexer.span_ref());
733                self.buffer2.extend(&lexer.buffer);
734            }
735            Rule::LeftBrace => {
736                lexer.begin(Mode::Script);
737                lexer.clear();
738                lexer.accum();
739                self.span2 = lexer.span();
740            }
741            Rule::ScriptNotBraces => {
742                self.span2.merge(lexer.span_ref());
743            }
744            Rule::ScriptLeftBrace => {
745                self.span2.merge(lexer.span_ref());
746                self.script_curly_nest_count += 1;
747            }
748            Rule::ScriptRightBrace => {
749                self.span2.merge(lexer.span_ref());
750                if self.script_curly_nest_count != 0 {
751                    self.script_curly_nest_count -= 1;
752                } else {
753                    lexer.buffer.pop();
754                    let s = self.take_str(lexer)?;
755                    yield_term(lexer, TokenID::Str, arena.str(s), self.span2);
756                    lexer.begin(Mode::Expr);
757                }
758            }
759            Rule::ScriptNewLine => {
760                // Normalize \r\n → \n inside script blocks `{...}` (match legacy).
761                // Accumulation is on, so the matched newline is at the tail of the buffer.
762                self.span2.merge(lexer.span_ref());
763                let len = lexer.buffer.len();
764                if len >= 2 && lexer.buffer[len - 2] == b'\r' {
765                    lexer.buffer.truncate(len - 2);
766                    lexer.buffer.push(b'\n');
767                }
768            }
769            Rule::HexConst => {
770                lexer.buffer.drain(0..2);
771                let s = self.take_str(lexer)?;
772                let val = parse_i64(s.as_str(), 16)
773                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
774                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
775            }
776            Rule::BaseConst => {
777                let s = self.take_str(lexer)?;
778                let (base_str, digits) = s.split_once('\'').ok_or(ParlexError {
779                    message: format!("missing `'` separator"),
780                    span: Some(lexer.span()),
781                })?;
782                let base: u32 = base_str
783                    .parse()
784                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
785                let val = parse_i64(digits, base)
786                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
787                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
788            }
789            Rule::CharHex => {
790                let mut s = self.take_str(lexer)?;
791                s.drain(0..4);
792                let val = parse_i64(s.as_str(), 16)
793                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
794                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
795            }
796            Rule::CharOct => {
797                let mut s = self.take_str(lexer)?;
798                s.drain(0..3);
799                let val = parse_i64(s.as_str(), 8)
800                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
801                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
802            }
803            Rule::CharNewLine1 | Rule::CharNewLine2 | Rule::CharNewLine4 => {
804                // New line
805                yield_term(lexer, TokenID::Int, arena.int('\n' as i64), lexer.span());
806            }
807            Rule::CharNotBackslash => {
808                let mut s = self.take_str(lexer)?;
809                s.drain(0..2);
810                let val = s.chars().next().ok_or(ParlexError {
811                    message: format!("invalid char"),
812                    span: Some(lexer.span()),
813                })? as i64;
814                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
815            }
816            Rule::CharCtrl => {
817                let mut s = self.take_str(lexer)?;
818                s.drain(0..4);
819                let val = s.chars().next().ok_or(ParlexError {
820                    message: format!("invalid char"),
821                    span: Some(lexer.span()),
822                })? as i64
823                    - '@' as i64;
824                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
825            }
826            Rule::CharDel1 | Rule::CharDel2 => {
827                yield_term(lexer, TokenID::Int, arena.int('\x7F' as i64), lexer.span());
828            }
829            Rule::CharEsc => {
830                yield_term(lexer, TokenID::Int, arena.int('\x1B' as i64), lexer.span());
831            }
832            Rule::CharBell => {
833                yield_term(
834                    lexer,
835                    TokenID::Int,
836                    arena.int('\u{0007}' as i64),
837                    lexer.span(),
838                );
839            }
840            Rule::CharBackspace => {
841                yield_term(
842                    lexer,
843                    TokenID::Int,
844                    arena.int('\u{0008}' as i64),
845                    lexer.span(),
846                );
847            }
848            Rule::CharFormFeed => {
849                yield_term(
850                    lexer,
851                    TokenID::Int,
852                    arena.int('\u{000C}' as i64),
853                    lexer.span(),
854                );
855            }
856            Rule::CharNewLine3 => {
857                yield_term(lexer, TokenID::Int, arena.int('\n' as i64), lexer.span());
858            }
859            Rule::CharCarriageReturn => {
860                yield_term(lexer, TokenID::Int, arena.int('\r' as i64), lexer.span());
861            }
862            Rule::CharTab => {
863                yield_term(lexer, TokenID::Int, arena.int('\t' as i64), lexer.span());
864            }
865            Rule::CharVerticalTab => {
866                yield_term(
867                    lexer,
868                    TokenID::Int,
869                    arena.int('\u{000B}' as i64),
870                    lexer.span(),
871                );
872            }
873            Rule::CharAny => {
874                let mut s = self.take_str(lexer)?;
875                s.drain(0..3);
876                let val = s.chars().next().ok_or(ParlexError {
877                    message: format!("invalid char"),
878                    span: Some(lexer.span()),
879                })? as i64;
880                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
881            }
882            Rule::OctConst => {
883                let s = self.take_str(lexer)?;
884                let val = parse_i64(s.as_str(), 8)
885                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
886                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
887            }
888            Rule::DecConst => {
889                let s = self.take_str(lexer)?;
890                let val = parse_i64(s.as_str(), 10)
891                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
892                yield_term(lexer, TokenID::Int, arena.int(val), lexer.span());
893            }
894            Rule::FPConst => {
895                let s = self.take_str(lexer)?;
896                let val: f64 = s
897                    .parse()
898                    .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
899                yield_term(lexer, TokenID::Real, arena.real(val), lexer.span());
900            }
901            Rule::DoubleQuote => {
902                // Wrap the whole string in `( ... )` to match legacy emission.
903                // This isolates interpolated strings from surrounding operators
904                // at the same precedence as `++` but with different associativity.
905                // For bare strings, the unary tuple is unwrapped by the parser.
906                self.nest_count += 1;
907                yield_id(lexer, TokenID::LeftParen);
908                lexer.begin(Mode::Str);
909                lexer.clear();
910                lexer.accum();
911            }
912            Rule::SingleQuote => {
913                lexer.begin(Mode::Atom);
914                lexer.clear();
915                lexer.accum();
916            }
917            Rule::StrAtomCharHex => {
918                let len = lexer.buffer.len();
919                let b: u8 = parse_i64(
920                    str::from_utf8(&lexer.buffer[len - 2..])
921                        .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?,
922                    16,
923                )
924                .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?
925                .try_into()
926                .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
927                lexer.buffer.truncate(len - 4);
928                lexer.buffer.push(b);
929            }
930            Rule::StrAtomCharOct => {
931                let slash_pos = lexer.buffer.iter().rposition(|&b| b == b'\\').unwrap();
932                let b: u8 = parse_i64(
933                    str::from_utf8(&lexer.buffer[slash_pos + 1..])
934                        .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?,
935                    8,
936                )
937                .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?
938                .try_into()
939                .map_err(|e| ParlexError::from_err(e, Some(lexer.span())))?;
940                lexer.buffer.truncate(slash_pos);
941                lexer.buffer.push(b);
942            }
943            Rule::StrAtomCharCtrl => {
944                let len = lexer.buffer.len();
945                let b = lexer.buffer[len - 1] - b'@';
946                lexer.buffer.truncate(len - 3);
947                lexer.buffer.push(b);
948            }
949            Rule::StrAtomCharDel1 => {
950                let idx = lexer.buffer.len() - 2;
951                lexer.buffer.truncate(idx);
952                lexer.buffer.push(b'\x7F');
953            }
954            Rule::StrAtomCharDel2 => {
955                let idx = lexer.buffer.len() - 3;
956                lexer.buffer.truncate(idx);
957                lexer.buffer.push(b'\x7F');
958            }
959            Rule::StrAtomCharEsc => {
960                let idx = lexer.buffer.len() - 2;
961                lexer.buffer.truncate(idx);
962                lexer.buffer.push(b'\x1B');
963            }
964            Rule::StrAtomCharBell => {
965                let idx = lexer.buffer.len() - 2;
966                lexer.buffer.truncate(idx);
967                lexer.buffer.push(b'\x07');
968            }
969            Rule::StrAtomCharBackspace => {
970                let idx = lexer.buffer.len() - 2;
971                lexer.buffer.truncate(idx);
972                lexer.buffer.push(b'\x08');
973            }
974            Rule::StrAtomCharFormFeed => {
975                let idx = lexer.buffer.len() - 2;
976                lexer.buffer.truncate(idx);
977                lexer.buffer.push(b'\x0C');
978            }
979            Rule::StrAtomCharNewLine => {
980                let idx = lexer.buffer.len() - 2;
981                lexer.buffer.truncate(idx);
982                lexer.buffer.push(b'\n');
983            }
984            Rule::StrAtomCharCarriageReturn => {
985                let idx = lexer.buffer.len() - 2;
986                lexer.buffer.truncate(idx);
987                lexer.buffer.push(b'\r');
988            }
989            Rule::StrAtomCharTab => {
990                let idx = lexer.buffer.len() - 2;
991                lexer.buffer.truncate(idx);
992                lexer.buffer.push(b'\t');
993            }
994            Rule::StrAtomVerticalTab => {
995                let idx = lexer.buffer.len() - 2;
996                lexer.buffer.truncate(idx);
997                lexer.buffer.push(b'\x0B');
998            }
999            Rule::StrAtomCharSkipNewLine => {
1000                // New line
1001                lexer.buffer.pop();
1002                let idx = lexer.buffer.len() - 1;
1003                if lexer.buffer[idx] == b'\r' {
1004                    lexer.buffer.pop();
1005                }
1006                lexer.buffer.pop();
1007            }
1008            Rule::StrAtomCharAny | Rule::StrAtomCharBackslash => {
1009                let idx = lexer.buffer.len() - 2;
1010                lexer.buffer.remove(idx);
1011            }
1012            Rule::StrChar | Rule::AtomChar | Rule::StrAtomCarriageReturn => {}
1013            Rule::StrDoubleQuote => {
1014                lexer.begin(Mode::Expr);
1015                lexer.buffer.pop();
1016                let s = self.take_str(lexer)?;
1017                yield_term(lexer, TokenID::Str, arena.str(s), lexer.span());
1018                // Close the outer wrap opened by `DoubleQuote`.
1019                self.nest_count -= 1;
1020                yield_id(lexer, TokenID::RightParen);
1021            }
1022            Rule::AtomSingleQuote => {
1023                lexer.begin(Mode::Expr);
1024                lexer.buffer.pop();
1025                let s = self.take_str(lexer)?;
1026                yield_term(lexer, TokenID::Atom, arena.atom(s), lexer.span());
1027            }
1028            Rule::AtomLeftParen => {
1029                lexer.begin(Mode::Expr);
1030                self.nest_count += 1;
1031                let mut s = self.take_str(lexer)?;
1032                s.truncate(s.len() - 2);
1033                yield_term(lexer, TokenID::Func, arena.atom(s), lexer.span());
1034            }
1035            Rule::AtomLeftBrace => {}
1036            Rule::StrLeftBrace => {
1037                lexer.begin(Mode::Expr);
1038                self.nest_count += 1;
1039                self.curly_nest_count += 1;
1040                let mut s = self.take_str(lexer)?;
1041                s.pop();
1042                yield_term(lexer, TokenID::Str, arena.str(s), lexer.span());
1043                let op_tab_idx = arena.lookup_oper("++");
1044                yield_optab(
1045                    lexer,
1046                    TokenID::AtomOper,
1047                    arena.atom("++"),
1048                    op_tab_idx,
1049                    lexer.span(),
1050                );
1051                yield_id(lexer, TokenID::LeftParen);
1052            }
1053            Rule::StrAtomNewLine => {
1054                // Normalize \r\n → \n inside quoted strings/atoms (matches legacy behavior).
1055                // The DFA pattern `\r?\n` pushed either 1 or 2 bytes to the buffer.
1056                // If the last two bytes are `\r\n`, replace with a single `\n`.
1057                let len = lexer.buffer.len();
1058                if len >= 2 && lexer.buffer[len - 2] == b'\r' {
1059                    lexer.buffer.truncate(len - 2);
1060                    lexer.buffer.push(b'\n');
1061                }
1062            }
1063            Rule::Error => {
1064                let s = self.take_str(lexer)?;
1065                return Err(ParlexError {
1066                    message: format!("error on lexeme {:?}", s),
1067                    span: Some(lexer.span()),
1068                });
1069            }
1070            Rule::End => {
1071                if lexer.mode() == Mode::Expr {
1072                    yield_id(lexer, TokenID::End);
1073                } else {
1074                    return Err(ParlexError {
1075                        message: format!("unexpected end of stream"),
1076                        span: Some(lexer.span()),
1077                    });
1078                }
1079            }
1080        }
1081
1082        log::trace!(
1083            "ACTION end:   mode {:?}, rule {:?}, buf {:?}, buf2 {:?}, label {:?}, accum {}",
1084            lexer.mode(),
1085            rule,
1086            std::string::String::from_utf8_lossy(&lexer.buffer),
1087            std::string::String::from_utf8_lossy(&self.buffer2),
1088            std::string::String::from_utf8_lossy(&self.bin_label),
1089            lexer.accum_flag,
1090        );
1091
1092        Ok(())
1093    }
1094}
1095
1096/// The lexer for Prolog-like terms.
1097///
1098/// `TermLexer` tokenizes input stream into [`TermToken`]s using DFA tables
1099/// generated by **parlex-gen**’s [`alex`] tool. It maintains lexer state,
1100/// manages nested constructs, and recognizes operators defined in [`OperDefs`].
1101///
1102/// `TermLexer<I>` adapts a byte-oriented input stream `I` (that supports
1103/// contextual access to an [`Arena`]) into an iterator-like interface that
1104/// yields [`TermToken`]s. Internally, it owns a lower-level [`Lexer`] driven by
1105/// [`TermLexerDriver`], which handles rule actions (e.g., interning terms,
1106/// parsing numbers, skipping comments/whitespace).
1107///
1108/// The generic parameter `I` must implement
1109/// [`TryNextWithContext<Item = u8, Context = Arena>`], allowing the lexer to
1110/// pull bytes and intern terms while tokenizing.
1111///
1112/// # Type Parameters
1113/// - `I`: The input source implementing [`TryNextWithContext<Arena>`] over bytes.
1114///
1115/// # Output
1116///
1117/// Each successful step yields a [`TermToken`], which carries:
1118/// - a token kind ([`TokenID`]),
1119/// - an optional payload ([`Value`]),
1120/// - a 1-based line number (`line_no`),
1121/// - an optional index into operator definition table.
1122///
1123/// # Errors
1124///
1125/// Methods return a [`LexerError<I::Error, TermParseError>`], where:
1126/// - `I::Error` is any error produced by the underlying input,
1127/// - [`TermParseError`] covers lexical/parsing/UTF-8/term errors.
1128///
1129/// # Example
1130///
1131/// ```rust
1132/// # use arena_terms_parser::{Encoding, TermToken, TermLexer, TokenID, Value};
1133/// # use arena_terms::{Arena};
1134/// # use try_next::{IterInput, TryNextWithContext};
1135/// let mut arena = Arena::new();
1136/// let input = IterInput::from("hello\n +\n world\n\n123".bytes());
1137/// let mut lexer = TermLexer::try_new(input, Encoding::Utf8).unwrap();
1138/// let vs = lexer.try_collect_with_context(&mut arena).unwrap();
1139/// assert_eq!(vs.len(), 5);
1140/// ```
1141///
1142/// [`TermToken`]: crate::lexer::TermToken
1143/// [`OperDefs`]: crate::oper::OperDefs
1144/// [`alex`]: https://crates.io/crates/parlex-gen
1145pub struct TermLexer<I>
1146where
1147    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
1148{
1149    /// The underlying DFA/engine that drives tokenization, parameterized by the
1150    /// input `I` and the driver that executes rule actions.
1151    pub(crate) lexer: Lexer<I, TermLexerDriver<I>, Arena>,
1152}
1153
1154impl<I> TermLexer<I>
1155where
1156    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
1157{
1158    /// # Parameters
1159    /// - `input`: The input byte stream to be lexed.
1160    /// - `opers`: Optional operator definitions ([`OperDefs`]) used to
1161    ///   recognize operator tokens by fixity and precedence.
1162    ///   If `None`, an empty operator table is created.
1163    ///
1164    /// # Returns
1165    /// A ready-to-use [`TermLexer`] instance, or an error if the underlying
1166    /// [`LexerCtx`] initialization fails.
1167    ///
1168    /// # Errors
1169    /// Returns an error if DFA table deserialization in [`LexerCtx::try_new`]
1170    /// fails or the input cannot be processed.
1171
1172    /// Constructs a new term lexer from the given input stream and optional
1173    /// operator definition table.
1174    ///
1175    /// This initializes an internal [`Lexer`] with a [`TermLexerDriver`] that
1176    /// performs rule actions such as:
1177    /// - interning identifiers into the provided [`Arena`] (via context),
1178    /// - converting matched byte slices into numbers/idents,
1179    /// - tracking line numbers and comment nesting.
1180    ///
1181    /// # Parameters
1182    /// - `input`: The input byte stream to be lexed.
1183    ///
1184    /// # Returns
1185    /// A ready-to-use [`TermLexer`] instance, or an error if the underlying
1186    /// initializations failed.
1187    ///
1188    /// # Errors
1189    /// Returns a [`LexerError`] if the lexer cannot be constructed from the
1190    /// given input and operatir table.
1191    pub fn try_new(input: I, encoding: Encoding) -> Result<Self, ParlexError> {
1192        let driver = TermLexerDriver {
1193            _marker: PhantomData,
1194            encoding,
1195            nest_count: 0,
1196            comment_nest_count: 0,
1197            curly_nest_count: 0,
1198            script_curly_nest_count: 0,
1199            bin_count: 0,
1200            bin_label: Vec::new(),
1201            date_format: String::new(),
1202            buffer2: Vec::new(),
1203            span2: Span::default(),
1204        };
1205        let lexer = Lexer::try_new(input, driver)?;
1206        Ok(Self { lexer })
1207    }
1208}
1209
1210impl<I> TryNextWithContext<Arena, LexerStats> for TermLexer<I>
1211where
1212    I: TryNextWithContext<Arena, Item = u8, Error: std::fmt::Display + 'static>,
1213{
1214    /// Tokens produced by this lexer.
1215    type Item = TermToken;
1216
1217    /// Unified error type.
1218    type Error = ParlexError;
1219
1220    /// Advances the lexer and returns the next token, or `None` at end of input.
1221    ///
1222    /// The provided `context` (an [`Arena`]) may be mutated by rule
1223    /// actions (for example, to intern terms). This method is fallible;
1224    /// both input and lexical errors are converted into [`Self::Error`].
1225    ///
1226    /// # End of Input
1227    ///
1228    /// When the lexer reaches the end of the input stream, it will typically
1229    /// emit a final [`TokenID::End`] token before returning `None`.
1230    ///
1231    /// This explicit *End* token is expected by the **Parlex parser** to
1232    /// signal successful termination of a complete parsing unit.
1233    /// Consumers should treat this token as a logical *end-of-sentence* or
1234    /// *end-of-expression* marker, depending on the grammar.
1235    ///
1236    /// If the input contains **multiple independent sentences or expressions**,
1237    /// the lexer may emit multiple `End` tokens—one after each completed unit.
1238    /// In such cases, the parser can restart or resume parsing after each `End`
1239    /// to produce multiple parse results from a single input stream.
1240    ///
1241    /// Once all input has been consumed, the lexer returns `None`.
1242    fn try_next_with_context(
1243        &mut self,
1244        context: &mut Arena,
1245    ) -> Result<Option<TermToken>, ParlexError> {
1246        self.lexer.try_next_with_context(context)
1247    }
1248
1249    fn stats(&self) -> LexerStats {
1250        self.lexer.stats()
1251    }
1252}
1253
1254/// Unit tests for the [`TermLexer`] implementation.
1255#[cfg(test)]
1256mod tests {
1257    use arena_terms::View;
1258    use parlex::Token;
1259    use try_next::IterInput;
1260
1261    use super::*;
1262
1263    fn lex(arena: &mut Arena, s: &str) -> Vec<TermToken> {
1264        let input = IterInput::from(s.bytes());
1265        let mut lexer = TermLexer::try_new(input, Encoding::Utf8).expect("cannot create lexer");
1266        lexer.try_collect_with_context(arena).expect("lexer error")
1267    }
1268
1269    #[test]
1270    fn test_dates() {
1271        let _ = env_logger::builder().is_test(true).try_init();
1272        let mut arena = Arena::new();
1273        const DATES: &[(&str, u8)] = &[
1274            ("date{-5381856000000}", 0),
1275            ("date{-5381830320000}", 1),
1276            ("date{-5381830311000}", 2),
1277            ("date{-5381830310999}", 3),
1278            ("date{1799-06-16}", 0),
1279            ("date{1799-06-16Z}", 0),
1280            ("date{1799-06-16 Z}", 0),
1281            ("date{1799-06-16-00:00}", 0),
1282            ("date{1799-06-16 -00:00}", 0),
1283            ("date{1799-06-16T07:08}", 1),
1284            ("date{1799-06-16T07:08:09}", 2),
1285            ("date{1799-06-16T07:08:09Z}", 2),
1286            ("date{1799-06-16T07:08:09.001Z}", 3),
1287            ("date{1799-06-16T07:08:09 Z}", 2),
1288            ("date{1799-06-16T07:08:09.001 Z}", 3),
1289            ("date{1799-06-16T07:08:09+00:00}", 2),
1290            ("date{1799-06-16T07:08:09.001+00:00}", 3),
1291            ("date{1799-06-16T07:08:09 +00:00}", 2),
1292            ("date{1799-06-16T07:08:09.001 +00:00}", 3),
1293            ("date{1799-06-16T07:08:09Z}", 2),
1294            ("date{1799-06-16T07:08:09.001Z}", 3),
1295            ("date{1799-06-16 07:08:09 Z}", 2),
1296            ("date{1799-06-16T07:08:09.001 Z}", 3),
1297            ("date{1799-06-16 07:08:09+00:00}", 2),
1298            ("date{1799-06-16T07:08:09.001+00:00}", 3),
1299            ("date{1799-06-16 07:08:09 +00:00}", 2),
1300            ("date{1799-06-16 07:08:09.001 +00:00}", 3),
1301            ("date{1799-06-16T07:08Z}", 1),
1302            ("date{1799-06-16T07:08 Z  }", 1),
1303            ("date{  1799-06-16T07:08+00:00}", 1),
1304            ("date{ 1799-06-16T07:08 +00:00   }", 1),
1305            ("date{06/16/1799Z}", 0),
1306            ("date{06/16/1799 Z}", 0),
1307            ("date{06/16/1799+00:00}", 0),
1308            ("date{06/16/1799 +00:00}", 0),
1309            ("date{06/16/1799 07:08Z}", 1),
1310            ("date{06/16/1799 07:08:09Z}", 2),
1311            ("date{06/16/1799 07:08:09.001Z}", 3),
1312            ("date{06/16/1799 07:08 Z}", 1),
1313            ("date{06/16/1799 07:08:09 Z}", 2),
1314            ("date{06/16/1799 07:08:09.001 Z}", 3),
1315            ("date{06/16/1799 07:08+00:00}", 1),
1316            ("date{06/16/1799 07:08:09+00:00}", 2),
1317            ("date{06/16/1799 07:08:09.001+00:00}", 3),
1318            ("date{06/16/1799 07:08 +00:00}", 1),
1319            ("date{06/16/1799 07:08:09 +00:00}", 2),
1320            ("date{06/16/1799 07:08:09.001 +00:00}", 3),
1321            ("date{16-Jun-1799Z}", 0),
1322            ("date{16-jun-1799 Z}", 0),
1323            ("date{16-JUN-1799+00:00}", 0),
1324            ("date{16-Jun-1799 +00:00}", 0),
1325            ("date{16-Jun-1799 07:08Z}", 1),
1326            ("date{16-JUN-1799 07:08:09Z}", 2),
1327            ("date{16-Jun-1799 07:08:09.001Z}", 3),
1328            ("date{16-Jun-1799 07:08 Z}", 1),
1329            ("date{16-jun-1799 07:08:09 Z}", 2),
1330            ("date{16-Jun-1799 07:08:09.001 Z}", 3),
1331            ("date{16-Jun-1799 07:08+00:00}", 1),
1332            ("date{16-Jun-1799 07:08:09+00:00}", 2),
1333            ("date{16-Jun-1799 07:08:09.001+00:00}", 3),
1334            ("date{16-Jun-1799 07:08 +00:00}", 1),
1335            ("date{16-Jun-1799 07:08:09 +00:00}", 2),
1336            ("date{16-Jun-1799 07:08:09.001 +00:00}", 3),
1337        ];
1338        for (s, k) in DATES {
1339            let mut ts = lex(&mut arena, s);
1340            let tok = ts.remove(0);
1341            assert_eq!(tok.token_id, TokenID::Date);
1342            let term = Term::try_from(tok.value).unwrap();
1343            let d = term.unpack_date(&arena).unwrap();
1344            assert_eq!(
1345                d,
1346                match k {
1347                    0 => -5381856000000,
1348                    1 => -5381830320000,
1349                    2 => -5381830311000,
1350                    3 => -5381830310999,
1351                    _ => unreachable!(),
1352                }
1353            );
1354        }
1355    }
1356
1357    #[test]
1358    fn test_atoms() {
1359        let mut arena = Arena::new();
1360        let ts = lex(&mut arena, "\na+foo-x '^&%^&%^&%''abc' 'AAA'");
1361        dbg!(&ts);
1362        assert!(ts.len() == 9);
1363        assert!(ts.iter().take(ts.len() - 1).all(|t| {
1364            t.span().unwrap().start.line == 1
1365                && matches!(
1366                    Term::try_from(t.value.clone())
1367                        .unwrap()
1368                        .view(&arena)
1369                        .unwrap(),
1370                    View::Atom(_)
1371                )
1372        }));
1373    }
1374
1375    #[test]
1376    fn test_bin() {
1377        let mut arena = Arena::new();
1378        let ts = lex(
1379            &mut arena,
1380            "% single line comment\nbin{3:\x00\x01\x02 eob:\x00\x01:aaa\x02:eob eob\n\x00\neob eob\r\n\x00\r\neob\r\n}\r\nhex{   0203 0405 FE }",
1381        );
1382        dbg!(&ts);
1383        assert!(ts.len() == 3);
1384        assert!(matches!(
1385            Term::try_from(ts[0].value.clone())
1386                .unwrap()
1387                .view(&arena)
1388                .unwrap(),
1389            View::Bin(_)
1390        ));
1391        match Term::try_from(ts[0].value.clone())
1392            .unwrap()
1393            .view(&arena)
1394            .unwrap()
1395        {
1396            View::Bin(bytes) => assert!(bytes == &[0, 1, 2, 0, 1, 58, 97, 97, 97, 2, 0, 0,]),
1397            _ => unreachable!(),
1398        }
1399    }
1400
1401    #[test]
1402    fn test_text() {
1403        let mut arena = Arena::new();
1404        let ts = lex(
1405            &mut arena,
1406            "/* single /* line */ comment */\ntext{3:abc eob:de:aaa:eob eob\n0\neob eob\r\n1\r\neob\r\n}\r\n",
1407        );
1408        dbg!(&ts);
1409        assert!(ts.len() == 2);
1410        assert!(matches!(
1411            Term::try_from(ts[0].value.clone())
1412                .unwrap()
1413                .view(&arena)
1414                .unwrap(),
1415            View::Str(_)
1416        ));
1417        match Term::try_from(ts[0].value.clone())
1418            .unwrap()
1419            .view(&arena)
1420            .unwrap()
1421        {
1422            View::Str(s) => assert!(s == "abcde:aaa01"),
1423            _ => unreachable!(),
1424        }
1425    }
1426
1427    #[test]
1428    fn test_texts() {
1429        let mut arena = Arena::new();
1430        let ts = lex(
1431            &mut arena,
1432            "/* single [ ( { /* line */ comment */\n\"hello\" {hello} text{5:hello} text{e:hello:e} text{e:h:e e:e:e 2:ll e:o:e} text{\ne\nhello\ne}",
1433        );
1434        dbg!(&ts);
1435        // "hello" emits ( STR ), so we get 6 string-bearing tokens + paren pair + End.
1436        // Token sequence: ( STR(hello) ) STR(hello) STR(hello) STR(hello) STR(hello) STR(hello) End
1437        assert!(ts.len() == 9);
1438        // Filter to just tokens that carry a Str term.
1439        let strs: Vec<&TermToken> = ts
1440            .iter()
1441            .filter(|t| {
1442                matches!(t.token_id, TokenID::Str)
1443            })
1444            .collect();
1445        assert_eq!(strs.len(), 6);
1446        assert!(strs.iter().all(|t| {
1447            match Term::try_from(t.value.clone())
1448                .unwrap()
1449                .view(&arena)
1450                .unwrap()
1451            {
1452                View::Str(s) => s == "hello",
1453                _ => false,
1454            }
1455        }));
1456    }
1457
1458    #[test]
1459    fn test_integers() {
1460        let mut arena = Arena::new();
1461        let ts = lex(&mut arena, "[2'01010001111, 10'123, 36'AZ]");
1462        assert!(ts.len() == 8);
1463        assert!(matches!(ts[1].token_id, TokenID::Int));
1464    }
1465
1466    #[test]
1467    fn lex_string_subs() {
1468        let _ = env_logger::builder().is_test(true).try_init();
1469        let arena = &mut Arena::new();
1470        let ts = lex(arena, "\"aaa{1 + 2}bbb{3 * 4}ccc\"");
1471        // 20 tokens: ( STR(aaa) ++ ( 1 + 2 ) ++ STR(bbb) ++ ( 3 * 4 ) ++ STR(ccc) ) End
1472        assert_eq!(ts.len(), 20);
1473        assert_eq!(ts[0].token_id, TokenID::LeftParen);
1474        assert_eq!(ts[18].token_id, TokenID::RightParen);
1475        let t0: Term = ts[1].value.clone().try_into().unwrap();
1476        let t1: Term = ts[9].value.clone().try_into().unwrap();
1477        let t2: Term = ts[17].value.clone().try_into().unwrap();
1478        assert_eq!(t0.unpack_str(arena).unwrap(), "aaa");
1479        assert_eq!(t1.unpack_str(arena).unwrap(), "bbb");
1480        assert_eq!(t2.unpack_str(arena).unwrap(), "ccc");
1481    }
1482
1483    /// String literals preserve bare LF — matches legacy behavior.
1484    #[test]
1485    fn string_preserves_lf() {
1486        let mut arena = Arena::new();
1487        let ts = lex(&mut arena, "\"hello\nworld\"");
1488        // 4 tokens: ( STR ) End
1489        assert_eq!(ts.len(), 4);
1490        let t: Term = ts[1].value.clone().try_into().unwrap();
1491        assert_eq!(t.unpack_str(&arena).unwrap(), "hello\nworld");
1492    }
1493
1494    /// String literals normalize CRLF to LF — matches legacy behavior.
1495    #[test]
1496    fn string_normalizes_crlf_to_lf() {
1497        let mut arena = Arena::new();
1498        let ts = lex(&mut arena, "\"hello\r\nworld\"");
1499        assert_eq!(ts.len(), 4);
1500        let t: Term = ts[1].value.clone().try_into().unwrap();
1501        assert_eq!(t.unpack_str(&arena).unwrap(), "hello\nworld");
1502    }
1503
1504    /// String literals preserve bare CR (not followed by LF) — matches legacy behavior.
1505    #[test]
1506    fn string_preserves_bare_cr() {
1507        let mut arena = Arena::new();
1508        let ts = lex(&mut arena, "\"hello\rworld\"");
1509        assert_eq!(ts.len(), 4);
1510        let t: Term = ts[1].value.clone().try_into().unwrap();
1511        assert_eq!(t.unpack_str(&arena).unwrap(), "hello\rworld");
1512    }
1513
1514    /// Quoted atoms preserve bare LF — matches legacy behavior.
1515    #[test]
1516    fn atom_preserves_lf() {
1517        let mut arena = Arena::new();
1518        let ts = lex(&mut arena, "'hello\nworld'");
1519        assert_eq!(ts.len(), 2);
1520        let t: Term = ts[0].value.clone().try_into().unwrap();
1521        assert_eq!(t.unpack_atom(&arena, &[]).unwrap(), "hello\nworld");
1522    }
1523
1524    /// Quoted atoms normalize CRLF to LF — matches legacy behavior.
1525    #[test]
1526    fn atom_normalizes_crlf_to_lf() {
1527        let mut arena = Arena::new();
1528        let ts = lex(&mut arena, "'hello\r\nworld'");
1529        assert_eq!(ts.len(), 2);
1530        let t: Term = ts[0].value.clone().try_into().unwrap();
1531        assert_eq!(t.unpack_atom(&arena, &[]).unwrap(), "hello\nworld");
1532    }
1533
1534    /// Quoted atoms preserve bare CR — matches legacy behavior.
1535    #[test]
1536    fn atom_preserves_bare_cr() {
1537        let mut arena = Arena::new();
1538        let ts = lex(&mut arena, "'hello\rworld'");
1539        assert_eq!(ts.len(), 2);
1540        let t: Term = ts[0].value.clone().try_into().unwrap();
1541        assert_eq!(t.unpack_atom(&arena, &[]).unwrap(), "hello\rworld");
1542    }
1543
1544    /// CR at the very start of a string (adjacent to opening quote) is preserved.
1545    #[test]
1546    fn string_preserves_cr_at_start() {
1547        let mut arena = Arena::new();
1548        let ts = lex(&mut arena, "\"\rhello\"");
1549        assert_eq!(ts.len(), 4);
1550        let t: Term = ts[1].value.clone().try_into().unwrap();
1551        assert_eq!(t.unpack_str(&arena).unwrap(), "\rhello");
1552    }
1553
1554    /// CRLF at the very start of a string normalizes to LF.
1555    #[test]
1556    fn string_normalizes_crlf_at_start() {
1557        let mut arena = Arena::new();
1558        let ts = lex(&mut arena, "\"\r\nhello\"");
1559        assert_eq!(ts.len(), 4);
1560        let t: Term = ts[1].value.clone().try_into().unwrap();
1561        assert_eq!(t.unpack_str(&arena).unwrap(), "\nhello");
1562    }
1563
1564    /// Script block `{...}` normalizes CRLF to LF — matches legacy.
1565    #[test]
1566    fn script_block_normalizes_crlf() {
1567        let mut arena = Arena::new();
1568        let ts = lex(&mut arena, "{foo\r\nbar}");
1569        assert_eq!(ts.len(), 2);
1570        let t: Term = ts[0].value.clone().try_into().unwrap();
1571        assert_eq!(t.unpack_str(&arena).unwrap(), "foo\nbar");
1572    }
1573
1574    /// Script block preserves bare LF.
1575    #[test]
1576    fn script_block_preserves_lf() {
1577        let mut arena = Arena::new();
1578        let ts = lex(&mut arena, "{foo\nbar}");
1579        assert_eq!(ts.len(), 2);
1580        let t: Term = ts[0].value.clone().try_into().unwrap();
1581        assert_eq!(t.unpack_str(&arena).unwrap(), "foo\nbar");
1582    }
1583
1584    /// bin{N:...} preserves ALL raw bytes including CRLF sequences.
1585    /// Legacy: `bin{2:\r\n}` → [13, 10] (2 bytes, `\r\n` preserved).
1586    #[test]
1587    fn bin_counted_preserves_leading_crlf() {
1588        let mut arena = Arena::new();
1589        let ts = lex(&mut arena, "bin{2:\r\n}");
1590        assert_eq!(ts.len(), 2);
1591        let t: Term = ts[0].value.clone().try_into().unwrap();
1592        match t.view(&arena).unwrap() {
1593            View::Bin(bytes) => assert_eq!(bytes, &[0x0D, 0x0A]),
1594            v => panic!("expected Bin, got {:?}", v),
1595        }
1596    }
1597
1598    /// bin{N:A\r\n} — CRLF after an ASCII byte preserved verbatim.
1599    #[test]
1600    fn bin_counted_preserves_nonleading_crlf() {
1601        let mut arena = Arena::new();
1602        let ts = lex(&mut arena, "bin{3:A\r\n}");
1603        assert_eq!(ts.len(), 2);
1604        let t: Term = ts[0].value.clone().try_into().unwrap();
1605        match t.view(&arena).unwrap() {
1606            View::Bin(bytes) => assert_eq!(bytes, &[0x41, 0x0D, 0x0A]),
1607            v => panic!("expected Bin, got {:?}", v),
1608        }
1609    }
1610
1611    /// text{N:...} normalizes CRLF to LF — matches legacy.
1612    /// Legacy's `{NL}` rule always appends a single "\n".
1613    /// `text{1:\r\n}` → "\n" (1 char).
1614    #[test]
1615    fn text_counted_normalizes_leading_crlf() {
1616        let mut arena = Arena::new();
1617        let ts = lex(&mut arena, "text{1:\r\n}");
1618        assert_eq!(ts.len(), 2);
1619        let t: Term = ts[0].value.clone().try_into().unwrap();
1620        assert_eq!(t.unpack_str(&arena).unwrap(), "\n");
1621    }
1622
1623    /// text{N:A\r\n} — CRLF after an ASCII byte normalizes to LF.
1624    /// Legacy `text{2:A\r\n}` → "A\n" (2 chars).
1625    #[test]
1626    fn text_counted_normalizes_nonleading_crlf() {
1627        let mut arena = Arena::new();
1628        let ts = lex(&mut arena, "text{2:A\r\n}");
1629        assert_eq!(ts.len(), 2);
1630        let t: Term = ts[0].value.clone().try_into().unwrap();
1631        assert_eq!(t.unpack_str(&arena).unwrap(), "A\n");
1632    }
1633
1634    /// Multiple CRLF sequences all normalize to LF.
1635    #[test]
1636    fn string_normalizes_multiple_crlf() {
1637        let mut arena = Arena::new();
1638        let ts = lex(&mut arena, "\"a\r\nb\r\nc\"");
1639        assert_eq!(ts.len(), 4);
1640        let t: Term = ts[1].value.clone().try_into().unwrap();
1641        assert_eq!(t.unpack_str(&arena).unwrap(), "a\nb\nc");
1642    }
1643}