Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
3d943ad
Added the initial version of the graph symmetry detection using GAP
mlaveaux Aug 6, 2026
47b405a
Updated mCRL2-sys upstream
mlaveaux Aug 6, 2026
3053b4f
Added the PBES examples used in the tests.
mlaveaux Aug 6, 2026
c15a766
Removed the now obsolete JSON export
mlaveaux Aug 6, 2026
35f9c52
Added a proper DataBinder term for the binders
mlaveaux Aug 6, 2026
a1ccb4c
Explain the legend of the dot export in the README
mlaveaux Aug 6, 2026
a9eab8a
Use the proper binders, and updated the dot export to be more clear. …
mlaveaux Aug 6, 2026
b948b63
Fixed some smaller CLI issues.
mlaveaux Aug 6, 2026
ba8f616
Fixed some of the review suggestions
mlaveaux Aug 6, 2026
a75b4af
Some small improvements to the dot export
mlaveaux Aug 6, 2026
0031627
Removed the old export snapshots
mlaveaux Aug 7, 2026
284c760
Added rewriting with a substitution, where every sigma(x) is assumed …
mlaveaux Aug 7, 2026
b3a7f7d
Also thread a context through state_info for the PBES exploration
mlaveaux Aug 7, 2026
bfa60c9
Removed various glob exports
mlaveaux Aug 7, 2026
0c94190
Fixed small issues, moved common explore parts to its own module
mlaveaux Aug 10, 2026
a9e2241
Use the existing random_pbes, and also minor improvements
mlaveaux Aug 10, 2026
f62bec2
Added a general PBES exploration algorithm
mlaveaux Aug 10, 2026
37840ae
Some more improvements to the PBES generation
mlaveaux Aug 10, 2026
ee0d475
Added the quotienting as well
mlaveaux Aug 10, 2026
4adc4f4
Merge all the VisitResult and Descend variants, and some smaller changes
mlaveaux Aug 10, 2026
0ead9b8
Implement the caching, and some formatting changes
mlaveaux Aug 10, 2026
3591f98
Made parameters use their sort in the graph encoding
mlaveaux Aug 10, 2026
85d98e9
Renamed DensePerm to DensePermutation, and removed useless comments
mlaveaux Aug 10, 2026
66f6981
Added the caching for parallel exploration
mlaveaux Aug 10, 2026
0f97eff
Added a reference to the webpage for full documentation
mlaveaux Aug 10, 2026
0c25059
Added the current cache LPS strategy
mlaveaux Aug 10, 2026
bbf2d04
Updated mCRL2-sys
mlaveaux Aug 10, 2026
b03e14e
Added some error handling for the passed quotients
mlaveaux Aug 11, 2026
39c8dcb
Removed the empty write positions special value, and use a specific enum
mlaveaux Aug 11, 2026
2c9bbba
Made the naive canon implementation test only
mlaveaux Aug 11, 2026
3b8ed8f
Introduced a special ParameterLayout that can be used for the quotien…
mlaveaux Aug 11, 2026
c78c08d
Check that the cached PBES agree on all the solutions
mlaveaux Aug 11, 2026
3aa76ca
Added some tests for the quotienting
mlaveaux Aug 11, 2026
c447cdf
Added an actualy effect enum instead of only write_positions for the …
mlaveaux Aug 11, 2026
2a5c1da
Added some more tests and error handling for the BSGS
mlaveaux Aug 11, 2026
14716da
Added quotienting tests.
mlaveaux Aug 11, 2026
53bd1fc
Added a missing LPS update
mlaveaux Aug 11, 2026
d0f303b
Made the exports in merc_data explicit
mlaveaux Aug 11, 2026
2fceb9f
Added visitors for DataExpressions
mlaveaux Aug 12, 2026
f081478
Replaced builder and visitor by a unified Traverse trait for the AST …
mlaveaux Aug 12, 2026
6f6d65b
Made some functions faillable, and added more checks
mlaveaux Aug 12, 2026
270370a
Several smaller changes
mlaveaux Aug 12, 2026
3fa4749
Tried to fix garbage collection
mlaveaux Aug 12, 2026
e154c44
Added PBES preprocessing and outputting of parity games
mlaveaux Aug 12, 2026
19591ee
Rewrite the initial state as well
mlaveaux Aug 12, 2026
97395eb
Avoided various allocations in the hot path of PBES explorations
mlaveaux Aug 12, 2026
5b49568
Added progress messages for the parallel PBES exploration
mlaveaux Aug 12, 2026
825b6df
Fixed some issues with the garbage collection
mlaveaux Aug 12, 2026
37a6389
Updated mCRL2-sys
mlaveaux Aug 13, 2026
2109920
Fixed compilation issues
mlaveaux Aug 13, 2026
1284740
Moved the library functions to their own crates, added timers
mlaveaux Aug 13, 2026
7d0b517
Changed the GC slightly, and applied formatting
mlaveaux Aug 13, 2026
099570f
Added an option to dump the unified pbes, and removed some code dupli…
mlaveaux Aug 13, 2026
e5bd6a7
Added a PBES print command
mlaveaux Aug 13, 2026
bce9dec
Use Pbes/DataExpression in graph_symmetry to print it properly, and p…
mlaveaux Aug 13, 2026
8f04acf
Added trace logging to the exploration
mlaveaux Aug 13, 2026
7d9159e
Fixed rustdoc and removed some trace logging
mlaveaux Aug 13, 2026
ae1d2a0
Added tests for BSGS compared to GAP
mlaveaux Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions crates/aterm/src/random_term.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#![forbid(unsafe_code)]

