arena_terms_parser/
oper.rs

1//! Operator definitions and precedence handling.
2//!
3//! This module defines enums and utilities for representing and managing
4//! operator fixity, associativity, and precedence in the arena terms parser.
5//! Operators may appear in functional (`fun`), prefix, infix, or postfix
6//! positions, and each is characterized by its [`Fixity`] and [`Assoc`].
7//!
8//! # Overview
9//! Operators in Prolog-like syntax are context-sensitive and may serve multiple
10//! syntactic roles depending on precedence and associativity. This module provides
11//! data structures and functions for registering, validating, and querying
12//! operator definitions used during parsing.
13//!
14//! # Components
15//! - [`Fixity`]: Enumerates operator positions (`Fun`, `Prefix`, `Infix`, `Postfix`).
16//! - [`Assoc`]: Describes associativity (`Left`, `Right`, `NonAssoc`).
17//! - [`OperDefs`]: Stores and manages operator definitions, providing fast lookup
18//!   by name and fixity.
19//!
20//! # Usage
21//! Operator definitions are typically registered at parser initialization,
22//! enabling grammar-aware expression parsing and disambiguation.
23//!
24//! # See Also
25//! - [`crate::parser`]: Consumes operator definitions during expression parsing.
26//! - [`arena_terms`]: Provides the underlying term representation used in operators.
27
28use anyhow::{Context, Result, anyhow, bail};
29use arena_terms::{Arena, Term, View};
30use indexmap::IndexMap;
31use smartstring::alias::String;
32use std::collections::HashSet;
33use std::fmt;
34use std::str::FromStr;
35
36/// Defines the syntactic position (fixity) of an operator.
37///
38/// Operators in Prolog-like syntax can appear in different structural positions
39/// depending on their form. The `Fixity` enum captures these roles and is used
40/// to categorize operators within the parser and operator definition tables.
41///
42/// # Variants
43/// - [`Fun`]: A functional (non-operator) form, e.g., `f(x, y)`.
44/// - [`Prefix`]: A prefix operator appearing before its operand, e.g., `-x`.
45/// - [`Infix`]: An infix operator appearing between two operands, e.g., `x + y`.
46/// - [`Postfix`]: A postfix operator appearing after its operand, e.g., `x!`.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[repr(u8)]
49pub enum Fixity {
50    /// Functional (non-operator) position, e.g. `f(x)`.
51    Fun = 0,
52
53    /// Prefix operator, appearing before its operand, e.g. `-x`.
54    Prefix = 1,
55
56    /// Infix operator, appearing between operands, e.g. `x + y`.
57    Infix = 2,
58
59    /// Postfix operator, appearing after its operand, e.g., `x!`.
60    Postfix = 3,
61}
62
63impl Fixity {
64    /// The total number of fixity variants.
65    pub const COUNT: usize = 4;
66
67    /// String representations of each fixity variant, in declaration order.
68    pub const STRS: &[&str] = &["fun", "prefix", "infix", "postfix"];
69}
70
71impl From<Fixity> for String {
72    /// Converts a [`Fixity`] into its lowercase string representation.
73    fn from(f: Fixity) -> Self {
74        Fixity::STRS[Into::<usize>::into(f)].into()
75    }
76}
77
78impl From<Fixity> for usize {
79    /// Converts a [`Fixity`] value into its numeric index (0–3).
80    fn from(f: Fixity) -> Self {
81        f as usize
82    }
83}
84
85impl fmt::Display for Fixity {
86    /// Formats the fixity as its canonical lowercase name.
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str(String::from(*self).as_str())
89    }
90}
91
92impl TryFrom<&str> for Fixity {
93    type Error = ParseFixityError;
94    fn try_from(s: &str) -> Result<Self, Self::Error> {
95        s.parse()
96    }
97}
98
99impl TryFrom<String> for Fixity {
100    type Error = ParseFixityError;
101    fn try_from(s: String) -> Result<Self, Self::Error> {
102        s.as_str().parse()
103    }
104}
105
106/// Error type returned when parsing a [`Fixity`] from a string fails.
107///
108/// This error indicates that the provided input string does not correspond
109/// to any known fixity variant (`"fun"`, `"prefix"`, `"infix"`, or `"postfix"`).
110#[derive(Debug, Clone)]
111pub struct ParseFixityError(String);
112
113impl fmt::Display for ParseFixityError {
114    /// Formats the error message indicating the invalid fixity string.
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "invalid fixity: {}", self.0)
117    }
118}
119impl std::error::Error for ParseFixityError {}
120
121/// Parses a string into a [`Fixity`] variant.
122///
123/// Accepts canonical lowercase names: `"fun"`, `"prefix"`, `"infix"`, or `"postfix"`.
124/// Returns a [`ParseFixityError`] if the input string does not match any known fixity.
125impl FromStr for Fixity {
126    type Err = ParseFixityError;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        match s {
129            "fun" => Ok(Fixity::Fun),
130            "prefix" => Ok(Fixity::Prefix),
131            "infix" => Ok(Fixity::Infix),
132            "postfix" => Ok(Fixity::Postfix),
133            other => Err(ParseFixityError(String::from(other))),
134        }
135    }
136}
137
138/// Operator associativity classification.
139///
140/// [`Assoc`] determines how operators of the same precedence are grouped during parsing.
141/// It supports left-, right-, and non-associative operators.
142///
143/// | Variant | Description |
144/// |----------|--------------|
145/// | [`Assoc::None`]  | Non-associative — cannot chain with itself. |
146/// | [`Assoc::Left`]  | Left-associative — groups from left to right. |
147/// | [`Assoc::Right`] | Right-associative — groups from right to left. |
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149#[repr(u8)]
150pub enum Assoc {
151    /// Non-associative operator.
152    None = 0,
153    /// Left-associative operator.
154    Left = 1,
155    /// Right-associative operator.
156    Right = 2,
157}
158
159impl Assoc {
160    /// Total number of associativity variants.
161    pub const COUNT: usize = 3;
162
163    /// Canonical string representations for each variant.
164    pub const STRS: &[&str] = &["none", "left", "right"];
165}
166
167impl From<Assoc> for String {
168    /// Converts an [`Assoc`] variant into its canonical lowercase string.
169    fn from(a: Assoc) -> Self {
170        Assoc::STRS[Into::<usize>::into(a)].into()
171    }
172}
173
174impl From<Assoc> for usize {
175    /// Converts an [`Assoc`] variant into its numeric discriminant.
176    fn from(a: Assoc) -> Self {
177        a as usize
178    }
179}
180
181impl fmt::Display for Assoc {
182    /// Formats the associativity as its canonical lowercase name.
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str(String::from(*self).as_str())
185    }
186}
187
188/// Error type returned when parsing an [`Assoc`] from a string fails.
189///
190/// This error indicates that the provided input string does not correspond
191/// to any known associativity variant (`"none"`, `"left"`, or `"right"`).
192#[derive(Debug, Clone)]
193pub struct ParseAssocError(String);
194
195impl fmt::Display for ParseAssocError {
196    /// Formats the error message indicating the invalid associativity string.
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(f, "invalid associativity: {}", self.0)
199    }
200}
201
202impl std::error::Error for ParseAssocError {}
203
204impl FromStr for Assoc {
205    type Err = ParseAssocError;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        match s {
209            "none" => Ok(Assoc::None),
210            "left" => Ok(Assoc::Left),
211            "right" => Ok(Assoc::Right),
212            other => Err(ParseAssocError(String::from(other))),
213        }
214    }
215}
216
217impl TryFrom<&str> for Assoc {
218    type Error = ParseAssocError;
219    fn try_from(s: &str) -> Result<Self, Self::Error> {
220        s.parse()
221    }
222}
223
224impl TryFrom<String> for Assoc {
225    type Error = ParseAssocError;
226    fn try_from(s: String) -> Result<Self, Self::Error> {
227        s.as_str().parse()
228    }
229}
230
231/// Represents an additional argument associated with an operator definition.
232///
233/// Each [`OperArg`] defines a named parameter (an atom) and an optional
234/// default value stored as an [`arena_terms::Term`].
235#[derive(Debug, Clone)]
236pub struct OperArg {
237    /// The argument name (atom identifier).
238    pub name: String,
239    /// Optional default value for the argument.
240    pub default: Option<Term>,
241}
242
243/// Default precedence values used for operator definitions.
244///
245/// - [`NON_OPER_PREC`] — precedence value for non-operators (0).
246/// - [`MIN_OPER_PREC`] — minimum allowed precedence (0).
247/// - [`MAX_OPER_PREC`] — maximum allowed precedence (1200).
248pub const NON_OPER_PREC: usize = 0;
249pub const MIN_OPER_PREC: usize = 0;
250pub const MAX_OPER_PREC: usize = 1200;
251
252/// Defines a single operator, including its fixity, precedence, associativity,
253/// and optional additional parameters.
254///
255/// Each operator definition describes how the parser should treat a symbol
256/// syntactically, including its argument behavior and binding strength.
257///
258/// # Field Rules
259/// - `prec` must be `0` for [`Fixity::Fun`].
260/// - `assoc` must be:
261///   - [`Assoc::None`] for [`Fixity::Fun`],
262///   - [`Assoc::Right`] for [`Fixity::Prefix`],
263///   - [`Assoc::Left`] for [`Fixity::Postfix`].
264#[derive(Debug, Clone)]
265pub struct OperDef {
266    /// Operator fixity (function, prefix, infix, or postfix).
267    pub fixity: Fixity,
268    /// Operator precedence (`0`–`1200`).
269    ///
270    /// Higher numbers indicate **tighter binding**.
271    /// Must be `0` for [`Fixity::Fun`].
272    pub prec: usize,
273    /// Operator associativity (depends on fixity).
274    pub assoc: Assoc,
275    /// Optional extra arguments beyond the operator’s required operands.
276    pub args: Vec<OperArg>,
277    /// Optional renaming target (atom term).
278    pub rename_to: Option<Term>,
279    /// Whether this operator’s fixity should be embedded in generated term.
280    pub embed_fixity: bool,
281}
282
283/// Container for operator definitions indexed by [`Fixity`].
284///
285/// Each entry in the internal array corresponds to one fixity variant
286/// (function, prefix, infix, or postfix).
287#[derive(Debug, Clone)]
288pub struct OperDefTab {
289    tab: [Option<OperDef>; Fixity::COUNT],
290}
291
292/// Central registry of all operator definitions.
293///
294/// [`OperDefs`] maps operator names to a table of definitions by fixity.
295/// Provides fast lookup of operator behavior and metadata.
296#[derive(Debug, Clone, Default)]
297pub struct OperDefs {
298    map: IndexMap<String, OperDefTab>,
299}
300
301/// Shared empty operator definition table constant.
302static EMPTY_OPER_DEF_TAB: OperDefTab = OperDefTab::new();
303
304impl OperDef {
305    /// Returns the number of required operands for a given [`Fixity`].
306    ///
307    /// - [`Fixity::Fun`] → `0`
308    /// - [`Fixity::Prefix`] → `1`
309    /// - [`Fixity::Infix`] → `2`
310    /// - [`Fixity::Postfix`] → `1`
311    pub fn required_arity(fixity: Fixity) -> usize {
312        match fixity {
313            Fixity::Fun => 0,
314            Fixity::Prefix => 1,
315            Fixity::Infix => 2,
316            Fixity::Postfix => 1,
317        }
318    }
319}
320
321impl OperDefTab {
322    /// Creates a new, empty [`OperDefTab`] with all fixity slots unset.
323    ///
324    /// Each entry in the table corresponds to a [`Fixity`] variant
325    /// (`fun`, `prefix`, `infix`, or `postfix`), all initialized to `None`.
326    pub const fn new() -> Self {
327        Self {
328            tab: [const { None }; Fixity::COUNT],
329        }
330    }
331
332    /// Returns `true` if this table defines a function (`fun`) operator.
333    pub fn is_fun(&self) -> bool {
334        self.tab[0].is_some()
335    }
336
337    /// Returns `true` if this table defines at least one operator fixity.
338    pub fn is_oper(&self) -> bool {
339        self.tab[1..].iter().any(|x| x.is_some())
340    }
341
342    /// Retrieves the operator definition for the given [`Fixity`], if present.
343    pub fn get_op_def(&self, fixity: Fixity) -> Option<&OperDef> {
344        self.tab[usize::from(fixity)].as_ref()
345    }
346}
347
348impl std::ops::Index<Fixity> for OperDefTab {
349    type Output = Option<OperDef>;
350
351    /// Indexes the table by [`Fixity`], returning the corresponding definition.
352    ///
353    /// # Panics
354    /// Panics if the fixity discriminant is out of range (should never occur).
355    fn index(&self, i: Fixity) -> &Self::Output {
356        let i: usize = i.into();
357        &self.tab[i]
358    }
359}
360
361impl std::ops::IndexMut<Fixity> for OperDefTab {
362    /// Mutable indexing by [`Fixity`], allowing modification of the definition.
363    ///
364    /// # Panics
365    /// Panics if the fixity discriminant is out of range (should never occur).
366    fn index_mut(&mut self, i: Fixity) -> &mut Self::Output {
367        let i: usize = i.into();
368        &mut self.tab[i]
369    }
370}
371
372impl OperDefs {
373    /// Creates an empty [`OperDefs`] registry.
374    ///
375    /// Initializes an operator definition map with no entries.
376    pub fn new() -> Self {
377        Self {
378            map: IndexMap::new(),
379        }
380    }
381
382    /// Builds an [`OperDefs`] table from a parsed term representation.
383    ///
384    /// This helper reads operator specifications encoded as [`arena_terms::Term`] values
385    /// and populates the operator definition registry accordingly.
386    ///
387    /// # Parameters
388    /// - `arena`: The [`Arena`] used to allocate or access term data.
389    /// - `ops`: The root [`Term`] containing operator definitions.
390    ///
391    /// # Errors
392    /// Returns an error if operator parsing or validation fails.
393    pub fn try_from_ops(arena: &Arena, ops: Term) -> Result<Self> {
394        let mut oper_defs = Self::new();
395        oper_defs.define_opers(arena, ops)?;
396        Ok(oper_defs)
397    }
398
399    /// Returns the total number of operator entries in this registry.
400    pub fn len(&self) -> usize {
401        self.map.len()
402    }
403
404    /// Looks up an operator by name and returns its index, if defined.
405    ///
406    /// # Parameters
407    /// - `name`: The operator name to query.
408    ///
409    /// # Returns
410    /// The operator’s internal index if found, or `None` if not present.
411    pub fn lookup(&self, name: &str) -> Option<usize> {
412        self.map.get_index_of(name)
413    }
414
415    /// Retrieves an operator definition table by index.
416    ///
417    /// Returns a reference to the corresponding [`OperDefTab`],
418    /// or [`EMPTY_OPER_DEF_TAB`] if the index is `None` or out of bounds.
419    pub fn get(&self, index: Option<usize>) -> &OperDefTab {
420        match index {
421            Some(index) => match self.map.get_index(index) {
422                Some((_, tab)) => tab,
423                None => &EMPTY_OPER_DEF_TAB,
424            },
425            None => &EMPTY_OPER_DEF_TAB,
426        }
427    }
428
429    /// Defines a single operator entry from a parsed [`arena_terms::Term`] structure.
430    ///
431    /// This function ingests a Prolog-style operator definition term of the form:
432    ///
433    /// ```prolog
434    /// op(
435    ///     oper: atom | func(arg: atom | '='(name: atom, default: term)), ...,
436    ///     type: 'fun' | 'prefix' | 'infix' | 'postfix',
437    ///     prec: 0..1200,          % must be 0 for fixity = 'fun'
438    ///     assoc: 'none' | 'left' | 'right',
439    ///     rename_to: 'none' | some(new_name: atom),
440    ///     embed_type: 'false' | 'true'
441    /// ).
442    /// ```
443    ///
444    /// Each `op/1` term specifies one operator, including its name, fixity, precedence,
445    /// associativity, optional renaming target, and embedding behavior.
446    ///
447    /// # Parameters
448    /// - `arena`: The [`Arena`] providing term access and allocation.
449    /// - `op`: The [`Term`] describing the operator declaration.
450    ///
451    /// # Returns
452    /// - `Ok(())` if the operator was successfully parsed and registered.
453    ///
454    /// # Errors
455    /// Returns an error if the operator definition is invalid, malformed, or violates
456    /// fixity/precedence/associativity constraints.
457    pub fn define_oper(&mut self, arena: &Arena, op: Term) -> Result<()> {
458        const BOOLS: &[&str] = &["false", "true"];
459
460        let (_, [oper, fixity, prec, assoc, rename_to, embed_fixity]) =
461            op.unpack_func(arena, &["op"])?;
462
463        let (functor, args) = oper.unpack_func_any(arena, &[])?;
464        let name = functor.atom_name(arena)?;
465
466        let fixity = Fixity::try_from(fixity.unpack_atom(arena, Fixity::STRS)?)?;
467        let prec = prec.unpack_int(arena)?.try_into()?;
468        let assoc = Assoc::try_from(assoc.unpack_atom(arena, Assoc::STRS)?)?;
469        let embed_fixity = embed_fixity.unpack_atom(arena, BOOLS)? == "true";
470
471        let args = args
472            .into_iter()
473            .map(|arg| {
474                Ok(match arg.view(arena)? {
475                    View::Atom(name) => OperArg {
476                        name: String::from(name),
477                        default: None,
478                    },
479                    View::Func(ar, _, _) => {
480                        let (_, [name, term]) = arg.unpack_func(ar, &["="])?;
481                        OperArg {
482                            name: String::from(name.atom_name(ar)?),
483                            default: Some(term),
484                        }
485                    }
486                    _ => bail!("oper arg must be an atom or =(atom, term) in {:?}", name),
487                })
488            })
489            .collect::<Result<Vec<_>>>()?;
490
491        let required_arity = OperDef::required_arity(fixity);
492        if args.len() < required_arity {
493            bail!(
494                "operator {:?} requires at least {} argument(s)",
495                name,
496                required_arity
497            );
498        }
499
500        if args[..required_arity].iter().any(|x| x.default.is_some()) {
501            bail!("defaults are not allowed for required operator arguments");
502        }
503
504        let unique_arg_names: HashSet<_> = args.iter().map(|x| &x.name).cloned().collect();
505        if unique_arg_names.len() != args.len() {
506            bail!("duplicate arguments in {:?}", name);
507        }
508
509        let rename_to = match rename_to.view(arena)? {
510            View::Atom("none") => None,
511            View::Func(ar, _, _) => {
512                let (_, [rename_to]) = rename_to.unpack_func(ar, &["some"])?;
513                Some(rename_to)
514            }
515            _ => bail!("rename_to must be 'none' | some(atom)"),
516        };
517
518        if matches!(fixity, Fixity::Fun) && prec != NON_OPER_PREC {
519            bail!("{:?} must be assigned precedence 0", name);
520        }
521        if !matches!(fixity, Fixity::Fun) && (prec < MIN_OPER_PREC || prec > MAX_OPER_PREC) {
522            bail!(
523                "precedence {} is out of range for operator {:?} with type {:?} (expected {}–{})",
524                prec,
525                name,
526                fixity,
527                MIN_OPER_PREC,
528                MAX_OPER_PREC,
529            );
530        }
531        if matches!((fixity, assoc), (Fixity::Prefix, Assoc::Left))
532            || matches!((fixity, assoc), (Fixity::Postfix, Assoc::Right))
533        {
534            bail!(
535                "operator {:?} with type {:?} cannot have associativity {:?}",
536                name,
537                fixity,
538                assoc
539            );
540        }
541
542        // This check is intentionally disabled to preserve compatibility
543        // with the behavior of the original C implementation
544        #[cfg(false)]
545        if matches!((fixity, assoc), (Fixity::Fun, Assoc::Left | Assoc::Right)) {
546            bail!(
547                "{:?} with type {:?} cannot have associativity {:?}",
548                name,
549                fixity,
550                assoc
551            );
552        }
553
554        let tab = self
555            .map
556            .entry(String::from(name))
557            .or_insert_with(OperDefTab::new);
558
559        if matches!(fixity, Fixity::Fun) && tab.is_oper() {
560            bail!(
561                "cannot define {:?} with type {:?}; it is already defined as an operator with a different type",
562                name,
563                fixity,
564            );
565        }
566
567        if matches!(fixity, Fixity::Prefix | Fixity::Infix | Fixity::Postfix)
568            && tab.tab[Into::<usize>::into(Fixity::Fun)].is_some()
569        {
570            bail!(
571                "cannot define {:?} as an operator with type {:?}; it is already defined with type Fun",
572                name,
573                fixity,
574            );
575        }
576
577        if tab[fixity].is_some() {
578            bail!("cannot re-define {:?}", name);
579        }
580
581        tab[fixity] = Some(OperDef {
582            fixity,
583            prec,
584            assoc,
585            rename_to,
586            embed_fixity,
587            args,
588        });
589
590        Ok(())
591    }
592
593    /// Defines one or more operators from a parsed [`arena_terms::Term`] structure.
594    ///
595    /// This method accepts either:
596    /// - A list of operator terms (each of which is passed to [`define_oper`]), or
597    /// - A single operator term (`op(...)`) to be defined directly.
598    ///
599    /// Each term is ingested and registered according to its fixity, precedence,
600    /// associativity, and optional metadata.
601    ///
602    /// # Parameters
603    /// - `arena`: The [`Arena`] providing term access and allocation.
604    /// - `term`: Either a list of operator definitions or a single operator term.
605    ///
606    /// # Returns
607    /// - `Ok(())` if all operator definitions were successfully processed.
608    ///
609    /// # Errors
610    /// Returns an error if any individual operator definition is invalid,
611    /// malformed, or violates fixity/precedence/associativity constraints.
612    pub fn define_opers(&mut self, arena: &Arena, term: Term) -> Result<()> {
613        match term.view(arena)? {
614            View::List(arena, ts, _) => {
615                for t in ts {
616                    self.define_oper(arena, *t)?;
617                }
618            }
619            _ => {
620                self.define_oper(arena, term)?;
621            }
622        }
623        Ok(())
624    }
625}