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}