arena_terms/
oper.rs

1//! Operator definitions, precedence, and compound term argument metadata.
2//!
3//! This module defines types and utilities for representing and managing
4//! operator fixity, associativity, precedence, and compound term argument
5//! specifications. Operators may appear in prefix, infix, or postfix positions,
6//! each characterized by its [`Fixity`] and [`Assoc`]. Non-operator compound terms
7//! are represented using the `fun` fixity for uniform handling within
8//! the same framework.
9//!
10//! These definitions may also include **named arguments** and **default
11//! values**, which describe the structure of compound terms. Using this
12//! metadata, the [`Arena`] can "normalize" partially defined compounds by
13//! filling in defaults and automatically reordering arguments according to
14//! their declared names. The arena also exposes this information to the
15//! `arena-terms-parser`, allowing the parser to interpret and construct
16//! terms consistently with defined operators.
17
18use crate::{Arena, IntoTerm, Term, TermError, View, atom, func, list};
19use indexmap::IndexMap;
20use smartstring::alias::String;
21use std::collections::HashSet;
22use std::fmt;
23use std::str::FromStr;
24
25/// Returns `TermError::OperDef` with a formatted message.
26///
27/// # Example
28/// ```rust, ignore
29/// bail!("invalid value: {}", val);
30/// ```
31macro_rules! bail {
32    ($($arg:tt)*) => {
33        return Err(crate::TermError::OperDef(String::from(format!($($arg)*))))
34    }
35}
36
37/// Defines the syntactic position (fixity) of an operator.
38///
39/// Operators in Prolog-like syntax can appear in different structural positions
40/// depending on their form. The `Fixity` enum captures these roles and is used
41/// to categorize operators within the parser and operator definition tables.
42///
43/// # Variants
44/// - [`Fun`]: A functional (non-operator) form, e.g., `f(x, y)`.
45/// - [`Prefix`]: A prefix operator appearing before its operand, e.g., `-x`.
46/// - [`Infix`]: An infix operator appearing between two operands, e.g., `x + y`.
47/// - [`Postfix`]: A postfix operator appearing after its operand, e.g., `x!`.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[repr(u8)]
50pub enum Fixity {
51    /// Functional (non-operator) position, e.g. `f(x)`.
52    Fun = 0,
53
54    /// Prefix operator, appearing before its operand, e.g. `-x`.
55    Prefix = 1,
56
57    /// Infix operator, appearing between operands, e.g. `x + y`.
58    Infix = 2,
59
60    /// Postfix operator, appearing after its operand, e.g., `x!`.
61    Postfix = 3,
62}
63
64impl Fixity {
65    /// The total number of fixity variants.
66    pub const COUNT: usize = 4;
67
68    /// String representations of each fixity variant, in declaration order.
69    pub const STRS: &[&str] = &["fun", "prefix", "infix", "postfix"];
70}
71
72impl From<Fixity> for String {
73    /// Converts a [`Fixity`] into its lowercase string representation.
74    fn from(f: Fixity) -> Self {
75        Fixity::STRS[Into::<usize>::into(f)].into()
76    }
77}
78
79impl From<Fixity> for usize {
80    /// Converts a [`Fixity`] value into its numeric index (0–3).
81    fn from(f: Fixity) -> Self {
82        f as usize
83    }
84}
85
86impl fmt::Display for Fixity {
87    /// Formats the fixity as its canonical lowercase name.
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str(String::from(*self).as_str())
90    }
91}
92
93impl TryFrom<&str> for Fixity {
94    type Error = TermError;
95    fn try_from(s: &str) -> Result<Self, Self::Error> {
96        s.parse()
97    }
98}
99
100impl TryFrom<String> for Fixity {
101    type Error = TermError;
102    fn try_from(s: String) -> Result<Self, Self::Error> {
103        s.as_str().parse()
104    }
105}
106
107/// Parses a string into a [`Fixity`] variant.
108///
109/// Accepts canonical lowercase names: `"fun"`, `"prefix"`, `"infix"`, or `"postfix"`.
110/// Returns a [`ParseFixityError`] if the input string does not match any known fixity.
111impl FromStr for Fixity {
112    type Err = TermError;
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        match s {
115            "fun" => Ok(Fixity::Fun),
116            "prefix" => Ok(Fixity::Prefix),
117            "infix" => Ok(Fixity::Infix),
118            "postfix" => Ok(Fixity::Postfix),
119            other => Err(TermError::InvalidFixity(String::from(other))),
120        }
121    }
122}
123
124/// Operator associativity classification.
125///
126/// [`Assoc`] determines how operators of the same precedence are grouped during parsing.
127/// It supports left-, right-, and non-associative operators.
128///
129/// | Variant | Description |
130/// |----------|--------------|
131/// | [`Assoc::None`]  | Non-associative — cannot chain with itself. |
132/// | [`Assoc::Left`]  | Left-associative — groups from left to right. |
133/// | [`Assoc::Right`] | Right-associative — groups from right to left. |
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[repr(u8)]
136pub enum Assoc {
137    /// Non-associative operator.
138    None = 0,
139    /// Left-associative operator.
140    Left = 1,
141    /// Right-associative operator.
142    Right = 2,
143}
144
145impl Assoc {
146    /// Total number of associativity variants.
147    pub const COUNT: usize = 3;
148
149    /// Canonical string representations for each variant.
150    pub const STRS: &[&str] = &["none", "left", "right"];
151}
152
153impl From<Assoc> for String {
154    /// Converts an [`Assoc`] variant into its canonical lowercase string.
155    fn from(a: Assoc) -> Self {
156        Assoc::STRS[Into::<usize>::into(a)].into()
157    }
158}
159
160impl From<Assoc> for usize {
161    /// Converts an [`Assoc`] variant into its numeric discriminant.
162    fn from(a: Assoc) -> Self {
163        a as usize
164    }
165}
166
167impl fmt::Display for Assoc {
168    /// Formats the associativity as its canonical lowercase name.
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        f.write_str(String::from(*self).as_str())
171    }
172}
173
174impl FromStr for Assoc {
175    type Err = TermError;
176
177    fn from_str(s: &str) -> Result<Self, Self::Err> {
178        match s {
179            "none" => Ok(Assoc::None),
180            "left" => Ok(Assoc::Left),
181            "right" => Ok(Assoc::Right),
182            other => Err(TermError::InvalidAssoc(String::from(other))),
183        }
184    }
185}
186
187impl TryFrom<&str> for Assoc {
188    type Error = TermError;
189    fn try_from(s: &str) -> Result<Self, Self::Error> {
190        s.parse()
191    }
192}
193
194impl TryFrom<String> for Assoc {
195    type Error = TermError;
196    fn try_from(s: String) -> Result<Self, Self::Error> {
197        s.as_str().parse()
198    }
199}
200
201/// Represents an additional argument associated with an operator definition.
202///
203/// Each [`OperArg`] defines a named parameter (an atom) and an optional
204/// default value stored as an [`arena_terms::Term`].
205#[derive(Debug, Clone)]
206pub struct OperArg {
207    /// The argument name (atom identifier).
208    pub name: String,
209    /// Optional default value for the argument.
210    pub default: Option<Term>,
211}
212
213/// Default precedence values used for operator definitions.
214///
215/// - [`NON_OPER_PREC`] — precedence value for non-operators (0).
216/// - [`MIN_OPER_PREC`] — minimum allowed precedence (0).
217/// - [`MAX_OPER_PREC`] — maximum allowed precedence (1200).
218pub const NON_OPER_PREC: i64 = 0;
219pub const MIN_OPER_PREC: i64 = 0;
220pub const MAX_OPER_PREC: i64 = 1200;
221
222/// Defines a single operator, including its fixity, precedence, associativity,
223/// and optional additional parameters.
224///
225/// Each operator definition describes how the parser should treat a symbol
226/// syntactically, including its argument behavior and binding strength.
227///
228/// # Field Rules
229/// - `prec` must be `0` for [`Fixity::Fun`].
230/// - `assoc` must be:
231///   - [`Assoc::None`] for [`Fixity::Fun`],
232///   - [`Assoc::Right`] for [`Fixity::Prefix`],
233///   - [`Assoc::Left`] for [`Fixity::Postfix`].
234#[derive(Debug, Clone)]
235pub struct OperDef {
236    /// Operator fixity (function, prefix, infix, or postfix).
237    pub fixity: Fixity,
238    /// Operator precedence (`0`–`1200`).
239    ///
240    /// Higher numbers indicate **tighter binding**.
241    /// Must be `0` for [`Fixity::Fun`].
242    pub prec: i64,
243    /// Operator associativity (depends on fixity).
244    pub assoc: Assoc,
245    /// Optional extra arguments beyond the operator’s required operands.
246    pub args: Vec<OperArg>,
247    /// Optional renaming target (atom term).
248    pub rename_to: Option<Term>,
249    /// Whether this operator’s fixity should be embedded in generated term.
250    pub embed_fixity: bool,
251}
252
253/// Container for operator definitions indexed by [`Fixity`].
254///
255/// Each entry in the internal array corresponds to one fixity variant
256/// (function, prefix, infix, or postfix).
257#[derive(Debug, Clone)]
258pub struct OperDefTab {
259    tab: [Option<OperDef>; Fixity::COUNT],
260}
261
262/// Central registry of all operator definitions.
263///
264/// [`OperDefs`] maps operator names to a table of definitions by fixity.
265/// Provides fast lookup of operator behavior and metadata.
266#[derive(Debug, Clone, Default)]
267pub struct OperDefs {
268    map: IndexMap<String, OperDefTab>,
269}
270
271/// Shared empty operator definition table constant.
272static EMPTY_OPER_DEF_TAB: OperDefTab = OperDefTab::new();
273
274impl OperDef {
275    /// Returns the number of required operands for a given [`Fixity`].
276    ///
277    /// - [`Fixity::Fun`] → `0`
278    /// - [`Fixity::Prefix`] → `1`
279    /// - [`Fixity::Infix`] → `2`
280    /// - [`Fixity::Postfix`] → `1`
281    pub fn required_arity(fixity: Fixity) -> usize {
282        match fixity {
283            Fixity::Fun => 0,
284            Fixity::Prefix => 1,
285            Fixity::Infix => 2,
286            Fixity::Postfix => 1,
287        }
288    }
289}
290
291impl OperDefTab {
292    /// Creates a new, empty [`OperDefTab`] with all fixity slots unset.
293    ///
294    /// Each entry in the table corresponds to a [`Fixity`] variant
295    /// (`fun`, `prefix`, `infix`, or `postfix`), all initialized to `None`.
296    pub const fn new() -> Self {
297        Self {
298            tab: [const { None }; Fixity::COUNT],
299        }
300    }
301
302    /// Returns `true` if this table defines a function (`fun`) operator.
303    pub fn is_fun(&self) -> bool {
304        self.tab[0].is_some()
305    }
306
307    /// Returns `true` if this table defines at least one operator fixity.
308    pub fn is_oper(&self) -> bool {
309        self.tab[1..].iter().any(|x| x.is_some())
310    }
311
312    /// Retrieves the operator definition for the given [`Fixity`], if present.
313    pub fn get_op_def(&self, fixity: Fixity) -> Option<&OperDef> {
314        self.tab[usize::from(fixity)].as_ref()
315    }
316}
317
318impl std::ops::Index<Fixity> for OperDefTab {
319    type Output = Option<OperDef>;
320
321    /// Indexes the table by [`Fixity`], returning the corresponding definition.
322    ///
323    /// # Panics
324    /// Panics if the fixity discriminant is out of range (should never occur).
325    fn index(&self, i: Fixity) -> &Self::Output {
326        let i: usize = i.into();
327        &self.tab[i]
328    }
329}
330
331impl std::ops::IndexMut<Fixity> for OperDefTab {
332    /// Mutable indexing by [`Fixity`], allowing modification of the definition.
333    ///
334    /// # Panics
335    /// Panics if the fixity discriminant is out of range (should never occur).
336    fn index_mut(&mut self, i: Fixity) -> &mut Self::Output {
337        let i: usize = i.into();
338        &mut self.tab[i]
339    }
340}
341
342impl OperDefs {
343    /// Creates an empty [`OperDefs`] registry.
344    ///
345    /// Initializes an operator definition map with no entries.
346    pub fn new() -> Self {
347        Self {
348            map: IndexMap::new(),
349        }
350    }
351}
352
353impl Arena {
354    /// Looks up an operator by name and returns its index, if defined.
355    ///
356    /// # Parameters
357    /// - `name`: The operator name to query.
358    ///
359    /// # Returns
360    /// The operator’s internal index if found, or `None` if not present.
361    pub fn lookup_oper(&self, name: &str) -> Option<usize> {
362        self.opers.map.get_index_of(name)
363    }
364
365    /// Retrieves an operator definition table by index.
366    ///
367    /// Returns a reference to the corresponding [`OperDefTab`],
368    /// or [`EMPTY_OPER_DEF_TAB`] if the index is `None` or out of bounds.
369    pub fn get_oper(&self, index: Option<usize>) -> &OperDefTab {
370        match index {
371            Some(index) => match self.opers.map.get_index(index) {
372                Some((_, tab)) => tab,
373                None => &EMPTY_OPER_DEF_TAB,
374            },
375            None => &EMPTY_OPER_DEF_TAB,
376        }
377    }
378
379    /// Returns the total number of operator entries in this registry.
380    pub fn opers_len(&self) -> usize {
381        self.opers.map.len()
382    }
383
384    /// Defines a single operator entry from a parsed [`arena_terms::Term`] structure.
385    ///
386    /// This function ingests a Prolog-style operator definition term of the form:
387    ///
388    /// ```prolog
389    /// op(
390    ///     oper: atom | func(arg: atom | '='(name: atom, default: term)), ...,
391    ///     type: 'fun' | 'prefix' | 'infix' | 'postfix',
392    ///     prec: 0..1200,          % must be 0 for fixity = 'fun'
393    ///     assoc: 'none' | 'left' | 'right',
394    ///     rename_to: 'none' | some(new_name: atom),
395    ///     embed_type: 'false' | 'true'
396    /// ).
397    /// ```
398    ///
399    /// Each `op/1` term specifies one operator, including its name, fixity, precedence,
400    /// associativity, optional renaming target, and embedding behavior.
401    ///
402    /// # Parameters
403    /// - `arena`: The [`Arena`] providing term access and allocation.
404    /// - `op`: The [`Term`] describing the operator declaration.
405    ///
406    /// # Returns
407    /// - `Ok(())` if the operator was successfully parsed and registered.
408    ///
409    /// # Errors
410    /// Returns an error if the operator definition is invalid, malformed, or violates
411    /// fixity/precedence/associativity constraints.
412    pub fn define_oper(&mut self, op: Term) -> Result<(), TermError> {
413        const BOOLS: &[&str] = &["false", "true"];
414
415        let (_, [oper, fixity, prec, assoc, rename_to, embed_fixity]) =
416            op.unpack_func(self, &["op"])?;
417
418        let (functor, args) = oper.unpack_func_any(self, &[])?;
419        let name = String::from(functor.atom_name(self)?);
420
421        let fixity = Fixity::try_from(fixity.unpack_atom(self, Fixity::STRS)?)?;
422
423        let prec = prec.unpack_int(self)?;
424
425        if prec < MIN_OPER_PREC || prec > MAX_OPER_PREC {
426            bail!(
427                "precedence {} out of range {}..={}",
428                prec,
429                MIN_OPER_PREC,
430                MAX_OPER_PREC
431            );
432        }
433
434        let assoc = Assoc::try_from(assoc.unpack_atom(self, Assoc::STRS)?)?;
435        let embed_fixity = embed_fixity.unpack_atom(self, BOOLS)? == "true";
436
437        let args = args
438            .into_iter()
439            .map(|arg| {
440                Ok(match arg.view(self)? {
441                    View::Atom(name) => OperArg {
442                        name: String::from(name),
443                        default: None,
444                    },
445                    View::Func(ar, _, _) => {
446                        let (_, [name, term]) = arg.unpack_func(ar, &["="])?;
447                        OperArg {
448                            name: String::from(name.atom_name(ar)?),
449                            default: Some(term),
450                        }
451                    }
452                    _ => bail!("oper arg must be an atom or =(atom, term) in {:?}", name),
453                })
454            })
455            .collect::<Result<Vec<_>, TermError>>()?;
456
457        let required_arity = OperDef::required_arity(fixity);
458        if args.len() < required_arity {
459            bail!(
460                "operator {:?} requires at least {} argument(s)",
461                name,
462                required_arity
463            );
464        }
465
466        if args[..required_arity].iter().any(|x| x.default.is_some()) {
467            bail!("defaults are not allowed for required operator arguments");
468        }
469
470        let unique_arg_names: HashSet<_> = args.iter().map(|x| &x.name).cloned().collect();
471        if unique_arg_names.len() != args.len() {
472            bail!("duplicate arguments in {:?}", name);
473        }
474
475        let rename_to = match rename_to.view(self)? {
476            View::Atom("none") => None,
477            View::Func(ar, _, _) => {
478                let (_, [rename_to]) = rename_to.unpack_func(ar, &["some"])?;
479                Some(rename_to)
480            }
481            _ => bail!("rename_to must be 'none' | some(atom)"),
482        };
483
484        if matches!(fixity, Fixity::Fun) && prec != NON_OPER_PREC {
485            bail!("{:?} must be assigned precedence 0", name);
486        }
487        if !matches!(fixity, Fixity::Fun) && (prec < MIN_OPER_PREC || prec > MAX_OPER_PREC) {
488            bail!(
489                "precedence {} is out of range for operator {:?} with type {:?} (expected {}–{})",
490                prec,
491                name,
492                fixity,
493                MIN_OPER_PREC,
494                MAX_OPER_PREC,
495            );
496        }
497        if matches!((fixity, assoc), (Fixity::Prefix, Assoc::Left))
498            || matches!((fixity, assoc), (Fixity::Postfix, Assoc::Right))
499        {
500            bail!(
501                "operator {:?} with type {:?} cannot have associativity {:?}",
502                name,
503                fixity,
504                assoc
505            );
506        }
507
508        // This check is intentionally disabled to preserve compatibility
509        // with the behavior of the original C implementation
510        #[cfg(false)]
511        if matches!((fixity, assoc), (Fixity::Fun, Assoc::Left | Assoc::Right)) {
512            bail!(
513                "{:?} with type {:?} cannot have associativity {:?}",
514                name,
515                fixity,
516                assoc
517            );
518        }
519
520        let tab = self
521            .opers
522            .map
523            .entry(name.clone())
524            .or_insert_with(OperDefTab::new);
525
526        if matches!(fixity, Fixity::Fun) && tab.is_oper() {
527            bail!(
528                "cannot define {:?} with type {:?}; it is already defined as an operator with a different type",
529                name,
530                fixity,
531            );
532        }
533
534        if matches!(fixity, Fixity::Prefix | Fixity::Infix | Fixity::Postfix)
535            && tab.tab[Into::<usize>::into(Fixity::Fun)].is_some()
536        {
537            bail!(
538                "cannot define {:?} as an operator with type {:?}; it is already defined with type Fun",
539                name,
540                fixity,
541            );
542        }
543
544        if tab[fixity].is_some() {
545            bail!("cannot re-define {:?}", name);
546        }
547
548        tab[fixity] = Some(OperDef {
549            fixity,
550            prec,
551            assoc,
552            rename_to,
553            embed_fixity,
554            args,
555        });
556
557        Ok(())
558    }
559
560    /// Defines one or more operators from [`arena_terms::Term`].
561    ///
562    /// This method accepts either:
563    /// - A list of operator terms (each of which is passed to [`define_oper`]), or
564    /// - A single operator term (`op(...)`) to be defined directly.
565    ///
566    /// Each term is ingested and registered according to its fixity, precedence,
567    /// associativity, and optional metadata.
568    ///
569    /// # Parameters
570    /// - `arena`: The [`Arena`] providing term access and allocation.
571    /// - `term`: Either a list of operator definitions or a single operator term.
572    ///
573    /// # Returns
574    /// - `Ok(())` if all operator definitions were successfully processed.
575    ///
576    /// # Errors
577    /// Returns an error if any individual operator definition is invalid,
578    /// malformed, or violates fixity/precedence/associativity constraints.
579    pub fn define_opers(&mut self, term: Term) -> Result<(), TermError> {
580        let ts = match term.view(self)? {
581            View::List(_, ts, _) => ts.to_vec(),
582            _ => {
583                vec![term]
584            }
585        };
586        for t in ts {
587            self.define_oper(t)?;
588        }
589        Ok(())
590    }
591
592    /// Clears all operator definitions and compound term metadata.
593    pub fn clear_opers(&mut self) {
594        self.opers.map.clear();
595    }
596
597    /// Normalizes a parsed term using its operator definition.
598    ///
599    /// This process transforms terms according to their declared fixity,
600    /// applying named default arguments and other attributes specified
601    /// in the corresponding operator definition.
602    ///
603    /// # Parameters
604    /// - `arena`: Arena used to store normalized term structures.
605    /// - `term`: The parsed term to normalize.
606    /// - `fixity`: Operator fixity (`fun`, `prefix`, `infix`, or `postfix`).
607    /// - `op_tab_index`: Optional index into the operator definition table, if the
608    ///   term corresponds to a defined operator.
609    ///
610    /// # Returns
611    /// A normalized [`Term`] allocated in the given arena, ready for evaluation or
612    /// further semantic analysis.
613    ///
614    /// # Errors
615    /// Returns an error if normalization fails due to invalid fixity, mismatched
616    /// arity, or inconsistent operator metadata.
617    pub fn normalize_term(
618        &mut self,
619        term: Term,
620        fixity: Fixity,
621        op_tab_index: Option<usize>,
622    ) -> Result<Term, TermError> {
623        match self.get_oper(op_tab_index)[fixity] {
624            Some(ref op_def) => {
625                let (functor, vs) = match term.view(self)? {
626                    View::Atom(_) => (term, &[] as &[Term]),
627                    View::Func(_, functor, args) => {
628                        if args.is_empty() {
629                            bail!("invalid Func");
630                        }
631                        (*functor, args)
632                    }
633                    _ => {
634                        return Ok(term);
635                    }
636                };
637                let name = functor.atom_name(self)?;
638
639                let n_required_args = OperDef::required_arity(fixity);
640                if vs.len() < n_required_args {
641                    bail!(
642                        "missing {} required arguments in term {:?}",
643                        n_required_args - vs.len(),
644                        name
645                    );
646                }
647
648                let args = &op_def.args;
649                let mut xs: Vec<Option<Term>> = vec![None; args.len()];
650
651                for (i, value) in vs.iter().enumerate() {
652                    if i < n_required_args {
653                        xs[i] = Some(*value);
654                    } else {
655                        match value.view(self)? {
656                            View::Func(ar, functor, vs)
657                                if vs.len() == 2 && functor.atom_name(ar)? == "=" =>
658                            {
659                                let arg_name = vs[0].atom_name(self)?;
660
661                                if let Some(pos) = args.iter().position(|x| x.name == arg_name) {
662                                    if xs[pos].is_none() {
663                                        xs[pos] = Some(vs[1]);
664                                    } else {
665                                        bail!(
666                                            "cannot redefine argument {:?} at position {} in {:?}",
667                                            arg_name,
668                                            pos,
669                                            name
670                                        );
671                                    }
672                                } else {
673                                    bail!("invalid argument name {:?} in {:?}", arg_name, name);
674                                }
675                            }
676                            _ => {
677                                if xs[i].is_none() {
678                                    xs[i] = Some(*value);
679                                } else {
680                                    bail!(
681                                        "cannot redefine argument {:?} at position {} in {:?}",
682                                        args[i].name,
683                                        i,
684                                        name
685                                    );
686                                }
687                            }
688                        }
689                    }
690                }
691
692                let vs: Option<Vec<_>> = xs
693                    .into_iter()
694                    .enumerate()
695                    .map(|(i, x)| x.or(args[i].default))
696                    .collect();
697                let mut vs = match vs {
698                    Some(vs) => vs,
699                    None => bail!("missing arguments in {:?}", name),
700                };
701
702                let rename_to = match op_def.rename_to {
703                    Some(rename_to) => rename_to,
704                    None => functor,
705                };
706
707                if op_def.embed_fixity {
708                    vs.insert(0, self.atom(String::from(fixity)));
709                }
710
711                if vs.is_empty() {
712                    Ok(rename_to)
713                } else {
714                    Ok(self.funcv(std::iter::once(&rename_to).chain(vs.iter()))?)
715                }
716            }
717            None => match fixity {
718                Fixity::Fun => Ok(term),
719                _ => bail!("missing opdef for fixity {:?}", fixity),
720            },
721        }
722    }
723
724    /// Constructs the default operator definitions used by the [`TermParser`].
725    ///
726    /// This function populates an [`OperDefs`] table in the given [`Arena`],
727    /// defining built-in operators such as `-` (prefix), `++` (infix), and `=` (infix),
728    /// along with their precedence and associativity rules.
729    ///
730    /// ```prolog
731    /// [ op(-(x), prefix, 800, right, none, false),
732    ///   op(++(x, y), infix, 500, left, none, false),
733    ///   op(=(x, y), infix, 100, right, none, false),
734    ///   op(op(f,
735    ///         =(type, fun),
736    ///         =(prec, 0),
737    ///         =(assoc, none),
738    ///         =(rename_to, none),
739    ///         =(embed_type, false)),
740    ///      fun, 0, none, none, false)
741    /// ]
742    /// ```
743    ///
744    /// The resulting definitions form the standard operator environment available
745    /// to the parser when no user-defined operator table is provided.
746    ///
747    /// # Parameters
748    /// - `arena`: The [`Arena`] used for allocating operator term structures.
749    ///
750    /// # Returns
751    /// An initialized [`OperDefs`] instance containing the default operator set.
752    ///
753    /// [`TermParser`]: crate::parser::TermParser
754    /// [`OperDefs`]: crate::oper::OperDefs
755    /// [`Arena`]: arena_terms::Arena
756    /// [`aslr`]: https://crates.io/crates/parlex-gen
757    pub fn define_default_opers(&mut self) -> Result<(), TermError> {
758        let term = list![
759            func!(
760                "op";
761                func!("-"; atom!("x")),
762                atom!("prefix"),
763                800,
764                atom!("right"),
765                atom!("none"),
766                atom!("false"),
767            ),
768            func!(
769                "op";
770                func!("++"; atom!("x"), atom!("y")),
771                atom!("infix"),
772                500,
773                atom!("left"),
774                atom!("none"),
775                atom!("false"),
776            ),
777            func!(
778                "op";
779                func!("="; atom!("x"), atom!("y")),
780                atom!("infix"),
781                100,
782                atom!("right"),
783                atom!("none"),
784                atom!("false"),
785            ),
786            func!(
787                "op";
788                func!(
789                    "op";
790                    atom!("f"),
791                    func!("="; atom!("type"), atom!("fun")),
792                    func!("="; atom!("prec"), 0),
793                    func!("="; atom!("assoc"), atom!("none")),
794                    func!("="; atom!("rename_to"), atom!("none")),
795                    func!("="; atom!("embed_type"), atom!("false")),
796                ),
797                atom!("fun"),
798                0,
799                atom!("none"),
800                atom!("none"),
801                atom!("false"),
802            ),
803            => self
804        ];
805        self.define_opers(term)
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn fixity_from_str_valid() {
815        assert_eq!("fun".parse::<Fixity>().unwrap(), Fixity::Fun);
816        assert_eq!("prefix".parse::<Fixity>().unwrap(), Fixity::Prefix);
817        assert_eq!("infix".parse::<Fixity>().unwrap(), Fixity::Infix);
818        assert_eq!("postfix".parse::<Fixity>().unwrap(), Fixity::Postfix);
819    }
820
821    #[test]
822    fn fixity_from_str_invalid() {
823        let err = "pre_fix".parse::<Fixity>().unwrap_err();
824        assert_eq!(err.to_string(), "invalid fixity: pre_fix");
825    }
826
827    #[test]
828    fn fixity_display_and_string_from() {
829        assert_eq!(Fixity::Fun.to_string(), "fun");
830        assert_eq!(Fixity::Prefix.to_string(), "prefix");
831        assert_eq!(Fixity::Infix.to_string(), "infix");
832        assert_eq!(Fixity::Postfix.to_string(), "postfix");
833
834        let s: smartstring::alias::String = Fixity::Infix.into();
835        assert_eq!(s.as_str(), "infix");
836    }
837
838    #[test]
839    fn fixity_into_usize_indices() {
840        assert_eq!(usize::from(Fixity::Fun), 0);
841        assert_eq!(usize::from(Fixity::Prefix), 1);
842        assert_eq!(usize::from(Fixity::Infix), 2);
843        assert_eq!(usize::from(Fixity::Postfix), 3);
844        assert_eq!(Fixity::STRS.len(), Fixity::COUNT);
845    }
846
847    #[test]
848    fn assoc_from_str_valid() {
849        assert_eq!("none".parse::<Assoc>().unwrap(), Assoc::None);
850        assert_eq!("left".parse::<Assoc>().unwrap(), Assoc::Left);
851        assert_eq!("right".parse::<Assoc>().unwrap(), Assoc::Right);
852    }
853
854    #[test]
855    fn assoc_from_str_invalid() {
856        let err = "center".parse::<Assoc>().unwrap_err();
857        assert_eq!(err.to_string(), "invalid associativity: center");
858    }
859
860    #[test]
861    fn assoc_display_and_string_from() {
862        assert_eq!(Assoc::None.to_string(), "none");
863        assert_eq!(Assoc::Left.to_string(), "left");
864        assert_eq!(Assoc::Right.to_string(), "right");
865
866        let s: smartstring::alias::String = Assoc::Right.into();
867        assert_eq!(s.as_str(), "right");
868    }
869
870    #[test]
871    fn assoc_into_usize_indices() {
872        assert_eq!(usize::from(Assoc::None), 0);
873        assert_eq!(usize::from(Assoc::Left), 1);
874        assert_eq!(usize::from(Assoc::Right), 2);
875        assert_eq!(Assoc::STRS.len(), Assoc::COUNT);
876    }
877
878    #[test]
879    fn required_arity_matches_fixity() {
880        assert_eq!(OperDef::required_arity(Fixity::Fun), 0);
881        assert_eq!(OperDef::required_arity(Fixity::Prefix), 1);
882        assert_eq!(OperDef::required_arity(Fixity::Infix), 2);
883        assert_eq!(OperDef::required_arity(Fixity::Postfix), 1);
884    }
885
886    fn minimal_def(fixity: Fixity, prec: i64, assoc: Assoc) -> OperDef {
887        OperDef {
888            fixity,
889            prec,
890            assoc,
891            args: Vec::new(),
892            rename_to: None,
893            embed_fixity: false,
894        }
895    }
896
897    #[test]
898    fn oper_def_tab_new_is_empty() {
899        let tab = OperDefTab::new();
900        assert!(!tab.is_fun());
901        assert!(!tab.is_oper());
902        assert!(tab.get_op_def(Fixity::Fun).is_none());
903        assert!(tab.get_op_def(Fixity::Prefix).is_none());
904        assert!(tab.get_op_def(Fixity::Infix).is_none());
905        assert!(tab.get_op_def(Fixity::Postfix).is_none());
906    }
907
908    #[test]
909    fn oper_def_tab_flags_update_correctly() {
910        let mut tab = OperDefTab::new();
911
912        // set Fun → is_fun true, is_oper still false
913        tab[Fixity::Fun] = Some(minimal_def(Fixity::Fun, 0, Assoc::None));
914        assert!(tab.is_fun());
915        assert!(!tab.is_oper());
916
917        // set Infix → is_oper true
918        tab[Fixity::Infix] = Some(minimal_def(Fixity::Infix, 500, Assoc::Left));
919        assert!(tab.is_oper());
920
921        // get_op_def returns exactly what we put in
922        let inf = tab.get_op_def(Fixity::Infix).unwrap();
923        assert_eq!(inf.fixity, Fixity::Infix);
924        assert_eq!(inf.prec, 500);
925        assert_eq!(inf.assoc, Assoc::Left);
926    }
927
928    #[test]
929    fn oper_defs_empty_behavior() {
930        let arena = Arena::new();
931        assert_eq!(arena.opers_len(), 0);
932        assert_eq!(arena.lookup_oper("nope"), None);
933
934        // get(None) and get(Some(0)) when empty should return the shared empty tab
935        let empty1 = arena.get_oper(None);
936        let empty2 = arena.get_oper(Some(0));
937        assert!(!empty1.is_fun());
938        assert!(!empty1.is_oper());
939        assert!(!empty2.is_fun());
940        assert!(!empty2.is_oper());
941    }
942
943    #[test]
944    fn oper_defs_with_one_entry() {
945        let mut arena = Arena::new();
946
947        // manually create a table with one operator definition
948        let mut tab = OperDefTab::new();
949        let def = OperDef {
950            fixity: Fixity::Infix,
951            prec: 500,
952            assoc: Assoc::Left,
953            args: vec![
954                OperArg {
955                    name: "lhs".into(),
956                    default: None,
957                },
958                OperArg {
959                    name: "rhs".into(),
960                    default: None,
961                },
962            ],
963            rename_to: None,
964            embed_fixity: false,
965        };
966        tab[Fixity::Infix] = Some(def.clone());
967
968        arena.opers.map.insert("+".into(), tab);
969
970        // verify map state
971        assert_eq!(arena.opers_len(), 1);
972        let idx = arena.lookup_oper("+").unwrap();
973        let retrieved_tab = arena.get_oper(Some(idx));
974        assert!(retrieved_tab.is_oper());
975        assert!(!retrieved_tab.is_fun());
976
977        let inf = retrieved_tab.get_op_def(Fixity::Infix).unwrap();
978        assert_eq!(inf.fixity, Fixity::Infix);
979        assert_eq!(inf.prec, 500);
980        assert_eq!(inf.assoc, Assoc::Left);
981        assert_eq!(inf.args.len(), 2);
982        assert_eq!(inf.args[0].name, "lhs");
983        assert_eq!(inf.args[1].name, "rhs");
984    }
985}