pub mod cache;
mod error_handling;
mod function_collector;
pub mod pipeline;
mod result;
pub use error_handling::extract_parse_error;
pub use result::CompilationResult;
use crate::ir::ast::StoredDefinition;
use crate::modelica_grammar::ModelicaGrammar;
use crate::modelica_parser::parse;
use anyhow::{Context, Result};
use error_handling::create_syntax_error;
use indexmap::IndexSet;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
pub fn parse_source_simple(source: &str, file_name: &str) -> Option<StoredDefinition> {
let mut grammar = ModelicaGrammar::new();
if parse(source, file_name, &mut grammar).is_ok() {
grammar.modelica
} else {
None
}
}
pub fn parse_source(source: &str, file_name: &str) -> Result<StoredDefinition> {
let mut grammar = ModelicaGrammar::new();
if let Err(e) = parse(source, file_name, &mut grammar) {
let diagnostic = create_syntax_error(&e, source);
let report = miette::Report::new(diagnostic);
return Err(anyhow::anyhow!("{:?}", report));
}
grammar
.modelica
.ok_or_else(|| anyhow::anyhow!("Parser succeeded but produced no AST for {}", file_name))
}
pub fn parse_file_cached(path: &Path) -> Option<StoredDefinition> {
let file_hash = cache::compute_file_hash(path).ok()?;
if let Some(ast) = cache::load_cached_ast(path, &file_hash) {
return Some(ast);
}
let text = fs::read_to_string(path).ok()?;
let path_str = path.to_string_lossy().to_string();
let ast = parse_source_simple(&text, &path_str)?;
let _ = cache::store_cached_ast(path, &file_hash, &ast);
Some(ast)
}
pub fn parse_file_cached_result(path: &Path) -> Result<StoredDefinition> {
let file_hash = cache::compute_file_hash(path)?;
if let Some(ast) = cache::load_cached_ast(path, &file_hash) {
return Ok(ast);
}
let text = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
let path_str = path.to_string_lossy().to_string();
let ast = parse_source(&text, &path_str)?;
let _ = cache::store_cached_ast(path, &file_hash, &ast);
Ok(ast)
}
#[derive(Debug, Clone)]
pub struct Compiler {
verbose: bool,
model_name: Option<String>,
additional_files: IndexSet<PathBuf>,
modelica_paths: Vec<std::path::PathBuf>,
threads: Option<usize>,
use_cache: bool,
}
impl Default for Compiler {
fn default() -> Self {
Self {
verbose: false,
model_name: None,
additional_files: IndexSet::new(),
modelica_paths: Vec::new(),
threads: None, use_cache: true, }
}
}
impl Compiler {
pub fn new() -> Self {
Self::default()
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn model(mut self, name: &str) -> Self {
self.model_name = Some(name.to_string());
self
}
pub fn modelica_path(mut self, paths: &[&str]) -> Self {
self.modelica_paths = paths.iter().map(std::path::PathBuf::from).collect();
self
}
pub fn threads(mut self, threads: usize) -> Self {
self.threads = Some(threads.max(1));
self
}
#[cfg(not(target_arch = "wasm32"))]
fn get_thread_count(&self) -> usize {
if let Some(threads) = self.threads {
return threads;
}
let in_pool = rayon::current_num_threads() > 1;
if in_pool {
return 1;
}
std::cmp::max(1, num_cpus::get().saturating_sub(1))
}
pub fn cache(mut self, enable: bool) -> Self {
self.use_cache = enable;
self
}
pub fn include(mut self, path: &str) -> Self {
let path_buf = PathBuf::from(path);
let canonical = path_buf.canonicalize().unwrap_or(path_buf);
self.additional_files.insert(canonical);
self
}
pub fn include_all(mut self, paths: &[&str]) -> Self {
for path in paths {
self = self.include(path);
}
self
}
pub fn include_package(mut self, path: &str) -> Result<Self> {
use crate::ir::transform::multi_file::discover_modelica_files;
let package_path = std::path::Path::new(path);
let files = discover_modelica_files(package_path)?;
for file in files {
let canonical = file.canonicalize().unwrap_or(file);
self.additional_files.insert(canonical);
}
Ok(self)
}
pub fn include_from_modelica_path(self, package_name: &str) -> Result<Self> {
use crate::ir::transform::multi_file::{find_package_in_paths, get_modelica_path};
let search_paths = if self.modelica_paths.is_empty() {
get_modelica_path()
} else {
self.modelica_paths.clone()
};
let package_path = find_package_in_paths(package_name, &search_paths).ok_or_else(|| {
anyhow::anyhow!(
"Package '{}' not found in library paths: {:?}",
package_name,
search_paths
)
})?;
self.include_package(&package_path.to_string_lossy())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn compile_package(&self, path: &str) -> Result<CompilationResult> {
use crate::ir::transform::multi_file::discover_modelica_files;
let package_path = std::path::Path::new(path);
let files = discover_modelica_files(package_path)?;
if files.is_empty() {
anyhow::bail!("No Modelica files found in package: {}", path);
}
let file_strs: Vec<&str> = files
.iter()
.map(|p| p.to_str().expect("path contains invalid UTF-8"))
.collect();
self.compile_files(&file_strs)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn compile_file(&self, path: &str) -> Result<CompilationResult> {
use std::sync::atomic::{AtomicUsize, Ordering};
let thread_count = self.get_thread_count();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.with_context(|| "Failed to create thread pool")?;
let cache_hits = AtomicUsize::new(0);
let cache_misses = AtomicUsize::new(0);
if self.verbose {
println!(
"Parsing {} files using {} threads (cache: {})...",
self.additional_files.len() + 1,
thread_count,
if self.use_cache {
"enabled"
} else {
"disabled"
}
);
}
let additional_paths: Vec<_> = self.additional_files.iter().collect();
let use_cache = self.use_cache;
let parsed_additional: Result<Vec<_>> = pool.install(|| {
additional_paths
.par_iter()
.map(|additional_path| {
let path_str = additional_path.to_string_lossy().to_string();
let file_hash = cache::compute_file_hash(additional_path)
.unwrap_or_else(|_| "unknown".to_string());
if use_cache
&& let Some(cached_def) =
cache::load_cached_ast(additional_path, &file_hash)
{
cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok((path_str, cached_def, file_hash));
}
cache_misses.fetch_add(1, Ordering::Relaxed);
let additional_source = fs::read_to_string(additional_path)
.with_context(|| format!("Failed to read file: {}", path_str))?;
let def = self.parse_source(&additional_source, &path_str)?;
if use_cache {
let _ = cache::store_cached_ast(additional_path, &file_hash, &def);
}
Ok((path_str, def, file_hash))
})
.collect()
});
let additional_results = parsed_additional?;
if self.verbose {
println!(
"Cache: {} hits, {} misses",
cache_hits.load(Ordering::Relaxed),
cache_misses.load(Ordering::Relaxed)
);
}
let input =
fs::read_to_string(path).with_context(|| format!("Failed to read file: {}", path))?;
let main_hash = format!("{:x}", chksum_md5::hash(&input));
let main_def = self.parse_source(&input, path)?;
let mut all_definitions: Vec<(String, StoredDefinition)> = additional_results
.iter()
.map(|(p, d, _)| (p.clone(), d.clone()))
.collect();
all_definitions.push((path.to_string(), main_def));
let mut all_hashes: Vec<String> =
additional_results.into_iter().map(|(_, _, h)| h).collect();
all_hashes.push(main_hash);
self.compile_definitions_with_hashes(all_definitions, &input, path, Some(all_hashes))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn compile_files(&self, paths: &[&str]) -> Result<CompilationResult> {
if paths.is_empty() {
anyhow::bail!("At least one file must be provided");
}
let thread_count = self.get_thread_count();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.with_context(|| "Failed to create thread pool")?;
if self.verbose {
println!(
"Parsing {} files using {} threads...",
paths.len(),
thread_count
);
}
let parsed_results: Result<Vec<_>> = pool.install(|| {
paths
.par_iter()
.map(|path| {
let source = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path))?;
let def = self.parse_source(&source, path)?;
Ok((path.to_string(), def, source))
})
.collect()
});
let results = parsed_results?;
let all_definitions: Vec<_> = results
.iter()
.map(|(path, def, _)| (path.clone(), def.clone()))
.collect();
let main_source = &results.last().expect("no files were compiled").2;
let main_path = &results.last().expect("no files were compiled").0;
self.compile_definitions(all_definitions, main_source, main_path)
}
fn parse_source(&self, source: &str, file_name: &str) -> Result<StoredDefinition> {
let mut grammar = ModelicaGrammar::new();
if let Err(e) = parse(source, file_name, &mut grammar) {
let diagnostic = create_syntax_error(&e, source);
let report = miette::Report::new(diagnostic);
return Err(anyhow::anyhow!("{:?}", report));
}
grammar.modelica.ok_or_else(|| {
anyhow::anyhow!("Parser succeeded but produced no AST for {}", file_name)
})
}
pub fn compile_definitions(
&self,
definitions: Vec<(String, StoredDefinition)>,
main_source: &str,
_main_file_name: &str,
) -> Result<CompilationResult> {
self.compile_definitions_with_hashes(definitions, main_source, _main_file_name, None)
}
fn compile_definitions_with_hashes(
&self,
definitions: Vec<(String, StoredDefinition)>,
main_source: &str,
_main_file_name: &str,
_source_hashes: Option<Vec<String>>,
) -> Result<CompilationResult> {
use crate::ir::transform::multi_file::merge_stored_definitions;
let start = Instant::now();
let def = if definitions.len() == 1 {
definitions
.into_iter()
.next()
.expect("definitions is non-empty")
.1
} else {
if self.verbose {
println!("Merging {} files...", definitions.len());
}
merge_stored_definitions(definitions)?
};
let model_hash = format!("{:x}", chksum_md5::hash(main_source));
let parse_time = start.elapsed();
if self.verbose {
println!("Parsing took {} ms", parse_time.as_millis());
println!("AST:\n{:#?}\n", def);
}
pipeline::compile_from_ast_ref(
&def,
self.model_name.as_deref(),
model_hash,
parse_time,
self.verbose,
)
}
pub fn compile_str(&self, source: &str, file_name: &str) -> Result<CompilationResult> {
#[cfg(target_arch = "wasm32")]
{
let def = self.parse_source(source, file_name)?;
let all_definitions = vec![(file_name.to_string(), def)];
return self.compile_definitions(all_definitions, source, file_name);
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::sync::atomic::{AtomicUsize, Ordering};
let thread_count = self.get_thread_count();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.with_context(|| "Failed to create thread pool")?;
let cache_hits = AtomicUsize::new(0);
let cache_misses = AtomicUsize::new(0);
if self.verbose && !self.additional_files.is_empty() {
println!(
"Parsing {} additional files using {} threads (cache: {})...",
self.additional_files.len(),
thread_count,
if self.use_cache {
"enabled"
} else {
"disabled"
}
);
}
let additional_paths: Vec<_> = self.additional_files.iter().collect();
let use_cache = self.use_cache;
let parsed_additional: Result<Vec<_>> = pool.install(|| {
additional_paths
.par_iter()
.map(|additional_path| {
let path_str = additional_path.to_string_lossy().to_string();
if use_cache
&& let Ok(hash) = cache::compute_file_hash(additional_path)
&& let Some(cached_def) = cache::load_cached_ast(additional_path, &hash)
{
cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok((path_str, cached_def));
}
cache_misses.fetch_add(1, Ordering::Relaxed);
let additional_source = fs::read_to_string(additional_path)
.with_context(|| format!("Failed to read file: {}", path_str))?;
let def = self.parse_source(&additional_source, &path_str)?;
if use_cache && let Ok(hash) = cache::compute_file_hash(additional_path) {
let _ = cache::store_cached_ast(additional_path, &hash, &def);
}
Ok((path_str, def))
})
.collect()
});
let mut all_definitions = parsed_additional?;
if self.verbose && !self.additional_files.is_empty() {
println!(
"Cache: {} hits, {} misses",
cache_hits.load(Ordering::Relaxed),
cache_misses.load(Ordering::Relaxed)
);
}
let def = self.parse_source(source, file_name)?;
all_definitions.push((file_name.to_string(), def));
self.compile_definitions(all_definitions, source, file_name)
}
}
pub fn compile_parsed(&self, def: StoredDefinition, source: &str) -> Result<CompilationResult> {
let model_hash = format!("{:x}", chksum_md5::hash(source));
pipeline::compile_from_ast(
def,
self.model_name.as_deref(),
model_hash,
std::time::Duration::ZERO, self.verbose,
)
}
pub fn compile_parsed_ref(
&self,
def: &StoredDefinition,
source: &str,
) -> Result<CompilationResult> {
let model_hash = format!("{:x}", chksum_md5::hash(source));
pipeline::compile_from_ast_ref(
def,
self.model_name.as_deref(),
model_hash,
std::time::Duration::ZERO,
self.verbose,
)
}
pub fn check_balance(
&self,
def: &StoredDefinition,
) -> Result<crate::dae::balance::BalanceResult> {
pipeline::check_balance_only(def, self.model_name.as_deref())
}
pub fn compile_str_with_definitions(
&self,
source: &str,
file_name: &str,
libraries: &[(String, StoredDefinition)],
) -> Result<CompilationResult> {
let main_def = self.parse_source(source, file_name)?;
let mut all_definitions: Vec<(String, StoredDefinition)> = libraries.to_vec();
all_definitions.push((file_name.to_string(), main_def));
self.compile_definitions(all_definitions, source, file_name)
}
pub fn compile_str_with_merged_libraries(
&self,
source: &str,
file_name: &str,
libraries: &[&StoredDefinition],
) -> Result<CompilationResult> {
let main_def = self.parse_source(source, file_name)?;
let mut all_definitions: Vec<(String, StoredDefinition)> = libraries
.iter()
.enumerate()
.map(|(i, lib)| (format!("<library-{}>", i), (*lib).clone()))
.collect();
all_definitions.push((file_name.to_string(), main_def));
self.compile_definitions(all_definitions, source, file_name)
}
pub fn compile_str_with_arc_libraries(
&self,
source: &str,
file_name: &str,
libraries: &[Arc<StoredDefinition>],
) -> Result<CompilationResult> {
use crate::ir::transform::multi_file::merge_with_arc_libraries;
let main_def = self.parse_source(source, file_name)?;
let start = Instant::now();
let def = merge_with_arc_libraries(libraries, main_def, file_name)?;
let model_hash = format!("{:x}", chksum_md5::hash(source));
let parse_time = start.elapsed();
if self.verbose {
println!("Parsing and merging took {} ms", parse_time.as_millis());
println!("AST:\n{:#?}\n", def);
}
pipeline::compile_from_ast_ref(
&def,
self.model_name.as_deref(),
model_hash,
parse_time,
self.verbose,
)
}
pub fn compile_str_with_sources(
&self,
source: &str,
file_name: &str,
libraries: Vec<(&str, &str)>,
) -> Result<CompilationResult> {
let main_def = self.parse_source(source, file_name)?;
let mut all_definitions = Vec::with_capacity(libraries.len() + 1);
for (lib_name, lib_source) in libraries {
let lib_def = self.parse_source(lib_source, lib_name)?;
all_definitions.push((lib_name.to_string(), lib_def));
}
all_definitions.push((file_name.to_string(), main_def));
self.compile_definitions(all_definitions, source, file_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compiler_default() {
let compiler = Compiler::new();
assert!(!compiler.verbose);
}
#[test]
fn test_compiler_verbose() {
let compiler = Compiler::new().verbose(true);
assert!(compiler.verbose);
}
#[test]
fn test_compile_simple_model() {
let source = r#"
model Integrator
Real x(start=0);
equation
der(x) = 1;
end Integrator;
"#;
let result = Compiler::new()
.model("Integrator")
.compile_str(source, "test.mo");
assert!(result.is_ok(), "Failed to compile: {:?}", result.err());
let result = result.unwrap();
assert!(!result.dae.x.is_empty(), "Should have state variables");
assert_eq!(result.dae.x.len(), 1, "Should have exactly one state");
}
#[test]
fn test_compile_requires_model_name() {
let source = r#"
model Test
Real x;
equation
der(x) = 1;
end Test;
"#;
let result = Compiler::new().compile_str(source, "test.mo");
assert!(result.is_err(), "Should error when model name not provided");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Model name is required"),
"Error should mention model name is required: {}",
err_msg
);
}
#[test]
fn test_compilation_result_total_time() {
let source = r#"
model Test
Real x;
equation
der(x) = 1;
end Test;
"#;
let result = Compiler::new()
.model("Test")
.compile_str(source, "test.mo")
.unwrap();
let total = result.total_time();
assert!(total > std::time::Duration::from_nanos(0));
assert_eq!(
total,
result.parse_time + result.flatten_time + result.dae_time
);
}
}