Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve docs #356

Merged
merged 13 commits into from
Oct 9, 2024
22 changes: 22 additions & 0 deletions crates/conjure_core/src/ast/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ use serde::{Deserialize, Serialize};

use crate::ast::domains::{Domain, Range};

/// Represents a decision variable within a computational model.
///
/// A `DecisionVariable` has a domain that defines the set of values it can take. The domain could be:
/// - A boolean domain, meaning the variable can only be `true` or `false`.
/// - An integer domain, meaning the variable can only take specific integer values or a range of integers.
///
/// # Fields
/// - `domain`:
/// - Type: `Domain`
/// - Represents the set of possible values that this decision variable can assume. The domain can be a range of integers
/// (`IntDomain`) or a boolean domain (`BoolDomain`).
///
/// # Example
/// ```
/// use crate::ast::domains::{DecisionVariable, Domain, Range};
///
/// let bool_var = DecisionVariable::new(Domain::BoolDomain);
/// let int_var = DecisionVariable::new(Domain::IntDomain(vec![Range::Bounded(1, 10)]));
///
/// println!("Boolean Variable: {}", bool_var);
/// println!("Integer Variable: {}", int_var);
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DecisionVariable {
pub domain: Domain,
Expand Down
30 changes: 30 additions & 0 deletions crates/conjure_core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ use crate::ast::{DecisionVariable, Domain, Expression, Name, SymbolTable};
use crate::context::Context;
use crate::metadata::Metadata;

/// Represents a computational model containing variables, constraints, and a shared context.
///
/// The `Model` struct holds a set of variables and constraints for manipulating and evaluating symbolic expressions.
///
/// # Fields
/// - `variables`:
/// - Type: `SymbolTable`
/// - A table that links each variable's name to its corresponding `DecisionVariable`.
/// - For example, the name `x` might be linked to a `DecisionVariable` that says `x` can only take values between 1 and 10.
///
/// - `constraints`:
/// - Type: `Expression`
/// - Represents the logical constraints applied to the model's variables.
/// - Can be a single constraint or a combination of various expressions, such as logical operations (e.g., `AND`, `OR`),
/// arithmetic operations (e.g., `SafeDiv`, `UnsafeDiv`), or specialized constraints like `SumEq`.
///
/// - `context`:
/// - Type: `Arc<RwLock<Context<'static>>>`
/// - A shared object that stores global settings and state for the model.
/// - Can be safely read or changed by multiple parts of the program at the same time, making it good for multi-threaded use.
///
/// - `next_var`:
/// - Type: `RefCell<i32>`
/// - A counter used to create new, unique variable names.
/// - Allows updating the counter inside the model without making the whole model mutable.
///
/// # Usage
/// This struct is typically used to:
/// - Define a set of variables and constraints for rule-based evaluation.
/// - Have transformations, optimizations, and simplifications applied to it using a set of rules.
#[serde_as]
#[derive(Derivative, Clone, Debug, Serialize, Deserialize)]
#[derivative(PartialEq, Eq)]
Expand Down
157 changes: 147 additions & 10 deletions crates/conjure_core/src/rule_engine/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,52 @@ fn optimizations_enabled() -> bool {
}
}

