aimx/rule.rs
1//! Agentic Inference Markup (AIM) Rules
2//!
3//! This module defines the core Rule structure used in AIM workflows.
4//! A Rule represents a single entry in an AIM structure, consisting of
5//! an identifier, type definition, expression, and computed value.
6//!
7//! # Rule Structure
8//!
9//! ```text
10//! rule_name: Type = expression @ value
11//! formatted_name: Format = "Instruction" "Example1" "Example2"
12//! evaluation_name: Eval = 80/100
13//! _assignment: Number = @reference
14//! ```
15//!
16//! # Categories
17//!
18//! Rules fall into three categories based on their identifier format:
19//! - **Standard rules**: lowercase identifiers (e.g., `temperature`)
20//! - **Modifier rules**: all-uppercase identifiers (e.g., `UNIT`)
21//! - **Assignment rules**: identifiers starting with underscore (e.g., `_result`)
22
23use anyhow::{Result, anyhow};
24use bitflags::bitflags;
25use std::{
26 fmt,
27};
28use nom::{
29 Finish, IResult,
30 character::complete::{char, multispace0},
31 error::{Error, ErrorKind},
32};
33use crate::{
34 ContextLike,
35 ExpressionLike,
36 statically_evaluate,
37 Expression,
38 parse_expression,
39 expressions::{parse_identifier, parse_reference},
40 Node,
41 Typedef,
42 parse_typedef,
43 Value,
44 values::{Errata, parse_eval, parse_format},
45 parse_value,
46 Writer,
47};
48
49// ----- FLAGS -----
50
51bitflags! {
52 /// Internal flags that track the state and category of a Rule
53 ///
54 /// These bitflags are used to efficiently track various properties without
55 /// requiring additional storage space. The flags are determined automatically
56 /// from the rule's identifier and updated when the rule changes.
57 #[derive(Debug, Clone, PartialEq)]
58 struct Flags: usize {
59 /// The rule has been modified and needs saving
60 const IS_TOUCHED = 0b0001;
61 /// Rule identifier is all uppercase (modifier rule)
62 const IS_MODIFIER = 0b0010;
63 /// Rule identifier starts with underscore (assignment rule)
64 const IS_ASSIGNMENT = 0b0100;
65 /// Expression has been statically evaluated to constant value
66 const IS_STATIC = 0b1000;
67 }
68}
69
70/// Check if a string contains only uppercase characters.
71pub fn is_uppercase(s: &str) -> bool {
72 s.chars().all(|c| c.is_uppercase())
73}
74
75/// Check if a string contains only lowercase characters.
76pub fn is_lowercase(s: &str) -> bool {
77 s.chars().all(|c| c.is_lowercase())
78}
79
80/// Test if an identifier should be considered a modifier rule.
81pub fn test_for_modifier(identifier: &str) -> bool {
82 if is_uppercase(identifier) {
83 true
84 } else {
85 false
86 }
87}
88
89/// Test if an identifier should be considered an assignment rule.
90pub fn test_for_assignment(identifier: &str) -> bool {
91 identifier.starts_with('_')
92}
93
94/// Initialize rule flags based on an identifier.
95fn init_flags(identifier: &str) -> Flags {
96 let mut flags = Flags::empty();
97 if test_for_modifier(&identifier) {
98 flags.insert(Flags::IS_MODIFIER);
99 }
100 if test_for_assignment(&identifier) {
101 flags.insert(Flags::IS_ASSIGNMENT);
102 }
103 flags
104}
105
106// ----- RULE -----
107
108/// A single rule in an AIM structure representing a workflow component
109///
110/// Rules are the fundamental building blocks of AIM workflows, defining
111/// how values are computed, formatted, evaluated, or assigned. Each rule
112/// consists of:
113/// - An identifier that uniquely names the rule
114/// - A type definition that constrains valid values
115/// - An expression that computes the rule's value
116/// - A current value (computed or assigned)
117/// - Internal flags tracking state and category
118///
119/// # Examples
120///
121/// ```rust
122/// # use aimx::rule::Rule;
123/// # use aimx::{Typedef, Expression, Value, Literal};
124/// // Create a simple rule
125/// let rule = Rule::new(
126/// "temperature".to_string(),
127/// Typedef::Number,
128/// Expression::Empty,
129/// Value::Literal(Literal::from_number(25.0))
130/// );
131/// ```
132#[derive(Debug, Clone, PartialEq)]
133pub struct Rule {
134 /// The rule's identifier, which uniquely identifies it within an AIM structure
135 identifier: String,
136 /// The type definition that constrains what values this rule can hold
137 typedef: Typedef,
138 /// The expression that defines how the rule's value is computed or assigned
139 expression: Expression,
140 /// The current value of the rule, computed from the expression or set directly
141 value: Value,
142 /// Flags indicating the rule's state and category
143 flags: Flags,
144}
145
146impl Rule {
147 /// Create a new Rule with the given components
148 ///
149 /// # Parameters
150 /// - `identifier`: The rule's unique identifier
151 /// - `typedef`: The type definition for the rule
152 /// - `expression`: The expression that computes the rule's value
153 /// - `value`: The initial value of the rule
154 ///
155 /// # Returns
156 /// A new Rule instance with appropriate flags set based on the identifier
157 ///
158 /// # Notes
159 /// - Automatically performs static evaluation of expressions when possible
160 /// - Sets flags based on identifier format (modifier/assignment)
161 pub fn new(identifier: String, typedef: Typedef, expression: Expression, value: Value) -> Self {
162 let mut flags = init_flags(&identifier);
163 if expression.is_empty() {
164 Self {
165 identifier,
166 typedef,
167 expression,
168 value,
169 flags,
170 }
171 } else {
172 let (exp, val) = match statically_evaluate(&expression) {
173 Ok(value) => {
174 flags.insert(Flags::IS_STATIC);
175 flags.insert(Flags::IS_TOUCHED);
176 (Expression::Empty, value)
177 }
178 _ => (expression, Value::Empty),
179 };
180 Self {
181 identifier,
182 typedef,
183 expression: exp,
184
185 value: val,
186 flags,
187 }
188 }
189 }
190
191 /// Create a new Rule by parsing operands from a string
192 ///
193 /// This constructor parses the expression/value portion of a rule definition
194 /// based on the rule's type definition and assignment status.
195 ///
196 /// # Parameters
197 /// - `input`: The string containing the rule's operands
198 /// - `identifier`: The rule's identifier
199 /// - `typedef`: The rule's type definition
200 /// - `is_assignment`: Whether this is an assignment rule
201 ///
202 /// # Returns
203 /// A new Rule instance with parsed expression and value
204 ///
205 /// # Errors
206 /// Returns error expressions for incomplete or malformed input
207 pub fn new_from(input: &str, identifier: &str, typedef: &Typedef, is_assignment: bool) -> Self {
208 match parse_rule_operands(input, typedef, is_assignment) {
209 Ok((_, (expression, value))) => {
210 Self::new(identifier.to_string(), typedef.clone(), expression, value)
211 }
212 Err(nom::Err::Incomplete(_)) => {
213 let expression = Errata::new_reason_expression(
214 "Incomplete Expression".to_owned(),
215 input.to_string()
216 );
217 Self::new(identifier.to_string(), typedef.clone(), expression, Value::Empty)
218 }
219 Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
220 let expression = Errata::new_reason_expression_location(
221 format!("Syntax Error ({})", e),
222 input.to_string(),
223 e.input.to_string()
224 );
225 Self::new(identifier.to_string(), typedef.clone(), expression, Value::Empty)
226 }
227 }
228 }
229
230 /// Check if the rule is completely empty (no expression and no value)
231 pub fn is_empty(&self) -> bool {
232 match (&self.expression, &self.value) {
233 (Expression::Empty, Value::Empty) => true,
234 _ => false,
235 }
236 }
237
238 /// Check if the rule contains any errors
239 pub fn has_error(&self) -> bool {
240 self.value.has_error() | self.expression.has_error()
241 }
242
243 /// Check if the rule has a non-empty expression
244 pub fn has_expression(&self) -> bool {
245 match &self.expression {
246 Expression::Empty
247 | Expression::Errata {..} => false,
248 _ => true,
249 }
250 }
251
252 /// Get the rule's identifier
253 pub fn identifier(&self) -> &str {
254 self.identifier.as_str()
255 }
256
257 /// Get the uppercase identifier (trimming leading underscore if present)
258 ///
259 /// Useful for case-insensitive comparisons and display purposes
260 pub fn ucid(&self) -> String {
261 let mut ucid = self.identifier.to_uppercase();
262 // Trim the leading underscore
263 if ucid.starts_with('_') {
264 // Trim first character
265 ucid.remove(0);
266 }
267 ucid
268 }
269
270 /// Get the lowercase identifier (trimming leading underscore if present)
271 ///
272 /// Useful for case-insensitive comparisons and display purposes
273 pub fn lcid(&self) -> String {
274 let mut ucid = self.identifier.to_lowercase();
275 // Trim the leading underscore
276 if ucid.starts_with('_') {
277 // Trim first character
278 ucid.remove(0);
279 }
280 ucid
281 }
282
283 /// Set a new identifier for the rule
284 ///
285 /// Updates the identifier and automatically adjusts flags based on the new identifier format
286 pub fn set_identifier(&mut self, identifier: String) {
287 // Updating the identifier potentially changes a lot of flags
288 if self.identifier != identifier {
289 // Update flags
290 if test_for_modifier(&identifier) {
291 self.flags.insert(Flags::IS_MODIFIER);
292 } else {
293 self.flags.remove(Flags::IS_MODIFIER);
294 }
295 if test_for_assignment(&identifier) {
296 self.flags.insert(Flags::IS_ASSIGNMENT);
297 } else {
298 self.flags.remove(Flags::IS_ASSIGNMENT);
299 }
300 self.identifier = identifier;
301 self.flags.insert(Flags::IS_TOUCHED);
302 }
303 }
304
305 /// Check if this is a modifier rule (all uppercase identifier)
306 pub fn is_modifier(&self) -> bool {
307 self.flags.contains(Flags::IS_MODIFIER)
308 }
309
310 /// Check if this is an assignment rule (identifier starts with underscore)
311 pub fn is_assignment(&self) -> bool {
312 self.flags.contains(Flags::IS_ASSIGNMENT)
313 }
314
315 /// Assign a value to this rule
316 ///
317 /// # Parameters
318 /// - `value`: The value to assign
319 /// - `context`: The evaluation context for resolving references
320 ///
321 /// # Returns
322 /// `Ok(())` if assignment was successful, `Err` if type mismatch occurs
323 ///
324 /// # Notes
325 /// - For assignment rules, sets the referenced value in the context
326 /// - For other rules, directly sets the rule's value
327 pub fn assign(&mut self, value: Value, context: &mut dyn ContextLike) -> Result<()> {
328 // Check the value type matches the rule typedef.
329 if value.is_literal() && value.is_of_type(&self.typedef) {
330 // Only literal types are allowed
331 match &self.value {
332 // The rule value is a reference
333 Value::Assignment(reference) => {
334 context.set_referenced(reference, value)
335 }
336 // The Rule operand is a literal so assign the value
337 _ => self.set_value(value),
338 }
339 } else {
340 Err(anyhow!("Type mismatch with assignment"))
341 }
342 }
343
344 /// Get the rule's type definition
345 pub fn typedef(&self) -> &Typedef {
346 &self.typedef
347 }
348
349 /// Set a new type definition for the rule
350 ///
351 /// Resets the value to empty and marks the rule as touched
352 pub fn set_typedef(&mut self, typedef: Typedef) {
353 if self.typedef != typedef {
354 self.typedef = typedef;
355 self.value = Value::Empty;
356 self.flags.insert(Flags::IS_TOUCHED);
357 }
358 }
359
360 /// Get the rule's expression
361 pub fn expression(&self) -> &Expression {
362 &self.expression
363 }
364
365 /// Set a new expression for the rule
366 ///
367 /// Marks the rule as touched if the expression differs from the current one
368 pub fn set_expression(&mut self, expression: Expression) {
369 if self.expression.to_formula() != expression.to_formula() {
370 self.expression = expression;
371 self.flags.insert(Flags::IS_TOUCHED);
372 }
373 }
374
375 /// Get the rule's current value
376 pub fn value(&self) -> &Value {
377 &self.value
378 }
379
380 /// Set the rule's value
381 ///
382 /// # Parameters
383 /// - `value`: The value to set
384 ///
385 /// # Returns
386 /// `Ok(())` if the value matches the rule's type, `Err` otherwise
387 pub fn set_value(&mut self, value: Value) -> Result<()> {
388 if !self.typedef.is_special() && value.is_of_type(&self.typedef) {
389 self.value = value;
390 Ok(())
391 } else {
392 Err(anyhow!("Type mismatch, expecting {} found {}",
393 self.typedef.as_string(), value.type_as_string()))
394 }
395 }
396
397 /// Check if this is a format rule
398 pub fn is_format(&self) -> bool {
399 self.typedef.is_format() && self.value.is_format()
400 }
401
402 /// Check if this is an evaluation rule
403 pub fn is_eval(&self) -> bool {
404 self.typedef.is_eval() && self.value.is_eval()
405 }
406
407 /// Mark an evaluation rule as passing
408 ///
409 /// Only affects rules with Eval type definitions
410 pub fn set_pass(&mut self) {
411 if self.is_eval() {
412 match &mut self.value {
413 Value::Eval(eval) => {
414 eval.set_pass();
415 self.flags.insert(Flags::IS_TOUCHED);
416 }
417 _ => {}
418 }
419 }
420 }
421
422 /// Mark an evaluation rule as failing
423 ///
424 /// Only affects rules with Eval type definitions
425 pub fn set_fail(&mut self) {
426 if self.is_eval() {
427 match &mut self.value {
428 Value::Eval(eval) => {
429 eval.set_fail();
430 self.flags.insert(Flags::IS_TOUCHED);
431 }
432 _ => {}
433 }
434 }
435 }
436
437 /// Check if this is a node rule
438 pub fn is_node(&self) -> bool {
439 self.typedef.is_node() && self.value.is_node()
440 }
441
442 /// Get the node from this rule if it is a node rule
443 ///
444 /// # Returns
445 /// `Some(Node)` if this is a node rule with a node value, `None` otherwise
446 pub fn get_node(&self) -> Option<Node> {
447 // Check the Rule is a Node
448 if self.is_node() {
449 match &self.value {
450 // Get the Rule Node
451 Value::Node(node) => Some(node.clone()),
452 _ => None,
453 }
454 } else {
455 None
456 }
457 }
458
459 /// Mark the rule as touched (modified and needing saving)
460 pub fn touch(&mut self) {
461 self.flags.insert(Flags::IS_TOUCHED);
462 }
463
464 /// Internal method for displaying rule content
465 fn print(&self, writer: &mut Writer) {
466 if self.value.is_empty() {
467 self.expression.write(writer);
468 }
469 else {
470 self.value.write(writer);
471 }
472 }
473
474 /// Save the rule to a writer
475 ///
476 /// Writes the complete rule definition and clears the touched flag
477 pub fn save(&mut self, writer: &mut Writer) {
478 self.write(writer);
479 writer.write_eol();
480 self.flags.remove(Flags::IS_TOUCHED);
481 }
482
483 /// Perform a partial save of the rule
484 ///
485 /// Only saves if the rule has been touched, prepending the rule index
486 ///
487 /// # Parameters
488 /// - `index`: The index of this rule in its parent structure
489 /// - `writer`: The writer to save to
490 pub fn partial_save(&mut self, index: usize, writer: &mut Writer) {
491 if self.flags.contains(Flags::IS_TOUCHED) {
492 // prepend the row index
493 writer.write_unsigned(index as u32);
494 writer.write_char(' ');
495 self.write(writer);
496 writer.write_eol();
497 self.flags.remove(Flags::IS_TOUCHED);
498 }
499 }
500
501}
502
503/// Parse a rule from a string according to the AIM grammar.
504///
505/// This function parses a complete rule definition from a string, including its identifier,
506/// optional quick evaluation status, type definition, operand, and optional value.
507///
508/// The grammar for a rule is:
509/// `rule = WS?, identifier, WS?, ':', WS?, typedef, WS?, '=', WS?, value, WS?, EOL`
510///
511/// # Parameters
512/// - `input`: The string to parse as a rule definition
513///
514/// # Returns
515/// `Ok(Rule)` if parsing was successful, or an error with details if parsing failed
516///
517/// # Examples
518///
519/// ```
520/// # use aimx::rule::parse_rule;
521/// let rule = parse_rule(r#"test_rule: Number = 42"#).unwrap();
522/// assert_eq!(rule.identifier(), "test_rule");
523/// ```
524pub fn parse_rule(input: &str) -> Result<Rule> {
525 match parse_rule_elements(input).finish() {
526 Ok((_, rule)) => Ok(rule),
527 Err(e) => Err(anyhow!("Parse error: {}", e)),
528 }
529}
530
531/// Parse the individual elements of a rule from a string according to AIM grammar.
532fn parse_rule_elements(input: &str) -> IResult<&str, Rule> {
533 // Parse identifier
534 let (input, _) = multispace0(input)?; // WS ?
535 let (input, identifier) = parse_identifier(input)?;
536 let is_assignment = identifier.starts_with('_');
537
538 // Parse colon
539 let (input, _) = multispace0(input)?; // WS ?
540 let (input, _) = char(':')(input)?;
541
542 // Parse type declaration
543 let (input, _) = multispace0(input)?; // WS ?
544 let (input, typedef) = parse_typedef(input)?;
545
546 // Parse equals sign
547 let (input, _) = multispace0(input)?; // WS ?
548 let (input, _) = char('=')(input)?;
549
550 let (input, (expression, value)) = parse_rule_operands(input, &typedef, is_assignment)?;
551
552 let rule = Rule::new(identifier, typedef, expression, value);
553 Ok((input, rule))
554}
555
556/// Parse the expression and value operands of a rule.
557fn parse_rule_operands<'a>(input: &'a str, typedef: &Typedef, is_assignment: bool)
558 -> IResult<&'a str, (Expression, Value)>
559 {
560
561 // Parse expression @ value
562 let input = input.trim(); // WS ?
563
564 // The parsing strategy depends on the typedef:
565 // - Empty input results in `Expression::Empty`
566 let (input, value) = if input.is_empty() {
567 if typedef.is_format() {
568 // Format should never be empty
569 return Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)))
570 } else {
571 (input, Value::Empty)
572 }
573 // - Constant values are from statically evaluated expressions
574 } else if input.starts_with('@') {
575 let (input, _) = char('@')(input)?;
576 let (input, value) = parse_value(input)?;
577 (input, value)
578 // - Format types are parsed as "Instruction" <format> "Example", "Example", "Example"...
579 } else if typedef.is_format() {
580 let (input, value) = parse_format(input)?;
581 (input, value)
582 // - Eval types are parsed as ratio, score, count
583 } else if typedef.is_eval() {
584 let (input, value) = parse_eval(input)?;
585 (input, value)
586 // - Node types are parsed as a "Name" (Human readable)
587 } else if typedef.is_node() {
588 // Empty for now
589 (input, Value::Empty)
590 // - Assignment references are rules with a leading underscore
591 } else if is_assignment {
592 // Parse reference
593 if let Ok((input, reference)) = parse_reference(input) {
594 (input, Value::Assignment(reference))
595 }
596 else {
597 let (input, value) = parse_value(input)?;
598 (input, value)
599 }
600 // - Expression types are parsed as normal
601 } else {
602 // Parse expression
603 let (input, expression) = parse_expression(input)?;
604 let (input, _) = multispace0(input)?; // WS ?
605 let (input, value) = if input.starts_with('@') {
606 let (input, _) = char('@')(input)?;
607 let (input, value) = parse_value(input)?;
608 (input, value)
609 } else {
610 (input, Value::Empty)
611 };
612 return Ok((input, (expression, value)))
613 };
614 Ok((input, (Expression::Empty, value)))
615}
616
617impl ExpressionLike for Rule {
618 /// Evaluate the rule's expression or value in the given context
619 ///
620 /// If the rule has an expression, evaluates it. Otherwise, evaluates the rule's value.
621 ///
622 /// # Parameters
623 /// - `context`: The evaluation context
624 ///
625 /// # Returns
626 /// `Ok(Value)` containing the evaluated result
627 fn evaluate(&self, context: &mut dyn ContextLike) -> Result<Value> {
628 if self.expression.is_empty() {
629 self.value.evaluate(context)
630 }
631 else {
632 self.expression.evaluate(context)
633 }
634 }
635
636 /// Write the rule's complete definition to a writer
637 ///
638 /// Outputs the rule in AIM format: `identifier: typedef = expression @ value`
639 fn write(&self, writer: &mut Writer) {
640 writer.write_str(&self.identifier);
641 writer.write_str(": ");
642 self.typedef.write(writer);
643 writer.write_str(" = ");
644 if self.expression.is_empty() {
645 if self.value.is_constant() {
646 writer.write_str("@ ");
647 }
648 self.value.write(writer);
649 } else {
650 self.expression.write(writer);
651 if self.value.is_constant() {
652 writer.write_str(" @ ");
653 self.value.write(writer);
654 }
655 }
656 }
657
658 /// Get a sanitized string representation of the rule
659 ///
660 /// Removes sensitive information and produces a safe representation
661 fn to_sanitized(&self) -> String {
662 let mut writer = Writer::sanitizer();
663 self.write(&mut writer);
664 writer.finish()
665 }
666
667 /// Get the rule's formula representation
668 ///
669 /// Produces a string suitable for formula evaluation
670 fn to_formula(&self) -> String {
671 let mut writer = Writer::formulizer();
672 self.write(&mut writer);
673 writer.finish()
674 }
675}
676
677impl fmt::Display for Rule {
678 /// Display the rule's content (expression or value) for user-facing output
679 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680 let mut writer = Writer::formulizer();
681 self.print(&mut writer);
682 write!(f, "{}", writer.finish())
683 }
684}