#[cfg(test)]
mod inner {
pub use rand::Rng;
pub use rustc_hash::FxHashSet;

pub use crate::ATerm;
pub use crate::Symbol;
pub use crate::Term;
pub use crate::storage::THREAD_TERM_POOL;
}
use rand::Rng;
#[cfg(test)]
use rustc_hash::FxHashSet;

#[cfg(test)]
use inner::*;
use crate::ATerm;
#[cfg(test)]
use crate::Symbol;
#[cfg(test)]
use crate::Term;
#[cfg(test)]
use crate::storage::THREAD_TERM_POOL;

/// Create a random term consisting of the given symbol and constants. Performs
/// iterations number of constructions, and uses chance_duplicates to choose the
Expand Down
32 changes: 10 additions & 22 deletions crates/aterm/src/storage/global_aterm_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub(crate) static GLOBAL_TERM_POOL: LazyLock<GlobalBfSharedMutex<GlobalTermPool>
/// Enables aggressive garbage collection, which is used for testing.
pub(crate) const AGGRESSIVE_GC: bool = false;

/// The number of terms a thread pool reserves from the shared budget at a time, see
/// [crate::storage::ThreadTermPool].
pub(crate) const GC_BUDGET_CHUNK: usize = 4096;

/// A type alias for the global term pool guard
pub(crate) type GlobalTermPoolGuard<'a> = RecursiveLockReadGuard<'a, GlobalTermPool>;

Expand Down Expand Up @@ -73,7 +77,7 @@ pub(crate) struct GlobalTermPool {

/// The number of terms that may still be created before garbage collection should be
/// triggered. Set to roughly `capacity - len` after each collection and consumed by the
/// thread pools in chunks (see [crate::storage::ThreadTermPool]) to avoid contention.
/// thread pools in [GC_BUDGET_CHUNK] sized chunks to avoid contention on this counter.
gc_budget: AtomicUsize,

/// Default terms
Expand Down Expand Up @@ -269,30 +273,25 @@ impl GlobalTermPool {
}
}