/// Rewrites the model by applying the rules to all constraints.
/// Rewrites the given model by applying a set of rules to all its constraints.
///
/// Any side-effects such as symbol table updates and top-level constraints are applied to the returned model.
/// This function iteratively applies transformations to the model's constraints using the specified rule sets.
/// It returns a modified version of the model with all applicable rules applied, ensuring that any side-effects
/// such as updates to the symbol table and top-level constraints are properly reflected in the returned model.
///
/// # Parameters
/// - `model`: A reference to the [`Model`] to be rewritten. The function will clone this model to produce a modified version.
/// - `rule_sets`: A vector of references to [`RuleSet`]s that define the rules to be applied to the model's constraints.
/// Each `RuleSet` is expected to contain a collection of rules that can transform one or more constraints
/// within the model. The lifetime parameter `'a` ensures that the rules' references are valid for the
/// duration of the function execution.
///
/// # Returns
/// A copy of the model after all, if any, possible rules are applied to its constraints.
/// - `Ok(Model)`: If successful, it returns a modified copy of the [`Model`] after all applicable rules have been
/// applied. This new model includes any side-effects such as updates to the symbol table or modifications
/// to the constraints.
/// - `Err(RewriteError)`: If an error occurs during rule application (e.g., invalid rules or failed constraints),
/// it returns a [`RewriteError`] with details about the failure.
///
/// # Side-Effects
/// - When the model is rewritten, related data structures such as the symbol table (which tracks variable names and types)
/// or other top-level constraints may also be updated to reflect these changes. These updates are applied to the returned model,
/// ensuring that all related components stay consistent and aligned with the changes made during the rewrite.
/// - The function collects statistics about the rewriting process, including the number of rule applications
/// and the total runtime of the rewriter. These statistics are then stored in the model's context for
/// performance monitoring and analysis.
///
/// # Example
/// ```
/// // Add an example
/// ```
///
/// # Performance Considerations
/// - The function checks if optimizations are enabled before applying rules, which may affect the performance
/// of the rewriting process.
/// - Depending on the size of the model and the number of rules, the rewriting process might take a significant
/// amount of time. Use the statistics collected (`rewriter_run_time` and `rewriter_rule_application_attempts`)
/// to monitor and optimize performance.
///
/// # Panics
/// - This function may panic if the model's context is unavailable or if there is an issue with locking the context.
///
/// # See Also
/// - [`get_rule_priorities`]: Retrieves the priorities for the given rules.
/// - [`rewrite_iteration`]: Executes a single iteration of rewriting the model using the specified rules.
pub fn rewrite_model<'a>(
model: &Model,
rule_sets: &Vec<&'a RuleSet<'a>>,
Expand Down Expand Up @@ -91,9 +131,43 @@ pub fn rewrite_model<'a>(
Ok(new_model)
}

/// Attempts to apply a set of rules to the given expression and its sub-expressions in the model.
///
/// This function recursively traverses the provided expression, applying any applicable rules from the given set.
/// If a rule is successfully applied to the expression or any of its sub-expressions, it returns a `Reduction`
/// containing the new expression, modified top-level constraints, and any changes to symbols. If no rules can be
/// applied at any level, it returns `None`.
///
/// # Parameters
/// - `expression`: A reference to the [`Expression`] to be rewritten. This is the main expression that the function
/// attempts to modify using the given rules.
/// - `model`: A reference to the [`Model`] that provides context and additional constraints for evaluating the rules.
/// - `rules`: A vector of references to [`Rule`]s that define the transformations to apply to the expression.
/// - `apply_optimizations`: A boolean flag that indicates whether optimization checks should be applied during the rewriting process.
/// If `true`, the function skips already "clean" (fully optimized or processed) expressions and marks them accordingly
/// to avoid redundant work.
/// - `stats`: A mutable reference to [`RewriterStats`] to collect statistics about the rule application process, such as
/// the number of rules applied and the time taken for each iteration.
///
/// # Returns
/// - Some(<new_expression>) after applying the first applicable rule to `expr` or a sub-expression.
/// - None if no rule is applicable to the expression or any sub-expression.
/// - `Some(<Reduction>)`: A [`Reduction`] containing the new expression and any associated modifications if a rule was applied
/// to `expr` or one of its sub-expressions.
/// - `None`: If no rule is applicable to the expression or any of its sub-expressions.
///
/// # Side-Effects
/// - If `apply_optimizations` is enabled, the function will skip "clean" expressions and mark successfully rewritten
/// expressions as "dirty". This is done to avoid unnecessary recomputation of expressions that have already been
/// optimized or processed.
///
/// # Example
/// ```
/// // Add an example
/// ```
///
/// # Notes
/// - This function works recursively, meaning it traverses all sub-expressions within the given `expression` to find the
/// first rule that can be applied. If a rule is applied, it immediately returns the modified expression and stops
/// further traversal for that branch.
fn rewrite_iteration<'a>(
expression: &'a Expression,
model: &'a Model,
Expand Down Expand Up @@ -132,9 +206,53 @@ fn rewrite_iteration<'a>(
None
}

