aimx/expressions/
reference.rs

1//! Variable reference parsing for the AIMX language.
2//!
3//! This module provides the core functionality for parsing variable references in AIMX
4//! expressions. References are used to access values from the evaluation context and
5//! can represent simple identifiers or chained property accessors.
6//!
7//! # Reference Types
8//!
9//! - **Simple Identifiers**: Single variable names like `variable` or `$inference`
10//! - **Chained Accessors**: Dot-separated references like `object.property` or `object.property.method`
11//! - **Inference Calls**: References prefixed with `$` that are used for AI inference
12//!
13//! # Examples
14//!
15//! ```rust
16//! use aimx::expressions::reference::{Reference, parse_reference};
17//!
18//! // Parse simple identifier
19//! let (_, reference) = parse_reference("variable").unwrap();
20//! assert_eq!(reference.to_string(), "variable");
21//!
22//! // Parse chained property access
23//! let (_, reference) = parse_reference("object.property").unwrap();
24//! assert_eq!(reference.to_string(), "object.property");
25//!
26//! // Parse inference call (note the $ is stripped during parsing)
27//! let (_, reference) = parse_reference("$workflow.inference").unwrap();
28//! assert_eq!(reference.to_string(), "workflow.inference");
29//! ```
30
31use nom::{
32    IResult, Parser,
33    error::Error,
34    Err as NomErr,
35    branch::alt,
36    bytes::complete::tag,
37    character::complete::{alpha1, alphanumeric1, multispace0},
38    combinator::{map, opt, recognize},
39    multi::{many0_count},
40    sequence::{pair},
41};
42use crate::{
43    ContextLike,
44    ExpressionLike,
45    expressions::parse_accessor,
46    Value,
47    Writer,
48};
49use std::{
50    fmt,
51    fs,
52    path::{Path, PathBuf},
53};
54use anyhow::{anyhow, Result};
55
56/// Represents a variable reference in an AIMX expression.
57///
58/// The `Reference` enum defines the structure of variable references used throughout
59/// the AIMX language. References can be simple identifiers or chained property accessors.
60/// They are used to access values from the evaluation context.
61///
62/// # Variants
63///
64/// - `One(String)` - A single identifier reference (e.g., `variable`)
65/// - `Two(String, String)` - A two-part chained reference (e.g., `object.property`)
66/// - `Three(String, String, String)` - A three-part chained reference (e.g., `object.property.method`)
67///
68/// # Examples
69///
70/// ```rust
71/// use aimx::expressions::reference::Reference;
72///
73/// // Create references programmatically
74/// let simple = Reference::One("variable".to_string());
75/// let chained = Reference::Two("object".to_string(), "property".to_string());
76/// let nested = Reference::Three("parent".to_string(), "child".to_string(), "method".to_string());
77///
78/// // References implement Display for easy formatting
79/// assert_eq!(simple.to_string(), "variable");
80/// assert_eq!(chained.to_string(), "object.property");
81/// assert_eq!(nested.to_string(), "parent.child.method");
82/// ```
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum Reference {
85    /// A single identifier reference
86    One(String),
87    /// A two-part chained property reference
88    Two(String, String),
89    /// A three-part chained property reference
90    Three(String, String, String),
91}
92
93impl Reference {
94    pub fn new(identifier: &str) -> Self {
95        Reference::One(identifier.to_string())
96    }
97
98    pub fn append(&self, identifier: &str) -> Result<Reference> {
99        match self {
100            Reference::One(scope_one)
101                => Ok(Reference::Two(scope_one.clone(), identifier.to_string())),
102            Reference::Two(scope_one , scope_two)
103                => Ok(Reference::Three(scope_one.clone(), scope_two.clone(), identifier.to_string())),
104            Reference::Three(..) => Err(anyhow!("Cannot append {} to {} scope~{}", identifier, self, identifier)),
105        }
106    }
107
108    pub fn normalize(&self, reference: &Reference) -> Result<Reference> {
109        match self {
110            Reference::One(scope_one) => {
111                match reference {
112                    Reference::One(reference_one)
113                        => Ok(Reference::Two(scope_one.clone(), reference_one.clone())),
114                    Reference::Two(reference_one, reference_two)
115                        => Ok(Reference::Three(scope_one.clone(), reference_one.clone(), reference_two.clone())),
116                    Reference::Three(..) => Err(anyhow!("Cannot normalize {} within {} scope~{}", reference, self, reference))
117,
118                }
119            }
120            Reference::Two(scope_one , scope_two) => {
121                match reference {
122                    Reference::One(reference_one)
123                        => Ok(Reference::Three(scope_one.clone(), scope_two.clone(), reference_one.clone())),
124                    _ => Err(anyhow!("Cannot normalize {} within {} scope~{}", reference, self, reference)),
125                }
126            }
127            Reference::Three(..) => Err(anyhow!("Cannot normalize {} within {} scope~{}", reference, self, reference)),
128        }
129    }
130
131    pub fn to_path(&self, workspace: &Path) -> PathBuf {        
132        match self {
133            Reference::One(one) => {
134                // Single identifier: workspace/{one}.aim
135                workspace.join(format!("{}.aim", one))
136            }
137            Reference::Two(one, two) => {
138                // Two-part reference: workspace/{one}/{two}.aim
139                workspace.join(one).join(format!("{}.aim", two))
140            }
141            Reference::Three(one, two, three) => {
142                // Three-part reference: workspace/{one}/{two}/{three}.aim
143                workspace.join(one).join(two).join(format!("{}.aim", three))
144            }
145        }
146    }
147
148    pub fn create_path(&self, workspace: &Path) -> Result<PathBuf> {        
149        match self {
150            Reference::One(one) => {
151                if !workspace.exists() {
152                    fs::create_dir_all(&workspace)?;
153                }
154                // Single identifier: workspace/{one}.aim
155                Ok(workspace.join(format!("{}.aim", one)))
156            }
157            Reference::Two(one, two) => {
158                let new_dir = workspace.join(one);
159                if !new_dir.exists() {
160                    fs::create_dir_all(&new_dir)?;
161                }
162                // Two-part reference: workspace/{one}/{two}.aim
163                Ok(new_dir.join(format!("{}.aim", two)))
164            }
165            Reference::Three(one, two, three) => {
166                let new_dir = workspace.join(one).join(two);
167                if !new_dir.exists() {
168                    fs::create_dir_all(&new_dir)?;
169                }
170                // Three-part reference: workspace/{one}/{two}/{three}.aim
171                Ok(new_dir.join(format!("{}.aim", three)))
172            }
173        }
174    }
175}
176
177impl PartialEq<String> for Reference {
178    fn eq(&self, other: &String) -> bool {
179        match self {
180            Reference::One(s) => s == other,
181            _ => false
182        }
183    }
184}
185
186impl PartialEq<Reference> for String {
187    fn eq(&self, other: &Reference) -> bool {
188        match other {
189            Reference::One(s) => other == s,
190            _ => false
191        }
192    }
193}
194
195/// Parse an identifier according to JSON language specification.
196/// 
197/// Identifiers in AIM follow standard JSON naming conventions: they must start
198/// with a letter, dollar sign or underscore, followed by any combination of
199/// letters, digits, and underscores.
200/// 
201/// Leading underscore identifiers represent an alignable reference for inference
202/// output.
203/// 
204/// # Arguments
205/// 
206/// * `input` - A string slice to parse for an identifier
207/// 
208/// # Returns
209/// 
210/// * `IResult<&str, String>` - A nom result with remaining input and parsed identifier
211/// 
212/// # Examples
213/// 
214/// ```rust
215/// use aimx::expressions::reference::parse_identifier;
216/// 
217/// assert_eq!(parse_identifier("foo"), Ok(("", "foo".to_string())));
218/// assert_eq!(parse_identifier("_bar123"), Ok(("", "_bar123".to_string())));
219/// assert_eq!(parse_identifier("my_function"), Ok(("", "my_function".to_string())));
220/// ```
221pub fn parse_identifier(input: &str) -> IResult<&str, String> {
222    map(
223        recognize(pair(
224            alt((alpha1, tag("_"))),
225            many0_count(alt((alphanumeric1, tag("_")))),
226        )),
227        |s: &str| s.to_string(),
228    )
229    .parse(input)
230}
231
232/// Parse a reference chain: identifier (accessor identifier)*
233///
234/// This function parses variable references that can consist of one to three
235/// chained identifiers separated by accessors (dots). The parser handles
236/// inference calls (prefixed with `$`) and respects AIMX's syntax rules
237/// for method calls and function references.
238///
239/// # Syntax Rules
240///
241/// - **Simple references**: `variable`, `_assignment`, `$inference`
242/// - **Chained references**: `object.property`, `object.property.method`
243/// - **Inference calls**: `$tool.summarize`, `$workflow.process`
244/// - **Method calls**: When followed by `(`, chained references are truncated
245///   to support method syntax like `object.property.method()`
246///
247/// # Arguments
248///
249/// * `input` - The input string to parse
250///
251/// # Returns
252///
253/// Returns a `IResult<&str, Reference>` containing the remaining unparsed input
254/// and the parsed `Reference`.
255///
256/// # Examples
257///
258/// ```rust
259/// use aimx::expressions::reference::{parse_reference, Reference};
260///
261/// // Parse simple references
262/// assert_eq!(parse_reference("variable").unwrap().1, Reference::One("variable".to_string()));
263/// assert_eq!(parse_reference("_assignment").unwrap().1, Reference::One("_assignment".to_string()));
264/// assert_eq!(parse_reference("$inference").unwrap().1, Reference::One("inference".to_string()));
265///
266/// // Parse chained references
267/// let (_, reference) = parse_reference("object.property").unwrap();
268/// assert_eq!(reference, Reference::Two("object".to_string(), "property".to_string()));
269///
270/// let (_, reference) = parse_reference("parent.child.grandchild").unwrap();
271/// assert_eq!(reference, Reference::Three("parent".to_string(), "child".to_string(), "grandchild".to_string()));
272///
273/// // Parse inference calls
274/// let (_, reference) = parse_reference("$tool.summarize").unwrap();
275/// assert_eq!(reference, Reference::Two("tool".to_string(), "summarize".to_string()));
276/// ```
277///
278/// # Error Cases
279///
280/// The parser will fail for:
281/// - Empty identifiers (`_` followed by accessor)
282/// - Identifiers starting with `_` followed by parentheses
283/// - References that would exceed three parts
284pub fn parse_reference(input: &str) -> IResult<&str, Reference> {
285    // Check for leading dollar
286    let (input, dollar) = opt(tag("$")).parse(input)?;
287    let function = dollar.is_none();
288    // Parse the one identifier
289    let (input, one) = parse_identifier(input)?;
290    let (input, _) = opt(multispace0).parse(input)?;
291    // Empty or Function
292    if function && (&one == "_" || input.starts_with('(')) {
293        return Err(NomErr::Failure(Error::new(input, nom::error::ErrorKind::Fail)));
294    }
295    if let Ok((remainder, _)) = parse_accessor(input) {
296        let (remainder, two) = parse_identifier(remainder)?;
297        let (remainder, _) = opt(multispace0).parse(remainder)?;
298        // Method
299        if function && remainder.starts_with('(') {
300            // roll back input
301            let reference = Reference::One(one.clone());
302            return Ok((input, reference));
303        }
304        let input = remainder;
305
306        if let Ok((remainder, _)) = parse_accessor(input) {
307            let (remainder, three) = parse_identifier(remainder)?;
308            let (remainder, _) = opt(multispace0).parse(remainder)?;
309            if function && remainder.starts_with('(') {
310                // roll back input
311                let reference =
312                    Reference::Two(one.clone(), two.clone());
313                return Ok((input, reference));
314            }
315            
316            let reference = Reference::Three(one.clone(), two.clone(), three.clone());
317            Ok((remainder, reference))
318        } else {
319            let reference = Reference::Two(one.clone(), two.clone());
320            Ok((input, reference))
321        }
322    } else {
323        let reference = Reference::One(one.clone());
324        Ok((input, reference))
325    }
326}
327
328
329impl ExpressionLike for Reference {
330    /// Evaluate this reference by looking up its value in the context.
331    ///
332    /// # Arguments
333    ///
334    /// * `context` - The evaluation context to lookup the reference in
335    ///
336    /// # Returns
337    ///
338    /// Returns a `Result<Value>` containing the value of the referenced variable
339    /// or an error if the reference cannot be resolved.
340    fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
341        context.get_referenced(self)
342    }
343
344    fn write(&self, writer: &mut Writer) {
345        writer.write_reference(self);
346    }
347    fn to_sanitized(&self) -> String {
348        let mut writer = Writer::sanitizer();
349        writer.write_reference(self);
350        writer.finish()
351    }
352    fn to_formula(&self) -> String {
353        let mut writer = Writer::formulizer();
354        writer.write_reference(self);
355        writer.finish()
356    }
357}
358
359impl fmt::Display for Reference {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let mut writer = Writer::stringizer();
362        writer.write_reference(self);
363        write!(f, "{}", writer.finish())
364    }
365}