/// Triggers garbage collection if necessary, refreshes the global budget and returns the
/// per-thread chunk the calling thread pool should count down before touching the budget
/// again.
pub(crate) fn trigger_garbage_collection(&mut self) -> usize {
/// Triggers garbage collection if necessary and refreshes the global budget.
pub(crate) fn trigger_garbage_collection(&mut self) {
if self.garbage_collection {
// Garbage collection is enabled.
self.collect_garbage();
}

self.reset_gc_budget()
self.reset_gc_budget();
}

/// Recomputes the global GC budget from the free storage capacity and returns the per-thread
/// chunk (the budget divided over the registered thread pools, to avoid every thread
/// contending on the shared counter).
pub(crate) fn reset_gc_budget(&self) -> usize {
/// Recomputes the global GC budget from the free storage capacity.
pub(crate) fn reset_gc_budget(&self) {
let budget = if AGGRESSIVE_GC {
1
} else {
self.terms.capacity().saturating_sub(self.len()).max(1)
};

self.gc_budget.store(budget, Ordering::Relaxed);
(budget / self.num_thread_pools()).max(1)
}

/// Subtracts `amount` from the global GC budget, saturating at zero so it never wraps, and
Expand All @@ -306,17 +305,6 @@ impl GlobalTermPool {
.expect("the update closure always returns Some")
}

/// Returns the current per-thread budget chunk without recomputing the global budget. Used
/// by a newly registered thread pool to obtain its initial counter.
pub(crate) fn gc_budget_chunk(&self) -> usize {
(self.gc_budget.load(Ordering::Relaxed) / self.num_thread_pools()).max(1)
}

/// Returns the number of registered (live) thread pools, at least one.
fn num_thread_pools(&self) -> usize {
self.thread_pools.iter().flatten().count().max(1)
}

/// Enables or disables automatic garbage collection.
pub fn automatic_garbage_collection(&mut self, enabled: bool) {
self.garbage_collection = enabled;
Expand Down
62 changes: 26 additions & 36 deletions crates/aterm/src/storage/thread_aterm_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::storage::GlobalTermPoolGuard;
use crate::storage::MAX_FIXED_ARITY;
use crate::storage::SharedTerm;
use crate::storage::SharedTermProtection;
use crate::storage::global_aterm_pool::GC_BUDGET_CHUNK;
use crate::storage::global_aterm_pool::GLOBAL_TERM_POOL;

thread_local! {
Expand All @@ -56,14 +57,11 @@ pub struct ThreadTermPool {
send_term_protection_set: Arc<Mutex<ProtectionSet<ATermIndex>>>,

/// Counts down the number of terms this thread may still create before it must consume the
/// next chunk from the global budget (see [GlobalTermPool::reset_gc_budget]).
/// next [GC_BUDGET_CHUNK] from the global budget (see [GlobalTermPool::reset_gc_budget]).
///
/// Zero means this thread holds no reservation, so the next term creation has to claim one.
garbage_collection_counter: Cell<usize>,

/// The size of a single budget chunk: the amount subtracted from the global budget, and the
/// value [Self::garbage_collection_counter] is refilled to, each time the counter reaches
/// zero.
garbage_collection_chunk: Cell<usize>,

/// A vector of terms that are used to store the arguments of a term for lookup.
tmp_arguments: RefCell<Vec<ATermRef<'static>>>,

Expand All @@ -89,15 +87,13 @@ impl ThreadTermPool {
let empty_list_symbol = pool.get_empty_list_symbol().copy();
let list_symbol = pool.get_list_symbol().copy();

// Claim a share of the current global budget as this thread's initial chunk.
let chunk = pool.gc_budget_chunk();
drop(pool);

Self {
protection_sets,
send_term_protection_set,
garbage_collection_counter: Cell::new(chunk),
garbage_collection_chunk: Cell::new(chunk),
// Start without a reservation so the first created term claims (and charges) one.
garbage_collection_counter: Cell::new(0),
tmp_arguments: RefCell::new(Vec::new()),
int_symbol,
empty_list_symbol,
Expand Down Expand Up @@ -465,28 +461,26 @@ impl ThreadTermPool {
guard.automatic_garbage_collection(enabled);
}

/// Forces a garbage collection to occur, regardless of the current counter value or whether it is enabled.
/// Forces a garbage collection to occur regardless of the current GC budget.
pub fn force_collect_garbage(&self) {
let mut guard = self.term_pool.write().expect("Lock poisoned!");
guard.collect_garbage();
self.set_budget_chunk(guard.reset_gc_budget());
guard.reset_gc_budget();
self.drop_budget_reservation();
}

/// Perform a garbage collection.
/// Perform a garbage collection if the global aterm pool is not locked.
pub fn collect_garbage(&self) {
if !self.term_pool.is_locked() {
// Trigger garbage collection and acquire a fresh budget chunk.
if let Some(mut guard) = self.term_pool.try_write().expect("Lock poisoned!") {
self.set_budget_chunk(guard.trigger_garbage_collection());
}
if let Some(mut guard) = self.term_pool.try_write().expect("Lock poisoned!") {
guard.trigger_garbage_collection();
self.drop_budget_reservation();
}
}

/// Records the per-thread budget chunk returned by the global pool, resetting the local
/// counter to count it down.
fn set_budget_chunk(&self, chunk: usize) {
self.garbage_collection_chunk.set(chunk);
self.garbage_collection_counter.set(chunk);
/// Drops this thread's outstanding reservation after the global budget was refreshed, so that
/// the next created term claims a chunk from (and charges it to) the new budget.
fn drop_budget_reservation(&self) {
self.garbage_collection_counter.set(0);
}

/// Triggers delayed garbage collection if the counter has reached zero.
Expand Down Expand Up @@ -519,27 +513,24 @@ impl ThreadTermPool {
self.trigger_garbage_collection();
}

/// Consumes the next budget chunk once the local counter is exhausted, triggering garbage
/// Reserves the next budget chunk once the local counter is exhausted, triggering garbage
/// collection when the shared global budget has run out.
fn trigger_garbage_collection(&self) {
if self.garbage_collection_counter.get() == 0 && !self.term_pool.is_locked() {
let chunk = self.garbage_collection_chunk.get();

// Subtract this thread's chunk from the shared budget. The read guard is only needed
// to reach the atomic and is dropped before a potential collection acquires the write
// lock.
// Subtract a chunk from the shared budget. The read guard is only needed to reach the
// atomic and is dropped before a potential collection acquires the write lock.
let previous = self
.term_pool
.read_recursive()
.expect("Lock poisoned!")
.consume_gc_budget(chunk);
.consume_gc_budget(GC_BUDGET_CHUNK);

if previous <= chunk {
// The global budget is exhausted, so collect and obtain a fresh chunk.
if previous <= GC_BUDGET_CHUNK {
// The global budget is exhausted, so collect and start over on a fresh budget.
self.collect_garbage();
} else {
// Budget remains; refill the local counter to count down the next chunk.
self.garbage_collection_counter.set(chunk);
// The chunk is now charged to the budget, so count it down locally.
self.garbage_collection_counter.set(GC_BUDGET_CHUNK);
}
}
}
Expand Down Expand Up @@ -652,7 +643,6 @@ impl DerefMut for ProtectionSetGuard<'_> {

#[cfg(test)]
mod tests {
use std::mem::ManuallyDrop;
use std::sync::mpsc;

use crate::ATerm;
Expand Down Expand Up @@ -748,7 +738,7 @@ mod tests {

// Leak the term so it is still protected at thread teardown, exercising the orphan
// adoption path. The bug we guard against is post-teardown read UB.
std::mem::forget(ManuallyDrop::new(term));
std::mem::forget(term);
});

handle.join().expect("Thread should join without panic");
Expand Down
66 changes: 66 additions & 0 deletions crates/data/src/closed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::ops::ControlFlow;

use ahash::AHashSet;
use merc_aterm::Term;
use merc_utilities::Step;

use crate::DataExpressionRef;
use crate::is_data_variable;
use crate::visit_data_expr;

/// Returns true iff `term` contains no data variables, i.e. it is a ground term.
///
/// A closed term normalises to the same result under every substitution, which is what makes
/// it sound to cache its normal form across calls that pass different substitutions.
///
/// Panics for binders and where clauses, which have no flat argument list.
pub fn is_closed<'a, 'b, T: Term<'a, 'b>>(term: &'b T) -> bool {
// Terms are maximally shared, so the same subterm is typically reachable along many paths.
// Remembering the ones already seen keeps this linear in the size of the term graph rather
// than the size of the tree it unfolds to. Keying on the term address is only valid because
// no terms are created here, so no garbage collection can run during the traversal.
let mut visited = AHashSet::new();

let variable: Option<()> = visit_data_expr(&DataExpressionRef::from(term.copy()), (), |expr, context| {
if !visited.insert(expr.index()) {
ControlFlow::Continue(Step::Prune)
} else if is_data_variable(expr) {
ControlFlow::Break(())
} else {
// A function symbol and a machine number have no children, and the head function
// symbol of an application is closed by definition, so neither needs to be recognised
// separately here.
ControlFlow::Continue(Step::Into(context))
}
});

variable.is_none()
}

#[cfg(test)]
mod tests {
use ahash::AHashSet;

use crate::DataExpression;
use crate::is_closed;

#[test]
fn test_is_closed_ground_term() {
let term = DataExpression::from_string("s(s(a), b)").unwrap();
assert!(is_closed(&term));
}

#[test]
fn test_is_closed_with_variable() {
let variables = AHashSet::from_iter(["x".to_string()]);
let term = DataExpression::from_string_untyped("s(s(x), b)", &variables).unwrap();
assert!(!is_closed(&term));
}

#[test]
fn test_is_closed_bare_variable() {
let variables = AHashSet::from_iter(["x".to_string()]);
let term = DataExpression::from_string_untyped("x", &variables).unwrap();
assert!(!is_closed(&term));
}
}
37 changes: 33 additions & 4 deletions crates/data/src/data_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ use crate::is_data_variable;
use crate::is_data_where_clause;
use crate::is_data_whr_decl;

/// The kind of a binder in a `DataAbstraction` — mirrors mCRL2's
/// `data::binder_type` enum (the 0-arity marker term that is the first child
/// of every `Binder(type, vars, body)` aterm).
/// The kind of a binder in a `DataAbstraction`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BinderType {
Lambda,
Expand Down Expand Up @@ -99,6 +97,24 @@ mod inner {
}
}

/// Same as [DataExpression::data_function_symbol], but returns `None` for a variable
/// instead of panicking.
///
/// Pattern matching uses this to observe a symbol: a variable in the subject term has no
/// head symbol and therefore matches no pattern position. The variable is only tested
/// after the two cases that do have one, so the common path costs the same.
pub fn try_data_function_symbol(&self) -> Option<DataFunctionSymbolRef<'_>> {
if is_data_application(&self.term) {
Some(self.term.arg(0).into())
} else if is_data_function_symbol(&self.term) {
Some(self.term.copy().into())
} else if is_data_variable(&self.term) {
None
} else {
panic!("try_data_function_symbol not implemented for {self}");
}
}

/// Returns the data sub-expressions of a data expression.
/// - function symbol f -> []
/// - variable x -> []
Expand Down Expand Up @@ -622,7 +638,20 @@ mod inner {
}
}

pub use inner::*;
pub use inner::DataAbstraction;
pub use inner::DataApplication;
pub use inner::DataApplicationRef;
pub use inner::DataEquation;
pub use inner::DataExpression;
pub use inner::DataExpressionRef;
pub use inner::DataFunctionSymbol;
pub use inner::DataFunctionSymbolRef;
pub use inner::DataVariable;
pub use inner::DataVariableRef;
pub use inner::DataWhereClause;
pub use inner::DataWhrDecl;
pub use inner::MachineNumber;
pub use inner::MachineNumberRef;

/// Returns the number of leading `ATerm` arguments that are *not* data sub-expressions and
/// must therefore be skipped by `data_arguments`, or `None` for binders/where clauses which
Expand Down
Loading
Loading