pub mod ast;
pub mod error;
pub mod lexer;
pub mod parser;
pub mod translator;
pub use ast::*;
pub use error::{Error, Result};
pub use lexer::Lexer;
pub use parser::Parser;
pub use translator::{Translator, TranslatedModel, ObjectiveType};
pub use selen;
pub use selen::prelude::{Model, Solution, VarId};
#[derive(Debug, Clone)]
pub struct SolverConfig {
pub time_limit_ms: Option<u64>,
pub memory_limit_mb: Option<u64>,
pub all_solutions: bool,
pub max_solutions: Option<usize>,
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
time_limit_ms: None,
memory_limit_mb: None,
all_solutions: false,
max_solutions: None,
}
}
}
impl SolverConfig {
pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
self.time_limit_ms = if ms > 0 { Some(ms) } else { None };
self
}
pub fn with_memory_limit_mb(mut self, mb: u64) -> Self {
self.memory_limit_mb = if mb > 0 { Some(mb) } else { None };
self
}
pub fn with_all_solutions(mut self, all: bool) -> Self {
self.all_solutions = all;
self
}
pub fn with_max_solutions(mut self, n: usize) -> Self {
self.max_solutions = if n > 0 { Some(n) } else { None };
self
}
fn to_selen_config(&self) -> selen::utils::config::SolverConfig {
let mut config = selen::utils::config::SolverConfig::default();
if let Some(ms) = self.time_limit_ms {
config.timeout_ms = Some(ms);
}
if let Some(mb) = self.memory_limit_mb {
config.max_memory_mb = Some(mb);
}
config
}
}
pub fn parse(source: &str) -> Result<ast::Model> {
let lexer = Lexer::new(source);
let mut parser = Parser::new(lexer).with_source(source.to_string());
parser.parse_model()
}
pub fn translate(ast: &ast::Model) -> Result<selen::prelude::Model> {
Translator::translate(ast)
}
pub fn build_model(source: &str) -> Result<selen::prelude::Model> {
let ast = parse(source)?;
translate(&ast)
}
pub fn build_model_with_config(source: &str, config: SolverConfig) -> Result<selen::prelude::Model> {
let ast = parse(source)?;
let selen_config = config.to_selen_config();
Translator::translate_with_config(&ast, selen_config)
}
pub fn solve_with_config(
source: &str,
config: SolverConfig,
) -> Result<Vec<selen::core::Solution>> {
let model = build_model_with_config(source, config.clone())?;
if config.all_solutions {
let max = config.max_solutions.unwrap_or(usize::MAX);
Ok(model.enumerate().take(max).collect())
} else {
match model.solve() {
Ok(solution) => Ok(vec![solution]),
Err(_) => Ok(Vec::new()), }
}
}
pub fn solve(source: &str) -> Result<std::result::Result<selen::core::Solution, selen::core::SolverError>> {
let model = build_model(source)?;
Ok(model.solve())
}
pub fn load_dzn_data(dzn_source: &str, mzn_source: &str) -> Result<String> {
let mut data_params = String::new();
let mut current_stmt = String::new();
for ch in dzn_source.chars() {
current_stmt.push(ch);
if ch == ';' {
let trimmed = current_stmt.trim();
if trimmed.len() > 1 {
let code = if let Some(pos) = trimmed.find('%') {
&trimmed[..pos]
} else {
trimmed
};
let code = code.trim_end_matches(';').trim();
if !code.is_empty() && !code.starts_with('%') {
if let Some(eq_pos) = code.find('=') {
let name = code[..eq_pos].trim();
let value = code[eq_pos + 1..].trim();
let type_decl = infer_dzn_type(value);
data_params.push_str(&format!("{}: {} = {};\n", type_decl, name, value));
}
}
}
current_stmt.clear();
}
}
let mut filtered_model = String::new();
let mut param_names = std::collections::HashSet::new();
for line in data_params.lines() {
if let Some(eq_pos) = line.find('=') {
let before_eq = &line[..eq_pos];
if let Some(last_colon) = before_eq.rfind(':') {
let name_part = &before_eq[last_colon+1..].trim();
if let Some(name) = name_part.split_whitespace().next() {
param_names.insert(name.to_string());
}
}
}
}
for line in mzn_source.lines() {
let code_line = if let Some(pos) = line.find('%') {
&line[..pos]
} else {
line
};
let trimmed = code_line.trim();
let mut skip = false;
if !trimmed.starts_with("var ") && !trimmed.starts_with("constraint ")
&& !trimmed.starts_with("solve ") && trimmed.contains(':')
&& !trimmed.contains('=') && trimmed.ends_with(';') {
for param_name in ¶m_names {
if let Some(last_colon) = trimmed.rfind(':') {
let after_colon = &trimmed[last_colon+1..].trim_end_matches(';');
if after_colon.trim().ends_with(param_name) {
skip = true;
break;
}
}
}
}
if !skip {
filtered_model.push_str(line);
filtered_model.push('\n');
}
}
Ok(format!("{}\n{}", data_params, filtered_model))
}
fn infer_dzn_type(value: &str) -> String {
let trimmed = value.trim();
if trimmed.starts_with('[') {
if trimmed.contains('{') {
"array[int] of int".to_string()
} else {
let inner = &trimmed[1..trimmed.len().saturating_sub(1)];
if inner.is_empty() {
"array[int] of int".to_string()
} else {
let elem_count = count_array_elements(inner);
let elem_type = determine_element_type(inner);
format!("array[1..{}] of {}", elem_count, elem_type)
}
}
} else if trimmed == "true" || trimmed == "false" {
"bool".to_string()
} else if trimmed.parse::<f64>().is_ok() && trimmed.contains('.') {
"float".to_string()
} else if trimmed.parse::<i64>().is_ok() {
"int".to_string()
} else {
"int".to_string()
}
}
fn count_array_elements(inner: &str) -> usize {
if inner.trim().is_empty() {
return 0;
}
let mut depth: i32 = 0;
let mut count = 1;
for ch in inner.chars() {
match ch {
'{' | '[' => depth += 1,
'}' | ']' => depth = (depth - 1).max(0),
',' if depth == 0 => count += 1,
_ => {}
}
}
count
}
fn determine_element_type(inner: &str) -> &'static str {
if inner.contains('.') {
"float"
} else if inner.contains("true") || inner.contains("false") {
"bool"
} else {
"int"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_model() {
let source = r#"
int: n = 5;
var 1..n: x;
constraint x > 2;
solve satisfy;
"#;
let result = parse(source);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let model = result.unwrap();
assert_eq!(model.items.len(), 4);
}
#[test]
fn test_parse_nqueens() {
let source = r#"
int: n = 4;
array[1..n] of var 1..n: queens;
constraint alldifferent(queens);
solve satisfy;
"#;
let result = parse(source);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
let model = result.unwrap();
assert_eq!(model.items.len(), 4);
}
#[test]
fn test_parse_with_expressions() {
let source = r#"
int: n = 10;
array[1..n] of var int: x;
constraint sum(x) == 100;
constraint forall(i in 1..n)(x[i] >= 0);
solve minimize sum(i in 1..n)(x[i] * x[i]);
"#;
let result = parse(source);
assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
}
#[test]
fn test_error_reporting() {
let source = "int n = 5";
let result = parse(source);
assert!(result.is_err());
if let Err(e) = result {
let error_msg = format!("{}", e);
assert!(error_msg.contains("line 1"));
}
}
}