mod cache;
mod class_dict;
mod connections;
mod expansion;
mod hash;
mod helpers;
mod imports;
mod validation;
pub use cache::{
clear_all_caches, clear_caches, disable_cache, enable_cache, get_cache_stats, is_cache_enabled,
};
pub use hash::{FileDependencies, FlattenResult};
pub use class_dict::{
build_combined_class_dict, clear_library_dict_cache, get_or_build_library_dict,
};
use class_dict::{get_or_build_class_dict, lookup_class};
use connections::expand_connect_equations;
use expansion::ExpansionContext;
use hash::{
FILE_HASH_CACHE, build_dependency_graph, compute_def_hash, compute_dependency_levels,
record_file_dep,
};
use imports::{
build_import_aliases_for_class, collect_imported_packages_for_class,
extract_extends_modifications, resolve_class_name_with_imports, validate_imports,
};
use validation::check_cardinality_array_connectors;
use crate::ir;
use crate::ir::analysis::reference_checker::collect_imported_packages;
use crate::ir::analysis::symbol_table::SymbolTable;
use crate::ir::ast::{Expression, Import, OpBinary};
use crate::ir::error::IrError;
use crate::ir::transform::constants::is_primitive_type;
use crate::ir::visitor::MutVisitor;
use anyhow::Result;
use indexmap::{IndexMap, IndexSet};
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::collections::HashMap;
use std::sync::RwLock;
use std::sync::{Arc, LazyLock};
pub type ClassDict = IndexMap<String, Arc<ir::ast::ClassDefinition>>;
static CLASS_DICT_CACHE: LazyLock<RwLock<HashMap<u64, Arc<ClassDict>>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
type ResolvedClassEntry = (Arc<ir::ast::ClassDefinition>, FileDependencies);
type ResolvedClassKey = (u64, String);
static RESOLVED_CLASS_CACHE: LazyLock<RwLock<HashMap<ResolvedClassKey, ResolvedClassEntry>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
type ExtendsChainCache = HashMap<(String, String), Option<String>>;
static EXTENDS_CHAIN_CACHE: LazyLock<RwLock<ExtendsChainCache>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
pub fn prewarm_class_cache(def: &ir::ast::StoredDefinition) -> usize {
let def_hash = compute_def_hash(def);
let class_dict = get_or_build_class_dict(def, def_hash);
let deps = build_dependency_graph(&class_dict);
let levels = compute_dependency_levels(&class_dict, &deps);
let mut total_prewarmed = 0;
for level in &levels {
#[cfg(not(target_arch = "wasm32"))]
level.par_iter().for_each(|class_name| {
if let Some(class_arc) = class_dict.get(class_name) {
let _ = resolve_class(class_arc, class_name, &class_dict, def_hash);
}
});
#[cfg(target_arch = "wasm32")]
level.iter().for_each(|class_name| {
if let Some(class_arc) = class_dict.get(class_name) {
let _ = resolve_class(class_arc, class_name, &class_dict, def_hash);
}
});
total_prewarmed += level.len();
}
total_prewarmed
}
#[derive(Debug, Clone)]
struct ScopeRenamer<'a> {
symbol_table: &'a SymbolTable,
scope_prefix: String,
component_globals: std::collections::HashSet<String>,
}
impl<'a> ScopeRenamer<'a> {
fn with_class_imports(
symbol_table: &'a SymbolTable,
scope_prefix: &str,
class_path: &str,
class_dict: &ClassDict,
) -> Self {
Self {
symbol_table,
scope_prefix: scope_prefix.to_string(),
component_globals: collect_imported_packages_for_class(class_path, class_dict),
}
}
fn is_global(&self, name: &str) -> bool {
self.symbol_table.is_global(name) || self.component_globals.contains(name)
}
}
impl MutVisitor for ScopeRenamer<'_> {
fn exit_component_reference(&mut self, node: &mut ir::ast::ComponentReference) {
let first_part_is_global = node
.parts
.first()
.map(|p| self.is_global(&p.ident.text))
.unwrap_or(false);
if !first_part_is_global {
node.parts.insert(
0,
ir::ast::ComponentRefPart {
ident: ir::ast::Token {
text: self.scope_prefix.clone(),
..Default::default()
},
subs: None,
},
);
}
}
}
fn resolve_class(
class: &ir::ast::ClassDefinition,
current_class_path: &str,
class_dict: &ClassDict,
def_hash: u64,
) -> Result<(Arc<ir::ast::ClassDefinition>, FileDependencies)> {
let cache_key = (def_hash, current_class_path.to_string());
if is_cache_enabled()
&& let Some((resolved, deps)) = RESOLVED_CLASS_CACHE.read().unwrap().get(&cache_key)
{
return Ok((Arc::clone(resolved), deps.clone()));
}
let mut visited = IndexSet::new();
let mut deps = FileDependencies::new();
let resolved = resolve_class_internal(
class,
current_class_path,
class_dict,
&mut visited,
&mut deps,
)?;
let resolved_arc = Arc::new(resolved);
if is_cache_enabled() {
RESOLVED_CLASS_CACHE
.write()
.unwrap()
.insert(cache_key, (Arc::clone(&resolved_arc), deps.clone()));
}
Ok((resolved_arc, deps))
}
fn resolve_class_internal(
class: &ir::ast::ClassDefinition,
current_class_path: &str,
class_dict: &ClassDict,
visited: &mut IndexSet<String>,
deps: &mut FileDependencies,
) -> Result<ir::ast::ClassDefinition> {
if visited.contains(current_class_path) {
return Ok(class.clone());
}
visited.insert(current_class_path.to_string());
record_file_dep(deps, &class.location.file_name);
let mut resolved = class.clone();
let import_aliases = build_import_aliases_for_class(current_class_path, class_dict);
for import in &class.imports {
let targets = match import {
Import::Renamed { path, .. } | Import::Qualified { path, .. } => {
vec![path.to_string()]
}
Import::Selective { path, names, .. } => names
.iter()
.map(|n| format!("{}.{}", path, n.text))
.collect(),
Import::Unqualified { path, .. } => {
vec![path.to_string()]
}
};
for target in targets {
if let Some(imported_class) = class_dict.get(&target) {
record_file_dep(deps, &imported_class.location.file_name);
}
}
}
for extend in &class.extends {
let parent_name = extend.comp.to_string();
if is_primitive_type(&parent_name) {
continue;
}
let resolved_name = match resolve_class_name_with_imports(
&parent_name,
current_class_path,
class_dict,
&import_aliases,
) {
Some(name) => name,
None => continue, };
if visited.contains(&resolved_name) {
continue;
}
let parent_class = match class_dict.get(&resolved_name) {
Some(c) => c,
None => continue, };
let resolved_parent =
resolve_class_internal(parent_class, &resolved_name, class_dict, visited, deps)?;
let extends_mods = extract_extends_modifications(&extend.modifications);
for mod_expr in &extend.modifications {
if let Expression::FunctionCall { comp, args } = mod_expr {
let comp_name = comp.to_string();
if let Some(parent_comp) = resolved_parent.components.get(&comp_name) {
for arg in args {
if let Expression::Binary { op, lhs, .. } = arg
&& matches!(op, OpBinary::Assign(_) | OpBinary::Eq(_))
&& let Expression::ComponentReference(attr_ref) = &**lhs
{
let attr_name = attr_ref.to_string();
if parent_comp.final_attributes.contains(&attr_name) {
anyhow::bail!(
"Trying to override final element {} with modifier in extends clause for component '{}'",
attr_name,
comp_name
);
}
}
}
}
}
}
let parent_import_aliases = build_import_aliases_for_class(&resolved_name, class_dict);
for (comp_name, comp) in resolved_parent.components.iter().rev() {
if !resolved.components.contains_key(comp_name) {
let mut modified_comp = comp.clone();
let type_name = comp.type_name.to_string();
if !is_primitive_type(&type_name)
&& let Some(fq_name) = resolve_class_name_with_imports(
&type_name,
&resolved_name,
class_dict,
&parent_import_aliases,
)
{
modified_comp.type_name = ir::ast::Name {
name: fq_name
.split('.')
.map(|s| ir::ast::Token {
text: s.to_string(),
..Default::default()
})
.collect(),
};
}
if let Some(mod_value) = extends_mods.get(comp_name) {
modified_comp.start = mod_value.clone();
modified_comp.start_is_modification = true;
}
resolved.components.insert(comp_name.clone(), modified_comp);
resolved
.components
.move_index(resolved.components.len() - 1, 0);
}
}
let mut new_equations = resolved_parent.equations.clone();
new_equations.append(&mut resolved.equations);
resolved.equations = new_equations;
for import in &resolved_parent.imports {
if !resolved.imports.contains(import) {
resolved.imports.push(import.clone());
}
}
}
apply_type_causality(&mut resolved, current_class_path, class_dict);
Ok(resolved)
}
fn apply_type_causality(
class: &mut ir::ast::ClassDefinition,
current_class_path: &str,
class_dict: &ClassDict,
) {
use crate::ir::ast::Causality;
let import_aliases = build_import_aliases_for_class(current_class_path, class_dict);
for (_comp_name, comp) in class.components.iter_mut() {
if !matches!(comp.causality, Causality::Empty) {
continue;
}
let type_name = comp.type_name.to_string();
let resolved_type_name = resolve_class_name_with_imports(
&type_name,
current_class_path,
class_dict,
&import_aliases,
);
if let Some(resolved_name) = resolved_type_name
&& let Some(type_class) = class_dict.get(&resolved_name)
{
if !matches!(type_class.causality, Causality::Empty) {
comp.causality = type_class.causality.clone();
}
}
}
}
pub fn flatten(
def: &ir::ast::StoredDefinition,
model_name: Option<&str>,
) -> Result<ir::ast::ClassDefinition> {
let result = flatten_with_deps(def, model_name)?;
Ok(result.class)
}
pub fn flatten_with_deps(
def: &ir::ast::StoredDefinition,
model_name: Option<&str>,
) -> Result<FlattenResult> {
let def_hash = compute_def_hash(def);
let class_dict = get_or_build_class_dict(def, def_hash);
let main_class_name = model_name.ok_or(IrError::ModelNameRequired)?.to_string();
flatten_with_class_dict(def, &class_dict, &main_class_name, def_hash)
}
pub fn flatten_with_library_dicts(
user_def: &ir::ast::StoredDefinition,
library_dicts: &[Arc<ClassDict>],
model_name: Option<&str>,
) -> Result<FlattenResult> {
let class_dict = class_dict::build_combined_class_dict(user_def, library_dicts);
let def_hash = compute_def_hash(user_def);
let main_class_name = model_name.ok_or(IrError::ModelNameRequired)?.to_string();
flatten_with_class_dict(user_def, &class_dict, &main_class_name, def_hash)
}
fn flatten_with_class_dict(
def: &ir::ast::StoredDefinition,
class_dict: &Arc<ClassDict>,
main_class_name: &str,
def_hash: u64,
) -> Result<FlattenResult> {
let main_class_name = main_class_name.to_string();
let main_class =
lookup_class(def, class_dict, &main_class_name).ok_or(IrError::MainClassNotFound)?;
let (resolved_main, mut deps) =
resolve_class(&main_class, &main_class_name, class_dict, def_hash)?;
validate_imports(&resolved_main.imports, class_dict)?;
let mut fclass = (*resolved_main).clone();
let mut symbol_table = SymbolTable::new();
let imported_packages = collect_imported_packages(&resolved_main.imports);
for pkg in &imported_packages {
symbol_table.add_global(pkg);
}
let comp_shapes: std::collections::HashMap<String, Vec<usize>> = resolved_main
.components
.iter()
.map(|(name, comp)| (name.clone(), comp.shape.clone()))
.collect();
check_cardinality_array_connectors(&fclass, &comp_shapes)?;
let mut ctx = ExpansionContext::new(&mut fclass, class_dict, &symbol_table, def_hash);
ctx.register_inner_components(&resolved_main.components);
let components_to_expand: Vec<(String, ir::ast::Component)> = resolved_main
.components
.iter()
.filter(|(_, comp)| {
!is_primitive_type(&comp.type_name.to_string())
})
.map(|(name, comp)| (name.clone(), comp.clone()))
.collect();
for (comp_name, comp) in &components_to_expand {
ctx.expand_component(comp_name, comp, &main_class_name)?;
}
ctx.apply_outer_renaming();
let pin_types = ctx.pin_types;
for (file, hash) in ctx.deps.files {
deps.record(&file, &hash);
}
expand_connect_equations(&mut fclass, class_dict, &pin_types)?;
Ok(FlattenResult {
class: fclass,
dependencies: deps,
})
}
#[cfg(test)]
mod tests;