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}