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}