/// Applies all the given rules to a specific expression within the model.
///
/// This function iterates through the provided rules and attempts to apply each rule to the given `expression`.
/// If a rule is successfully applied, it creates a [`RuleResult`] containing the original rule and the resulting
/// [`Reduction`]. The statistics (`stats`) are updated to reflect the number of rule application attempts and successful
/// applications.
///
/// The function does not modify the provided `expression` directly. Instead, it collects all applicable rule results
/// into a vector, which can then be used for further processing or selection (e.g., with [`choose_rewrite`]).
///
/// # Parameters
/// - `expression`: A reference to the [`Expression`] that will be evaluated against the given rules. This is the main
/// target for rule transformations and is expected to remain unchanged during the function execution.
/// - `model`: A reference to the [`Model`] that provides context for rule evaluation, such as constraints and symbols.
/// Rules may depend on information in the model to determine if they can be applied.
/// - `rules`: A vector of references to [`Rule`]s that define the transformations to be applied to the expression.
/// Each rule is applied independently, and all applicable rules are collected.
/// - `stats`: A mutable reference to [`RewriterStats`] used to track statistics about rule application, such as
/// the number of attempts and successful applications.
///
/// # Returns
/// - A list of RuleResults after applying all rules to `expression`.
/// - An empty list if no rules are applicable.
/// - A `Vec<RuleResult>` containing all rule applications that were successful. Each element in the vector represents
/// a rule that was applied to the given `expression` along with the resulting transformation.
/// - An empty vector if no rules were applicable to the expression.
///
/// # Side-Effects
/// - The function updates the provided `stats` with the number of rule application attempts and successful applications.
/// - Debug or trace logging may be performed to track which rules were applicable or not for a given expression.
///
/// # Example
/// ```rust
/// let applicable_rules = apply_all_rules(&expr, &model, &rules, &mut stats);
/// if !applicable_rules.is_empty() {
/// for result in applicable_rules {
/// println!("Rule applied: {:?}", result.rule);
/// }
/// }
/// ```
///
/// # Notes
/// - This function does not modify the input `expression` or `model` directly. The returned `RuleResult` vector
/// provides information about successful transformations, allowing the caller to decide how to process them.
/// - The function performs independent rule applications. If rules have dependencies or should be applied in a
/// specific order, consider handling that logic outside of this function.
///
/// # See Also
/// - [`choose_rewrite`]: Chooses a single reduction from the rule results provided by `apply_all_rules`.
fn apply_all_rules<'a>(
expression: &'a Expression,
model: &'a Model,
Expand All @@ -143,7 +261,7 @@ fn apply_all_rules<'a>(
) -> Vec<RuleResult<'a>> {
let mut results = Vec::new();
for rule in rules {
match rule.apply(expression, model) {
Copy link
Contributor Author

@YehorBoiar YehorBoiar Oct 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidentally changed this line

match rule.apply(expression, modchooseel) {
Ok(red) => {
log::trace!(target: "file", "Rule applicable: {:?}, to Expression: {:?}, resulting in: {:?}", rule, expression, red.new_expression);
stats.rewriter_rule_application_attempts =
Expand All @@ -169,9 +287,28 @@ fn apply_all_rules<'a>(
results
}

/// Chooses the first applicable rule result from a list of rule applications.
///
/// This function selects a reduction from the provided `RuleResult` list, prioritizing the first rule
/// that successfully transforms the expression. This strategy can be modified in the future to incorporate
/// more complex selection criteria, such as prioritizing rules based on cost, complexity, or other heuristic metrics.
///
/// # Parameters
/// - `results`: A slice of [`RuleResult`] containing potential rule applications to be considered. Each element
/// represents a rule that was successfully applied to the expression, along with the resulting transformation.
///
/// # Returns
/// - Some(<reduction>) after applying the first rule in `results`.
/// - None if `results` is empty.
/// - `Some(<Reduction>)`: Returns a [`Reduction`] representing the first rule's application if there is at least one
/// rule that produced a successful transformation.
/// - `None`: If no rule applications are available in the `results` slice (i.e., it is empty), it returns `None`.
///
/// # Example
/// ```rust
/// let rule_results = vec![rule1_result, rule2_result];
/// if let Some(reduction) = choose_rewrite(&rule_results) {
/// // Process the chosen reduction
/// }
/// ```
fn choose_rewrite(results: &[RuleResult]) -> Option<Reduction> {
if results.is_empty() {
return None;
Expand Down
35 changes: 33 additions & 2 deletions crates/conjure_core/src/rule_engine/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,40 @@ pub enum ApplicationError {
DomainError,
}

/// The result of applying a rule to an expression.
/// Represents the result of applying a rule to an expression within a model.
///
/// A `Reduction` encapsulates the changes made to a model during a rule application.
/// It includes a new expression to replace the original one, an optional top-level constraint
/// to be added to the model, and any updates to the model's symbol table.
///
/// This struct allows for representing side-effects of rule applications, ensuring that
/// all modifications, including symbol table expansions and additional constraints, are
/// accounted for and can be applied to the model consistently.
///
/// # Fields
/// - `new_expression`: The updated [`Expression`] that replaces the original one after applying the rule.
/// - `new_top`: An additional top-level [`Expression`] constraint that should be added to the model. If no top-level
/// constraint is needed, this field can be set to `Expression::Nothing`.
/// - `symbols`: A [`SymbolTable`] containing any new symbol definitions or modifications to be added to the model's
/// symbol table. If no symbols are modified, this field can be set to an empty symbol table.
///
/// # Usage
/// A `Reduction` can be created using one of the provided constructors:
/// - [`Reduction::new`]: Creates a reduction with a new expression, top-level constraint, and symbol modifications.
/// - [`Reduction::pure`]: Creates a reduction with only a new expression and no side-effects on the symbol table or constraints.
/// - [`Reduction::with_symbols`]: Creates a reduction with a new expression and symbol table modifications, but no top-level constraint.
/// - [`Reduction::with_top`]: Creates a reduction with a new expression and a top-level constraint, but no symbol table modifications.
///
/// The `apply` method allows for applying the changes represented by the `Reduction` to a [`Model`].
///
/// # Example
/// ```
/// // Need to add an example
/// ```
///
/// Contains an expression to replace the original, a top-level constraint to add to the top of the constraint AST, and an expansion to the model symbol table.
/// # See Also
/// - [`ApplicationResult`]: Represents the result of applying a rule, which may either be a `Reduction` or an `ApplicationError`.
/// - [`Model`]: The structure to which the `Reduction` changes are applied.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Reduction {
Expand Down
60 changes: 59 additions & 1 deletion crates/conjure_core/src/stats/rewriter_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,69 @@ use schemars::JsonSchema;
use serde::Serialize;
use serde_with::skip_serializing_none;

/// Represents the statistical data collected during the model rewriting process.
///
/// The `RewriterStats` struct is used to track various metrics and statistics related to the rewriting
/// of a model using a set of rules. These statistics can be used for performance monitoring, debugging,
/// and optimization purposes. The structure supports optional fields, allowing selective tracking of data
/// without requiring all fields to be set.
///
/// The struct uses the following features:
/// - `#[skip_serializing_none]`: Skips serializing fields that have a value of `None`, resulting in cleaner JSON output.
/// - `#[serde(rename_all = "camelCase")]`: Uses camelCase for all serialized field names, adhering to common JSON naming conventions.
///
/// # Fields
/// - `is_optimization_enabled`:
/// - Type: `Option<bool>`
/// - Indicates whether optimizations were enabled during the rewriting process.
/// - If `Some(true)`, it means optimizations were enabled.
/// - If `Some(false)`, optimizations were explicitly disabled.
/// - If `None`, the status of optimizations is unknown or not tracked.
///
/// - `rewriter_run_time`:
/// - Type: `Option<std::time::Duration>`
/// - The total runtime duration of the rewriter in the current session.
/// - If set, it indicates the amount of time spent on rewriting, measured as a `Duration`.
/// - If `None`, the runtime is either unknown or not tracked.
///
/// - `rewriter_rule_application_attempts`:
/// - Type: `Option<usize>`
/// - The number of rule application attempts made during the rewriting process.
/// - An attempt is counted each time a rule is evaluated, regardless of whether it was successfully applied.
/// - If `None`, this metric is not tracked or not applicable for the current session.
///
/// - `rewriter_rule_applications`:
/// - Type: `Option<usize>`
/// - The number of successful rule applications during the rewriting process.
/// - A successful application means the rule was successfully applied to transform the expression or constraint.
/// - If `None`, this metric is not tracked or not applicable for the current session.
///
/// # Example
/// ```
/// let stats = RewriterStats {
/// is_optimization_enabled: Some(true),
/// rewriter_run_time: Some(std::time::Duration::new(2, 0)),
/// rewriter_rule_application_attempts: Some(15),
/// rewriter_rule_applications: Some(10),
/// };
///
/// // Serialize the stats to JSON
/// let serialized_stats = serde_json::to_string(&stats).unwrap();
/// println!("Serialized Stats: {}", serialized_stats);
/// ```
///
/// # Usage Notes
/// - This struct is intended to be used in contexts where tracking the performance and behavior of rule-based
/// rewriting systems is necessary. It is designed to be easily serialized and deserialized to/from JSON, making it
/// suitable for logging, analytics, and reporting purposes.
///
/// # See Also
/// - [`serde_with::skip_serializing_none`]: For skipping `None` values during serialization.
/// - [`std::time::Duration`]: For measuring and representing time intervals.
#[skip_serializing_none]
#[derive(Default, Serialize, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]

pub struct RewriterStats {
pub is_optimization_enabled: Option<bool>,
pub rewriter_run_time: Option<std::time::Duration>,
Expand Down
Loading