diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt
index 6bc4c3a..37a9edd 100644
--- a/deps/src/CMakeLists.txt
+++ b/deps/src/CMakeLists.txt
@@ -46,6 +46,8 @@ add_library(libsemigroups_julia SHARED
libsemigroups_julia.cpp
bmat8.cpp
constants.cpp
+ froidure-pin-base.cpp
+ froidure-pin.cpp
order.cpp
report.cpp
runner.cpp
diff --git a/deps/src/froidure-pin-base.cpp b/deps/src/froidure-pin-base.cpp
new file mode 100644
index 0000000..50a0eb5
--- /dev/null
+++ b/deps/src/froidure-pin-base.cpp
@@ -0,0 +1,408 @@
+// froidure-pin-base.cpp - FroidurePinBase bindings for libsemigroups_julia
+//
+// Copyright (c) 2026 James W. Swent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+// This file exposes the libsemigroups FroidurePinBase class to Julia via
+// CxxWrap. FroidurePinBase is an abstract base class inheriting from Runner
+// that provides non-element-specific member functions for FroidurePin.
+
+// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix)
+#include "libsemigroups_julia.hpp"
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace jlcxx {
+ template <>
+ struct IsMirroredType : std::false_type {};
+
+ template <>
+ struct SuperType {
+ using type = libsemigroups::Runner;
+ };
+} // namespace jlcxx
+
+namespace libsemigroups_julia {
+
+ void define_froidure_pin_base(jl::Module& m) {
+ using libsemigroups::FroidurePinBase;
+ using libsemigroups::Runner;
+ using libsemigroups::word_type;
+
+ // Register FroidurePinBase inheriting from Runner.
+ // No constructors — this is an abstract base class only instantiated
+ // through FroidurePin.
+ auto type = m.add_type("FroidurePinBase",
+ jlcxx::julia_base_type());
+
+ ////////////////////////////////////////////////////////////////////////
+ // Settings (getter/setter split per CxxWrap convention)
+ ////////////////////////////////////////////////////////////////////////
+
+ // batch_size - Returns the current batch size
+ type.method("batch_size", [](FroidurePinBase const& self) -> size_t {
+ return self.batch_size();
+ });
+
+ // set_batch_size! - Sets the batch size
+ type.method("set_batch_size!", [](FroidurePinBase& self, size_t val) {
+ self.batch_size(val);
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Size / enumeration
+ ////////////////////////////////////////////////////////////////////////
+
+ // size - Returns the total number of elements (triggers full enumeration)
+ type.method("size",
+ [](FroidurePinBase& self) -> size_t { return self.size(); });
+
+ // current_size - Returns elements enumerated so far (no enumeration)
+ type.method("current_size", [](FroidurePinBase const& self) -> size_t {
+ return self.current_size();
+ });
+
+ // degree - Returns the degree of the elements
+ type.method("degree", [](FroidurePinBase const& self) -> size_t {
+ return self.degree();
+ });
+
+ // number_of_generators - Returns the number of generators
+ type.method("number_of_generators",
+ [](FroidurePinBase const& self) -> size_t {
+ return self.number_of_generators();
+ });
+
+ // enumerate! - Enumerate until at least `limit` elements are found
+ type.method("enumerate!", [](FroidurePinBase& self, size_t limit) {
+ self.enumerate(limit);
+ });
+
+ // number_of_rules - Total number of rules (triggers full enumeration)
+ type.method("number_of_rules", [](FroidurePinBase& self) -> size_t {
+ return self.number_of_rules();
+ });
+
+ // current_number_of_rules - Rules found so far (no enumeration)
+ type.method("current_number_of_rules",
+ [](FroidurePinBase const& self) -> size_t {
+ return self.current_number_of_rules();
+ });
+
+ // current_max_word_length - Max word length so far (no enumeration)
+ type.method("current_max_word_length",
+ [](FroidurePinBase const& self) -> size_t {
+ return self.current_max_word_length();
+ });
+
+ // contains_one - Is the identity an element? (triggers full enumeration)
+ type.method("contains_one", [](FroidurePinBase& self) -> bool {
+ return self.contains_one();
+ });
+
+ // currently_contains_one - Is the identity known to be an element?
+ type.method("currently_contains_one",
+ [](FroidurePinBase const& self) -> bool {
+ return self.currently_contains_one();
+ });
+
+ // number_of_elements_of_length (one-arg) - Elements with given length
+ type.method("number_of_elements_of_length",
+ [](FroidurePinBase const& self, size_t len) -> size_t {
+ return self.number_of_elements_of_length(len);
+ });
+
+ // number_of_elements_of_length (two-arg) - Elements with length in
+ // [min, max)
+ type.method(
+ "number_of_elements_of_length_range",
+ [](FroidurePinBase const& self, size_t min, size_t max) -> size_t {
+ return self.number_of_elements_of_length(min, max);
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Index queries — checked and _no_checks variants
+ ////////////////////////////////////////////////////////////////////////
+
+ // prefix / prefix_no_checks
+ type.method("prefix",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.prefix(pos);
+ });
+ type.method("prefix_no_checks",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.prefix_no_checks(pos);
+ });
+
+ // suffix / suffix_no_checks
+ type.method("suffix",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.suffix(pos);
+ });
+ type.method("suffix_no_checks",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.suffix_no_checks(pos);
+ });
+
+ // first_letter / first_letter_no_checks
+ type.method("first_letter",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.first_letter(pos);
+ });
+ type.method("first_letter_no_checks",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.first_letter_no_checks(pos);
+ });
+
+ // final_letter / final_letter_no_checks
+ type.method("final_letter",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.final_letter(pos);
+ });
+ type.method("final_letter_no_checks",
+ [](FroidurePinBase const& self, uint32_t pos) -> uint32_t {
+ return self.final_letter_no_checks(pos);
+ });
+
+ // current_length / current_length_no_checks
+ type.method("current_length",
+ [](FroidurePinBase const& self, uint32_t pos) -> size_t {
+ return self.current_length(pos);
+ });
+ type.method("current_length_no_checks",
+ [](FroidurePinBase const& self, uint32_t pos) -> size_t {
+ return self.current_length_no_checks(pos);
+ });
+
+ // length / length_no_checks (trigger full enumeration)
+ type.method("length", [](FroidurePinBase& self, uint32_t pos) -> size_t {
+ return self.length(pos);
+ });
+ type.method("length_no_checks",
+ [](FroidurePinBase& self, uint32_t pos) -> size_t {
+ return self.length_no_checks(pos);
+ });
+
+ // position_of_generator / position_of_generator_no_checks
+ type.method("position_of_generator",
+ [](FroidurePinBase const& self, uint32_t i) -> uint32_t {
+ return self.position_of_generator(i);
+ });
+ type.method("position_of_generator_no_checks",
+ [](FroidurePinBase const& self, uint32_t i) -> uint32_t {
+ return self.position_of_generator_no_checks(i);
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Factorisation — froidure_pin:: free functions returning word_type
+ ////////////////////////////////////////////////////////////////////////
+
+ // current_minimal_factorisation (checked, no enumeration)
+ m.method(
+ "current_minimal_factorisation",
+ [](FroidurePinBase const& fpb, uint32_t pos) -> word_type {
+ return libsemigroups::froidure_pin::current_minimal_factorisation(
+ fpb, pos);
+ });
+
+ // current_minimal_factorisation_no_checks (unchecked, no enumeration)
+ m.method("current_minimal_factorisation_no_checks",
+ [](FroidurePinBase const& fpb, uint32_t pos) -> word_type {
+ return libsemigroups::froidure_pin::
+ current_minimal_factorisation_no_checks(fpb, pos);
+ });
+
+ // minimal_factorisation (checked, triggers partial enumeration)
+ m.method("minimal_factorisation",
+ [](FroidurePinBase& fpb, uint32_t pos) -> word_type {
+ return libsemigroups::froidure_pin::minimal_factorisation(fpb,
+ pos);
+ });
+
+ // factorisation (checked, triggers partial enumeration)
+ m.method("factorisation",
+ [](FroidurePinBase& fpb, uint32_t pos) -> word_type {
+ return libsemigroups::froidure_pin::factorisation(fpb, pos);
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Word-position queries — ArrayRef for Julia Vector{UInt}
+ ////////////////////////////////////////////////////////////////////////
+
+ // froidure_pin::current_position (checked, no enumeration)
+ m.method("current_position",
+ [](FroidurePinBase const& fpb,
+ jlcxx::ArrayRef arr) -> uint32_t {
+ word_type w(arr.begin(), arr.end());
+ return libsemigroups::froidure_pin::current_position(fpb, w);
+ });
+
+ // froidure_pin::current_position_no_checks (unchecked, no enumeration)
+ m.method("current_position_no_checks",
+ [](FroidurePinBase const& fpb,
+ jlcxx::ArrayRef arr) -> uint32_t {
+ word_type w(arr.begin(), arr.end());
+ return libsemigroups::froidure_pin::current_position_no_checks(
+ fpb, w);
+ });
+
+ // froidure_pin::position (checked, triggers full enumeration)
+ m.method("position",
+ [](FroidurePinBase& fpb, jlcxx::ArrayRef arr) -> uint32_t {
+ word_type w(arr.begin(), arr.end());
+ return libsemigroups::froidure_pin::position(fpb, w);
+ });
+
+ // froidure_pin::position_no_checks (unchecked, triggers full enumeration)
+ m.method("position_no_checks",
+ [](FroidurePinBase& fpb, jlcxx::ArrayRef arr) -> uint32_t {
+ word_type w(arr.begin(), arr.end());
+ return libsemigroups::froidure_pin::position_no_checks(fpb, w);
+ });
+
+ // froidure_pin::product_by_reduction (checked)
+ m.method(
+ "product_by_reduction",
+ [](FroidurePinBase const& fpb, uint32_t i, uint32_t j) -> uint32_t {
+ return libsemigroups::froidure_pin::product_by_reduction(fpb, i, j);
+ });
+
+ // froidure_pin::product_by_reduction_no_checks (unchecked)
+ m.method(
+ "product_by_reduction_no_checks",
+ [](FroidurePinBase const& fpb, uint32_t i, uint32_t j) -> uint32_t {
+ return libsemigroups::froidure_pin::product_by_reduction_no_checks(
+ fpb, i, j);
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Cayley graphs — return const& to already-bound WordGraph
+ ////////////////////////////////////////////////////////////////////////
+
+ // right_cayley_graph (triggers full enumeration)
+ type.method(
+ "right_cayley_graph",
+ [](FroidurePinBase& self) -> FroidurePinBase::cayley_graph_type const& {
+ return self.right_cayley_graph();
+ });
+
+ // current_right_cayley_graph (no enumeration)
+ type.method("current_right_cayley_graph",
+ [](FroidurePinBase const& self)
+ -> FroidurePinBase::cayley_graph_type const& {
+ return self.current_right_cayley_graph();
+ });
+
+ // left_cayley_graph (triggers full enumeration)
+ type.method(
+ "left_cayley_graph",
+ [](FroidurePinBase& self) -> FroidurePinBase::cayley_graph_type const& {
+ return self.left_cayley_graph();
+ });
+
+ // current_left_cayley_graph (no enumeration)
+ type.method("current_left_cayley_graph",
+ [](FroidurePinBase const& self)
+ -> FroidurePinBase::cayley_graph_type const& {
+ return self.current_left_cayley_graph();
+ });
+
+ ////////////////////////////////////////////////////////////////////////
+ // Materialized collections — rules and normal forms
+ ////////////////////////////////////////////////////////////////////////
+ // const_rule_iterator dereferences to relation_type =
+ // std::pair
+ // CxxWrap cannot return std::pair, so we split into two parallel
+ // vectors: rules_lhs and rules_rhs.
+
+ // rules_lhs / rules_rhs — full enumeration, then collect
+ m.method("rules_lhs", [](FroidurePinBase& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_rules();
+ auto end = fpb.cend_rules();
+ for (; it != end; ++it) {
+ result.push_back((*it).first);
+ }
+ return result;
+ });
+
+ m.method("rules_rhs", [](FroidurePinBase& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_rules();
+ auto end = fpb.cend_rules();
+ for (; it != end; ++it) {
+ result.push_back((*it).second);
+ }
+ return result;
+ });
+
+ // current_rules_lhs / current_rules_rhs — no enumeration
+ m.method("current_rules_lhs",
+ [](FroidurePinBase const& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_current_rules();
+ auto end = fpb.cend_current_rules();
+ for (; it != end; ++it) {
+ result.push_back((*it).first);
+ }
+ return result;
+ });
+
+ m.method("current_rules_rhs",
+ [](FroidurePinBase const& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_current_rules();
+ auto end = fpb.cend_current_rules();
+ for (; it != end; ++it) {
+ result.push_back((*it).second);
+ }
+ return result;
+ });
+
+ // normal_forms — full enumeration, collect into vector
+ m.method("normal_forms",
+ [](FroidurePinBase& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_normal_forms();
+ auto end = fpb.cend_normal_forms();
+ for (; it != end; ++it) {
+ result.push_back(*it);
+ }
+ return result;
+ });
+
+ // current_normal_forms — no enumeration
+ m.method("current_normal_forms",
+ [](FroidurePinBase const& fpb) -> std::vector {
+ std::vector result;
+ auto it = fpb.cbegin_current_normal_forms();
+ auto end = fpb.cend_current_normal_forms();
+ for (; it != end; ++it) {
+ result.push_back(*it);
+ }
+ return result;
+ });
+ }
+
+} // namespace libsemigroups_julia
diff --git a/deps/src/froidure-pin.cpp b/deps/src/froidure-pin.cpp
new file mode 100644
index 0000000..f7ba4c9
--- /dev/null
+++ b/deps/src/froidure-pin.cpp
@@ -0,0 +1,370 @@
+// froidure-pin.cpp - FroidurePin bindings for libsemigroups_julia
+//
+// Copyright (c) 2026 James W. Swent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+// This file exposes the libsemigroups FroidurePin template class to Julia
+// via CxxWrap for all 10 element types (Transf1/2/4, PPerm1/2/4, Perm1/2/4,
+// BMat8). FroidurePin inherits from FroidurePinBase.
+
+// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix)
+#include "libsemigroups_julia.hpp"
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+////////////////////////////////////////////////////////////////////////
+// CxxWrap type traits — IsMirroredType and SuperType specializations
+////////////////////////////////////////////////////////////////////////
+
+namespace jlcxx {
+
+ // IsMirroredType — one per concrete FroidurePin instantiation
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+ template <>
+ struct IsMirroredType<
+ libsemigroups::FroidurePin>>
+ : std::false_type {};
+
+ template <>
+ struct IsMirroredType>
+ : std::false_type {};
+
+ // SuperType — partial specialization for all FroidurePin
+ template
+ struct SuperType> {
+ using type = libsemigroups::FroidurePinBase;
+ };
+
+} // namespace jlcxx
+
+namespace libsemigroups_julia {
+
+ namespace {
+
+ ////////////////////////////////////////////////////////////////////////
+ // bind_froidure_pin — template helper that registers all
+ // element-typed methods for a single FroidurePin instantiation.
+ ////////////////////////////////////////////////////////////////////////
+
+ template
+ void bind_froidure_pin(jl::Module& m, std::string const& name) {
+ using FP = libsemigroups::FroidurePin;
+ using libsemigroups::FroidurePinBase;
+ using libsemigroups::word_type;
+
+ auto type
+ = m.add_type(name, jlcxx::julia_base_type());
+
+ ////////////////////////////////////////////////////////////////////
+ // 1. Constructors — 1-4 generator arg lambdas
+ // (Can't construct StdVector of wrapped types Julia-side)
+ ////////////////////////////////////////////////////////////////////
+
+ m.method(name, [](E const& g1) {
+ std::vector v{g1};
+ return FP(v.begin(), v.end());
+ });
+
+ m.method(name, [](E const& g1, E const& g2) {
+ std::vector v{g1, g2};
+ return FP(v.begin(), v.end());
+ });
+
+ m.method(name, [](E const& g1, E const& g2, E const& g3) {
+ std::vector v{g1, g2, g3};
+ return FP(v.begin(), v.end());
+ });
+
+ m.method(name, [](E const& g1, E const& g2, E const& g3, E const& g4) {
+ std::vector v{g1, g2, g3, g4};
+ return FP(v.begin(), v.end());
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 2. Element access — ALL return by copy (GC safety)
+ ////////////////////////////////////////////////////////////////////
+
+ // at (triggers partial enumeration)
+ type.method("at", [](FP& self, size_t i) -> E { return self.at(i); });
+
+ // sorted_at (triggers full enumeration)
+ type.method("sorted_at",
+ [](FP& self, size_t i) -> E { return self.sorted_at(i); });
+
+ // sorted_at_no_checks
+ type.method("sorted_at_no_checks", [](FP& self, size_t i) -> E {
+ return self.sorted_at_no_checks(i);
+ });
+
+ // generator (const self)
+ type.method("generator", [](FP const& self, size_t i) -> E {
+ return self.generator(i);
+ });
+
+ // generator_no_checks (const self)
+ type.method("generator_no_checks", [](FP const& self, size_t i) -> E {
+ return self.generator_no_checks(i);
+ });
+
+ // getindex_no_checks — binds operator[], const self
+ type.method("getindex_no_checks",
+ [](FP const& self, size_t i) -> E { return self[i]; });
+
+ ////////////////////////////////////////////////////////////////////
+ // 3. Containment / position
+ ////////////////////////////////////////////////////////////////////
+
+ // contains(FP&, E const&) -> bool
+ type.method("contains", [](FP& self, E const& x) -> bool {
+ return self.contains(x);
+ });
+
+ // position(FP&, E const&) -> element_index_type
+ type.method("position", [](FP& self, E const& x) -> uint32_t {
+ return self.position(x);
+ });
+
+ // current_position(FP const&, E const&) -> element_index_type
+ type.method("current_position",
+ [](FP const& self, E const& x) -> uint32_t {
+ return self.current_position(x);
+ });
+
+ // sorted_position(FP&, E const&) -> element_index_type
+ type.method("sorted_position", [](FP& self, E const& x) -> uint32_t {
+ return self.sorted_position(x);
+ });
+
+ // to_sorted_position(FP&, size_t) -> element_index_type
+ type.method("to_sorted_position", [](FP& self, size_t i) -> uint32_t {
+ return self.to_sorted_position(i);
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 4. Fast product
+ ////////////////////////////////////////////////////////////////////
+
+ type.method("fast_product",
+ [](FP const& self, size_t i, size_t j) -> uint32_t {
+ return self.fast_product(i, j);
+ });
+
+ type.method("fast_product_no_checks",
+ [](FP const& self, size_t i, size_t j) -> uint32_t {
+ return self.fast_product_no_checks(i, j);
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 5. Idempotents
+ ////////////////////////////////////////////////////////////////////
+
+ type.method("number_of_idempotents", [](FP& self) -> size_t {
+ return self.number_of_idempotents();
+ });
+
+ type.method("is_idempotent", [](FP& self, size_t i) -> bool {
+ return self.is_idempotent(i);
+ });
+
+ type.method("is_idempotent_no_checks", [](FP& self, size_t i) -> bool {
+ return self.is_idempotent_no_checks(i);
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 6. Modification
+ ////////////////////////////////////////////////////////////////////
+
+ // add_generator! (void — Julia wrapper returns self)
+ type.method("add_generator!",
+ [](FP& self, E const& x) { self.add_generator(x); });
+
+ // add_generator_no_checks!
+ type.method("add_generator_no_checks!", [](FP& self, E const& x) {
+ self.add_generator_no_checks(x);
+ });
+
+ // closure! — single element wrapped in 1-element vector
+ type.method("closure!", [](FP& self, E const& x) {
+ std::vector v{x};
+ self.closure(v.begin(), v.end());
+ });
+
+ // copy_closure — single element, returns new FP by value
+ // NOTE: copy_closure is not const in C++ (it may enumerate)
+ type.method("copy_closure", [](FP& self, E const& x) -> FP {
+ std::vector v{x};
+ return self.copy_closure(v.begin(), v.end());
+ });
+
+ // copy_add_generators — single element, returns new FP by value
+ type.method("copy_add_generators", [](FP const& self, E const& x) -> FP {
+ std::vector v{x};
+ return self.copy_add_generators(v.begin(), v.end());
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 7. Word-element conversion (ArrayRef for Julia Vector{UInt})
+ ////////////////////////////////////////////////////////////////////
+
+ // to_element — returns by copy (volatile const_reference!)
+ m.method("to_element",
+ [](FP const& self, jlcxx::ArrayRef arr) -> E {
+ word_type w(arr.begin(), arr.end());
+ return self.to_element(w.begin(), w.end());
+ });
+
+ // to_element_no_checks
+ m.method("to_element_no_checks",
+ [](FP const& self, jlcxx::ArrayRef arr) -> E {
+ word_type w(arr.begin(), arr.end());
+ return self.to_element_no_checks(w.begin(), w.end());
+ });
+
+ // equal_to — two words
+ m.method("equal_to",
+ [](FP const& self,
+ jlcxx::ArrayRef arr1,
+ jlcxx::ArrayRef arr2) -> bool {
+ word_type w1(arr1.begin(), arr1.end());
+ word_type w2(arr2.begin(), arr2.end());
+ return self.equal_to(
+ w1.begin(), w1.end(), w2.begin(), w2.end());
+ });
+
+ // equal_to_no_checks
+ m.method("equal_to_no_checks",
+ [](FP const& self,
+ jlcxx::ArrayRef arr1,
+ jlcxx::ArrayRef arr2) -> bool {
+ word_type w1(arr1.begin(), arr1.end());
+ word_type w2(arr2.begin(), arr2.end());
+ return self.equal_to_no_checks(
+ w1.begin(), w1.end(), w2.begin(), w2.end());
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 8. Materialized collections
+ ////////////////////////////////////////////////////////////////////
+
+ // idempotents — iterate cbegin_idempotents..cend_idempotents
+ // Use !(it == end) to avoid C++20 ambiguous reversed operator!=
+ m.method("idempotents", [](FP& self) -> std::vector {
+ std::vector result;
+ auto it = self.cbegin_idempotents();
+ auto end = self.cend_idempotents();
+ for (; !(it == end); ++it) {
+ result.push_back(*it);
+ }
+ return result;
+ });
+
+ // sorted_elements — iterate cbegin_sorted..cend_sorted
+ // Use !(it == end) to avoid C++20 ambiguous reversed operator!=
+ m.method("sorted_elements", [](FP& self) -> std::vector {
+ std::vector result;
+ auto it = self.cbegin_sorted();
+ auto end = self.cend_sorted();
+ for (; !(it == end); ++it) {
+ result.push_back(*it);
+ }
+ return result;
+ });
+
+ ////////////////////////////////////////////////////////////////////
+ // 9. Memory
+ ////////////////////////////////////////////////////////////////////
+
+ type.method("reserve!", [](FP& self, size_t val) { self.reserve(val); });
+
+ ////////////////////////////////////////////////////////////////////
+ // 10. Display
+ ////////////////////////////////////////////////////////////////////
+
+ m.method("to_human_readable_repr", [](FP const& self) -> std::string {
+ return libsemigroups::to_human_readable_repr(self);
+ });
+ }
+
+ } // anonymous namespace
+
+ ////////////////////////////////////////////////////////////////////////
+ // define_froidure_pin — register all 10 FroidurePin instantiations
+ ////////////////////////////////////////////////////////////////////////
+
+ void define_froidure_pin(jl::Module& m) {
+ using namespace libsemigroups;
+
+ // Transf types
+ bind_froidure_pin>(m, "FroidurePinTransf1");
+ bind_froidure_pin>(m, "FroidurePinTransf2");
+ bind_froidure_pin>(m, "FroidurePinTransf4");
+
+ // PPerm types
+ bind_froidure_pin>(m, "FroidurePinPPerm1");
+ bind_froidure_pin>(m, "FroidurePinPPerm2");
+ bind_froidure_pin>(m, "FroidurePinPPerm4");
+
+ // Perm types
+ bind_froidure_pin>(m, "FroidurePinPerm1");
+ bind_froidure_pin>(m, "FroidurePinPerm2");
+ bind_froidure_pin>(m, "FroidurePinPerm4");
+
+ // BMat8
+ bind_froidure_pin(m, "FroidurePinBMat8");
+ }
+
+} // namespace libsemigroups_julia
diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp
index ba5d9d7..0cd8b6e 100644
--- a/deps/src/libsemigroups_julia.cpp
+++ b/deps/src/libsemigroups_julia.cpp
@@ -40,6 +40,8 @@ namespace libsemigroups_julia {
define_order(mod);
define_word_range(mod);
define_word_graph(mod);
+ define_froidure_pin_base(mod);
+ define_froidure_pin(mod);
define_presentation(mod);
define_presentation_examples(mod);
}
diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp
index b582894..3af5852 100644
--- a/deps/src/libsemigroups_julia.hpp
+++ b/deps/src/libsemigroups_julia.hpp
@@ -62,6 +62,8 @@ namespace libsemigroups_julia {
void define_order(jl::Module& mod);
void define_word_range(jl::Module& mod);
void define_word_graph(jl::Module& mod);
+ void define_froidure_pin_base(jl::Module& mod);
+ void define_froidure_pin(jl::Module& mod);
void define_presentation(jl::Module& mod);
void define_presentation_examples(jl::Module& mod);
diff --git a/docs/make.jl b/docs/make.jl
index d20887f..f0def1e 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -66,6 +66,11 @@ makedocs(;
"main-algorithms/core-classes/index.md",
"main-algorithms/core-classes/runner.md",
],
+ "Froidure-Pin" => [
+ "Overview" => "main-algorithms/froidure-pin/index.md",
+ "The FroidurePin type" => "main-algorithms/froidure-pin/froidure-pin.md",
+ "Helper functions" => "main-algorithms/froidure-pin/helpers.md",
+ ],
],
],
warnonly = [:missing_docs, :linkcheck, :cross_references],
diff --git a/docs/src/main-algorithms/froidure-pin/froidure-pin.md b/docs/src/main-algorithms/froidure-pin/froidure-pin.md
new file mode 100644
index 0000000..0d5bdf4
--- /dev/null
+++ b/docs/src/main-algorithms/froidure-pin/froidure-pin.md
@@ -0,0 +1,204 @@
+# The FroidurePin type
+
+This page documents the parametric type
+[`FroidurePin{E}`](@ref Semigroups.FroidurePin), which implements the
+Froidure-Pin algorithm for enumerating the elements of a finitely
+generated semigroup.
+
+`FroidurePin{E}` is a subtype of [`Runner`](@ref Semigroups.Runner)
+(via the internal `FroidurePinBase`), so all runner methods
+([`run!`](@ref), [`run_for!`](@ref), [`finished`](@ref), etc.) are
+available.
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `FroidurePin{E}` for the element types
+ [`Transf`](@ref Semigroups.Transf),
+ [`PPerm`](@ref Semigroups.PPerm),
+ [`Perm`](@ref Semigroups.Perm), and
+ [`BMat8`](@ref Semigroups.BMat8) only.
+
+## Table of contents
+
+| Section | Description |
+| ------- | ----------- |
+| [Construction](@ref) | Constructors from vectors or variadic generators. |
+| [Size and enumeration](@ref) | Element count, degree, generator count, partial enumeration. |
+| [Element access](@ref) | Access elements by index, generator index, or sorted index. |
+| [Containment and position](@ref) | Membership testing and element position queries. |
+| [Modification](@ref) | Adding generators, closure, copy-and-extend operations. |
+| [Settings](@ref) | Batch size for partial enumeration. |
+| [Predicates](@ref) | Identity containment, idempotent checks. |
+| [Index queries](@ref) | Prefix/suffix, first/final letter, products, word lengths, rule counts. |
+| [Iteration and display](@ref) | `for` loop iteration, `copy`, `show`. |
+| [Runner interface](@ref) | Inherited `run!`, `run_for!`, `finished`, etc. |
+
+```@docs
+Semigroups.FroidurePin
+```
+
+## Construction
+
+| Function | Description |
+| -------- | ----------- |
+| [`FroidurePin(gens::Vector{E})`](@ref Semigroups.FroidurePin(::Vector{E}) where E) | Construct from a vector of generators. |
+| [`FroidurePin(x, xs...)`](@ref Semigroups.FroidurePin(::E, ::Vararg{E}) where E) | Construct from one or more generators (variadic). |
+
+```@docs
+Semigroups.FroidurePin(::Vector{E}) where E
+Semigroups.FroidurePin(::E, ::Vararg{E}) where E
+```
+
+## Size and enumeration
+
+| Function | Description |
+| -------- | ----------- |
+| [`length`](@ref Base.length(::FroidurePin)) | Total number of elements (triggers full enumeration). |
+| [`current_size`](@ref Semigroups.current_size(::FroidurePin)) | Number of elements enumerated so far. |
+| [`degree`](@ref Semigroups.degree(::FroidurePin)) | Degree of the elements. |
+| [`number_of_generators`](@ref Semigroups.number_of_generators(::FroidurePin)) | Number of generators. |
+| [`enumerate!`](@ref Semigroups.enumerate!(::FroidurePin, ::Integer)) | Enumerate until at least a given number of elements are found. |
+
+```@docs
+Base.length(::FroidurePin)
+Semigroups.current_size(::FroidurePin)
+Semigroups.degree(::FroidurePin)
+Semigroups.number_of_generators(::FroidurePin)
+Semigroups.enumerate!(::FroidurePin, ::Integer)
+```
+
+## Element access
+
+| Function | Description |
+| -------- | ----------- |
+| [`getindex`](@ref Base.getindex(::FroidurePin{E}, ::Integer) where E) | Access element by 1-based index (triggers enumeration). |
+| [`generator`](@ref Semigroups.generator(::FroidurePin{E}, ::Integer) where E) | Return the `i`-th generator. |
+| [`sorted_at`](@ref Semigroups.sorted_at(::FroidurePin{E}, ::Integer) where E) | Access element by sorted index. |
+
+```@docs
+Base.getindex(::FroidurePin{E}, ::Integer) where E
+Semigroups.generator(::FroidurePin{E}, ::Integer) where E
+Semigroups.sorted_at(::FroidurePin{E}, ::Integer) where E
+```
+
+## Containment and position
+
+| Function | Description |
+| -------- | ----------- |
+| [`in`](@ref Base.in(::E, ::FroidurePin{E}) where E) | Test membership of an element. |
+| [`position`](@ref Semigroups.position(::FroidurePin{E}, ::E) where E) | Position of an element (triggers full enumeration). |
+| [`sorted_position`](@ref Semigroups.sorted_position(::FroidurePin{E}, ::E) where E) | Sorted position of an element. |
+| [`to_sorted_position`](@ref Semigroups.to_sorted_position(::FroidurePin, ::Integer)) | Convert element index to sorted index. |
+| [`current_position`](@ref Semigroups.current_position(::FroidurePin{E}, ::E) where E) | Position among elements enumerated so far. |
+| [`current_position`](@ref Semigroups.current_position(::FroidurePin, ::AbstractVector{<:Integer})) | Position of a word among elements enumerated so far. |
+
+```@docs
+Base.in(::E, ::FroidurePin{E}) where E
+Semigroups.position(::FroidurePin{E}, ::E) where E
+Semigroups.sorted_position(::FroidurePin{E}, ::E) where E
+Semigroups.to_sorted_position(::FroidurePin, ::Integer)
+Semigroups.current_position(::FroidurePin{E}, ::E) where E
+Semigroups.current_position(::FroidurePin, ::AbstractVector{<:Integer})
+```
+
+## Modification
+
+| Function | Description |
+| -------- | ----------- |
+| [`push!`](@ref Base.push!(::FroidurePin{E}, ::E) where E) | Add a new generator. |
+| [`closure!`](@ref Semigroups.closure!(::FroidurePin{E}, ::E) where E) | Add a non-redundant generator and re-enumerate. |
+| [`copy_closure`](@ref Semigroups.copy_closure(::FroidurePin{E}, ::E) where E) | Copy and add a non-redundant generator. |
+| [`copy_add_generators`](@ref Semigroups.copy_add_generators(::FroidurePin{E}, ::E) where E) | Copy and add a generator. |
+| [`reserve!`](@ref Semigroups.reserve!(::FroidurePin, ::Integer)) | Pre-allocate storage for elements. |
+
+```@docs
+Base.push!(::FroidurePin{E}, ::E) where E
+Semigroups.closure!(::FroidurePin{E}, ::E) where E
+Semigroups.copy_closure(::FroidurePin{E}, ::E) where E
+Semigroups.copy_add_generators(::FroidurePin{E}, ::E) where E
+Semigroups.reserve!(::FroidurePin, ::Integer)
+```
+
+## Settings
+
+| Function | Description |
+| -------- | ----------- |
+| [`batch_size`](@ref Semigroups.batch_size(::FroidurePin)) | Return the current batch size. |
+| [`set_batch_size!`](@ref Semigroups.set_batch_size!(::FroidurePin, ::Integer)) | Set the batch size for partial enumeration. |
+
+```@docs
+Semigroups.batch_size(::FroidurePin)
+Semigroups.set_batch_size!(::FroidurePin, ::Integer)
+```
+
+## Predicates
+
+| Function | Description |
+| -------- | ----------- |
+| [`contains_one`](@ref Semigroups.contains_one(::FroidurePin)) | Check if the identity is an element. |
+| [`currently_contains_one`](@ref Semigroups.currently_contains_one(::FroidurePin)) | Check if the identity is known to be an element so far. |
+| [`is_idempotent`](@ref Semigroups.is_idempotent(::FroidurePin, ::Integer)) | Check if an element is an idempotent via its index. |
+
+```@docs
+Semigroups.contains_one(::FroidurePin)
+Semigroups.currently_contains_one(::FroidurePin)
+Semigroups.is_idempotent(::FroidurePin, ::Integer)
+```
+
+## Index queries
+
+| Function | Description |
+| -------- | ----------- |
+| [`prefix`](@ref Semigroups.prefix(::FroidurePin, ::Integer)) | Position of the longest proper prefix. |
+| [`suffix`](@ref Semigroups.suffix(::FroidurePin, ::Integer)) | Position of the longest proper suffix. |
+| [`first_letter`](@ref Semigroups.first_letter(::FroidurePin, ::Integer)) | Index of the first generator in the factorisation. |
+| [`final_letter`](@ref Semigroups.final_letter(::FroidurePin, ::Integer)) | Index of the last generator in the factorisation. |
+| [`fast_product`](@ref Semigroups.fast_product(::FroidurePin, ::Integer, ::Integer)) | Position of the product of two elements by index. |
+| [`product_by_reduction`](@ref Semigroups.product_by_reduction(::FroidurePin, ::Integer, ::Integer)) | Product using the Cayley graph. |
+| [`position_of_generator`](@ref Semigroups.position_of_generator(::FroidurePin, ::Integer)) | Position of the `i`-th generator in the enumerated elements. |
+| [`current_length`](@ref Semigroups.current_length(::FroidurePin, ::Integer)) | Length of the minimal factorisation (no enumeration). |
+| [`word_length`](@ref Semigroups.word_length(::FroidurePin, ::Integer)) | Length of the minimal factorisation (with enumeration). |
+| [`number_of_rules`](@ref Semigroups.number_of_rules(::FroidurePin)) | Total number of rules. |
+| [`current_number_of_rules`](@ref Semigroups.current_number_of_rules(::FroidurePin)) | Number of rules found so far. |
+| [`number_of_idempotents`](@ref Semigroups.number_of_idempotents(::FroidurePin)) | Total number of idempotent elements. |
+| [`current_max_word_length`](@ref Semigroups.current_max_word_length(::FroidurePin)) | Maximum word length among elements enumerated so far. |
+| [`number_of_elements_of_length`](@ref Semigroups.number_of_elements_of_length(::FroidurePin, ::Integer)) | Number of elements with a given factorisation length. |
+
+```@docs
+Semigroups.prefix(::FroidurePin, ::Integer)
+Semigroups.suffix(::FroidurePin, ::Integer)
+Semigroups.first_letter(::FroidurePin, ::Integer)
+Semigroups.final_letter(::FroidurePin, ::Integer)
+Semigroups.fast_product(::FroidurePin, ::Integer, ::Integer)
+Semigroups.product_by_reduction(::FroidurePin, ::Integer, ::Integer)
+Semigroups.position_of_generator(::FroidurePin, ::Integer)
+Semigroups.current_length(::FroidurePin, ::Integer)
+Semigroups.word_length(::FroidurePin, ::Integer)
+Semigroups.number_of_rules(::FroidurePin)
+Semigroups.current_number_of_rules(::FroidurePin)
+Semigroups.number_of_idempotents(::FroidurePin)
+Semigroups.current_max_word_length(::FroidurePin)
+Semigroups.number_of_elements_of_length(::FroidurePin, ::Integer)
+```
+
+## Iteration and display
+
+| Function | Description |
+| -------- | ----------- |
+| [`iterate`](@ref Base.iterate(::FroidurePin, ::Int)) | Iterate over all elements. |
+| [`eltype`](@ref Base.eltype(::Type{FroidurePin{E}}) where E) | Return the element type `E`. |
+| [`copy`](@ref Base.copy(::FroidurePin{E}) where E) | Create an independent copy. |
+| [`show`](@ref Base.show(::IO, ::FroidurePin)) | Display a human-readable representation. |
+
+```@docs
+Base.iterate(::FroidurePin, ::Int)
+Base.eltype(::Type{FroidurePin{E}}) where E
+Base.copy(::FroidurePin{E}) where E
+Base.show(::IO, ::FroidurePin)
+```
+
+## Runner interface
+
+`FroidurePin{E}` inherits the full [`Runner`](@ref Semigroups.Runner)
+interface. See the [Runner documentation](../core-classes/runner.md) for
+the complete list of methods (`run!`, `run_for!`, `run_until!`,
+`finished`, `started`, `timed_out`, `dead`, etc.).
diff --git a/docs/src/main-algorithms/froidure-pin/helpers.md b/docs/src/main-algorithms/froidure-pin/helpers.md
new file mode 100644
index 0000000..47354d9
--- /dev/null
+++ b/docs/src/main-algorithms/froidure-pin/helpers.md
@@ -0,0 +1,110 @@
+# Helper functions
+
+This page collects the free functions that operate on a
+[`FroidurePin`](@ref Semigroups.FroidurePin) instance. They mirror the
+`libsemigroups::froidure_pin::*` namespace and are organised into four
+groups: factorisations, collections, word-element conversion, and Cayley
+graphs.
+
+## Table of contents
+
+| Section | Description |
+| ------- | ----------- |
+| [Factorisations](@ref) | Minimal and non-minimal factorisations of elements as generator-index words. |
+| [Collections](@ref) | Materialized vectors of rules, normal forms, idempotents, and sorted elements. |
+| [Word-element conversion](@ref) | Convert between generator-index words and semigroup elements. |
+| [Cayley graphs](@ref) | Left and right Cayley graphs as `WordGraph` objects. |
+
+## Factorisations
+
+These functions return or query words (as `Vector{Int}` with 1-based
+generator indices) representing elements of the semigroup.
+
+### Contents
+
+| Function | Description |
+| -------- | ----------- |
+| [`minimal_factorisation`](@ref Semigroups.minimal_factorisation(::FroidurePin, ::Integer)) | Minimal factorisation of the element at a given position. |
+| [`current_minimal_factorisation`](@ref Semigroups.current_minimal_factorisation(::FroidurePin, ::Integer)) | Minimal factorisation without triggering enumeration. |
+| [`factorisation`](@ref Semigroups.factorisation(::FroidurePin, ::Integer)) | A (not necessarily minimal) factorisation. |
+
+### Full API
+
+```@docs
+Semigroups.minimal_factorisation(::FroidurePin, ::Integer)
+Semigroups.current_minimal_factorisation(::FroidurePin, ::Integer)
+Semigroups.factorisation(::FroidurePin, ::Integer)
+```
+
+## Collections
+
+These functions return materialized collections of elements, rules, or
+normal forms.
+
+### Contents
+
+| Function | Description |
+| -------- | ----------- |
+| [`rules`](@ref Semigroups.rules(::FroidurePin)) | All rules as `lhs => rhs` pairs with 1-based generator indices. |
+| [`current_rules`](@ref Semigroups.current_rules(::FroidurePin)) | Rules discovered so far. |
+| [`normal_forms`](@ref Semigroups.normal_forms(::FroidurePin)) | Normal forms for all elements. |
+| [`current_normal_forms`](@ref Semigroups.current_normal_forms(::FroidurePin)) | Normal forms discovered so far. |
+| [`idempotents`](@ref Semigroups.idempotents(::FroidurePin{E}) where E) | All idempotent elements. |
+| [`sorted_elements`](@ref Semigroups.sorted_elements(::FroidurePin{E}) where E) | All elements in sorted order. |
+
+### Full API
+
+```@docs
+Semigroups.rules(::FroidurePin)
+Semigroups.current_rules(::FroidurePin)
+Semigroups.normal_forms(::FroidurePin)
+Semigroups.current_normal_forms(::FroidurePin)
+Semigroups.idempotents(::FroidurePin{E}) where E
+Semigroups.sorted_elements(::FroidurePin{E}) where E
+```
+
+## Word-element conversion
+
+These functions convert between generator-index words (`Vector{Int}`
+with 1-based indices) and semigroup elements.
+
+### Contents
+
+| Function | Description |
+| -------- | ----------- |
+| [`to_element`](@ref Semigroups.to_element(::FroidurePin{E}, ::AbstractVector{<:Integer}) where E) | Convert a word to the corresponding element. |
+| [`equal_to`](@ref Semigroups.equal_to(::FroidurePin, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Check if two words represent the same element. |
+| [`position`](@ref Semigroups.position(::FroidurePin, ::AbstractVector{<:Integer})) | Position of the element represented by a word. |
+
+### Full API
+
+```@docs
+Semigroups.to_element(::FroidurePin{E}, ::AbstractVector{<:Integer}) where E
+Semigroups.equal_to(::FroidurePin, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.position(::FroidurePin, ::AbstractVector{<:Integer})
+```
+
+## Cayley graphs
+
+The left and right Cayley graphs of a `FroidurePin` instance are
+returned as [`WordGraph`](@ref Semigroups.WordGraph) objects. The
+`current_*` variants return the graph for elements enumerated so far
+without triggering further enumeration.
+
+### Contents
+
+| Function | Description |
+| -------- | ----------- |
+| [`right_cayley_graph`](@ref Semigroups.right_cayley_graph(::FroidurePin)) | Right Cayley graph (triggers full enumeration). |
+| [`current_right_cayley_graph`](@ref Semigroups.current_right_cayley_graph(::FroidurePin)) | Right Cayley graph for elements enumerated so far. |
+| [`left_cayley_graph`](@ref Semigroups.left_cayley_graph(::FroidurePin)) | Left Cayley graph (triggers full enumeration). |
+| [`current_left_cayley_graph`](@ref Semigroups.current_left_cayley_graph(::FroidurePin)) | Left Cayley graph for elements enumerated so far. |
+
+### Full API
+
+```@docs
+Semigroups.right_cayley_graph(::FroidurePin)
+Semigroups.current_right_cayley_graph(::FroidurePin)
+Semigroups.left_cayley_graph(::FroidurePin)
+Semigroups.current_left_cayley_graph(::FroidurePin)
+```
diff --git a/docs/src/main-algorithms/froidure-pin/index.md b/docs/src/main-algorithms/froidure-pin/index.md
new file mode 100644
index 0000000..36bd056
--- /dev/null
+++ b/docs/src/main-algorithms/froidure-pin/index.md
@@ -0,0 +1,22 @@
+# Froidure-Pin
+
+This section contains documentation related to the implementation of the
+Froidure-Pin algorithm in Semigroups.jl.
+
+The purpose of the Froidure-Pin algorithm is to determine the elements
+and structure of a semigroup or monoid generated by a set of generators
+whose multiplication and comparison for equality is decidable (such as
+matrices, transformations, and so on).
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `FroidurePin{E}` for the following element
+ types only: [`Transf`](@ref Semigroups.Transf),
+ [`PPerm`](@ref Semigroups.PPerm), [`Perm`](@ref Semigroups.Perm),
+ and [`BMat8`](@ref Semigroups.BMat8).
+
+## Contents
+
+| Page | Description |
+| -------------------------------------------------- | ------------------------------------------------------------------------------------- |
+| [The FroidurePin type](froidure-pin.md) | The main [`FroidurePin{E}`](@ref Semigroups.FroidurePin) parametric type. |
+| [Helper functions](helpers.md) | Free functions for factorisations, word queries, collections, and Cayley graphs. |
diff --git a/docs/src/main-algorithms/index.md b/docs/src/main-algorithms/index.md
index b7f41c9..317b30d 100644
--- a/docs/src/main-algorithms/index.md
+++ b/docs/src/main-algorithms/index.md
@@ -1,19 +1,21 @@
# Overview
-This section will document the main algorithms available in Semigroups.jl.
+This section documents the main algorithms available in Semigroups.jl.
-## Planned Algorithms
-
-The following algorithms from libsemigroups will be exposed through Semigroups.jl:
+## Available Algorithms
-### Froidure-Pin Algorithm
+### [Froidure-Pin Algorithm](froidure-pin/index.md)
-### Knuth-Bendix Completion
+The Froidure-Pin algorithm enumerates all elements and determines the
+structure of a finitely generated semigroup or monoid. See the
+[Froidure-Pin section](froidure-pin/index.md) for the full API.
-### Todd-Coxeter Algorithm
-
-### Schreier-Sims Algorithm
+## Planned Algorithms
-## Status
+The following algorithms from libsemigroups will be exposed in future
+versions of Semigroups.jl:
-This section is under development.
+- **Knuth-Bendix Completion**
+- **Todd-Coxeter Algorithm**
+- **Kambites' Algorithm** (small overlap monoids)
+- **Schreier-Sims Algorithm** (permutation groups)
diff --git a/src/Semigroups.jl b/src/Semigroups.jl
index f69ebba..f8f88e1 100644
--- a/src/Semigroups.jl
+++ b/src/Semigroups.jl
@@ -8,6 +8,7 @@ module Semigroups
using CxxWrap
using AbstractAlgebra
+import Dates
using Dates: TimePeriod, Nanosecond
using libsemigroups_jll
@@ -74,6 +75,9 @@ include("presentation-examples.jl")
include("bmat8.jl")
include("transf.jl")
+# Algorithm types (must come after element types)
+include("froidure-pin.jl")
+
# Module initialization
function __init__()
# Initialize the CxxWrap module
@@ -160,6 +164,26 @@ export degree, rank, image, domain, inverse
export increase_degree_by!, swap!
export left_one, right_one
+# FroidurePin
+export FroidurePin, current_size, number_of_generators, enumerate!
+export generator, sorted_at
+export sorted_position, to_sorted_position
+export closure!, copy_closure, copy_add_generators, reserve!
+export batch_size, set_batch_size!
+export current_position
+export contains_one, currently_contains_one, is_idempotent
+export prefix, suffix, first_letter, final_letter, fast_product
+export number_of_idempotents, number_of_elements_of_length
+export current_number_of_rules, current_max_word_length
+export position_of_generator, current_length, word_length
+export product_by_reduction
+export rules, current_rules, normal_forms, current_normal_forms
+export idempotents, sorted_elements
+export minimal_factorisation, current_minimal_factorisation, factorisation
+export right_cayley_graph, current_right_cayley_graph
+export left_cayley_graph, current_left_cayley_graph
+export to_element, equal_to
+
# BMat8
export BMat8, to_int, swap!, degree, random, row_space_basis
export col_space_basis, col_space_size, is_regular_element, minimum_dim
diff --git a/src/froidure-pin.jl b/src/froidure-pin.jl
new file mode 100644
index 0000000..983dc79
--- /dev/null
+++ b/src/froidure-pin.jl
@@ -0,0 +1,1921 @@
+# Copyright (c) 2026, James W. Swent, J. D. Mitchell
+#
+# Distributed under the terms of the GPL license version 3.
+#
+# The full license is in the file LICENSE, distributed with this software.
+
+"""
+froidure-pin.jl - FroidurePin{E} high-level Julia wrapper
+
+Provides a parametric `FroidurePin{E}` type wrapping the CxxWrap-bound
+C++ `FroidurePin` template instantiations. Follows the same three-layer
+pattern as `transf.jl`: CxxWrap type aliases, union type, private helpers,
+mutable struct, constructors, and method wrappers.
+"""
+
+# ============================================================================
+# CxxWrap type aliases — concrete FroidurePin instantiations
+# ============================================================================
+
+const FroidurePinBase = LibSemigroups.FroidurePinBase
+
+const FroidurePinTransf1 = LibSemigroups.FroidurePinTransf1
+const FroidurePinTransf2 = LibSemigroups.FroidurePinTransf2
+const FroidurePinTransf4 = LibSemigroups.FroidurePinTransf4
+
+const FroidurePinPPerm1 = LibSemigroups.FroidurePinPPerm1
+const FroidurePinPPerm2 = LibSemigroups.FroidurePinPPerm2
+const FroidurePinPPerm4 = LibSemigroups.FroidurePinPPerm4
+
+const FroidurePinPerm1 = LibSemigroups.FroidurePinPerm1
+const FroidurePinPerm2 = LibSemigroups.FroidurePinPerm2
+const FroidurePinPerm4 = LibSemigroups.FroidurePinPerm4
+
+const FroidurePinBMat8 = LibSemigroups.FroidurePinBMat8
+
+# ============================================================================
+# Union type for cxx_obj field
+# ============================================================================
+
+const _FroidurePinCxx = Union{
+ FroidurePinTransf1,
+ FroidurePinTransf2,
+ FroidurePinTransf4,
+ FroidurePinPPerm1,
+ FroidurePinPPerm2,
+ FroidurePinPPerm4,
+ FroidurePinPerm1,
+ FroidurePinPerm2,
+ FroidurePinPerm4,
+ FroidurePinBMat8,
+}
+
+# ============================================================================
+# Private helpers
+# ============================================================================
+# Index/word conversion helpers (_to_cpp, _from_cpp, _word_to_cpp,
+# _word_from_cpp) are defined in word-graph.jl, transf.jl, and order.jl
+# respectively. We reuse them here without redefinition.
+
+# ============================================================================
+# Type dispatch helpers
+# ============================================================================
+
+"""
+ _cxx_fp_type(::Type{E}) -> Type
+
+Map a high-level Julia element type to its CxxWrap FroidurePin constructor type.
+"""
+_cxx_fp_type(::Type{Transf{UInt8}}) = FroidurePinTransf1
+_cxx_fp_type(::Type{Transf{UInt16}}) = FroidurePinTransf2
+_cxx_fp_type(::Type{Transf{UInt32}}) = FroidurePinTransf4
+
+_cxx_fp_type(::Type{PPerm{UInt8}}) = FroidurePinPPerm1
+_cxx_fp_type(::Type{PPerm{UInt16}}) = FroidurePinPPerm2
+_cxx_fp_type(::Type{PPerm{UInt32}}) = FroidurePinPPerm4
+
+_cxx_fp_type(::Type{Perm{UInt8}}) = FroidurePinPerm1
+_cxx_fp_type(::Type{Perm{UInt16}}) = FroidurePinPerm2
+_cxx_fp_type(::Type{Perm{UInt32}}) = FroidurePinPerm4
+
+_cxx_fp_type(::Type{T}) where {T<:BMat8} = FroidurePinBMat8
+
+"""
+ _wrap_element(::Type{E}, raw) -> E
+
+Wrap a raw CxxWrap element back into the high-level Julia type.
+"""
+_wrap_element(::Type{Transf{T}}, raw) where {T} = Transf{T}(raw)
+_wrap_element(::Type{PPerm{T}}, raw) where {T} = PPerm{T}(raw)
+_wrap_element(::Type{Perm{T}}, raw) where {T} = Perm{T}(raw)
+_wrap_element(::Type{T}, raw) where {T<:BMat8} = raw # BMat8 is a direct alias
+
+"""
+ _cxx_element(x) -> CxxWrap object
+
+Extract the CxxWrap object from a high-level Julia element.
+"""
+_cxx_element(x::Transf) = x.cxx_obj
+_cxx_element(x::PPerm) = x.cxx_obj
+_cxx_element(x::Perm) = x.cxx_obj
+_cxx_element(x::BMat8) = x # BMat8 is a direct alias
+
+"""
+ _copy_cxx_element(raw) -> CxxWrap object (Allocated)
+
+Copy a raw CxxWrap element to produce an Allocated (owning) copy.
+Needed when iterating CxxWrap StdVectors, which yield Dereferenced
+references that don't own their memory.
+
+For Transf/PPerm/Perm types, calls LibSemigroups.copy().
+For BMat8, returns as-is (value type).
+"""
+_copy_cxx_element(raw::Union{_TransfTypes,_PPermTypes,_PermTypes}) = LibSemigroups.copy(raw)
+_copy_cxx_element(raw::BMat8) = raw
+
+"""
+ _fp_element_type(::Type{E}) -> Type
+
+Normalize element type for the FroidurePin{E} type parameter.
+CxxWrap creates BMat8Allocated as a subtype of BMat8; we normalize
+to BMat8 so that FroidurePin{BMat8} is the canonical type.
+"""
+_fp_element_type(::Type{E}) where {E} = E
+_fp_element_type(::Type{T}) where {T<:BMat8} = BMat8
+
+# ============================================================================
+# FroidurePin{E} mutable struct
+# ============================================================================
+
+"""
+ FroidurePin{E}
+
+Type implementing the Froidure-Pin algorithm.
+
+A `FroidurePin{E}` instance is defined by a generating set of elements of
+type `E`, and the main function is [`run!`](@ref), which implements the
+Froidure-Pin algorithm as described by Froidure and Pin. If [`run!`](@ref)
+is invoked and [`finished`](@ref) returns `true`, then the size
+[`length`](@ref), the left and right Cayley graphs
+[`left_cayley_graph`](@ref) and [`right_cayley_graph`](@ref) are
+determined, and a confluent terminating presentation for the semigroup is
+known.
+
+Supported element types:
+- [`Transf{UInt8}`](@ref Semigroups.Transf), [`Transf{UInt16}`](@ref Semigroups.Transf), [`Transf{UInt32}`](@ref Semigroups.Transf) (transformations)
+- [`PPerm{UInt8}`](@ref Semigroups.PPerm), [`PPerm{UInt16}`](@ref Semigroups.PPerm), [`PPerm{UInt32}`](@ref Semigroups.PPerm) (partial permutations)
+- [`Perm{UInt8}`](@ref Semigroups.Perm), [`Perm{UInt16}`](@ref Semigroups.Perm), [`Perm{UInt32}`](@ref Semigroups.Perm) (permutations)
+- [`BMat8`](@ref Semigroups.BMat8) (boolean matrices up to 8x8)
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+length(S) # 6 (symmetric group S_3)
+```
+
+# See also
+- [`FroidurePinBase`](@ref Semigroups.FroidurePinBase)
+- [`Runner`](@ref Semigroups.Runner)
+"""
+mutable struct FroidurePin{E}
+ cxx_obj::_FroidurePinCxx
+end
+
+# ============================================================================
+# Constructors
+# ============================================================================
+
+"""
+ FroidurePin(gens::Vector{E}) where {E}
+
+Construct a [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from a
+container of generators.
+
+This function constructs a `FroidurePin{E}` instance from the vector of
+generators `gens`, after verifying that the proposed generators all have
+equal degree.
+
+# Arguments
+- `gens::Vector{E}`: a non-empty vector of generators. All generators
+ must have the same degree.
+
+# Throws
+- `ErrorException`: if `gens` is empty.
+- `LibsemigroupsError`: if `degree(x) != degree(y)` for any `x` and `y`
+ in `gens`.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin([Transf([2, 1, 3]), Transf([2, 3, 1])])
+length(S) # 6
+```
+
+# See also
+- [`FroidurePin(x::E, xs::E...)`](@ref)
+"""
+function FroidurePin(gens::Vector{E}) where {E}
+ isempty(gens) && error("At least one generator is required")
+
+ # Normalize element type (BMat8Allocated -> BMat8)
+ NE = _fp_element_type(E)
+
+ FPType = _cxx_fp_type(NE)
+ cxx_gens = [_cxx_element(g) for g in gens]
+
+ n = length(cxx_gens)
+ if n == 1
+ cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1])
+ elseif n == 2
+ cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1], cxx_gens[2])
+ elseif n == 3
+ cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1], cxx_gens[2], cxx_gens[3])
+ elseif n == 4
+ cxx_obj = @wrap_libsemigroups_call FPType(
+ cxx_gens[1],
+ cxx_gens[2],
+ cxx_gens[3],
+ cxx_gens[4],
+ )
+ else
+ # >4 generators: construct with first, then add the rest
+ cxx_obj = @wrap_libsemigroups_call FPType(cxx_gens[1])
+ for i = 2:n
+ @wrap_libsemigroups_call LibSemigroups.add_generator!(cxx_obj, cxx_gens[i])
+ end
+ end
+
+ return FroidurePin{NE}(cxx_obj)
+end
+
+"""
+ FroidurePin(x::E, xs::E...) where {E}
+
+Construct a [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from one or
+more generators given as positional arguments.
+
+This is a convenience constructor equivalent to
+`FroidurePin(E[x, xs...])`.
+
+# Arguments
+- `x::E`: the first generator.
+- `xs::E...`: zero or more additional generators of the same type.
+
+# Throws
+- `LibsemigroupsError`: if `degree(x) != degree(y)` for any generators
+ `x` and `y`.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+length(S) # 6
+```
+
+# See also
+- [`FroidurePin(gens::Vector{E})`](@ref)
+"""
+function FroidurePin(x::E, xs::E...) where {E}
+ return FroidurePin(E[x, xs...])
+end
+
+# ============================================================================
+# Size queries
+# ============================================================================
+
+"""
+ Base.length(fp::FroidurePin) -> Int
+
+Return the size of a [`FroidurePin`](@ref Semigroups.FroidurePin) instance.
+
+This function fully enumerates `fp` and then returns the number of
+elements.
+
+# Complexity
+At worst ``O(|S| n)`` where ``S`` is the semigroup represented by `fp`
+and ``n`` is [`number_of_generators`](@ref)`(fp)`.
+
+!!! note
+ This function triggers a full enumeration.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+length(S) # 6
+```
+
+# See also
+- [`current_size`](@ref)
+"""
+Base.length(fp::FroidurePin) = Int(LibSemigroups.size(fp.cxx_obj))
+
+"""
+ current_size(fp::FroidurePin) -> Int
+
+Return the number of elements so far enumerated.
+
+This is only the actual size of the semigroup if `fp` is fully enumerated.
+
+# Complexity
+Constant.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+current_size(S) # 2 (only generators known)
+```
+
+# See also
+- [`length`](@ref Base.length)
+"""
+current_size(fp::FroidurePin) = Int(LibSemigroups.current_size(fp.cxx_obj))
+
+"""
+ degree(fp::FroidurePin) -> Int
+
+Return the degree of any and all elements.
+
+This function returns the degree of any (and hence all) elements of the
+semigroup represented by `fp`.
+
+# Complexity
+Constant.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+degree(S) # 3
+```
+"""
+degree(fp::FroidurePin) = Int(LibSemigroups.degree(fp.cxx_obj))
+
+"""
+ number_of_generators(fp::FroidurePin) -> Int
+
+Return the number of generators.
+
+This function returns the number of generators of the semigroup
+represented by `fp`.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+number_of_generators(S) # 2
+```
+
+# See also
+- [`generator`](@ref)
+"""
+number_of_generators(fp::FroidurePin) = Int(LibSemigroups.number_of_generators(fp.cxx_obj))
+
+"""
+ enumerate!(fp::FroidurePin, limit::Integer) -> FroidurePin
+
+Enumerate until at least a specified number of elements are found.
+
+If `fp` is already fully enumerated, or the number of elements
+previously enumerated exceeds `limit`, then calling this function does
+nothing. Otherwise, [`run!`](@ref) attempts to find at least the maximum
+of `limit` and [`batch_size`](@ref) additional elements of the semigroup.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `limit::Integer`: the approximate limit for
+ [`current_size`](@ref)`(fp)`.
+
+# Complexity
+At worst ``O(m n)`` where ``m`` equals `limit` and ``n`` is
+[`number_of_generators`](@ref)`(fp)`.
+
+# Example
+```julia
+using Semigroups
+
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+enumerate!(S, 4)
+current_size(S) # >= 4
+```
+
+# See also
+- [`run!`](@ref)
+- [`current_size`](@ref)
+"""
+function enumerate!(fp::FroidurePin, limit::Integer)
+ lim = UInt(limit)
+ @wrap_libsemigroups_call LibSemigroups.enumerate!(fp.cxx_obj, lim)
+ return fp
+end
+
+# ============================================================================
+# Runner delegation
+# ============================================================================
+# Every public Runner method is delegated through fp.cxx_obj.
+# Mutating methods return fp for chaining; query methods return the result.
+
+"""
+ run!(fp::FroidurePin) -> FroidurePin
+
+Run the Froidure-Pin algorithm until [`finished`](@ref).
+
+At the end of this call `fp` is either [`finished`](@ref) or
+[`dead`](@ref). Returns `fp` for method chaining.
+
+# See also
+- [`run_for!`](@ref)
+- [`run_until!`](@ref)
+- [`finished`](@ref)
+"""
+function run!(fp::FroidurePin)
+ LibSemigroups.run!(fp.cxx_obj)
+ return fp
+end
+
+"""
+ run_for!(fp::FroidurePin, t::TimePeriod) -> FroidurePin
+
+Run the Froidure-Pin algorithm for a specified amount of time.
+
+At the end of this call `fp` is either [`finished`](@ref),
+[`dead`](@ref), or [`timed_out`](@ref). Returns `fp` for method
+chaining.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `t::TimePeriod`: the duration to run for (e.g. `Second(1)`,
+ `Millisecond(500)`).
+
+# Example
+```julia
+run_for!(S, Second(1))
+run_for!(S, Millisecond(500))
+```
+
+# See also
+- [`run!`](@ref)
+- [`run_until!`](@ref)
+- [`timed_out`](@ref)
+"""
+function run_for!(fp::FroidurePin, t::TimePeriod)
+ ns = convert(Nanosecond, t)
+ Dates.value(ns) >= 0 ||
+ throw(ArgumentError("run_for! requires a non-negative duration, got $t"))
+ LibSemigroups.run_for!(fp.cxx_obj, Int64(Dates.value(ns)))
+ return fp
+end
+
+function run_until!(f::Function, fp::FroidurePin)
+ sf = @safe_cfunction($f, Cuchar, ())
+ GC.@preserve sf LibSemigroups.run_until!(fp.cxx_obj, sf)
+ return fp
+end
+
+"""
+ run_until!(fp::FroidurePin, f::Function) -> FroidurePin
+
+Run the Froidure-Pin algorithm until a nullary predicate returns `true`
+or [`finished`](@ref).
+
+At the end of this call `fp` is either [`finished`](@ref),
+[`dead`](@ref), or [`stopped_by_predicate`](@ref). Returns `fp` for
+method chaining.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `f::Function`: a nullary predicate (a callable taking no arguments and
+ returning `Bool`).
+
+Supports do-block syntax:
+
+```julia
+run_until!(S) do
+ current_size(S) >= 10
+end
+```
+
+# See also
+- [`run!`](@ref)
+- [`run_for!`](@ref)
+- [`stopped_by_predicate`](@ref)
+"""
+run_until!(fp::FroidurePin, f::Function) = run_until!(f, fp)
+
+"""
+ init!(fp::FroidurePin) -> FroidurePin
+
+Initialize an existing [`FroidurePin`](@ref Semigroups.FroidurePin)
+object.
+
+This function puts `fp` back into the same state as if it had been newly
+default constructed. Returns `fp` for method chaining.
+
+!!! warning
+ This function is not thread-safe.
+
+# See also
+- [`FroidurePin`](@ref Semigroups.FroidurePin)
+"""
+function init!(fp::FroidurePin)
+ LibSemigroups.init!(fp.cxx_obj)
+ return fp
+end
+
+"""
+ kill!(fp::FroidurePin)
+
+Stop [`run!`](@ref) from running (thread-safe).
+
+This function can be used to terminate [`run!`](@ref) from another
+thread. After [`kill!`](@ref) has been called, `fp` may no longer be in a
+valid state, but will return `true` from [`dead`](@ref).
+
+# See also
+- [`dead`](@ref)
+- [`finished`](@ref)
+"""
+kill!(fp::FroidurePin) = LibSemigroups.kill!(fp.cxx_obj)
+
+"""
+ finished(fp::FroidurePin) -> Bool
+
+Check if [`run!`](@ref) has been run to completion.
+
+Returns `true` if [`run!`](@ref) has been run to completion and `false`
+otherwise.
+
+# See also
+- [`started`](@ref)
+- [`stopped`](@ref)
+"""
+finished(fp::FroidurePin) = LibSemigroups.finished(fp.cxx_obj)
+
+"""
+ Base.success(fp::FroidurePin) -> Bool
+
+Check if [`run!`](@ref) has been run to completion successfully.
+
+Returns `true` if [`run!`](@ref) has been run to completion and it was
+successful. The default implementation is equivalent to calling
+[`finished`](@ref).
+
+# See also
+- [`finished`](@ref)
+"""
+Base.success(fp::FroidurePin) = LibSemigroups.success(fp.cxx_obj)
+
+"""
+ started(fp::FroidurePin) -> Bool
+
+Check if [`run!`](@ref) has been called at least once.
+
+Returns `true` if [`run!`](@ref) has started to run (it can be
+currently running or not).
+
+# See also
+- [`finished`](@ref)
+- [`running`](@ref)
+"""
+started(fp::FroidurePin) = LibSemigroups.started(fp.cxx_obj)
+
+"""
+ running(fp::FroidurePin) -> Bool
+
+Check if currently running.
+
+Returns `true` if [`run!`](@ref) is in the process of running and
+`false` otherwise.
+
+# See also
+- [`finished`](@ref)
+- [`started`](@ref)
+"""
+running(fp::FroidurePin) = LibSemigroups.running(fp.cxx_obj)
+
+"""
+ timed_out(fp::FroidurePin) -> Bool
+
+Check if the amount of time passed to [`run_for!`](@ref) has elapsed.
+
+Returns `true` if the last [`run_for!`](@ref) call timed out and `false`
+otherwise.
+
+# See also
+- [`run_for!`](@ref)
+- [`stopped`](@ref)
+"""
+timed_out(fp::FroidurePin) = LibSemigroups.timed_out(fp.cxx_obj)
+
+"""
+ stopped(fp::FroidurePin) -> Bool
+
+Check if the runner is stopped.
+
+This function can be used to check whether or not [`run!`](@ref) has
+been stopped for whatever reason. In other words, it checks if
+[`timed_out`](@ref), [`finished`](@ref), or [`dead`](@ref).
+
+# See also
+- [`timed_out`](@ref)
+- [`finished`](@ref)
+- [`dead`](@ref)
+"""
+stopped(fp::FroidurePin) = LibSemigroups.stopped(fp.cxx_obj)
+
+"""
+ dead(fp::FroidurePin) -> Bool
+
+Check if the runner is dead.
+
+This function can be used to check if [`run!`](@ref) should terminate
+because it has been killed by another thread via [`kill!`](@ref).
+
+# See also
+- [`kill!`](@ref)
+- [`stopped`](@ref)
+"""
+dead(fp::FroidurePin) = LibSemigroups.dead(fp.cxx_obj)
+
+"""
+ stopped_by_predicate(fp::FroidurePin) -> Bool
+
+Check if the runner was stopped by the predicate passed to
+[`run_until!`](@ref).
+
+If `fp` is running, then the nullary predicate is called and its return
+value is returned. If `fp` is not running, then `true` is returned if
+and only if the last time `fp` was running it was stopped by the
+predicate passed to [`run_until!`](@ref).
+
+# Complexity
+Constant.
+
+# See also
+- [`run_until!`](@ref)
+- [`stopped`](@ref)
+"""
+stopped_by_predicate(fp::FroidurePin) = LibSemigroups.stopped_by_predicate(fp.cxx_obj)
+
+"""
+ running_for(fp::FroidurePin) -> Bool
+
+Check if currently running for a particular length of time.
+
+If `fp` is currently running because [`run_for!`](@ref) has been
+invoked, then this function returns `true`. Otherwise, `false` is
+returned.
+
+# Complexity
+Constant.
+
+# See also
+- [`run_for!`](@ref)
+- [`running_for_how_long`](@ref)
+"""
+running_for(fp::FroidurePin) = LibSemigroups.running_for(fp.cxx_obj)
+
+"""
+ running_for_how_long(fp::FroidurePin) -> Nanosecond
+
+Return the last value passed to [`run_for!`](@ref).
+
+This function returns the last value passed as an argument to
+[`run_for!`](@ref) (if any) as a `Dates.Nanosecond`.
+
+# Complexity
+Constant.
+
+# See also
+- [`run_for!`](@ref)
+- [`running_for`](@ref)
+"""
+running_for_how_long(fp::FroidurePin) =
+ Nanosecond(LibSemigroups.running_for_how_long(fp.cxx_obj))
+
+"""
+ running_until(fp::FroidurePin) -> Bool
+
+Check if currently running until a nullary predicate returns `true`.
+
+If `fp` is currently running because [`run_until!`](@ref) has been
+invoked, then this function returns `true`. Otherwise, `false` is
+returned.
+
+# Complexity
+Constant.
+
+# See also
+- [`run_until!`](@ref)
+- [`stopped_by_predicate`](@ref)
+"""
+running_until(fp::FroidurePin) = LibSemigroups.running_until(fp.cxx_obj)
+
+"""
+ current_state(fp::FroidurePin) -> RunnerState
+
+Return the current state of the runner.
+
+Returns the current state of `fp` as a [`RunnerState`](@ref Semigroups.RunnerState)
+value.
+
+# Complexity
+Constant.
+
+# See also
+- [`RunnerState`](@ref Semigroups.RunnerState)
+"""
+current_state(fp::FroidurePin) = LibSemigroups.current_state(fp.cxx_obj)
+
+"""
+ report_why_we_stopped(fp::FroidurePin)
+
+Report why [`run!`](@ref) stopped.
+
+Reports whether [`run!`](@ref) was stopped because it is
+[`finished`](@ref), [`timed_out`](@ref), or [`dead`](@ref).
+
+# See also
+- [`string_why_we_stopped`](@ref)
+"""
+report_why_we_stopped(fp::FroidurePin) = LibSemigroups.report_why_we_stopped(fp.cxx_obj)
+
+"""
+ string_why_we_stopped(fp::FroidurePin) -> String
+
+Return a human-readable string describing why [`run!`](@ref) stopped.
+
+Returns a string indicating whether [`run!`](@ref) was stopped because
+it is [`finished`](@ref), [`timed_out`](@ref), or [`dead`](@ref).
+
+# See also
+- [`report_why_we_stopped`](@ref)
+"""
+string_why_we_stopped(fp::FroidurePin) = LibSemigroups.string_why_we_stopped(fp.cxx_obj)
+
+# ============================================================================
+# Element access
+# ============================================================================
+
+"""
+ Base.getindex(fp::FroidurePin{E}, i::Integer) -> E
+
+Access the element with index `i`.
+
+This function attempts to enumerate until at least `i` elements have
+been found, then returns the element at position `i`.
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `i::Integer`: the 1-based index of the element to access.
+
+# Throws
+- `BoundsError`: if `i` is less than `1` or greater than
+ [`length`](@ref Base.length)`(fp)`.
+
+!!! note
+ This function triggers a full enumeration.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+S[1] # first element
+```
+
+# See also
+- [`sorted_at`](@ref)
+- [`generator`](@ref)
+"""
+function Base.getindex(fp::FroidurePin{E}, i::Integer) where {E}
+ if i < 1 || i > length(fp)
+ throw(BoundsError(fp, i))
+ end
+ idx = _to_cpp(i, UInt)
+ raw = @wrap_libsemigroups_call LibSemigroups.at(fp.cxx_obj, idx)
+ return _wrap_element(E, raw)
+end
+
+"""
+ generator(fp::FroidurePin{E}, i::Integer) -> E
+
+Return the generator with the specified index.
+
+This function returns the generator with index `i`, where the order is
+that in which the generators were added at construction, or via
+[`push!`](@ref Base.push!), [`closure!`](@ref),
+[`copy_closure`](@ref), or [`copy_add_generators`](@ref).
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `i::Integer`: the 1-based index of a generator.
+
+# Throws
+- `LibsemigroupsError`: if `i` is greater than
+ [`number_of_generators`](@ref)`(fp)`.
+
+!!! note
+ `generator(fp, j)` is in general not in position `j`.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+generator(S, 1) # first generator
+```
+
+# See also
+- [`getindex`](@ref Base.getindex)
+- [`sorted_at`](@ref)
+"""
+function generator(fp::FroidurePin{E}, i::Integer) where {E}
+ idx = _to_cpp(i, UInt)
+ raw = @wrap_libsemigroups_call LibSemigroups.generator(fp.cxx_obj, idx)
+ return _wrap_element(E, raw)
+end
+
+"""
+ sorted_at(fp::FroidurePin{E}, i::Integer) -> E
+
+Access the element with the specified sorted index.
+
+This function triggers a full enumeration, and returns the element
+at position `i` when the elements are sorted.
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `i::Integer`: the 1-based sorted index of the element to access.
+
+# Throws
+- `LibsemigroupsError`: if `i` is greater than
+ [`length`](@ref Base.length)`(fp)`.
+
+!!! note
+ This function triggers a full enumeration.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+sorted_at(S, 1) # first element in sorted order
+```
+
+# See also
+- [`getindex`](@ref Base.getindex)
+- [`sorted_position`](@ref)
+"""
+function sorted_at(fp::FroidurePin{E}, i::Integer) where {E}
+ idx = _to_cpp(i, UInt)
+ raw = @wrap_libsemigroups_call LibSemigroups.sorted_at(fp.cxx_obj, idx)
+ return _wrap_element(E, raw)
+end
+
+# ============================================================================
+# Iteration
+# ============================================================================
+
+"""
+ Base.iterate(fp::FroidurePin{E}[, state]) -> Union{Tuple{E, Int}, Nothing}
+
+Iterate over all elements of the semigroup.
+
+This function allows a [`FroidurePin{E}`](@ref Semigroups.FroidurePin)
+instance to be used in `for` loops and with `collect`. Elements are
+yielded in the order they were enumerated by the Froidure-Pin algorithm.
+
+!!! note
+ This function triggers a full enumeration on the first call
+ (via [`length`](@ref Base.length)).
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+for x in S
+ println(x)
+end
+elts = collect(S) # Vector{Transf{UInt8}} of length 6
+```
+
+# See also
+- [`getindex`](@ref Base.getindex)
+- [`length`](@ref Base.length)
+"""
+function Base.iterate(fp::FroidurePin, state::Int = 1)
+ if state > length(fp)
+ return nothing
+ end
+ return (fp[state], state + 1)
+end
+
+"""
+ Base.eltype(::Type{FroidurePin{E}}) where E -> Type
+
+Return the element type `E` of a [`FroidurePin{E}`](@ref Semigroups.FroidurePin).
+
+# Complexity
+Constant.
+"""
+Base.eltype(::Type{FroidurePin{E}}) where {E} = E
+
+Base.IteratorSize(::Type{<:FroidurePin}) = Base.HasLength()
+
+# ============================================================================
+# Copy
+# ============================================================================
+
+"""
+ Base.copy(fp::FroidurePin{E}) -> FroidurePin{E}
+
+Create an independent copy of the [`FroidurePin{E}`](@ref Semigroups.FroidurePin) instance.
+
+This function constructs a new [`FroidurePin{E}`](@ref Semigroups.FroidurePin) from the
+generators of `fp`. The returned instance is fully independent of `fp`
+and can be enumerated or modified without affecting the original.
+
+!!! note
+ This function does not trigger any enumeration of `fp`.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+T = copy(S)
+length(T) # 6, independently computed
+```
+
+# See also
+- [`copy_closure`](@ref)
+- [`copy_add_generators`](@ref)
+"""
+function Base.copy(fp::FroidurePin{E}) where {E}
+ gens = [generator(fp, i) for i = 1:number_of_generators(fp)]
+ return FroidurePin(gens)
+end
+
+# ============================================================================
+# Display
+# ============================================================================
+
+"""
+ Base.show(io::IO, fp::FroidurePin)
+
+Display a human-readable summary of the [`FroidurePin`](@ref Semigroups.FroidurePin)
+instance `fp` to the I/O stream `io`.
+
+!!! note
+ This function does not trigger any enumeration.
+"""
+function Base.show(io::IO, fp::FroidurePin)
+ print(io, @wrap_libsemigroups_call LibSemigroups.to_human_readable_repr(fp.cxx_obj))
+end
+
+# ============================================================================
+# Containment
+# ============================================================================
+
+"""
+ Base.in(x::E, fp::FroidurePin{E}) where E -> Bool
+
+Test membership of an element.
+
+This function returns `true` if `x` belongs to `fp` and `false` if it
+does not.
+
+# Arguments
+- `x::E`: an element to test for membership.
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+
+!!! note
+ This function may trigger a (partial) enumeration.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+Transf([2, 1, 3]) in S # true
+Transf([1, 1, 1]) in S # false
+```
+
+# See also
+- [`position`](@ref)
+- [`current_position`](@ref)
+"""
+function Base.in(x::E, fp::FroidurePin{E}) where {E}
+ cxx_x = _cxx_element(x)
+ return @wrap_libsemigroups_call LibSemigroups.contains(fp.cxx_obj, cxx_x)
+end
+
+# BMat8 dispatch: BMat8Allocated <: BMat8, so we need a fallback
+function Base.in(x::BMat8, fp::FroidurePin{BMat8})
+ cxx_x = _cxx_element(x)
+ return @wrap_libsemigroups_call LibSemigroups.contains(fp.cxx_obj, cxx_x)
+end
+
+"""
+ position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED}
+
+Find the position of an element with enumeration if necessary.
+
+This function returns the 1-based position of `x` in `fp`, or
+[`UNDEFINED`](@ref Semigroups.UNDEFINED) if `x` is not an element of `fp`.
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `x::E`: an element whose position is sought.
+
+!!! note
+ This function triggers a full enumeration.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+position(S, Transf([2, 1, 3])) # 1-based index
+```
+
+# See also
+- [`current_position`](@ref)
+- [`sorted_position`](@ref)
+- [`in`](@ref Base.in)
+"""
+function position(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+function position(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+"""
+ sorted_position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED}
+
+Return the sorted index of an element.
+
+This function returns the 1-based position of `x` in the elements of
+`fp` when they are sorted, or [`UNDEFINED`](@ref Semigroups.UNDEFINED)
+if `x` is not an element of `fp`.
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `x::E`: an element whose sorted position is sought.
+
+!!! note
+ This function triggers a full enumeration.
+
+# See also
+- [`current_position`](@ref)
+- [`position`](@ref)
+- [`to_sorted_position`](@ref)
+- [`sorted_at`](@ref)
+"""
+function sorted_position(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.sorted_position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+function sorted_position(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.sorted_position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+"""
+ to_sorted_position(fp::FroidurePin, i::Integer) -> Union{Int, UNDEFINED}
+
+Return the sorted index of an element via its index.
+
+This function returns the 1-based position of the element with index `i`
+when the elements are sorted, or [`UNDEFINED`](@ref Semigroups.UNDEFINED)
+if `i` is greater than [`length`](@ref Base.length)`(fp)`.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based index of the element.
+
+!!! note
+ This function triggers a full enumeration.
+
+# See also
+- [`sorted_position`](@ref)
+- [`sorted_at`](@ref)
+"""
+function to_sorted_position(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt)
+ raw = @wrap_libsemigroups_call LibSemigroups.to_sorted_position(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ current_position(fp::FroidurePin{E}, x::E) where E -> Union{Int, UNDEFINED}
+
+Find the position of an element with no enumeration.
+
+This function returns the 1-based position of the element `x` in `fp`
+if it is already known to belong to `fp`, or
+[`UNDEFINED`](@ref Semigroups.UNDEFINED) if not. If `fp` is not yet
+fully enumerated, then this function may return
+[`UNDEFINED`](@ref Semigroups.UNDEFINED) even when `x` does belong to `fp`.
+
+# Arguments
+- `fp::FroidurePin{E}`: the FroidurePin instance.
+- `x::E`: an element whose position is sought.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# See also
+- [`position`](@ref)
+- [`sorted_position`](@ref)
+"""
+function current_position(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+function current_position(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cxx_x)
+ return _from_cpp(raw)
+end
+
+"""
+ current_position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Union{Int, UNDEFINED}
+
+Return the position corresponding to a word.
+
+This function returns the 1-based position of the element corresponding
+to the word `w` (a vector of 1-based generator indices). No enumeration
+is performed, and [`UNDEFINED`](@ref Semigroups.UNDEFINED) is returned
+if the word does not currently correspond to an element.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `w::AbstractVector{<:Integer}`: a word in the generators (1-based
+ generator indices).
+
+# Throws
+- `LibsemigroupsError`: if any letter in `w` is out of range (exceeds
+ [`number_of_generators`](@ref)`(fp)`).
+
+# Complexity
+``O(n)`` where ``n`` is the length of the word `w`.
+
+!!! note
+ This function does not trigger any enumeration.
+
+# See also
+- [`position`](@ref)
+- [`current_position`](@ref current_position(::FroidurePin{E}, ::E) where E)
+"""
+function current_position(fp::FroidurePin, w::AbstractVector{<:Integer})
+ cw = _word_to_cpp(w)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_position(fp.cxx_obj, cw)
+ return _from_cpp(raw)
+end
+
+# ============================================================================
+# Modification
+# ============================================================================
+
+"""
+ Base.push!(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E}
+
+Add a new generator `x` to the semigroup `fp`.
+
+Returns `fp` for method chaining.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]))
+push!(S, Transf([2, 3, 1]))
+length(S) # 6
+```
+"""
+function Base.push!(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ @wrap_libsemigroups_call LibSemigroups.add_generator!(fp.cxx_obj, cxx_x)
+ return fp
+end
+
+function Base.push!(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ @wrap_libsemigroups_call LibSemigroups.add_generator!(fp.cxx_obj, cxx_x)
+ return fp
+end
+
+"""
+ closure!(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E}
+
+Add element `x` to the semigroup `fp` if it is not already contained,
+and re-enumerate.
+
+Returns `fp` for method chaining.
+"""
+function closure!(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ @wrap_libsemigroups_call LibSemigroups.closure!(fp.cxx_obj, cxx_x)
+ return fp
+end
+
+function closure!(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ @wrap_libsemigroups_call LibSemigroups.closure!(fp.cxx_obj, cxx_x)
+ return fp
+end
+
+"""
+ copy_closure(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E}
+
+Return a new `FroidurePin{E}` that is a copy of `fp` with element `x`
+added as a generator (if not already contained), and re-enumerated.
+"""
+function copy_closure(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_closure(fp.cxx_obj, cxx_x)
+ return FroidurePin{E}(new_cxx)
+end
+
+function copy_closure(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_closure(fp.cxx_obj, cxx_x)
+ return FroidurePin{BMat8}(new_cxx)
+end
+
+"""
+ copy_add_generators(fp::FroidurePin{E}, x::E) where E -> FroidurePin{E}
+
+Return a new `FroidurePin{E}` that is a copy of `fp` with element `x`
+added as a generator (without checking containment).
+"""
+function copy_add_generators(fp::FroidurePin{E}, x::E) where {E}
+ cxx_x = _cxx_element(x)
+ new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_add_generators(fp.cxx_obj, cxx_x)
+ return FroidurePin{E}(new_cxx)
+end
+
+function copy_add_generators(fp::FroidurePin{BMat8}, x::BMat8)
+ cxx_x = _cxx_element(x)
+ new_cxx = @wrap_libsemigroups_call LibSemigroups.copy_add_generators(fp.cxx_obj, cxx_x)
+ return FroidurePin{BMat8}(new_cxx)
+end
+
+"""
+ reserve!(fp::FroidurePin, n::Integer) -> FroidurePin
+
+Pre-allocate storage for at least `n` elements. This is a performance
+hint and does not affect correctness.
+
+Returns `fp` for method chaining.
+"""
+function reserve!(fp::FroidurePin, n::Integer)
+ val = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.reserve!(fp.cxx_obj, val)
+ return fp
+end
+
+# ============================================================================
+# Settings
+# ============================================================================
+
+"""
+ batch_size(fp::FroidurePin) -> Int
+
+Return the current value of the batch size.
+
+The batch size is the minimum number of new elements to be found by any
+call to [`run!`](@ref). This is used by, for example,
+[`position`](@ref) so that it is possible to find the position of an
+element after only partially enumerating the semigroup.
+
+The default value of the batch size is `8192`.
+
+# Complexity
+Constant.
+
+# See also
+- [`set_batch_size!`](@ref)
+"""
+batch_size(fp::FroidurePin) = Int(LibSemigroups.batch_size(fp.cxx_obj))
+
+"""
+ set_batch_size!(fp::FroidurePin, n::Integer) -> FroidurePin
+
+Set a new value for the batch size.
+
+The *batch size* is the number of new elements to be found by any call
+to [`run!`](@ref). This is used by, for example,
+[`position`](@ref) so that it is possible to find the position of an
+element after only partially enumerating the semigroup.
+
+The default value of the batch size is `8192`.
+
+Returns `fp` for method chaining.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `n::Integer`: the new value for the batch size.
+
+# Complexity
+Constant.
+
+# See also
+- [`batch_size`](@ref)
+"""
+function set_batch_size!(fp::FroidurePin, n::Integer)
+ LibSemigroups.set_batch_size!(fp.cxx_obj, UInt(n))
+ return fp
+end
+
+# ============================================================================
+# Predicates
+# ============================================================================
+
+"""
+ contains_one(fp::FroidurePin) -> Bool
+
+Check if the multiplicative identity is an element of `fp`.
+
+This function returns `true` if the identity element (as returned by the
+`One` adapter for the element type) is an element of the semigroup
+represented by `fp`.
+
+# Complexity
+At worst ``O(|S| n)`` where ``S`` is the semigroup represented by `fp`
+and ``n`` is the number of generators.
+
+!!! note
+ This function triggers a full enumeration.
+
+# See also
+- [`currently_contains_one`](@ref)
+"""
+contains_one(fp::FroidurePin) = LibSemigroups.contains_one(fp.cxx_obj)
+
+"""
+ is_idempotent(fp::FroidurePin, i::Integer) -> Bool
+
+Check if the element at 1-based position `i` is an idempotent.
+
+This function returns `true` if the element in position `i` is an
+idempotent (i.e., `x * x == x`) and `false` if it is not.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based index of the element.
+
+# Throws
+- `LibsemigroupsError`: if `i` is less than `1` or greater than the
+ size of `fp`.
+
+!!! note
+ This function triggers a full enumeration.
+"""
+function is_idempotent(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt)
+ return @wrap_libsemigroups_call LibSemigroups.is_idempotent(fp.cxx_obj, idx)
+end
+
+# ============================================================================
+# Index queries (FroidurePinBase methods — uint32_t positions)
+# ============================================================================
+
+"""
+ prefix(fp::FroidurePin, i::Integer) -> Int
+
+Return the position of the longest proper prefix.
+
+This function returns the 1-based position of the prefix of the element
+`x` at 1-based position `i`, where the prefix is the element of length
+one less than `x` obtained by removing the last letter of the
+factorisation.
+
+For generators (elements of length 1), the prefix is
+[`UNDEFINED`](@ref).
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based position of the element.
+
+# Throws
+- `LibsemigroupsError`: if `i` is less than `1` or greater than or
+ equal to [`current_size`](@ref).
+
+# Complexity
+Constant.
+
+!!! note
+ This function does not trigger any enumeration.
+"""
+function prefix(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.prefix(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ suffix(fp::FroidurePin, i::Integer) -> Int
+
+Return the position of the longest proper suffix.
+
+This function returns the 1-based position of the suffix of the element
+`x` at 1-based position `i`, where the suffix is the element of length
+one less than `x` obtained by removing the first letter of the
+factorisation.
+
+For generators (elements of length 1), the suffix is
+[`UNDEFINED`](@ref).
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based position of the element.
+
+# Throws
+- `LibsemigroupsError`: if `i` is less than `1` or greater than or
+ equal to [`current_size`](@ref).
+
+# Complexity
+Constant.
+
+!!! note
+ This function does not trigger any enumeration.
+"""
+function suffix(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.suffix(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ first_letter(fp::FroidurePin, i::Integer) -> Int
+
+Return the first letter of the element with specified index.
+
+This function returns the 1-based index of the generator corresponding
+to the first letter of the element in position `i`.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based position of the element.
+
+# Throws
+- `LibsemigroupsError`: if `i` is less than `1` or greater than or
+ equal to [`current_size`](@ref).
+
+# Complexity
+Constant.
+
+!!! note
+ `generator(fp, first_letter(fp, i))` is only equal to `fp[first_letter(fp, i)]`
+ if there are no duplicate generators.
+
+!!! note
+ This function does not trigger any enumeration.
+"""
+function first_letter(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.first_letter(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ final_letter(fp::FroidurePin, i::Integer) -> Int
+
+Return the last letter of the element with specified index.
+
+This function returns the 1-based index of the generator corresponding
+to the final letter of the element in position `i`.
+
+# Arguments
+- `fp::FroidurePin`: the FroidurePin instance.
+- `i::Integer`: the 1-based position of the element.
+
+# Throws
+- `LibsemigroupsError`: if `i` is less than `1` or greater than or
+ equal to [`current_size`](@ref).
+
+# Complexity
+Constant.
+
+!!! note
+ `generator(fp, final_letter(fp, i))` is only equal to `fp[final_letter(fp, i)]`
+ if there are no duplicate generators.
+
+!!! note
+ This function does not trigger any enumeration.
+"""
+function final_letter(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.final_letter(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ fast_product(fp::FroidurePin, i::Integer, j::Integer) -> Int
+
+Return the 1-based position of the product of the elements at 1-based
+positions `i` and `j`.
+
+The semigroup must be fully enumerated before calling this method.
+"""
+function fast_product(fp::FroidurePin, i::Integer, j::Integer)
+ ci = _to_cpp(i, UInt)
+ cj = _to_cpp(j, UInt)
+ raw = @wrap_libsemigroups_call LibSemigroups.fast_product(fp.cxx_obj, ci, cj)
+ return _from_cpp(raw)
+end
+
+"""
+ number_of_rules(fp::FroidurePin) -> Int
+
+Return the total number of rules (relations) in the semigroup.
+
+Triggers full enumeration if not already complete.
+"""
+number_of_rules(fp::FroidurePin) = Int(LibSemigroups.number_of_rules(fp.cxx_obj))
+
+"""
+ current_number_of_rules(fp::FroidurePin) -> Int
+
+Return the number of rules discovered so far (without triggering
+further enumeration).
+"""
+current_number_of_rules(fp::FroidurePin) =
+ Int(LibSemigroups.current_number_of_rules(fp.cxx_obj))
+
+"""
+ number_of_idempotents(fp::FroidurePin) -> Int
+
+Return the total number of idempotent elements in the semigroup.
+
+Triggers full enumeration if not already complete.
+"""
+number_of_idempotents(fp::FroidurePin) =
+ Int(LibSemigroups.number_of_idempotents(fp.cxx_obj))
+
+"""
+ currently_contains_one(fp::FroidurePin) -> Bool
+
+Check whether the identity element has been discovered so far
+(without triggering further enumeration).
+"""
+currently_contains_one(fp::FroidurePin) = LibSemigroups.currently_contains_one(fp.cxx_obj)
+
+"""
+ current_max_word_length(fp::FroidurePin) -> Int
+
+Return the maximum word length of elements enumerated so far
+(without triggering further enumeration).
+"""
+current_max_word_length(fp::FroidurePin) =
+ Int(LibSemigroups.current_max_word_length(fp.cxx_obj))
+
+"""
+ number_of_elements_of_length(fp::FroidurePin, len::Integer) -> Int
+
+Return the number of elements whose minimal factorisation has
+exactly length `len`.
+"""
+function number_of_elements_of_length(fp::FroidurePin, len::Integer)
+ return Int(LibSemigroups.number_of_elements_of_length(fp.cxx_obj, UInt(len)))
+end
+
+"""
+ number_of_elements_of_length(fp::FroidurePin, min::Integer, max::Integer) -> Int
+
+Return the number of elements whose minimal factorisation has
+length in the range `[min, max)`.
+"""
+function number_of_elements_of_length(fp::FroidurePin, min::Integer, max::Integer)
+ return Int(
+ LibSemigroups.number_of_elements_of_length_range(fp.cxx_obj, UInt(min), UInt(max)),
+ )
+end
+
+"""
+ position_of_generator(fp::FroidurePin, i::Integer) -> Int
+
+Return the 1-based position of the `i`-th generator (1-based) in the
+enumerated elements.
+"""
+function position_of_generator(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.position_of_generator(fp.cxx_obj, idx)
+ return _from_cpp(raw)
+end
+
+"""
+ current_length(fp::FroidurePin, i::Integer) -> Int
+
+Return the length of the minimal factorisation of the element at
+1-based position `i` (without triggering further enumeration).
+"""
+function current_length(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_length(fp.cxx_obj, idx)
+ return Int(raw)
+end
+
+"""
+ word_length(fp::FroidurePin, i::Integer) -> Int
+
+Return the length of the minimal factorisation of the element at
+1-based position `i`.
+
+Triggers full enumeration if not already complete.
+
+Named `word_length` to avoid conflict with `Base.length`.
+"""
+function word_length(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.length(fp.cxx_obj, idx)
+ return Int(raw)
+end
+
+"""
+ product_by_reduction(fp::FroidurePin, i::Integer, j::Integer) -> Int
+
+Return the 1-based position of the product of the elements at 1-based
+positions `i` and `j`, computed using the Cayley graph (no full
+enumeration required, but the elements at positions `i` and `j` must
+already be enumerated).
+"""
+function product_by_reduction(fp::FroidurePin, i::Integer, j::Integer)
+ ci = _to_cpp(i, UInt32)
+ cj = _to_cpp(j, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.product_by_reduction(fp.cxx_obj, ci, cj)
+ return _from_cpp(raw)
+end
+
+# ============================================================================
+# Collections — rules, normal forms, idempotents, sorted elements
+# ============================================================================
+
+"""
+ rules(fp::FroidurePin) -> Vector{Pair{Vector{Int}, Vector{Int}}}
+
+Return all defining rules of the semigroup as a vector of `lhs => rhs`
+pairs, where each side is a 1-based generator-index word.
+
+Triggers full enumeration if not already complete.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+rs = rules(S)
+for (lhs, rhs) in rs
+ println(lhs, " => ", rhs)
+end
+```
+"""
+function rules(fp::FroidurePin)
+ lhs_raw = @wrap_libsemigroups_call LibSemigroups.rules_lhs(fp.cxx_obj)
+ rhs_raw = @wrap_libsemigroups_call LibSemigroups.rules_rhs(fp.cxx_obj)
+ n = length(lhs_raw)
+ result = Vector{Pair{Vector{Int},Vector{Int}}}(undef, n)
+ for i = 1:n
+ result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i])
+ end
+ return result
+end
+
+"""
+ current_rules(fp::FroidurePin) -> Vector{Pair{Vector{Int}, Vector{Int}}}
+
+Return all rules discovered so far (without triggering further enumeration)
+as a vector of `lhs => rhs` pairs with 1-based generator indices.
+"""
+function current_rules(fp::FroidurePin)
+ lhs_raw = @wrap_libsemigroups_call LibSemigroups.current_rules_lhs(fp.cxx_obj)
+ rhs_raw = @wrap_libsemigroups_call LibSemigroups.current_rules_rhs(fp.cxx_obj)
+ n = length(lhs_raw)
+ result = Vector{Pair{Vector{Int},Vector{Int}}}(undef, n)
+ for i = 1:n
+ result[i] = _word_from_cpp(lhs_raw[i]) => _word_from_cpp(rhs_raw[i])
+ end
+ return result
+end
+
+"""
+ normal_forms(fp::FroidurePin) -> Vector{Vector{Int}}
+
+Return the normal forms (canonical representatives) for all elements,
+as 1-based generator-index words.
+
+Triggers full enumeration if not already complete.
+"""
+function normal_forms(fp::FroidurePin)
+ raw = @wrap_libsemigroups_call LibSemigroups.normal_forms(fp.cxx_obj)
+ return [_word_from_cpp(w) for w in raw]
+end
+
+"""
+ current_normal_forms(fp::FroidurePin) -> Vector{Vector{Int}}
+
+Return the normal forms discovered so far (without triggering further
+enumeration) as 1-based generator-index words.
+"""
+function current_normal_forms(fp::FroidurePin)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_normal_forms(fp.cxx_obj)
+ return [_word_from_cpp(w) for w in raw]
+end
+
+"""
+ idempotents(fp::FroidurePin{E}) -> Vector{E}
+
+Return all idempotent elements of the semigroup (elements `x` such
+that `x * x == x`).
+
+Triggers full enumeration if not already complete.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+ids = idempotents(S) # [Transf([1, 2, 3])]
+```
+"""
+function idempotents(fp::FroidurePin{E}) where {E}
+ raw = @wrap_libsemigroups_call LibSemigroups.idempotents(fp.cxx_obj)
+ # GC.@preserve raw to keep StdVector alive while iterating;
+ # _copy_cxx_element converts Dereferenced refs to Allocated copies.
+ GC.@preserve raw begin
+ return E[_wrap_element(E, _copy_cxx_element(x)) for x in raw]
+ end
+end
+
+"""
+ sorted_elements(fp::FroidurePin{E}) -> Vector{E}
+
+Return all elements of the semigroup in sorted order.
+
+Triggers full enumeration if not already complete.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+se = sorted_elements(S)
+```
+"""
+function sorted_elements(fp::FroidurePin{E}) where {E}
+ raw = @wrap_libsemigroups_call LibSemigroups.sorted_elements(fp.cxx_obj)
+ # GC.@preserve raw to keep StdVector alive while iterating;
+ # _copy_cxx_element converts Dereferenced refs to Allocated copies.
+ GC.@preserve raw begin
+ return E[_wrap_element(E, _copy_cxx_element(x)) for x in raw]
+ end
+end
+
+# ============================================================================
+# Factorisations
+# ============================================================================
+
+"""
+ minimal_factorisation(fp::FroidurePin, i::Integer) -> Vector{Int}
+
+Return the minimal factorisation of the element at 1-based position `i`
+as a 1-based generator-index word.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+w = minimal_factorisation(S, 1) # [1] or [2] — a single generator
+```
+"""
+function minimal_factorisation(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.minimal_factorisation(fp.cxx_obj, idx)
+ return _word_from_cpp(raw)
+end
+
+"""
+ current_minimal_factorisation(fp::FroidurePin, i::Integer) -> Vector{Int}
+
+Return the minimal factorisation of the element at 1-based position `i`
+without triggering further enumeration.
+"""
+function current_minimal_factorisation(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.current_minimal_factorisation(
+ fp.cxx_obj,
+ idx,
+ )
+ return _word_from_cpp(raw)
+end
+
+"""
+ factorisation(fp::FroidurePin, i::Integer) -> Vector{Int}
+
+Return the factorisation of the element at 1-based position `i`
+as a 1-based generator-index word.
+
+This may not be the minimal factorisation.
+"""
+function factorisation(fp::FroidurePin, i::Integer)
+ idx = _to_cpp(i, UInt32)
+ raw = @wrap_libsemigroups_call LibSemigroups.factorisation(fp.cxx_obj, idx)
+ return _word_from_cpp(raw)
+end
+
+# ============================================================================
+# Word-position queries
+# ============================================================================
+
+"""
+ position(fp::FroidurePin, w::AbstractVector{<:Integer}) -> Int
+
+Return the 1-based position of the element represented by the 1-based
+generator-index word `w`.
+
+Triggers full enumeration if not already complete.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+position(S, [1]) # position of generator 1
+```
+"""
+function position(fp::FroidurePin, w::AbstractVector{<:Integer})
+ cw = _word_to_cpp(w)
+ raw = @wrap_libsemigroups_call LibSemigroups.position(fp.cxx_obj, cw)
+ return _from_cpp(raw)
+end
+
+# ============================================================================
+# Cayley graphs
+# ============================================================================
+
+"""
+ right_cayley_graph(fp::FroidurePin) -> WordGraph
+
+Return the right Cayley graph of the semigroup.
+
+Triggers full enumeration if not already complete.
+"""
+right_cayley_graph(fp::FroidurePin) = LibSemigroups.right_cayley_graph(fp.cxx_obj)
+
+"""
+ current_right_cayley_graph(fp::FroidurePin) -> WordGraph
+
+Return the right Cayley graph for elements enumerated so far.
+"""
+current_right_cayley_graph(fp::FroidurePin) =
+ LibSemigroups.current_right_cayley_graph(fp.cxx_obj)
+
+"""
+ left_cayley_graph(fp::FroidurePin) -> WordGraph
+
+Return the left Cayley graph of the semigroup.
+
+Triggers full enumeration if not already complete.
+"""
+left_cayley_graph(fp::FroidurePin) = LibSemigroups.left_cayley_graph(fp.cxx_obj)
+
+"""
+ current_left_cayley_graph(fp::FroidurePin) -> WordGraph
+
+Return the left Cayley graph for elements enumerated so far.
+"""
+current_left_cayley_graph(fp::FroidurePin) =
+ LibSemigroups.current_left_cayley_graph(fp.cxx_obj)
+
+# ============================================================================
+# Word-element conversion
+# ============================================================================
+
+"""
+ to_element(fp::FroidurePin{E}, w::AbstractVector{<:Integer}) -> E
+
+Convert a 1-based generator-index word `w` to the corresponding element.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+x = to_element(S, [1, 2]) # product of generators 1 and 2
+```
+"""
+function to_element(fp::FroidurePin{E}, w::AbstractVector{<:Integer}) where {E}
+ cw = _word_to_cpp(w)
+ raw = @wrap_libsemigroups_call LibSemigroups.to_element(fp.cxx_obj, cw)
+ return _wrap_element(E, raw)
+end
+
+"""
+ equal_to(fp::FroidurePin, w1::AbstractVector{<:Integer}, w2::AbstractVector{<:Integer}) -> Bool
+
+Check whether two 1-based generator-index words represent the same element.
+
+# Example
+```julia
+S = FroidurePin(Transf([2, 1, 3]), Transf([2, 3, 1]))
+equal_to(S, [1, 1], [1]) # does gen1*gen1 == gen1?
+```
+"""
+function equal_to(
+ fp::FroidurePin,
+ w1::AbstractVector{<:Integer},
+ w2::AbstractVector{<:Integer},
+)
+ cw1 = _word_to_cpp(w1)
+ cw2 = _word_to_cpp(w2)
+ return @wrap_libsemigroups_call LibSemigroups.equal_to(fp.cxx_obj, cw1, cw2)
+end
diff --git a/src/transf.jl b/src/transf.jl
index 0431c87..8f5c3cd 100644
--- a/src/transf.jl
+++ b/src/transf.jl
@@ -447,6 +447,7 @@ Base.:(<)(t1::Transf, t2::Transf) = LibSemigroups.is_less(t1.cxx_obj, t2.cxx_obj
Base.:(<=)(t1::Transf, t2::Transf) = LibSemigroups.is_less_equal(t1.cxx_obj, t2.cxx_obj)
Base.:(>)(t1::Transf, t2::Transf) = LibSemigroups.is_greater(t1.cxx_obj, t2.cxx_obj)
Base.:(>=)(t1::Transf, t2::Transf) = LibSemigroups.is_greater_equal(t1.cxx_obj, t2.cxx_obj)
+Base.isless(t1::Transf, t2::Transf) = t1 < t2
# Hash
Base.hash(t::Transf, h::UInt) = hash(hash_value(t.cxx_obj), h)
@@ -796,6 +797,7 @@ Base.:(<)(p1::PPerm, p2::PPerm) = LibSemigroups.is_less(p1.cxx_obj, p2.cxx_obj)
Base.:(<=)(p1::PPerm, p2::PPerm) = LibSemigroups.is_less_equal(p1.cxx_obj, p2.cxx_obj)
Base.:(>)(p1::PPerm, p2::PPerm) = LibSemigroups.is_greater(p1.cxx_obj, p2.cxx_obj)
Base.:(>=)(p1::PPerm, p2::PPerm) = LibSemigroups.is_greater_equal(p1.cxx_obj, p2.cxx_obj)
+Base.isless(p1::PPerm, p2::PPerm) = p1 < p2
# Hash
Base.hash(p::PPerm, h::UInt) = hash(hash_value(p.cxx_obj), h)
@@ -1145,6 +1147,7 @@ Base.:(<)(p1::Perm, p2::Perm) = LibSemigroups.is_less(p1.cxx_obj, p2.cxx_obj)
Base.:(<=)(p1::Perm, p2::Perm) = LibSemigroups.is_less_equal(p1.cxx_obj, p2.cxx_obj)
Base.:(>)(p1::Perm, p2::Perm) = LibSemigroups.is_greater(p1.cxx_obj, p2.cxx_obj)
Base.:(>=)(p1::Perm, p2::Perm) = LibSemigroups.is_greater_equal(p1.cxx_obj, p2.cxx_obj)
+Base.isless(p1::Perm, p2::Perm) = p1 < p2
# Hash
Base.hash(p::Perm, h::UInt) = hash(hash_value(p.cxx_obj), h)
diff --git a/src/word-graph.jl b/src/word-graph.jl
index 8ac2c76..91e8dcc 100644
--- a/src/word-graph.jl
+++ b/src/word-graph.jl
@@ -100,7 +100,7 @@ Constant.
See also [`out_degree`](@ref), [`add_nodes!`](@ref).
"""
-number_of_nodes(g::WordGraph) = Int(LibSemigroups.number_of_nodes(g))
+@cxxdereference number_of_nodes(g::WordGraph) = Int(LibSemigroups.number_of_nodes(g))
"""
out_degree(g::WordGraph) -> Int
@@ -117,7 +117,7 @@ Constant.
See also [`number_of_nodes`](@ref), [`target`](@ref).
"""
-out_degree(g::WordGraph) = Int(LibSemigroups.out_degree(g))
+@cxxdereference out_degree(g::WordGraph) = Int(LibSemigroups.out_degree(g))
"""
target(g::WordGraph, source::Integer, label::Integer) -> Union{Int, UndefinedType}
@@ -142,7 +142,7 @@ Constant.
See also [`target!`](@ref), [`is_undefined`](@ref Semigroups.is_undefined).
"""
-function target(g::WordGraph, source::Integer, label::Integer)
+@cxxdereference function target(g::WordGraph, source::Integer, label::Integer)
# Do _to_cpp conversion outside @wrap_libsemigroups_call so its
# InexactError (from zero / negative inputs) propagates as-is rather than
# being re-wrapped as LibsemigroupsError.
@@ -241,6 +241,6 @@ end
# Display
# ============================================================================
-function Base.show(io::IO, g::WordGraph)
+@cxxdereference function Base.show(io::IO, g::WordGraph)
print(io, "WordGraph($(number_of_nodes(g)), $(out_degree(g)))")
end
diff --git a/test/runtests.jl b/test/runtests.jl
index 9682068..214bd89 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -22,4 +22,5 @@ using Semigroups
include("test_presentation.jl")
include("test_presentation_examples.jl")
include("test_word_range.jl")
+ include("test_froidure_pin_transf.jl")
end
diff --git a/test/test_bmat8.jl b/test/test_bmat8.jl
index 4d2718c..78bd8e6 100644
--- a/test/test_bmat8.jl
+++ b/test/test_bmat8.jl
@@ -111,10 +111,12 @@ using Semigroups
@testset "random" begin
- @test minimum_dim(random(BMat8, 4)) == 4
+ # random(BMat8, 4) has nonzero entries only in the top-left 4×4 block,
+ # so minimum_dim is at most 4 (can be less if random rows/cols are zero)
+ @test minimum_dim(random(BMat8, 4)) <= 4
x = BMat8([[1, 0, 1], [0, 1, 0], [0, 0, 0]])
- @test minimum_dim(random(x, 4)) == 4
+ @test minimum_dim(random(x, 4)) <= 4
@test_throws LibsemigroupsError random(x, 0)
@test_throws LibsemigroupsError random(BMat8, 0)
diff --git a/test/test_froidure_pin_transf.jl b/test/test_froidure_pin_transf.jl
new file mode 100644
index 0000000..e761c92
--- /dev/null
+++ b/test/test_froidure_pin_transf.jl
@@ -0,0 +1,1479 @@
+# Copyright (c) 2026, James W. Swent
+#
+# Distributed under the terms of the GPL license version 3.
+#
+# The full license is in the file LICENSE, distributed with this software.
+
+"""
+test_froidure_pin_transf.jl - FroidurePin> correctness tests
+
+Ported from libsemigroups/tests/test-froidure-pin-transf.cpp.
+All indices are 1-based (Julia convention).
+"""
+
+# ============================================================================
+# Helper functions (mirroring C++ test helpers)
+# ============================================================================
+
+"""
+Test that element at position `pos` in `S` is idempotent:
+ 1. is_idempotent(S, pos) returns true
+ 2. x * x == x
+ 3. fast_product(S, pos, pos) == pos
+"""
+function test_idempotent(S, x)
+ pos = Semigroups.position(S, x)
+ @test is_idempotent(S, pos)
+ @test x * x == x
+ @test fast_product(S, pos, pos) == pos
+end
+
+"""
+Test that current_rules are consistent: for each rule (lhs, rhs),
+the current_position of both sides should be equal, and the count
+should match current_number_of_rules.
+"""
+function test_current_rules_iterator(S)
+ nr = 0
+ for (lhs, rhs) in current_rules(S)
+ @test current_position(S, lhs) == current_position(S, rhs)
+ nr += 1
+ end
+ @test nr == current_number_of_rules(S)
+end
+
+ReportGuard(false) do
+
+ @testset "FroidurePin correctness" begin
+
+ # -----------------------------------------------------------------------
+ # Test 042: "JDM favourite" [standard]
+ # Large semigroup: 597369 elements, 8 generators of degree 8
+ # -----------------------------------------------------------------------
+ @testset "042: JDM favourite [standard]" begin
+ S = FroidurePin(
+ Transf([2, 8, 3, 7, 1, 5, 2, 6]),
+ Transf([3, 5, 7, 2, 5, 6, 3, 8]),
+ Transf([4, 1, 8, 3, 5, 7, 3, 5]),
+ Transf([4, 3, 4, 5, 6, 4, 1, 2]),
+ Transf([5, 4, 8, 8, 5, 6, 1, 5]),
+ Transf([6, 7, 4, 1, 4, 1, 6, 2]),
+ Transf([7, 1, 2, 2, 2, 7, 4, 5]),
+ Transf([8, 8, 5, 1, 7, 5, 2, 8]),
+ )
+ reserve!(S, 597369)
+
+ @test length(S) == 597369
+ @test number_of_idempotents(S) == 8194
+
+ # Position of every element matches iteration order
+ for (i, x) in Base.enumerate(S)
+ @test Semigroups.position(S, x) == i
+ end
+
+ # add_generators increases size
+ push!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6]))
+ @test length(S) == 826713
+
+ # closure with already-present generator doesn't change size
+ closure!(S, Transf([8, 2, 3, 7, 8, 5, 2, 6]))
+ @test length(S) == 826713
+
+ # minimal_factorisation (1-based: C++ position 10 → Julia 11)
+ @test minimal_factorisation(S, 11) == [1, 3]
+ @test S[11] == Transf([1, 5, 8, 3, 4, 5, 1, 7])
+ @test_throws LibsemigroupsError minimal_factorisation(S, 1000000001)
+
+ # Every idempotent satisfies x * x == x
+ @test all(x * x == x for x in idempotents(S))
+ @test length(idempotents(S)) == number_of_idempotents(S)
+
+ # Sorted elements are strictly increasing
+ @test issorted(sorted_elements(S))
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 043: "no exception zero generators given" [quick]
+ # -----------------------------------------------------------------------
+ @testset "043: no exception zero generators" begin
+ # The C++ library accepts empty generator sets; the Julia wrapper
+ # currently requires at least one generator, so we test that
+ # constraint.
+ @test_throws ErrorException FroidurePin(Transf{UInt16}[])
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 044: "exception generators of different degrees" [quick]
+ # -----------------------------------------------------------------------
+ @testset "044: exception generators of different degrees" begin
+ S = FroidurePin(Transf([3, 5, 7, 2, 5, 6, 3, 8, 4])) # degree 9
+ @test_throws LibsemigroupsError push!(S, Transf([2, 8, 3, 7, 1, 1, 2, 3])) # degree 8
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 045: "exception current_position" [quick]
+ # -----------------------------------------------------------------------
+ @testset "045: exception current_position" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # Empty word → identity → position 1 (1-based)
+ @test current_position(U, Int[]) == 1
+ # Valid word, no throw (may return UNDEFINED if not yet enumerated)
+ @test_nowarn current_position(U, [1, 1, 2, 3])
+ # Generator index 6 out of range for 5 generators
+ @test_throws LibsemigroupsError current_position(U, [6])
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 046: "exception to_element" [quick]
+ # -----------------------------------------------------------------------
+ @testset "046: exception to_element" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # Empty word → identity element == generator(1)
+ @test to_element(U, Int[]) == generator(U, 1)
+ # Out-of-range generator index
+ @test_throws LibsemigroupsError to_element(U, [6])
+
+ # to_element([1,1,2,3]) == gen1 * gen1 * gen2 * gen3
+ gen1 = Transf([1, 2, 3, 4, 5, 6])
+ gen2 = Transf([2, 1, 3, 4, 5, 6])
+ gen3 = Transf([5, 1, 2, 3, 4, 6])
+ u = to_element(U, [1, 1, 2, 3])
+ @test u == gen1 * gen1 * gen2 * gen3
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 047: "exception gens" [quick]
+ # -----------------------------------------------------------------------
+ @testset "047: exception gens" begin
+ for i = 1:19
+ # Build cyclic shift transformations of degree i (1-based)
+ gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i]
+ S = FroidurePin(gens)
+ for j = 1:i
+ @test generator(S, j) isa Transf
+ end
+ @test_throws Exception generator(S, i + 1)
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 048: "exception prefix" [quick]
+ # -----------------------------------------------------------------------
+ @testset "048: exception prefix" begin
+ U = FroidurePin(
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:length(U)
+ # prefix returns UNDEFINED for generators, valid position otherwise
+ @test_nowarn prefix(U, i)
+ end
+ @test_throws Exception prefix(U, length(U) + 1)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 049: "exception suffix" [quick]
+ # -----------------------------------------------------------------------
+ @testset "049: exception suffix" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ @test length(U) == 7776
+
+ for i = 1:length(U)
+ # suffix returns UNDEFINED for generators, valid position otherwise
+ @test_nowarn suffix(U, i)
+ end
+ @test_throws Exception suffix(U, length(U) + 1)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 050: "exception first_letter" [quick]
+ # -----------------------------------------------------------------------
+ @testset "050: exception first_letter" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:length(U)
+ @test first_letter(U, i) isa Integer
+ @test_throws Exception first_letter(U, i + length(U))
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 051: "exception final_letter" [quick]
+ # -----------------------------------------------------------------------
+ @testset "051: exception final_letter" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:length(U)
+ @test final_letter(U, i) isa Integer
+ @test_throws Exception final_letter(U, i + length(U))
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 052: "exception current_length" [quick]
+ # -----------------------------------------------------------------------
+ @testset "052: exception current_length" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:length(U)
+ @test current_length(U, i) isa Integer
+ @test_throws Exception current_length(U, i + length(U))
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 053: "exception product_by_reduction" [quick]
+ # -----------------------------------------------------------------------
+ @testset "053: exception product_by_reduction" begin
+ U = FroidurePin(Transf([1, 2, 3, 4]), Transf([4, 2, 2, 3]))
+
+ n = length(U)
+ for i = 1:n
+ for j = 1:n
+ @test product_by_reduction(U, i, j) isa Integer
+ @test_throws Exception product_by_reduction(U, i + n, j)
+ @test_throws Exception product_by_reduction(U, i, j + n)
+ @test_throws Exception product_by_reduction(U, i + n, j + n)
+ end
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 054: "exception fast_product" [quick]
+ # -----------------------------------------------------------------------
+ @testset "054: exception fast_product" begin
+ U = FroidurePin(Transf([1, 2, 3, 4]), Transf([4, 2, 2, 3]))
+
+ n = length(U)
+ for i = 1:n
+ for j = 1:n
+ @test fast_product(U, i, j) isa Integer
+ @test_throws Exception fast_product(U, i + n, j)
+ @test_throws Exception fast_product(U, i, j + n)
+ @test_throws Exception fast_product(U, i + n, j + n)
+ end
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 055: "exception current_position" [quick]
+ # (Tests position_of_generator bounds)
+ # -----------------------------------------------------------------------
+ @testset "055: exception position_of_generator" begin
+ for i = 1:19
+ gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i]
+ S = FroidurePin(gens)
+ for j = 1:i
+ @test position_of_generator(S, j) isa Integer
+ end
+ @test_throws Exception position_of_generator(S, i + 1)
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 056: "exception is_idempotent" [quick]
+ # -----------------------------------------------------------------------
+ @testset "056: exception is_idempotent" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([6, 2, 4, 4, 3, 6]),
+ Transf([3, 2, 3, 4, 5, 5]),
+ Transf([6, 6, 3, 2, 2, 3]),
+ )
+
+ @test length(S) == 441
+ for i = 1:441
+ @test is_idempotent(S, i) isa Bool
+ end
+ for i = 0:19
+ @test_throws Exception is_idempotent(S, 442 + i)
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 057: "exception add_generators" [quick]
+ # -----------------------------------------------------------------------
+ @testset "057: exception add_generators" begin
+ T = FroidurePin(
+ Transf([2, 8, 3, 7, 1, 1, 2, 3]),
+ Transf([3, 5, 7, 2, 5, 6, 3, 8]),
+ )
+
+ # Adding generators of same degree succeeds
+ push!(T, Transf([2, 3, 3, 3, 2, 2, 4, 5]))
+ push!(T, Transf([2, 3, 2, 4, 2, 5, 2, 6]))
+ @test number_of_generators(T) == 4
+
+ # Adding generator with wrong degree throws
+ @test_throws LibsemigroupsError push!(T, Transf([2, 3, 2, 4, 2, 5, 2, 6, 2]))
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 058: "number_of_idempotents" [quick]
+ # -----------------------------------------------------------------------
+ @testset "058: number_of_idempotents" begin
+ S = FroidurePin(
+ Transf([2, 8, 3, 7, 1, 1, 2, 3]),
+ Transf([3, 5, 7, 2, 5, 6, 3, 8]),
+ )
+ @test number_of_idempotents(S) == 72
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 059: "small semigroup" [quick]
+ # -----------------------------------------------------------------------
+ @testset "059: small semigroup" begin
+ S = FroidurePin(Transf([1, 2, 1]), Transf([1, 2, 3]))
+
+ @test length(S) == 2
+ @test degree(S) == 3
+ @test number_of_idempotents(S) == 2
+ @test number_of_generators(S) == 2
+ @test number_of_rules(S) == 4
+
+ @test S[1] == Transf([1, 2, 1])
+ @test S[2] == Transf([1, 2, 3])
+
+ @test Semigroups.position(S, Transf([1, 2, 1])) == 1
+ @test Transf([1, 2, 1]) in S
+
+ @test Semigroups.position(S, Transf([1, 2, 3])) == 2
+ @test Transf([1, 2, 3]) in S
+
+ @test Semigroups.position(S, Transf([1, 1, 1])) == UNDEFINED
+ @test !(Transf([1, 1, 1]) in S)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 060: "large semigroup" [quick]
+ # -----------------------------------------------------------------------
+ @testset "060: large semigroup" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ @test length(S) == 7776
+ @test degree(S) == 6
+ @test number_of_idempotents(S) == 537
+ @test number_of_generators(S) == 5
+ @test number_of_rules(S) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 061: "at, position, current_*" [quick]
+ # NOTE: Julia's getindex calls length() which triggers full enumeration,
+ # so we test partial enumeration via enumerate! and verify element values
+ # after full enumeration.
+ # -----------------------------------------------------------------------
+ @testset "061: at, position, current_*" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ set_batch_size!(S, 1024)
+
+ # Test partial enumeration via enumerate!
+ enumerate!(S, 1029)
+ @test current_size(S) == 1029
+ @test current_number_of_rules(S) == 74
+ @test current_max_word_length(S) == 7
+
+ # Element value checks (after full enum via getindex)
+ @test S[101] == Transf([6, 4, 5, 2, 3, 6])
+ @test Semigroups.position(S, Transf([6, 4, 5, 2, 3, 6])) == 101
+
+ @test S[1024] == Transf([6, 5, 4, 5, 2, 6])
+ @test Semigroups.position(S, Transf([6, 5, 4, 5, 2, 6])) == 1024
+
+ @test S[3001] == Transf([6, 4, 6, 4, 5, 6])
+ @test Semigroups.position(S, Transf([6, 4, 6, 4, 5, 6])) == 3001
+
+ @test length(S) == 7776
+ @test degree(S) == 6
+ @test number_of_idempotents(S) == 537
+ @test number_of_generators(S) == 5
+ @test number_of_rules(S) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 062: "run" [quick]
+ # -----------------------------------------------------------------------
+ @testset "062: run (incremental enumerate)" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ set_batch_size!(S, 1024)
+
+ enumerate!(S, 3000)
+ @test current_size(S) == 3000
+ @test current_number_of_rules(S) == 526
+ @test current_max_word_length(S) == 9
+
+ enumerate!(S, 3001)
+ @test current_size(S) == 4024
+ @test current_number_of_rules(S) == 999
+ @test current_max_word_length(S) == 10
+
+ enumerate!(S, 7000)
+ @test current_size(S) == 7000
+ @test current_number_of_rules(S) == 2044
+ @test current_max_word_length(S) == 12
+
+ @test length(S) == 7776
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 063: "run [many stops and starts]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "063: run many stops and starts" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ set_batch_size!(S, 128)
+
+ i = 1
+ while !finished(S)
+ enumerate!(S, i * 128)
+ i += 1
+ end
+
+ @test length(S) == 7776
+ @test number_of_idempotents(S) == 537
+ @test number_of_generators(S) == 5
+ @test number_of_rules(S) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 064: "factorisation, length [1 element]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "064: factorisation, length [1 element]" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ set_batch_size!(S, 1024)
+
+ # C++ position 5537 → Julia 5538; C++ word {1,2,2,2,3,2,4,1,2,2,3} → Julia [2,3,3,3,4,3,5,2,3,3,4]
+ w = factorisation(S, 5538)
+ @test w == [2, 3, 3, 3, 4, 3, 5, 2, 3, 3, 4]
+ @test current_length(S, 5538) == 11
+ @test word_length(S, 5538) == 11
+
+ @test current_size(S) == 5539
+ @test current_number_of_rules(S) == 1484
+
+ @test word_length(S, 7776) == 16
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 065: "factorisation, products [all elements]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "065: factorisation roundtrip [all elements]" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:length(S)
+ w = factorisation(S, i)
+ @test current_position(S, w) == i
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 066: "first/final letter, prefix, suffix, products" [quick]
+ # -----------------------------------------------------------------------
+ @testset "066: first/final letter, prefix, suffix" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ run!(S)
+
+ # Element 6378 (C++ 6377, 1-based)
+ @test first_letter(S, 6378) == 3
+ @test prefix(S, 6378) == 5050
+ @test final_letter(S, 6378) == 3
+ @test suffix(S, 6378) == 5150
+ @test fast_product(S, prefix(S, 6378), final_letter(S, 6378)) == 6378
+ @test fast_product(S, first_letter(S, 6378), suffix(S, 6378)) == 6378
+ @test product_by_reduction(S, prefix(S, 6378), final_letter(S, 6378)) == 6378
+ @test product_by_reduction(S, first_letter(S, 6378), suffix(S, 6378)) == 6378
+
+ # Element 2104 (C++ 2103)
+ @test first_letter(S, 2104) == 4
+ @test prefix(S, 2104) == 1051
+ @test final_letter(S, 2104) == 2
+ @test suffix(S, 2104) == 861
+ @test fast_product(S, prefix(S, 2104), final_letter(S, 2104)) == 2104
+ @test fast_product(S, first_letter(S, 2104), suffix(S, 2104)) == 2104
+
+ # Element 3408 (C++ 3407)
+ @test first_letter(S, 3408) == 3
+ @test prefix(S, 3408) == 1924
+ @test final_letter(S, 3408) == 4
+ @test suffix(S, 3408) == 2116
+
+ # Element 4246 (C++ 4245)
+ @test first_letter(S, 4246) == 3
+ @test prefix(S, 4246) == 2768
+ @test final_letter(S, 4246) == 4
+ @test suffix(S, 4246) == 2320
+
+ # Element 3684 (C++ 3683)
+ @test first_letter(S, 3684) == 5
+ @test prefix(S, 3684) == 2247
+ @test final_letter(S, 3684) == 3
+ @test suffix(S, 3684) == 1686
+
+ # Element 1 (C++ 0): generator — prefix/suffix are UNDEFINED
+ @test first_letter(S, 1) == 1
+ @test prefix(S, 1) == UNDEFINED
+ @test final_letter(S, 1) == 1
+ @test suffix(S, 1) == UNDEFINED
+
+ # Element 7776 (C++ 7775): last element
+ @test first_letter(S, 7776) == 2
+ @test prefix(S, 7776) == 7761
+ @test final_letter(S, 7776) == 3
+ @test suffix(S, 7776) == 7769
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 067: "current_position [standard]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "067: position_of_generator" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ for i = 1:5
+ @test position_of_generator(S, i) == i
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 068: "current_position [duplicate gens]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "068: duplicate generators" begin
+ # Build semigroup with many duplicate generators using push!
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # gen 1: identity
+ push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 2: (1,2)
+ for _ = 1:10
+ push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 3-12: dups
+ end
+ push!(S, Transf([6, 2, 3, 4, 5, 6])) # gen 13: distinct
+ for _ = 1:8
+ push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 14-21: dups
+ end
+ push!(S, Transf([5, 1, 2, 3, 4, 6])) # gen 22: distinct
+ for _ = 1:9
+ push!(S, Transf([2, 1, 3, 4, 5, 6])) # gen 23-31: dups
+ end
+ push!(S, Transf([2, 2, 3, 4, 5, 6])) # gen 32: distinct
+
+ # Duplicates map to same element position
+ @test position_of_generator(S, 1) == 1 # identity
+ @test position_of_generator(S, 2) == 2 # (1,2)
+ @test position_of_generator(S, 3) == 2 # dup → same position
+ @test position_of_generator(S, 11) == 2 # dup → same position
+ @test number_of_generators(S) == 32
+ @test length(S) == 7776
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 069: "current_position [after add_generators]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "069: incremental add_generators" begin
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # identity
+ @test length(S) == 1
+ @test number_of_rules(S) == 1
+
+ push!(S, Transf([2, 1, 3, 4, 5, 6]))
+ @test length(S) == 2
+ @test number_of_rules(S) == 4
+
+ push!(S, Transf([5, 1, 2, 3, 4, 6]))
+ @test length(S) == 120
+ @test number_of_rules(S) == 25
+
+ push!(S, Transf([6, 2, 3, 4, 5, 6]))
+ @test length(S) == 1546
+ @test number_of_rules(S) == 495
+
+ push!(S, Transf([2, 2, 3, 4, 5, 6]))
+ @test length(S) == 7776
+ @test number_of_rules(S) == 2459
+
+ # Generator positions shift as elements are added (1-based)
+ @test position_of_generator(S, 1) == 1
+ @test position_of_generator(S, 2) == 2
+ @test position_of_generator(S, 3) == 3
+ @test position_of_generator(S, 4) == 121 # added when size was 120
+ @test position_of_generator(S, 5) == 1547 # added when size was 1546
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 070: "cbegin_idempotents/cend" [quick]
+ # -----------------------------------------------------------------------
+ @testset "070: idempotent iteration with test_idempotent" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ run!(S)
+
+ nr = 0
+ for x in idempotents(S)
+ test_idempotent(S, x)
+ nr += 1
+ end
+ @test nr == number_of_idempotents(S)
+ end
+
+ # Test 071: same as 070, skipped (functionally identical)
+
+ # -----------------------------------------------------------------------
+ # Test 072: "is_idempotent" [quick]
+ # -----------------------------------------------------------------------
+ @testset "072: is_idempotent counting" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ nr = count(i -> is_idempotent(S, i), 1:length(S))
+ @test nr == number_of_idempotents(S)
+ @test nr == 537
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 073: "cbegin_idempotents/cend, is_idempotent" [standard]
+ # Large degree-7 semigroup with 6322 idempotents
+ # -----------------------------------------------------------------------
+ @testset "073: idempotents [standard, degree 7]" begin
+ S = FroidurePin(
+ Transf([2, 3, 4, 5, 6, 7, 1]), # cyclic shift
+ Transf([2, 1, 3, 4, 5, 6, 7]), # (1,2)
+ Transf([1, 2, 3, 4, 5, 6, 1]), # collapse 7→1
+ )
+
+ ids = idempotents(S)
+ @test length(ids) == number_of_idempotents(S)
+ @test length(ids) == 6322
+
+ for x in ids
+ test_idempotent(S, x)
+ end
+
+ # Second pass gives same count (repeatability)
+ ids2 = idempotents(S)
+ @test length(ids2) == 6322
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 074: "finished, started" [quick]
+ # -----------------------------------------------------------------------
+ @testset "074: finished, started" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ @test !started(S)
+ @test !finished(S)
+
+ set_batch_size!(S, 1024)
+ enumerate!(S, 10)
+ @test started(S)
+ @test !finished(S)
+
+ enumerate!(S, 8000)
+ @test started(S)
+ @test finished(S)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 075: "current_position" [quick]
+ # -----------------------------------------------------------------------
+ @testset "075: current_position (element)" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # Generators have sequential positions
+ for i = 1:5
+ @test current_position(S, generator(S, i)) == i
+ end
+
+ set_batch_size!(S, 1024)
+ enumerate!(S, 1024)
+ @test current_size(S) == 1029
+
+ # Element within enumerated range
+ @test current_position(S, Transf([6, 2, 6, 6, 3, 6])) == 1029
+
+ # Wrong degree → UNDEFINED
+ @test current_position(S, Transf([6, 2, 6, 6, 3, 6, 7])) == UNDEFINED
+
+ # Not yet enumerated → UNDEFINED from current_position
+ @test current_position(S, Transf([6, 5, 6, 2, 1, 6])) == UNDEFINED
+
+ # But full position() finds it (triggers enumeration)
+ @test Semigroups.position(S, Transf([6, 5, 6, 2, 1, 6])) == 1030
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 076: "sorted_position, sorted_at" [quick]
+ # -----------------------------------------------------------------------
+ @testset "076: sorted_position, sorted_at" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # Generator sorted positions (C++ 0-based → Julia 1-based: +1)
+ @test sorted_position(S, generator(S, 1)) == 311
+ @test sorted_at(S, 311) == generator(S, 1)
+
+ @test sorted_position(S, generator(S, 2)) == 1391
+ @test sorted_at(S, 1391) == generator(S, 2)
+
+ @test sorted_position(S, generator(S, 3)) == 5236
+ @test sorted_at(S, 5236) == generator(S, 3)
+
+ @test sorted_position(S, generator(S, 4)) == 6791
+ @test sorted_at(S, 6791) == generator(S, 4)
+
+ @test sorted_position(S, generator(S, 5)) == 1607
+ @test sorted_at(S, 1607) == generator(S, 5)
+
+ @test finished(S)
+
+ # Position-to-sorted conversion (C++ 1024 → Julia 1025)
+ @test to_sorted_position(S, 1025) == 6811
+ @test sorted_at(S, 6811) == S[1025]
+
+ @test sorted_position(S, Transf([6, 2, 6, 6, 3, 6])) == 6909
+ @test sorted_at(S, 6909) == Transf([6, 2, 6, 6, 3, 6])
+
+ # Wrong degree → UNDEFINED
+ @test sorted_position(S, Transf([6, 6, 6, 2, 6, 6, 7])) == UNDEFINED
+
+ # Out of bounds
+ @test_throws Exception sorted_at(S, 100001)
+ @test_throws BoundsError S[100001]
+ @test to_sorted_position(S, 100001) == UNDEFINED
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 077: "right/left Cayley graph" [quick]
+ # -----------------------------------------------------------------------
+ @testset "077: Cayley graph consistency" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ run!(S)
+
+ rcg = right_cayley_graph(S)
+ lcg = left_cayley_graph(S)
+
+ # Identity self-loops
+ @test target(rcg, 1, 1) == 1
+ @test target(lcg, 1, 1) == 1
+
+ # Cayley graph consistency: for every element x and generator g,
+ # position(x * g) == right_cayley_graph target(pos(x), gen_idx)
+ # position(g * x) == left_cayley_graph target(pos(x), gen_idx)
+ for i = 1:length(S)
+ x = S[i]
+ for g = 1:5
+ @test Semigroups.position(S, x * generator(S, g)) == target(rcg, i, g)
+ @test Semigroups.position(S, generator(S, g) * x) == target(lcg, i, g)
+ end
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 078: "iterator" [quick]
+ # NOTE: Julia's iterate triggers full enumeration via length(),
+ # so pre-enumeration iteration is not directly testable.
+ # We verify that iteration covers all elements and containment.
+ # -----------------------------------------------------------------------
+ @testset "078: iterator" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ elts = collect(S)
+ @test length(elts) == 7776
+ # All elements are contained
+ for x in elts
+ @test x in S
+ end
+ end
+
+ # Test 079-080, 082: C++ iterator arithmetic — skipped (Julia iteration is sequential)
+
+ # -----------------------------------------------------------------------
+ # Test 081: "iterator sorted" [quick]
+ # -----------------------------------------------------------------------
+ @testset "081: sorted iteration consistency" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ se = sorted_elements(S)
+ @test finished(S)
+ @test length(se) == length(S)
+
+ for (i, x) in Base.enumerate(se)
+ @test sorted_position(S, x) == i
+ @test to_sorted_position(S, Semigroups.position(S, x)) == i
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 083: "copy [not enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "083: copy" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ T = copy(S)
+ @test number_of_generators(T) == 5
+ @test degree(T) == 6
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ @test number_of_rules(T) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 084: "copy_closure [not enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "084: copy_closure" begin
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 1, 3, 4, 5, 6]))
+
+ # copy_closure adds 3 more generators
+ T = copy_closure(S, Transf([5, 1, 2, 3, 4, 6]))
+ T = copy_closure(T, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ @test number_of_rules(T) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 085: "copy_add_generators [not enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "085: copy_add_generators" begin
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 1, 3, 4, 5, 6]))
+
+ T = copy_add_generators(S, Transf([5, 1, 2, 3, 4, 6]))
+ T = copy_add_generators(T, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ @test number_of_rules(T) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 086: "copy [partly enumerated]" [quick]
+ # NOTE: Julia copy() reconstructs from generators (starts fresh).
+ # We verify it produces the same semigroup.
+ # -----------------------------------------------------------------------
+ @testset "086: copy produces same semigroup" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ T = copy(S)
+ @test number_of_generators(T) == 5
+ @test degree(T) == 6
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ @test number_of_rules(T) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 087: "copy_closure [partly enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "087: copy_closure from partly enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ set_batch_size!(S, 60)
+ enumerate!(S, 60)
+ @test current_size(S) == 63
+
+ T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test generator(T, 4) == Transf([6, 2, 3, 4, 5, 6])
+ @test generator(T, 5) == Transf([2, 2, 3, 4, 5, 6])
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 088: "copy_add_generators [partly enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "088: copy_add_generators from partly enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ set_batch_size!(S, 60)
+ enumerate!(S, 60)
+
+ T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 089: "copy [fully enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "089: copy from fully enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ run!(S)
+ @test finished(S)
+
+ T = copy(S)
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ @test number_of_rules(T) == 2459
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 090: "copy_closure [fully enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "090: copy_closure from fully enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ run!(S)
+ @test finished(S)
+ @test length(S) == 120 # S₅
+
+ T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 091: "copy_add_generators [fully enumerated]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "091: copy_add_generators from fully enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ run!(S)
+ @test length(S) == 120
+
+ T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test number_of_generators(T) == 5
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 092: "rules [duplicate gens]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "092: rules with duplicate generators" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]), # gen 1: identity
+ Transf([1, 2, 3, 4, 5, 6]), # gen 2: dup of gen 1
+ Transf([2, 1, 3, 4, 5, 6]), # gen 3: (1,2)
+ Transf([2, 1, 3, 4, 5, 6]), # gen 4: dup of gen 3
+ Transf([5, 1, 2, 3, 4, 6]), # gen 5
+ )
+ run!(S)
+
+ rs = rules(S)
+ # First rule: gen 2 == gen 1 (duplicate)
+ @test rs[1] == ([2] => [1])
+ # Second rule: gen 4 == gen 3 (duplicate)
+ @test rs[2] == ([4] => [3])
+ # Total rules count is consistent
+ @test number_of_rules(S) == length(rs)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 093: "rules" [quick]
+ # -----------------------------------------------------------------------
+ @testset "093: rules before/during/after enumeration" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # No current rules before enumeration
+ @test length(current_rules(S)) == 0
+
+ # Partial enumeration to discover rules
+ set_batch_size!(S, 10)
+ enumerate!(S, 10)
+ @test !finished(S)
+
+ # First two rules (1-based generators): identity^2=identity, identity*gen2=gen2
+ cr = current_rules(S)
+ @test length(cr) >= 2
+ @test cr[1] == ([1, 1] => [1])
+ @test cr[2] == ([1, 2] => [2])
+ test_current_rules_iterator(S)
+
+ run!(S)
+ @test finished(S)
+ @test number_of_rules(S) == 2459
+
+ # Rules still consistent after full enumeration
+ test_current_rules_iterator(S)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 094: "rules [copy_closure, duplicate gens]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "094: rules copy_closure with duplicate gens" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([1, 2, 3, 4, 5, 6]), # dup
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]), # dup
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ run!(S)
+ @test length(S) == 120
+ @test number_of_rules(S) == 33
+
+ T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([1, 2, 3, 4, 5, 6])) # dup identity
+ T = copy_closure(T, Transf([2, 1, 3, 4, 5, 6])) # dup (1,2)
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test length(T) == 7776
+ @test number_of_idempotents(T) == 537
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 095: "rules [copy_add_generators, duplicate gens]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "095: rules copy_add_generators with duplicate gens" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ run!(S)
+
+ T = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_add_generators(T, Transf([2, 2, 3, 4, 5, 6]))
+
+ @test length(T) == 7776
+ end
+
+ # -----------------------------------------------------------------------
+ # Tests 096-098: rules from copy at various enumeration states
+ # -----------------------------------------------------------------------
+ @testset "096-098: rules from copy" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+
+ # Copy before enumeration
+ T = copy(S)
+ run!(T)
+ @test finished(T)
+ test_current_rules_iterator(T)
+ @test number_of_rules(T) == 2459
+
+ # Copy after full enumeration
+ run!(S)
+ T2 = copy(S)
+ run!(T2)
+ @test number_of_rules(T2) == number_of_rules(S)
+ test_current_rules_iterator(T2)
+ end
+
+ # -----------------------------------------------------------------------
+ # Tests 099-102: rules from copy_closure / copy_add_generators
+ # -----------------------------------------------------------------------
+ @testset "099-102: rules from copy_closure/add_generators" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+
+ # copy_closure
+ T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+ test_current_rules_iterator(T)
+ @test length(T) == 7776
+ @test number_of_rules(T) == 2459
+
+ # copy_add_generators
+ T2 = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6]))
+ T2 = copy_add_generators(T2, Transf([2, 2, 3, 4, 5, 6]))
+ @test number_of_rules(T2) == 2459
+ test_current_rules_iterator(T2)
+ end
+
+ # -----------------------------------------------------------------------
+ # Tests 103-104: rules from copy_closure/add_generators [fully enum]
+ # -----------------------------------------------------------------------
+ @testset "103-104: rules from copy ops fully enumerated" begin
+ S = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ )
+ run!(S)
+ @test finished(S)
+
+ T = copy_closure(S, Transf([6, 2, 3, 4, 5, 6]))
+ T = copy_closure(T, Transf([2, 2, 3, 4, 5, 6]))
+ test_current_rules_iterator(T)
+ @test number_of_rules(T) == 2459
+
+ T2 = copy_add_generators(S, Transf([6, 2, 3, 4, 5, 6]))
+ T2 = copy_add_generators(T2, Transf([2, 2, 3, 4, 5, 6]))
+ @test number_of_rules(T2) == 2459
+ test_current_rules_iterator(T2)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 105: "add_generators [duplicate generators]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "105: add_generators with duplicates" begin
+ S = FroidurePin(
+ Transf([1, 2, 1, 4, 5, 6]),
+ Transf([1, 2, 1, 4, 5, 6]), # duplicate
+ )
+ @test length(S) == 1
+ @test number_of_generators(S) == 2
+
+ # Add duplicate again
+ push!(S, generator(S, 1))
+ @test length(S) == 1
+ @test number_of_generators(S) == 3
+
+ # Add identity
+ push!(S, Transf([1, 2, 3, 4, 5, 6]))
+ @test length(S) == 2
+
+ # Incremental growth
+ push!(S, Transf([1, 2, 4, 6, 6, 5]))
+ @test length(S) == 7
+
+ push!(S, Transf([2, 1, 3, 5, 5, 6]))
+ @test length(S) == 18
+
+ push!(S, Transf([5, 4, 4, 2, 1, 6]))
+ @test length(S) == 87
+
+ push!(S, Transf([5, 4, 6, 2, 1, 6]))
+ @test length(S) == 97
+
+ push!(S, Transf([6, 6, 3, 4, 5, 1]))
+ @test length(S) == 119
+
+ # Add product of existing elements (already in S)
+ push!(S, Transf([2, 1, 3, 5, 5, 6]) * Transf([5, 4, 4, 2, 1, 6]))
+ @test length(S) == 119
+ @test number_of_generators(S) == 10
+
+ # position_of_generator tracks positions
+ @test position_of_generator(S, 1) == 1
+ @test position_of_generator(S, 2) == 1 # dup
+ @test position_of_generator(S, 3) == 1 # dup
+ @test position_of_generator(S, 4) == 2 # identity
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 108: "closure [duplicate generators]" [quick]
+ # -----------------------------------------------------------------------
+ @testset "108: closure with duplicates" begin
+ S = FroidurePin(
+ Transf([1, 2, 1, 4, 5, 6]),
+ Transf([1, 2, 1, 4, 5, 6]), # duplicate
+ )
+ @test length(S) == 1
+
+ # Closure with existing element: no new generator added
+ closure!(S, generator(S, 1))
+ @test length(S) == 1
+ @test number_of_generators(S) == 2 # unchanged
+
+ # Closure with new elements
+ closure!(S, Transf([1, 2, 3, 4, 5, 6]))
+ @test length(S) == 2
+ @test number_of_generators(S) == 3
+
+ closure!(S, Transf([1, 2, 4, 6, 6, 5]))
+ @test length(S) == 7
+
+ closure!(S, Transf([2, 1, 3, 5, 5, 6]))
+ @test length(S) == 18
+
+ closure!(S, Transf([5, 4, 4, 2, 1, 6]))
+ @test length(S) == 87
+
+ closure!(S, Transf([5, 4, 6, 2, 1, 6]))
+ @test length(S) == 97
+
+ closure!(S, Transf([6, 6, 3, 4, 5, 1]))
+ @test length(S) == 119
+ @test number_of_generators(S) == 8 # fewer than add_generators (skips dups)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 109: "closure" [quick] — all T₃ elements
+ # -----------------------------------------------------------------------
+ @testset "109: closure with all T₃" begin
+ # All 27 transformations of degree 3 (1-based images)
+ all_t3 = [Transf([a, b, c]) for a = 1:3 for b = 1:3 for c = 1:3]
+ @test length(all_t3) == 27
+
+ S = FroidurePin(all_t3[1:1])
+ for t in all_t3
+ closure!(S, t)
+ end
+ @test length(S) == 27
+ # closure selects a minimal generating set — fewer than 27
+ @test number_of_generators(S) <= 27
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 110: "factorisation" [quick]
+ # -----------------------------------------------------------------------
+ @testset "110: factorisation" begin
+ S = FroidurePin(Transf([2, 2, 5, 6, 5, 6]), Transf([3, 4, 3, 4, 6, 6]))
+ @test factorisation(S, 3) == [1, 2] # 3rd element = gen1 * gen2
+ end
+
+ # Tests 111, 114: large semigroup with/without reserve — covered by Test 042
+
+ # -----------------------------------------------------------------------
+ # Test 112: "minimal_factorisation" [quick]
+ # -----------------------------------------------------------------------
+ @testset "112: minimal_factorisation exceptions" begin
+ S = FroidurePin(Transf([2, 2, 5, 6, 5, 6]))
+
+ @test minimal_factorisation(S, 1) == [1]
+ @test_throws LibsemigroupsError minimal_factorisation(S, 10000001)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 113: "batch_size" [quick]
+ # -----------------------------------------------------------------------
+ @testset "113: batch_size large value" begin
+ S = FroidurePin(Transf([2, 2, 5, 6, 5, 6]), Transf([3, 4, 3, 4, 6, 6]))
+ run!(S)
+ @test length(S) == 5
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 115: "exception: generators of different degrees" [quick]
+ # NOTE: The 2-arg C++ constructor doesn't validate degree at
+ # construction. Degree mismatch is caught by add_generator! instead.
+ # This test verifies the add_generator! path (already covered by 044).
+ # -----------------------------------------------------------------------
+ @testset "115: mixed degree add_generator" begin
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6])) # degree 6
+ @test_throws LibsemigroupsError push!(S, Transf([1, 2, 3, 4, 5, 6, 6])) # degree 7
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 116: "exception: current_position" [quick] (near-duplicate of 045)
+ # -----------------------------------------------------------------------
+ @testset "116: current_position word exceptions" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ @test current_position(U, Int[]) == 1
+ @test_nowarn current_position(U, [1, 1, 2, 3])
+ @test_throws LibsemigroupsError current_position(U, [6])
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 117: "exception: to_element" [quick] (near-duplicate of 046)
+ # -----------------------------------------------------------------------
+ @testset "117: to_element exceptions" begin
+ U = FroidurePin(
+ Transf([1, 2, 3, 4, 5, 6]),
+ Transf([2, 1, 3, 4, 5, 6]),
+ Transf([5, 1, 2, 3, 4, 6]),
+ Transf([6, 2, 3, 4, 5, 6]),
+ Transf([2, 2, 3, 4, 5, 6]),
+ )
+ @test to_element(U, Int[]) == generator(U, 1)
+ @test_throws LibsemigroupsError to_element(U, [6])
+ @test to_element(U, [1, 1, 2, 3]) ==
+ generator(U, 1) * generator(U, 1) * generator(U, 2) * generator(U, 3)
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 118: "exception: gens, current_position" [quick]
+ # -----------------------------------------------------------------------
+ @testset "118: generator and position_of_generator bounds" begin
+ for i = 1:19
+ gens = [Transf([mod(k + j - 2, i) + 1 for k = 1:i]) for j = 1:i]
+ S = FroidurePin(gens)
+ for j = 1:i
+ @test generator(S, j) isa Transf
+ @test position_of_generator(S, j) isa Integer
+ end
+ @test_throws Exception generator(S, i + 1)
+ @test_throws Exception position_of_generator(S, i + 1)
+ end
+ end
+
+ # -----------------------------------------------------------------------
+ # Test 119: "exception: add_generators" [quick]
+ # -----------------------------------------------------------------------
+ @testset "119: add_generators degree exception" begin
+ S = FroidurePin(Transf([1, 2, 3, 4, 5, 6]), Transf([2, 3, 4, 3, 3, 4]))
+ push!(S, Transf([1, 2, 3, 4, 4, 4])) # same degree → OK
+ @test_throws LibsemigroupsError push!(S, Transf([1, 2, 3, 4, 4, 4, 4])) # degree 7
+ end
+
+ end # @testset "FroidurePin"
+
+end # ReportGuard(false)