diff --git a/Makefile b/Makefile
index f8eba83..6ceeff8 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,7 @@ docs:
$(JULIA) --project=docs -e 'using Pkg; Pkg.develop(path="."); Pkg.instantiate()'
$(JULIA) --project=docs docs/make.jl
-docs-serve: docs
+docs-serve:
$(JULIA) --project=docs -e 'using Pkg; Pkg.add("LiveServer")'
$(JULIA) --project=docs -e 'using LiveServer; servedocs(; include_dirs = ["src"])'
diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt
index 114859f..6bc4c3a 100644
--- a/deps/src/CMakeLists.txt
+++ b/deps/src/CMakeLists.txt
@@ -52,6 +52,8 @@ add_library(libsemigroups_julia SHARED
transf.cpp
word-graph.cpp
word-range.cpp
+ presentation.cpp
+ presentation-examples.cpp
)
# Include directories
diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp
index e59bb4c..ba5d9d7 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_presentation(mod);
+ define_presentation_examples(mod);
}
} // namespace libsemigroups_julia
diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp
index 4e42d19..b582894 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_presentation(jl::Module& mod);
+ void define_presentation_examples(jl::Module& mod);
} // namespace libsemigroups_julia
diff --git a/deps/src/presentation-examples.cpp b/deps/src/presentation-examples.cpp
new file mode 100644
index 0000000..c8bf1c1
--- /dev/null
+++ b/deps/src/presentation-examples.cpp
@@ -0,0 +1,184 @@
+//
+// Semigroups.jl
+// 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 .
+//
+
+#include "libsemigroups_julia.hpp"
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+namespace libsemigroups_julia {
+
+ void define_presentation_examples(jl::Module& m) {
+ using libsemigroups::Presentation;
+ using libsemigroups::word_type;
+ namespace examples = libsemigroups::presentation::examples;
+
+ m.method("example_symmetric_group",
+ [](size_t n) -> Presentation {
+ return examples::symmetric_group(n);
+ });
+ m.method("example_alternating_group",
+ [](size_t n) -> Presentation {
+ return examples::alternating_group(n);
+ });
+ m.method("example_braid_group", [](size_t n) -> Presentation {
+ return examples::braid_group(n);
+ });
+ m.method("example_not_symmetric_group",
+ [](size_t n) -> Presentation {
+ return examples::not_symmetric_group(n);
+ });
+ m.method("example_full_transformation_monoid",
+ [](size_t n) -> Presentation {
+ return examples::full_transformation_monoid(n);
+ });
+ m.method("example_partial_transformation_monoid",
+ [](size_t n) -> Presentation {
+ return examples::partial_transformation_monoid(n);
+ });
+ m.method("example_symmetric_inverse_monoid",
+ [](size_t n) -> Presentation {
+ return examples::symmetric_inverse_monoid(n);
+ });
+ m.method("example_cyclic_inverse_monoid",
+ [](size_t n) -> Presentation {
+ return examples::cyclic_inverse_monoid(n);
+ });
+ m.method("example_order_preserving_monoid",
+ [](size_t n) -> Presentation {
+ return examples::order_preserving_monoid(n);
+ });
+ m.method("example_order_preserving_cyclic_inverse_monoid",
+ [](size_t n) -> Presentation {
+ return examples::order_preserving_cyclic_inverse_monoid(n);
+ });
+ m.method("example_orientation_preserving_monoid",
+ [](size_t n) -> Presentation {
+ return examples::orientation_preserving_monoid(n);
+ });
+ m.method("example_orientation_preserving_reversing_monoid",
+ [](size_t n) -> Presentation {
+ return examples::orientation_preserving_reversing_monoid(n);
+ });
+ m.method("example_partition_monoid",
+ [](size_t n) -> Presentation {
+ return examples::partition_monoid(n);
+ });
+ m.method("example_partial_brauer_monoid",
+ [](size_t n) -> Presentation {
+ return examples::partial_brauer_monoid(n);
+ });
+ m.method("example_brauer_monoid", [](size_t n) -> Presentation {
+ return examples::brauer_monoid(n);
+ });
+ m.method("example_singular_brauer_monoid",
+ [](size_t n) -> Presentation {
+ return examples::singular_brauer_monoid(n);
+ });
+ m.method("example_temperley_lieb_monoid",
+ [](size_t n) -> Presentation {
+ return examples::temperley_lieb_monoid(n);
+ });
+ m.method("example_motzkin_monoid", [](size_t n) -> Presentation {
+ return examples::motzkin_monoid(n);
+ });
+ m.method("example_partial_isometries_cycle_graph_monoid",
+ [](size_t n) -> Presentation {
+ return examples::partial_isometries_cycle_graph_monoid(n);
+ });
+ m.method("example_uniform_block_bijection_monoid",
+ [](size_t n) -> Presentation {
+ return examples::uniform_block_bijection_monoid(n);
+ });
+ m.method("example_dual_symmetric_inverse_monoid",
+ [](size_t n) -> Presentation {
+ return examples::dual_symmetric_inverse_monoid(n);
+ });
+ m.method("example_stellar_monoid", [](size_t l) -> Presentation {
+ return examples::stellar_monoid(l);
+ });
+ m.method("example_zero_rook_monoid",
+ [](size_t n) -> Presentation {
+ return examples::zero_rook_monoid(n);
+ });
+ m.method("example_abacus_jones_monoid",
+ [](size_t n, size_t d) -> Presentation {
+ return examples::abacus_jones_monoid(n, d);
+ });
+
+ // ---- batch C: plactic / misc ----
+
+ m.method("example_plactic_monoid", [](size_t n) -> Presentation {
+ return examples::plactic_monoid(n);
+ });
+ m.method("example_chinese_monoid", [](size_t n) -> Presentation {
+ return examples::chinese_monoid(n);
+ });
+ m.method("example_hypo_plactic_monoid",
+ [](size_t n) -> Presentation {
+ return examples::hypo_plactic_monoid(n);
+ });
+ m.method("example_stylic_monoid", [](size_t n) -> Presentation {
+ return examples::stylic_monoid(n);
+ });
+ m.method("example_special_linear_group_2",
+ [](size_t q) -> Presentation {
+ return examples::special_linear_group_2(q);
+ });
+ m.method("example_fibonacci_semigroup",
+ [](size_t r, size_t n) -> Presentation {
+ return examples::fibonacci_semigroup(r, n);
+ });
+ m.method("example_monogenic_semigroup",
+ [](size_t m, size_t r) -> Presentation {
+ return examples::monogenic_semigroup(m, r);
+ });
+ m.method("example_rectangular_band",
+ [](size_t m, size_t n) -> Presentation {
+ return examples::rectangular_band(m, n);
+ });
+ m.method("example_sigma_plactic_monoid",
+ [](jlcxx::ArrayRef sigma) -> Presentation {
+ return examples::sigma_plactic_monoid(
+ std::vector(sigma.begin(), sigma.end()));
+ });
+ m.method("example_renner_type_B_monoid",
+ [](size_t l, int q) -> Presentation {
+ return examples::renner_type_B_monoid(l, q);
+ });
+ m.method("example_renner_type_D_monoid",
+ [](size_t l, int q) -> Presentation {
+ return examples::renner_type_D_monoid(l, q);
+ });
+ m.method("example_not_renner_type_B_monoid",
+ [](size_t l, int q) -> Presentation {
+ return examples::not_renner_type_B_monoid(l, q);
+ });
+ m.method("example_not_renner_type_D_monoid",
+ [](size_t l, int q) -> Presentation {
+ return examples::not_renner_type_D_monoid(l, q);
+ });
+ }
+
+} // namespace libsemigroups_julia
diff --git a/deps/src/presentation.cpp b/deps/src/presentation.cpp
new file mode 100644
index 0000000..71ac3fb
--- /dev/null
+++ b/deps/src/presentation.cpp
@@ -0,0 +1,373 @@
+//
+// Semigroups.jl
+// 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 .
+//
+
+#include "libsemigroups_julia.hpp"
+
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace jlcxx {
+ template <>
+ struct IsMirroredType>
+ : std::false_type {};
+
+ template <>
+ struct IsMirroredType<
+ libsemigroups::InversePresentation>
+ : std::false_type {};
+
+ template <>
+ struct SuperType<
+ libsemigroups::InversePresentation> {
+ using type = libsemigroups::Presentation;
+ };
+} // namespace jlcxx
+
+namespace libsemigroups_julia {
+
+ void define_presentation(jl::Module& m) {
+ using libsemigroups::Presentation;
+ using libsemigroups::word_type;
+
+ auto type = m.add_type>("Presentation");
+ type.constructor<>();
+ type.constructor const&>(); // copy ctor
+
+ type.method("init!", [](Presentation& self) { self.init(); });
+
+ type.method("alphabet",
+ [](Presentation const& self) -> word_type {
+ return self.alphabet();
+ });
+
+ type.method(
+ "set_alphabet_size!",
+ [](Presentation& self, size_t n) { self.alphabet(n); });
+
+ type.method("set_alphabet!",
+ [](Presentation& self, jlcxx::ArrayRef a) {
+ self.alphabet(word_type(a.begin(), a.end()));
+ });
+
+ type.method("alphabet_from_rules!", [](Presentation& self) {
+ self.alphabet_from_rules();
+ });
+
+ type.method("letter",
+ [](Presentation const& self, size_t i) -> size_t {
+ return self.letter(i);
+ });
+
+ type.method("index_of",
+ [](Presentation const& self, size_t x) -> size_t {
+ return self.index(x);
+ });
+
+ type.method("in_alphabet",
+ [](Presentation const& self, size_t x) -> bool {
+ return self.in_alphabet(x);
+ });
+
+ type.method("contains_empty_word",
+ [](Presentation const& self) -> bool {
+ return self.contains_empty_word();
+ });
+ type.method("set_contains_empty_word!",
+ [](Presentation& self, bool v) {
+ self.contains_empty_word(v);
+ });
+
+ type.method("add_generator_no_arg!",
+ [](Presentation& self) -> size_t {
+ return self.add_generator();
+ });
+ type.method("add_generator!", [](Presentation& self, size_t x) {
+ self.add_generator(x);
+ });
+ type.method("remove_generator!",
+ [](Presentation& self, size_t x) {
+ self.remove_generator(x);
+ });
+
+ m.method("add_rule_no_checks!",
+ [](Presentation& p,
+ jlcxx::ArrayRef lhs,
+ jlcxx::ArrayRef rhs) {
+ word_type l(lhs.begin(), lhs.end());
+ word_type r(rhs.begin(), rhs.end());
+ libsemigroups::presentation::add_rule_no_checks(p, l, r);
+ });
+
+ m.method("add_rule!",
+ [](Presentation& p,
+ jlcxx::ArrayRef lhs,
+ jlcxx::ArrayRef rhs) {
+ word_type l(lhs.begin(), lhs.end());
+ word_type r(rhs.begin(), rhs.end());
+ libsemigroups::presentation::add_rule(p, l, r);
+ });
+
+ type.method("number_of_rules",
+ [](Presentation const& self) -> size_t {
+ return self.rules.size() / 2;
+ });
+
+ type.method("rule_lhs",
+ [](Presentation const& self, size_t i) -> word_type {
+ return self.rules.at(2 * i);
+ });
+
+ type.method("rule_rhs",
+ [](Presentation const& self, size_t i) -> word_type {
+ return self.rules.at(2 * i + 1);
+ });
+
+ type.method("clear_rules!",
+ [](Presentation& self) { self.rules.clear(); });
+
+ type.method("throw_if_alphabet_has_duplicates",
+ [](Presentation const& self) {
+ self.throw_if_alphabet_has_duplicates();
+ });
+ type.method("throw_if_letter_not_in_alphabet",
+ [](Presentation const& self, size_t x) {
+ self.throw_if_letter_not_in_alphabet(x);
+ });
+ type.method("throw_if_bad_rules", [](Presentation const& self) {
+ self.throw_if_bad_rules();
+ });
+ type.method("throw_if_bad_alphabet_or_rules",
+ [](Presentation const& self) {
+ self.throw_if_bad_alphabet_or_rules();
+ });
+
+ m.method("length_of", [](Presentation const& p) -> size_t {
+ return libsemigroups::presentation::length(p);
+ });
+ m.method("longest_rule_length",
+ [](Presentation const& p) -> size_t {
+ return libsemigroups::presentation::longest_rule_length(p);
+ });
+ m.method("shortest_rule_length",
+ [](Presentation const& p) -> size_t {
+ return libsemigroups::presentation::shortest_rule_length(p);
+ });
+ m.method("is_normalized", [](Presentation const& p) -> bool {
+ return libsemigroups::presentation::is_normalized(p);
+ });
+ m.method("are_rules_sorted", [](Presentation const& p) -> bool {
+ return libsemigroups::presentation::are_rules_sorted(p);
+ });
+ m.method("contains_rule",
+ [](Presentation& p,
+ jlcxx::ArrayRef lhs,
+ jlcxx::ArrayRef rhs) -> bool {
+ word_type l(lhs.begin(), lhs.end());
+ word_type r(rhs.begin(), rhs.end());
+ return libsemigroups::presentation::contains_rule(p, l, r);
+ });
+ m.method("throw_if_odd_number_of_rules",
+ [](Presentation const& p) {
+ // Fully qualified: the iterator-pair overload is also in scope.
+ libsemigroups::presentation::throw_if_odd_number_of_rules(p);
+ });
+
+ m.method("normalize_alphabet!", [](Presentation& p) {
+ libsemigroups::presentation::normalize_alphabet(p);
+ });
+ m.method("change_alphabet!",
+ [](Presentation& p, jlcxx::ArrayRef a) {
+ libsemigroups::presentation::change_alphabet(
+ p, word_type(a.begin(), a.end()));
+ });
+ m.method("reverse_rules!", [](Presentation& p) {
+ libsemigroups::presentation::reverse(p);
+ });
+ m.method("sort_rules!", [](Presentation& p) {
+ libsemigroups::presentation::sort_rules(p);
+ });
+ m.method("sort_each_rule!", [](Presentation& p) -> bool {
+ return libsemigroups::presentation::sort_each_rule(p);
+ });
+
+ m.method("add_identity_rules!", [](Presentation& p, size_t e) {
+ libsemigroups::presentation::add_identity_rules(p, e);
+ });
+ m.method("add_zero_rules!", [](Presentation& p, size_t z) {
+ libsemigroups::presentation::add_zero_rules(p, z);
+ });
+ m.method("remove_duplicate_rules!", [](Presentation& p) {
+ libsemigroups::presentation::remove_duplicate_rules(p);
+ });
+ m.method("remove_trivial_rules!", [](Presentation& p) {
+ libsemigroups::presentation::remove_trivial_rules(p);
+ });
+
+ m.method("add_rules!",
+ [](Presentation& p, Presentation const& q) {
+ libsemigroups::presentation::add_rules(p, q);
+ });
+
+ m.method("add_inverse_rules!",
+ [](Presentation& p, jlcxx::ArrayRef inverses) {
+ word_type v(inverses.begin(), inverses.end());
+ libsemigroups::presentation::add_inverse_rules(p, v);
+ });
+
+ m.method("add_inverse_rules_with_identity!",
+ [](Presentation& p,
+ jlcxx::ArrayRef inverses,
+ size_t e) {
+ word_type v(inverses.begin(), inverses.end());
+ libsemigroups::presentation::add_inverse_rules(p, v, e);
+ });
+
+ m.method("replace_subword!",
+ [](Presentation& p,
+ jlcxx::ArrayRef existing,
+ jlcxx::ArrayRef replacement) {
+ word_type e(existing.begin(), existing.end());
+ word_type r(replacement.begin(), replacement.end());
+ libsemigroups::presentation::replace_subword(p, e, r);
+ });
+
+ m.method("replace_word!",
+ [](Presentation& p,
+ jlcxx::ArrayRef existing,
+ jlcxx::ArrayRef replacement) {
+ word_type e(existing.begin(), existing.end());
+ word_type r(replacement.begin(), replacement.end());
+ libsemigroups::presentation::replace_word(p, e, r);
+ });
+
+ m.method(
+ "replace_word_with_new_generator!",
+ [](Presentation& p, jlcxx::ArrayRef w) -> size_t {
+ word_type v(w.begin(), w.end());
+ return libsemigroups::presentation::replace_word_with_new_generator(
+ p, v);
+ });
+
+ m.method("first_unused_letter",
+ [](Presentation const& p) -> size_t {
+ return libsemigroups::presentation::first_unused_letter(p);
+ });
+
+ m.method("index_rule",
+ [](Presentation const& p,
+ jlcxx::ArrayRef lhs,
+ jlcxx::ArrayRef rhs) -> size_t {
+ word_type l(lhs.begin(), lhs.end());
+ word_type r(rhs.begin(), rhs.end());
+ return libsemigroups::presentation::index_rule(p, l, r);
+ });
+
+ m.method("is_rule",
+ [](Presentation const& p,
+ jlcxx::ArrayRef lhs,
+ jlcxx::ArrayRef rhs) -> bool {
+ word_type l(lhs.begin(), lhs.end());
+ word_type r(rhs.begin(), rhs.end());
+ return libsemigroups::presentation::is_rule(p, l, r);
+ });
+
+ m.method("longest_rule_index",
+ [](Presentation const& p) -> size_t {
+ auto it = libsemigroups::presentation::longest_rule(p);
+ return static_cast(std::distance(p.rules.cbegin(), it));
+ });
+
+ m.method("shortest_rule_index",
+ [](Presentation const& p) -> size_t {
+ auto it = libsemigroups::presentation::shortest_rule(p);
+ return static_cast(std::distance(p.rules.cbegin(), it));
+ });
+
+ m.method(
+ "throw_if_bad_inverses",
+ [](Presentation const& p, jlcxx::ArrayRef inverses) {
+ word_type v(inverses.begin(), inverses.end());
+ libsemigroups::presentation::throw_if_bad_inverses(p, v);
+ });
+
+ m.method("to_gap_string",
+ [](Presentation const& p,
+ std::string const& var_name) -> std::string {
+ return libsemigroups::presentation::to_gap_string(p, var_name);
+ });
+
+ m.method("rules_vector",
+ [](Presentation const& p) -> std::vector {
+ return std::vector(p.rules.cbegin(), p.rules.cend());
+ });
+
+ type.method(
+ "is_equal",
+ [](Presentation const& a,
+ Presentation const& b) -> bool { return a == b; });
+ type.method("to_human_readable_repr",
+ [](Presentation const& p) -> std::string {
+ return libsemigroups::to_human_readable_repr(p);
+ });
+
+ // -----------------------------------------------------------------------
+ // InversePresentation
+ // -----------------------------------------------------------------------
+ using libsemigroups::InversePresentation;
+
+ auto itype = m.add_type>(
+ "InversePresentation",
+ jlcxx::julia_base_type>());
+
+ itype.constructor const&>();
+ itype.constructor const&>(); // copy ctor
+
+ itype.method(
+ "set_inverses!",
+ [](InversePresentation& self, jlcxx::ArrayRef w) {
+ self.inverses(word_type(w.begin(), w.end()));
+ });
+ itype.method("inverses",
+ [](InversePresentation const& self) -> word_type {
+ return self.inverses();
+ });
+ itype.method("inverse_of",
+ [](InversePresentation const& self,
+ size_t x) -> size_t { return self.inverse(x); });
+ itype.method("throw_if_bad_alphabet_rules_or_inverses",
+ [](InversePresentation const& self) {
+ self.throw_if_bad_alphabet_rules_or_inverses();
+ });
+ itype.method(
+ "is_equal_inv",
+ [](InversePresentation const& a,
+ InversePresentation const& b) -> bool { return a == b; });
+ itype.method("to_human_readable_repr",
+ [](InversePresentation const& self) -> std::string {
+ return libsemigroups::to_human_readable_repr(self);
+ });
+ }
+
+} // namespace libsemigroups_julia
diff --git a/docs/make.jl b/docs/make.jl
index 704677b..d20887f 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -39,6 +39,13 @@ makedocs(;
"Orders" => "data-structures/order.md",
"Words" => "data-structures/word-range.md",
"Word Graphs" => "data-structures/word-graph.md",
+ "Presentations" => [
+ "Overview" => "data-structures/presentations/index.md",
+ "Presentation" => "data-structures/presentations/presentation.md",
+ "InversePresentation" => "data-structures/presentations/inverse-presentation.md",
+ "Helper functions" => "data-structures/presentations/helpers.md",
+ "Examples" => "data-structures/presentations/examples.md",
+ ],
"Elements" => [
"Overview" => "data-structures/elements/index.md",
"Transformations" => [
diff --git a/docs/src/data-structures/elements/transformations/index.md b/docs/src/data-structures/elements/transformations/index.md
index b861e40..8168f45 100644
--- a/docs/src/data-structures/elements/transformations/index.md
+++ b/docs/src/data-structures/elements/transformations/index.md
@@ -52,7 +52,7 @@ All transformation types use **1-based indexing** (the standard Julia
convention). Internally, indices are converted to 0-based for the underlying
C++ library ([libsemigroups](https://libsemigroups.github.io/libsemigroups/)).
-## Full API — shared functions
+## Full API - shared functions
```@docs
Base.copy(::Semigroups.Transf)
diff --git a/docs/src/data-structures/elements/transformations/perm.md b/docs/src/data-structures/elements/transformations/perm.md
index 6cf978b..c0b9de5 100644
--- a/docs/src/data-structures/elements/transformations/perm.md
+++ b/docs/src/data-structures/elements/transformations/perm.md
@@ -61,7 +61,7 @@ Permutations can be composed using `*`:
```julia
p = Perm([2, 3, 1])
q = Perm([3, 1, 2])
-r = p * q # Perm([1, 2, 3]) — the identity
+r = p * q # Perm([1, 2, 3]) - the identity
```
### Comparison
diff --git a/docs/src/data-structures/presentations/examples.md b/docs/src/data-structures/presentations/examples.md
new file mode 100644
index 0000000..0882e48
--- /dev/null
+++ b/docs/src/data-structures/presentations/examples.md
@@ -0,0 +1,132 @@
+# Example presentations
+
+This page documents the catalogue of standard presentations exposed via
+`libsemigroups::presentation::examples`. Each function returns a fresh
+[`Presentation`](@ref Semigroups.Presentation) with the alphabet and rules
+initialised to a well-known construction.
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `Presentation` only. Alphabets and
+ rules use `Vector{Int}` with 1-based letter indices.
+
+## Groups and transformation monoids
+
+### Contents
+
+| Function | Description |
+| --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
+| [`symmetric_group`](@ref Semigroups.symmetric_group(::Integer)) | Symmetric group `S_n`. |
+| [`alternating_group`](@ref Semigroups.alternating_group(::Integer)) | Alternating group `A_n`. |
+| [`braid_group`](@ref Semigroups.braid_group(::Integer)) | Braid group `B_n`. |
+| [`not_symmetric_group`](@ref Semigroups.not_symmetric_group(::Integer)) | A presentation that does not define `S_n`. |
+| [`full_transformation_monoid`](@ref Semigroups.full_transformation_monoid(::Integer)) | Full transformation monoid `T_n`. |
+| [`partial_transformation_monoid`](@ref Semigroups.partial_transformation_monoid(::Integer)) | Partial transformation monoid `PT_n`. |
+| [`symmetric_inverse_monoid`](@ref Semigroups.symmetric_inverse_monoid(::Integer)) | Symmetric inverse monoid `I_n`. |
+| [`cyclic_inverse_monoid`](@ref Semigroups.cyclic_inverse_monoid(::Integer)) | Cyclic inverse monoid. |
+| [`order_preserving_monoid`](@ref Semigroups.order_preserving_monoid(::Integer)) | Monoid of order-preserving transformations of `{1,...,n}`. |
+| [`order_preserving_cyclic_inverse_monoid`](@ref Semigroups.order_preserving_cyclic_inverse_monoid(::Integer)) | Order-preserving cyclic inverse monoid. |
+| [`orientation_preserving_monoid`](@ref Semigroups.orientation_preserving_monoid(::Integer)) | Orientation-preserving monoid. |
+| [`orientation_preserving_reversing_monoid`](@ref Semigroups.orientation_preserving_reversing_monoid(::Integer)) | Orientation-preserving-or-reversing monoid. |
+
+### Full API
+
+```@docs
+Semigroups.symmetric_group(::Integer)
+Semigroups.alternating_group(::Integer)
+Semigroups.braid_group(::Integer)
+Semigroups.not_symmetric_group(::Integer)
+Semigroups.full_transformation_monoid(::Integer)
+Semigroups.partial_transformation_monoid(::Integer)
+Semigroups.symmetric_inverse_monoid(::Integer)
+Semigroups.cyclic_inverse_monoid(::Integer)
+Semigroups.order_preserving_monoid(::Integer)
+Semigroups.order_preserving_cyclic_inverse_monoid(::Integer)
+Semigroups.orientation_preserving_monoid(::Integer)
+Semigroups.orientation_preserving_reversing_monoid(::Integer)
+```
+
+## Diagram and partition monoids
+
+### Contents
+
+| Function | Description |
+| ----------------------------------------------------------------------------------------------------------- | -------------------------------------- |
+| [`partition_monoid`](@ref Semigroups.partition_monoid(::Integer)) | Partition monoid `P_n`. |
+| [`partial_brauer_monoid`](@ref Semigroups.partial_brauer_monoid(::Integer)) | Partial Brauer monoid. |
+| [`brauer_monoid`](@ref Semigroups.brauer_monoid(::Integer)) | Brauer monoid. |
+| [`singular_brauer_monoid`](@ref Semigroups.singular_brauer_monoid(::Integer)) | Singular Brauer monoid. |
+| [`temperley_lieb_monoid`](@ref Semigroups.temperley_lieb_monoid(::Integer)) | Temperley-Lieb monoid. |
+| [`motzkin_monoid`](@ref Semigroups.motzkin_monoid(::Integer)) | Motzkin monoid. |
+| [`partial_isometries_cycle_graph_monoid`](@ref Semigroups.partial_isometries_cycle_graph_monoid(::Integer)) | Partial isometries of the cycle graph. |
+| [`uniform_block_bijection_monoid`](@ref Semigroups.uniform_block_bijection_monoid(::Integer)) | Uniform block bijection monoid. |
+| [`dual_symmetric_inverse_monoid`](@ref Semigroups.dual_symmetric_inverse_monoid(::Integer)) | Dual symmetric inverse monoid. |
+| [`stellar_monoid`](@ref Semigroups.stellar_monoid(::Integer)) | Stellar monoid. |
+| [`zero_rook_monoid`](@ref Semigroups.zero_rook_monoid(::Integer)) | 0-rook monoid. |
+| [`abacus_jones_monoid`](@ref Semigroups.abacus_jones_monoid(::Integer, ::Integer)) | Abacus Jones monoid. |
+
+### Full API
+
+```@docs
+Semigroups.partition_monoid(::Integer)
+Semigroups.partial_brauer_monoid(::Integer)
+Semigroups.brauer_monoid(::Integer)
+Semigroups.singular_brauer_monoid(::Integer)
+Semigroups.temperley_lieb_monoid(::Integer)
+Semigroups.motzkin_monoid(::Integer)
+Semigroups.partial_isometries_cycle_graph_monoid(::Integer)
+Semigroups.uniform_block_bijection_monoid(::Integer)
+Semigroups.dual_symmetric_inverse_monoid(::Integer)
+Semigroups.stellar_monoid(::Integer)
+Semigroups.zero_rook_monoid(::Integer)
+Semigroups.abacus_jones_monoid(::Integer, ::Integer)
+```
+
+## Plactic monoids
+
+### Contents
+
+| Function | Description |
+| ------------------------------------------------------------------------------------------- | ---------------------------------- |
+| [`plactic_monoid`](@ref Semigroups.plactic_monoid(::Integer)) | Plactic monoid on `n` letters. |
+| [`chinese_monoid`](@ref Semigroups.chinese_monoid(::Integer)) | Chinese monoid on `n` letters. |
+| [`hypo_plactic_monoid`](@ref Semigroups.hypo_plactic_monoid(::Integer)) | Hypoplactic monoid on `n` letters. |
+| [`sigma_plactic_monoid`](@ref Semigroups.sigma_plactic_monoid(::AbstractVector{<:Integer})) | ``\sigma``-plactic monoid for a given `sigma`. |
+| [`stylic_monoid`](@ref Semigroups.stylic_monoid(::Integer)) | Stylic monoid on `n` letters. |
+
+### Full API
+
+```@docs
+Semigroups.plactic_monoid(::Integer)
+Semigroups.chinese_monoid(::Integer)
+Semigroups.hypo_plactic_monoid(::Integer)
+Semigroups.sigma_plactic_monoid(::AbstractVector{<:Integer})
+Semigroups.stylic_monoid(::Integer)
+```
+
+## Other
+
+### Contents
+
+| Function | Description |
+| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
+| [`fibonacci_semigroup`](@ref Semigroups.fibonacci_semigroup(::Integer, ::Integer)) | Fibonacci semigroup `F(r, n)`. |
+| [`monogenic_semigroup`](@ref Semigroups.monogenic_semigroup(::Integer, ::Integer)) | Monogenic semigroup with index `m`, period `r`. |
+| [`rectangular_band`](@ref Semigroups.rectangular_band(::Integer, ::Integer)) | ``m \times n`` rectangular band. |
+| [`special_linear_group_2`](@ref Semigroups.special_linear_group_2(::Integer)) | `SL(2, q)`. |
+| [`renner_type_B_monoid`](@ref Semigroups.renner_type_B_monoid(::Integer, ::Integer)) | Renner monoid of type B. |
+| [`renner_type_D_monoid`](@ref Semigroups.renner_type_D_monoid(::Integer, ::Integer)) | Renner monoid of type D. |
+| [`not_renner_type_B_monoid`](@ref Semigroups.not_renner_type_B_monoid(::Integer, ::Integer)) | A presentation that does _not_ define the type-B Renner monoid. |
+| [`not_renner_type_D_monoid`](@ref Semigroups.not_renner_type_D_monoid(::Integer, ::Integer)) | A presentation that does _not_ define the type-D Renner monoid. |
+
+### Full API
+
+```@docs
+Semigroups.fibonacci_semigroup(::Integer, ::Integer)
+Semigroups.monogenic_semigroup(::Integer, ::Integer)
+Semigroups.rectangular_band(::Integer, ::Integer)
+Semigroups.special_linear_group_2(::Integer)
+Semigroups.renner_type_B_monoid(::Integer, ::Integer)
+Semigroups.renner_type_D_monoid(::Integer, ::Integer)
+Semigroups.not_renner_type_B_monoid(::Integer, ::Integer)
+Semigroups.not_renner_type_D_monoid(::Integer, ::Integer)
+```
diff --git a/docs/src/data-structures/presentations/helpers.md b/docs/src/data-structures/presentations/helpers.md
new file mode 100644
index 0000000..c03def1
--- /dev/null
+++ b/docs/src/data-structures/presentations/helpers.md
@@ -0,0 +1,135 @@
+# Helper functions
+
+This page collects the free functions that operate on a
+[`Presentation`](@ref Semigroups.Presentation). They mirror the
+`libsemigroups::presentation::*` namespace and are organised into three
+groups: validation, scalar queries, and shape / rule-set mutators.
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds helpers for `Presentation` only.
+ Alphabets and rules use `Vector{Int}` with 1-based letter indices.
+
+## Validation
+
+These functions throw `LibsemigroupsError` if the presentation is
+ill-formed in some way; otherwise they return `nothing`.
+
+### Contents
+
+| Function | Description |
+| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| [`throw_if_alphabet_has_duplicates`](@ref Semigroups.throw_if_alphabet_has_duplicates(::Presentation)) | Throw if the alphabet contains a repeated letter. |
+| [`throw_if_letter_not_in_alphabet`](@ref Semigroups.throw_if_letter_not_in_alphabet(::Presentation, ::Integer)) | Throw if the given letter is not in the alphabet. |
+| [`throw_if_bad_rules`](@ref Semigroups.throw_if_bad_rules(::Presentation)) | Throw if rules refer to letters outside the alphabet or have odd count. |
+| [`throw_if_bad_alphabet_or_rules`](@ref Semigroups.throw_if_bad_alphabet_or_rules(::Presentation)) | Combined alphabet-and-rules check. |
+| [`throw_if_odd_number_of_rules`](@ref Semigroups.throw_if_odd_number_of_rules(::Presentation)) | Throw if the underlying rule-word count is odd. |
+| [`throw_if_bad_inverses`](@ref Semigroups.throw_if_bad_inverses(::Presentation, ::AbstractVector{<:Integer})) | Throw if a proposed inverses vector is not a valid involution. |
+
+### Full API
+
+```@docs
+Semigroups.throw_if_alphabet_has_duplicates(::Presentation)
+Semigroups.throw_if_letter_not_in_alphabet(::Presentation, ::Integer)
+Semigroups.throw_if_bad_rules(::Presentation)
+Semigroups.throw_if_bad_alphabet_or_rules(::Presentation)
+Semigroups.throw_if_odd_number_of_rules(::Presentation)
+Semigroups.throw_if_bad_inverses(::Presentation, ::AbstractVector{<:Integer})
+```
+
+## Queries
+
+Scalar-valued, non-mutating queries.
+
+### Contents
+
+| Function | Description |
+| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [`length_of`](@ref Semigroups.length_of(::Presentation)) | Total length of all rule words. |
+| [`longest_rule_length`](@ref Semigroups.longest_rule_length(::Presentation)) | Length of the longest single rule word. |
+| [`shortest_rule_length`](@ref Semigroups.shortest_rule_length(::Presentation)) | Length of the shortest single rule word. |
+| [`is_normalized`](@ref Semigroups.is_normalized(::Presentation)) | `true` iff the alphabet is `[1, ..., n]`. |
+| [`are_rules_sorted`](@ref Semigroups.are_rules_sorted(::Presentation)) | `true` iff the rule list is sorted. |
+| [`contains_rule`](@ref Semigroups.contains_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Whether a given `lhs = rhs` appears as a rule. |
+| [`is_rule`](@ref Semigroups.is_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Validated-form test for `lhs = rhs`. |
+| [`index_rule`](@ref Semigroups.index_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | 1-based rule-pair index of `lhs = rhs`, or [`UNDEFINED`](@ref Semigroups.UNDEFINED). |
+| [`first_unused_letter`](@ref Semigroups.first_unused_letter(::Presentation)) | Smallest letter not in the alphabet. |
+| [`longest_rule_index`](@ref Semigroups.longest_rule_index(::Presentation)) | 1-based index of the first rule of maximal length. |
+| [`shortest_rule_index`](@ref Semigroups.shortest_rule_index(::Presentation)) | 1-based index of the first rule of minimal length. |
+
+### Full API
+
+```@docs
+Semigroups.length_of(::Presentation)
+Semigroups.longest_rule_length(::Presentation)
+Semigroups.shortest_rule_length(::Presentation)
+Semigroups.is_normalized(::Presentation)
+Semigroups.are_rules_sorted(::Presentation)
+Semigroups.contains_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.is_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.index_rule(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.first_unused_letter(::Presentation)
+Semigroups.longest_rule_index(::Presentation)
+Semigroups.shortest_rule_index(::Presentation)
+```
+
+## Mutators
+
+Shape mutators reorder or rewrite the alphabet / rule words; rule-set
+mutators add or remove rules.
+
+### Shape mutators
+
+| Function | Description |
+| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
+| [`normalize_alphabet!`](@ref Semigroups.normalize_alphabet!(::Presentation)) | Rewrite `p` so the alphabet is `[1, ..., n]`. |
+| [`change_alphabet!`](@ref Semigroups.change_alphabet!(::Presentation, ::AbstractVector{<:Integer})) | Rewrite under `old[i] -> new[i]`. |
+| [`Base.reverse!`](@ref Base.reverse!(::Presentation)) | Reverse each rule word in place (extends `Base.reverse!`; not exported). |
+| [`sort_rules!`](@ref Semigroups.sort_rules!(::Presentation)) | Sort the rule list. |
+| [`sort_each_rule!`](@ref Semigroups.sort_each_rule!(::Presentation)) | Order each rule so that `lhs >= rhs`. |
+
+### Rule-set mutators
+
+| Function | Description |
+| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
+| [`add_identity_rules!`](@ref Semigroups.add_identity_rules!(::Presentation, ::Integer)) | Add ``e \cdot a = a \cdot e = a`` for every letter ``a``. |
+| [`add_zero_rules!`](@ref Semigroups.add_zero_rules!(::Presentation, ::Integer)) | Add ``z \cdot a = a \cdot z = z`` for every letter ``a``. |
+| [`add_rules!`](@ref Semigroups.add_rules!(::Presentation, ::Presentation)) | Copy every rule of `q` into `p`. |
+| [`add_inverse_rules!`](@ref Semigroups.add_inverse_rules!(::Presentation, ::AbstractVector{<:Integer})) | Add rules ``a_i \cdot b_i = e`` for a matching list of inverses. |
+| [`replace_subword!`](@ref Semigroups.replace_subword!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Replace every non-overlapping occurrence of a subword. |
+| [`replace_word!`](@ref Semigroups.replace_word!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Replace a word wherever it appears as a full side of a rule. |
+| [`replace_word_with_new_generator!`](@ref Semigroups.replace_word_with_new_generator!(::Presentation, ::AbstractVector{<:Integer})) | Replace non-overlapping occurrences with a fresh generator. |
+| [`remove_duplicate_rules!`](@ref Semigroups.remove_duplicate_rules!(::Presentation)) | Drop duplicate rules, keeping the first occurrence. |
+| [`remove_trivial_rules!`](@ref Semigroups.remove_trivial_rules!(::Presentation)) | Drop rules of the form `u = u`. |
+
+### Full API
+
+```@docs
+Semigroups.normalize_alphabet!(::Presentation)
+Semigroups.change_alphabet!(::Presentation, ::AbstractVector{<:Integer})
+Base.reverse!(::Presentation)
+Semigroups.sort_rules!(::Presentation)
+Semigroups.sort_each_rule!(::Presentation)
+Semigroups.add_identity_rules!(::Presentation, ::Integer)
+Semigroups.add_zero_rules!(::Presentation, ::Integer)
+Semigroups.add_rules!(::Presentation, ::Presentation)
+Semigroups.add_inverse_rules!(::Presentation, ::AbstractVector{<:Integer})
+Semigroups.replace_subword!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.replace_word!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.replace_word_with_new_generator!(::Presentation, ::AbstractVector{<:Integer})
+Semigroups.remove_duplicate_rules!(::Presentation)
+Semigroups.remove_trivial_rules!(::Presentation)
+```
+
+## Export
+
+Helpers that serialize a presentation to another language's source code.
+
+| Function | Description |
+| ----------------------------------------------------------------------------------- | --------------------------------------------------------------- |
+| [`to_gap_string`](@ref Semigroups.to_gap_string(::Presentation, ::AbstractString)) | GAP source code that would reconstruct the presentation. |
+
+### Full API
+
+```@docs
+Semigroups.to_gap_string(::Presentation, ::AbstractString)
+```
diff --git a/docs/src/data-structures/presentations/index.md b/docs/src/data-structures/presentations/index.md
new file mode 100644
index 0000000..0bdeebc
--- /dev/null
+++ b/docs/src/data-structures/presentations/index.md
@@ -0,0 +1,20 @@
+# Presentations
+
+This section documents the [`Presentation`](@ref Semigroups.Presentation) and
+[`InversePresentation`](@ref Semigroups.InversePresentation) types, the
+namespace of helper free functions that operate on them, and the catalog of
+standard example presentations provided by
+`libsemigroups::presentation::examples`.
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `Presentation` only. Alphabets and
+ rules use `Vector{Int}` with 1-based letter indices.
+
+## Contents
+
+| Page | Description |
+| ---------------------------------------------- | -------------------------------------------------------------- |
+| [Presentation](presentation.md) | The main `Presentation{word_type}` type. |
+| [InversePresentation](inverse-presentation.md) | `InversePresentation{word_type}` with per-generator inverses. |
+| [Helper functions](helpers.md) | Free functions in `presentation::*`. |
+| [Examples](examples.md) | Standard presentations (symmetric group, partition monoid, ...). |
diff --git a/docs/src/data-structures/presentations/inverse-presentation.md b/docs/src/data-structures/presentations/inverse-presentation.md
new file mode 100644
index 0000000..9f0c0c9
--- /dev/null
+++ b/docs/src/data-structures/presentations/inverse-presentation.md
@@ -0,0 +1,32 @@
+# The InversePresentation type
+
+This page documents the type
+[`InversePresentation`](@ref Semigroups.InversePresentation), a
+[`Presentation`](@ref Semigroups.Presentation) equipped with per-generator
+inverses.
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `InversePresentation` only. Alphabets,
+ rules, and inverses use `Vector{Int}` with 1-based letter indices.
+
+```@docs
+Semigroups.InversePresentation
+```
+
+## Contents
+
+| Function | Description |
+| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [`set_inverses!`](@ref Semigroups.set_inverses!(::InversePresentation, ::AbstractVector{<:Integer})) | Set the vector of inverses, one per generator. |
+| [`inverses`](@ref Semigroups.inverses(::InversePresentation)) | Return the inverses as a `Vector{Int}`. |
+| [`inverse_of`](@ref Semigroups.inverse_of(::InversePresentation, ::Integer)) | Return the inverse of a given letter. |
+| [`throw_if_bad_alphabet_rules_or_inverses`](@ref Semigroups.throw_if_bad_alphabet_rules_or_inverses(::InversePresentation)) | Validate the alphabet, rules, and inverses jointly. |
+
+## Full API
+
+```@docs
+Semigroups.set_inverses!(::InversePresentation, ::AbstractVector{<:Integer})
+Semigroups.inverses(::InversePresentation)
+Semigroups.inverse_of(::InversePresentation, ::Integer)
+Semigroups.throw_if_bad_alphabet_rules_or_inverses(::InversePresentation)
+```
diff --git a/docs/src/data-structures/presentations/presentation.md b/docs/src/data-structures/presentations/presentation.md
new file mode 100644
index 0000000..f3915e3
--- /dev/null
+++ b/docs/src/data-structures/presentations/presentation.md
@@ -0,0 +1,74 @@
+# The Presentation type
+
+This page documents the type [`Presentation`](@ref Semigroups.Presentation),
+a finite semigroup or monoid presentation over `word_type` (Julia:
+`Vector{Int}` with 1-based letter indices).
+
+!!! warning "v1 limitation"
+ Semigroups.jl v1 binds `Presentation` only. Alphabets and
+ rules use `Vector{Int}` with 1-based letter indices.
+
+```@docs
+Semigroups.Presentation
+```
+
+## Contents
+
+| Function | Description |
+| -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
+| [`init!`](@ref Semigroups.init!(::Presentation)) | Reset a presentation to the empty state. |
+| [`alphabet`](@ref Semigroups.alphabet(::Presentation)) | Return the alphabet as a `Vector{Int}`. |
+| [`set_alphabet!`](@ref Semigroups.set_alphabet!(::Presentation, ::Integer)) | Set the alphabet to `[1, ..., n]`. |
+| [`set_alphabet!`](@ref Semigroups.set_alphabet!(::Presentation, ::AbstractVector{<:Integer})) | Set the alphabet to the given vector. |
+| [`alphabet_from_rules!`](@ref Semigroups.alphabet_from_rules!(::Presentation)) | Infer the alphabet from the rules. |
+| [`letter`](@ref Semigroups.letter(::Presentation, ::Integer)) | Return the `i`-th letter of the alphabet. |
+| [`index_of`](@ref Semigroups.index_of(::Presentation, ::Integer)) | Return the 1-based index of a letter. |
+| [`in_alphabet`](@ref Semigroups.in_alphabet(::Presentation, ::Integer)) | Test membership in the alphabet. |
+| [`contains_empty_word`](@ref Semigroups.contains_empty_word(::Presentation)) | Query whether the empty word is allowed. |
+| [`set_contains_empty_word!`](@ref Semigroups.set_contains_empty_word!(::Presentation, ::Bool)) | Set whether the empty word is allowed. |
+| [`add_generator!`](@ref Semigroups.add_generator!(::Presentation)) | Append a generator (no-arg or by letter). |
+| [`remove_generator!`](@ref Semigroups.remove_generator!(::Presentation, ::Integer)) | Remove a letter from the alphabet. |
+| [`number_of_rules`](@ref Semigroups.number_of_rules(::Presentation)) | Number of rules in the presentation. |
+| [`rule_lhs`](@ref Semigroups.rule_lhs(::Presentation, ::Integer)) | Left-hand side of the `i`-th rule. |
+| [`rule_rhs`](@ref Semigroups.rule_rhs(::Presentation, ::Integer)) | Right-hand side of the `i`-th rule. |
+| [`rule`](@ref Semigroups.rule(::Presentation, ::Integer)) | `(lhs, rhs)` tuple for the `i`-th rule. |
+| [`rules`](@ref Semigroups.rules(::Presentation)) | All rules as `(lhs, rhs)` tuples. |
+| [`clear_rules!`](@ref Semigroups.clear_rules!(::Presentation)) | Remove every rule. |
+| [`add_rule!`](@ref Semigroups.add_rule!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Append a checked rule. |
+| [`add_rule_no_checks!`](@ref Semigroups.add_rule_no_checks!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})) | Append a rule without checks. |
+
+## Full API
+
+```@docs
+Semigroups.init!(::Presentation)
+Semigroups.alphabet(::Presentation)
+Semigroups.set_alphabet!(::Presentation, ::Integer)
+Semigroups.set_alphabet!(::Presentation, ::AbstractVector{<:Integer})
+Semigroups.alphabet_from_rules!(::Presentation)
+Semigroups.letter(::Presentation, ::Integer)
+Semigroups.index_of(::Presentation, ::Integer)
+Semigroups.in_alphabet(::Presentation, ::Integer)
+Semigroups.contains_empty_word(::Presentation)
+Semigroups.set_contains_empty_word!(::Presentation, ::Bool)
+Semigroups.add_generator!(::Presentation)
+Semigroups.remove_generator!(::Presentation, ::Integer)
+Semigroups.number_of_rules(::Presentation)
+Semigroups.rule_lhs(::Presentation, ::Integer)
+Semigroups.rule_rhs(::Presentation, ::Integer)
+Semigroups.rule(::Presentation, ::Integer)
+Semigroups.rules(::Presentation)
+Semigroups.clear_rules!(::Presentation)
+Semigroups.add_rule!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+Semigroups.add_rule_no_checks!(::Presentation, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})
+```
+
+## `Base` interface
+
+A [`Presentation`](@ref Semigroups.Presentation) extends the standard
+value-semantics predicates from `Base`, making it usable as a dictionary
+key and interoperable with generic Julia code.
+
+```@docs
+Base.isempty(::Presentation)
+Base.hash(::Presentation, ::UInt)
+```
diff --git a/docs/src/data-structures/word-graph.md b/docs/src/data-structures/word-graph.md
index a2e8dca..bda3ba4 100644
--- a/docs/src/data-structures/word-graph.md
+++ b/docs/src/data-structures/word-graph.md
@@ -4,6 +4,10 @@ This page contains the documentation of the type [`WordGraph`](@ref
Semigroups.WordGraph), a representation of a word graph over an alphabet
of fixed out-degree.
+!!! warning "Minimal implementation"
+ Semigroups.jl v1 currently contains a minimal implementation of WordGraph
+ in order to support algorithm implementations.
+
```@docs
Semigroups.WordGraph
```
diff --git a/src/Semigroups.jl b/src/Semigroups.jl
index 6788c47..f69ebba 100644
--- a/src/Semigroups.jl
+++ b/src/Semigroups.jl
@@ -67,6 +67,8 @@ include("runner.jl")
include("order.jl")
include("word-range.jl")
include("word-graph.jl")
+include("presentation.jl")
+include("presentation-examples.jl")
# High-level element types
include("bmat8.jl")
@@ -111,6 +113,47 @@ export next!, at_end, valid, init!, size_hint, upper_bound
# WordGraph
export WordGraph, number_of_nodes, out_degree, target, target!, add_nodes!
+# Presentation
+export Presentation, alphabet, set_alphabet!, alphabet_from_rules!
+export letter, index_of, in_alphabet
+export contains_empty_word, set_contains_empty_word!
+export add_generator!, remove_generator!
+export add_rule!, add_rule_no_checks!, add_rules!
+export number_of_rules, rule, rule_lhs, rule_rhs, rules, clear_rules!
+export throw_if_alphabet_has_duplicates, throw_if_letter_not_in_alphabet
+export throw_if_bad_rules, throw_if_bad_alphabet_or_rules
+export length_of, longest_rule_length, shortest_rule_length
+export longest_rule_index, shortest_rule_index
+export first_unused_letter, index_rule, is_rule
+export is_normalized, are_rules_sorted, contains_rule
+export throw_if_odd_number_of_rules, throw_if_bad_inverses
+export normalize_alphabet!, change_alphabet!, sort_rules!, sort_each_rule!
+export add_identity_rules!, add_zero_rules!, add_inverse_rules!
+export replace_subword!, replace_word!, replace_word_with_new_generator!
+export remove_duplicate_rules!, remove_trivial_rules!
+export to_gap_string
+export InversePresentation, set_inverses!, inverses, inverse_of
+export throw_if_bad_alphabet_rules_or_inverses
+
+# presentation::examples
+export symmetric_group
+export alternating_group, braid_group, not_symmetric_group
+export full_transformation_monoid, partial_transformation_monoid
+export symmetric_inverse_monoid, cyclic_inverse_monoid
+export order_preserving_monoid, order_preserving_cyclic_inverse_monoid
+export orientation_preserving_monoid, orientation_preserving_reversing_monoid
+export partition_monoid, partial_brauer_monoid, brauer_monoid
+export singular_brauer_monoid, temperley_lieb_monoid, motzkin_monoid
+export partial_isometries_cycle_graph_monoid, uniform_block_bijection_monoid
+export dual_symmetric_inverse_monoid, stellar_monoid, zero_rook_monoid
+export abacus_jones_monoid
+export plactic_monoid, chinese_monoid, hypo_plactic_monoid, stylic_monoid
+export special_linear_group_2
+export fibonacci_semigroup, monogenic_semigroup, rectangular_band
+export sigma_plactic_monoid
+export renner_type_B_monoid, renner_type_D_monoid
+export not_renner_type_B_monoid, not_renner_type_D_monoid
+
# Transformation types and functions
export Transf, PPerm, Perm
export degree, rank, image, domain, inverse
diff --git a/src/bmat8.jl b/src/bmat8.jl
index f671e60..25ce908 100644
--- a/src/bmat8.jl
+++ b/src/bmat8.jl
@@ -171,7 +171,7 @@ Base.transpose(x::BMat8)::BMat8 = LibSemigroups.bmat8_transpose(x)
# Semigroups.jl functions
########################################################################
-# TODO document the 0-arg constructor
+# TODO document the 0-arg constructor
"""
BMat8(rows::Vector{Vector{T}})::BMat8 where {T<:Union{Bool,Int64}} -> BMat8
diff --git a/src/constants.jl b/src/constants.jl
index 1cccb6b..ad3fd5a 100644
--- a/src/constants.jl
+++ b/src/constants.jl
@@ -72,7 +72,7 @@ const LIMIT_MAX = LimitMaxType()
# Conversion functions to get the underlying integer values
-# UNDEFINED conversions — libsemigroups uses typemax(T) natively
+# UNDEFINED conversions - libsemigroups uses typemax(T) natively
Base.convert(::Type{T}, ::UndefinedType) where {T<:Integer} = typemax(T)
# POSITIVE_INFINITY conversions
@@ -105,7 +105,7 @@ Base.convert(::Type{Int64}, ::LimitMaxType) = LibSemigroups.LIMIT_MAX_Int64()
# Comparison operations
-# UNDEFINED comparisons — UNDEFINED is a distinct sentinel, never equal to any integer
+# UNDEFINED comparisons - UNDEFINED is a distinct sentinel, never equal to any integer
Base.:(==)(::Integer, ::UndefinedType) = false
Base.:(==)(::UndefinedType, ::Integer) = false
Base.:(==)(::UndefinedType, ::UndefinedType) = true
diff --git a/src/order.jl b/src/order.jl
index 5662bb6..bbb291a 100644
--- a/src/order.jl
+++ b/src/order.jl
@@ -5,7 +5,7 @@
"""
order.jl - Order enum and compare helpers
-Exposes `Order` (an enum selecting a word ordering — shortlex, lex, recursive-
+Exposes `Order` (an enum selecting a word ordering - shortlex, lex, recursive-
path) along with comparator free functions instantiated for Julia
`Vector{Int}` words. The Julia public API uses 1-based letter indices; the
private `_word_to_cpp` helper converts to 0-based at the C++ boundary.
@@ -25,10 +25,10 @@ The values of this enum can be passed as the argument to functions such as
for congruence classes are given with respect to one of these orders.
Values:
-- [`ORDER_NONE`](@ref) — no ordering
-- [`ORDER_SHORTLEX`](@ref) — short-lex: order by length, then lexicographically
-- [`ORDER_LEX`](@ref) — pure lexicographic (not a well-order in general)
-- [`ORDER_RECURSIVE`](@ref) — recursive-path ordering (Jantzen 2012,
+- [`ORDER_NONE`](@ref) - no ordering
+- [`ORDER_SHORTLEX`](@ref) - short-lex: order by length, then lexicographically
+- [`ORDER_LEX`](@ref) - pure lexicographic (not a well-order in general)
+- [`ORDER_RECURSIVE`](@ref) - recursive-path ordering (Jantzen 2012,
Definition 1.2.14, page 24)
"""
const Order = LibSemigroups.Order
@@ -71,7 +71,7 @@ const ORDER_RECURSIVE = LibSemigroups.order_recursive
# ----------------------------------------------------------------------------
# Julia uses 1-based letter indices; libsemigroups stores 0-based in
# `word_type = std::vector` (UInt64 on 64-bit systems). Conversion
-# is applied at the C++ boundary — these helpers are the only place the
+# is applied at the C++ boundary - these helpers are the only place the
# shift happens.
# ============================================================================
diff --git a/src/presentation-examples.jl b/src/presentation-examples.jl
new file mode 100644
index 0000000..0d22ca9
--- /dev/null
+++ b/src/presentation-examples.jl
@@ -0,0 +1,810 @@
+# Copyright (c) 2026, James W. Swent
+#
+# Distributed under the terms of the GPL license version 3.
+
+"""
+presentation-examples.jl - Julia wrappers for `libsemigroups::presentation::examples`
+
+Each binding returns a fresh [`Presentation`](@ref Semigroups.Presentation)
+over `word_type`, exposed in Julia as `Vector{Int}` words with 1-based
+letter indices. These wrappers mirror the default
+`libsemigroups::presentation::examples` constructors.
+
+!!! note "Argument conversion"
+ Most integer parameters are converted to the corresponding C++
+ `size_t` values with `UInt(...)` before the libsemigroups call, and
+ the Renner-family `q` parameters are converted with `Int32(...)`.
+ Negative or otherwise unrepresentable inputs therefore raise
+ `InexactError` in Julia before `libsemigroups` can throw
+ `LibsemigroupsError`.
+
+!!! warning "v1 limitation"
+ v1 of Semigroups.jl binds `Presentation` only. Alphabets and
+ rules use `Vector{Int}` with 1-based letter indices.
+"""
+
+"""
+ symmetric_group(n::Integer) -> Presentation
+
+A presentation for the symmetric group of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+symmetric group of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the symmetric group.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function symmetric_group(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_symmetric_group(m)
+end
+
+"""
+ alternating_group(n::Integer) -> Presentation
+
+A presentation for the alternating group of degree `n`.
+
+This function returns a monoid presentation defining the alternating
+group of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the alternating group.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 4`.
+"""
+function alternating_group(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_alternating_group(m)
+end
+
+"""
+ braid_group(n::Integer) -> Presentation
+
+A presentation for the braid group with `n - 1` generators.
+
+This function returns a monoid presentation defining the braid group with
+`n - 1` generators.
+
+# Arguments
+- `n::Integer`: the degree, or equivalently the number of generators plus `1`.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function braid_group(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_braid_group(m)
+end
+
+"""
+ not_symmetric_group(n::Integer) -> Presentation
+
+A presentation that is not a symmetric group but has the same number of
+generators and relations as the symmetric group of degree `n`.
+
+This function returns a monoid presentation which is claimed to define
+the symmetric group of degree `n`, but does not.
+
+# Arguments
+- `n::Integer`: the claimed degree of the symmetric group.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 4`.
+"""
+function not_symmetric_group(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_not_symmetric_group(m)
+end
+
+"""
+ full_transformation_monoid(n::Integer) -> Presentation
+
+A presentation for the full transformation monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the full
+transformation monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the full transformation monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function full_transformation_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_full_transformation_monoid(m)
+end
+
+"""
+ partial_transformation_monoid(n::Integer) -> Presentation
+
+A presentation for the partial transformation monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+partial transformation monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the partial transformation monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function partial_transformation_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_partial_transformation_monoid(m)
+end
+
+"""
+ symmetric_inverse_monoid(n::Integer) -> Presentation
+
+A presentation for the symmetric inverse monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+symmetric inverse monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the symmetric inverse monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 4`.
+"""
+function symmetric_inverse_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_symmetric_inverse_monoid(m)
+end
+
+"""
+ cyclic_inverse_monoid(n::Integer) -> Presentation
+
+A presentation for the cyclic inverse monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+cyclic inverse monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the cyclic inverse monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function cyclic_inverse_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_cyclic_inverse_monoid(m)
+end
+
+"""
+ order_preserving_monoid(n::Integer) -> Presentation
+
+A presentation for the order-preserving transformation monoid of degree `n`.
+
+This function returns a presentation for the monoid of order-preserving
+transformations of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function order_preserving_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_order_preserving_monoid(m)
+end
+
+"""
+ order_preserving_cyclic_inverse_monoid(n::Integer) -> Presentation
+
+A presentation for the order-preserving part of the cyclic inverse monoid of
+degree `n`.
+
+This function returns a presentation for the order-preserving part of the
+cyclic inverse monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function order_preserving_cyclic_inverse_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_order_preserving_cyclic_inverse_monoid(m)
+end
+
+"""
+ orientation_preserving_monoid(n::Integer) -> Presentation
+
+A presentation for the orientation-preserving transformation monoid of degree
+`n`.
+
+This function returns a presentation for the orientation-preserving
+transformation monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function orientation_preserving_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_orientation_preserving_monoid(m)
+end
+
+"""
+ orientation_preserving_reversing_monoid(n::Integer) -> Presentation
+
+A presentation for the orientation-preserving and -reversing transformation
+monoid of degree `n`.
+
+This function returns a presentation for the orientation-preserving and
+orientation-reversing transformation monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function orientation_preserving_reversing_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_orientation_preserving_reversing_monoid(
+ m,
+ )
+end
+
+"""
+ partition_monoid(n::Integer) -> Presentation
+
+A presentation for the partition monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+partition monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the partition monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 4`.
+"""
+function partition_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_partition_monoid(m)
+end
+
+"""
+ partial_brauer_monoid(n::Integer) -> Presentation
+
+A presentation for the partial Brauer monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+partial Brauer monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the partial Brauer monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function partial_brauer_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_partial_brauer_monoid(m)
+end
+
+"""
+ brauer_monoid(n::Integer) -> Presentation
+
+A presentation for the Brauer monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+Brauer monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the Brauer monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function brauer_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_brauer_monoid(m)
+end
+
+"""
+ singular_brauer_monoid(n::Integer) -> Presentation
+
+A presentation for the singular part of the Brauer monoid of degree `n`.
+
+This function returns a monoid presentation for the singular part of the
+Brauer monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function singular_brauer_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_singular_brauer_monoid(m)
+end
+
+"""
+ temperley_lieb_monoid(n::Integer) -> Presentation
+
+A presentation for the Temperley-Lieb monoid with `n - 1` generators.
+
+This function returns a monoid presentation defining the Temperley-Lieb
+monoid with `n - 1` generators.
+
+# Arguments
+- `n::Integer`: the degree, or equivalently the number of generators plus `1`.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function temperley_lieb_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_temperley_lieb_monoid(m)
+end
+
+"""
+ motzkin_monoid(n::Integer) -> Presentation
+
+A presentation for the Motzkin monoid of degree `n`.
+
+This function returns a monoid presentation defining the Motzkin monoid of
+degree `n`.
+
+# Arguments
+- `n::Integer`: the degree.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function motzkin_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_motzkin_monoid(m)
+end
+
+"""
+ partial_isometries_cycle_graph_monoid(n::Integer) -> Presentation
+
+A presentation for the monoid of partial isometries of an `n`-cycle graph.
+
+This function returns the default libsemigroups presentation of the
+monoid of partial isometries of an `n`-cycle graph.
+
+# Arguments
+- `n::Integer`: the degree of the cycle graph.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function partial_isometries_cycle_graph_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_partial_isometries_cycle_graph_monoid(m)
+end
+
+"""
+ uniform_block_bijection_monoid(n::Integer) -> Presentation
+
+A presentation for the uniform block bijection monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+uniform block bijection monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function uniform_block_bijection_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_uniform_block_bijection_monoid(m)
+end
+
+"""
+ dual_symmetric_inverse_monoid(n::Integer) -> Presentation
+
+A presentation for the dual symmetric inverse monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+dual symmetric inverse monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+"""
+function dual_symmetric_inverse_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_dual_symmetric_inverse_monoid(m)
+end
+
+"""
+ stellar_monoid(l::Integer) -> Presentation
+
+A presentation for the stellar monoid with `l` generators.
+
+This function returns a monoid presentation defining the stellar monoid
+with `l` generators.
+
+# Arguments
+- `l::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `l` is negative.
+- `LibsemigroupsError`: if `l < 2`.
+"""
+function stellar_monoid(l::Integer)
+ ll = UInt(l)
+ @wrap_libsemigroups_call LibSemigroups.example_stellar_monoid(ll)
+end
+
+"""
+ zero_rook_monoid(n::Integer) -> Presentation
+
+A presentation for the 0-rook monoid of degree `n`.
+
+This function returns the default libsemigroups presentation of the
+0-rook monoid of degree `n`.
+
+# Arguments
+- `n::Integer`: the degree of the monoid.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function zero_rook_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_zero_rook_monoid(m)
+end
+
+"""
+ abacus_jones_monoid(n::Integer, d::Integer) -> Presentation
+
+A presentation for the abacus Jones monoid of degree `n` with at most `d-1`
+beads per arc.
+
+This function returns a monoid presentation defining the abacus Jones
+monoid of degree `n`, where each arc carries at most `d - 1` beads.
+
+# Arguments
+- `n::Integer`: the degree.
+- `d::Integer`: one more than the maximum number of beads on each arc.
+
+# Throws
+- `InexactError`: if `n` or `d` is negative.
+- `LibsemigroupsError`: if `n < 3`.
+- `LibsemigroupsError`: if `d == 0`.
+"""
+function abacus_jones_monoid(n::Integer, d::Integer)
+ nn = UInt(n)
+ dd = UInt(d)
+ @wrap_libsemigroups_call LibSemigroups.example_abacus_jones_monoid(nn, dd)
+end
+
+# ---- batch C: plactic / misc ----
+
+"""
+ plactic_monoid(n::Integer) -> Presentation
+
+A presentation for the plactic monoid with `n` generators.
+
+This function returns a monoid presentation defining the plactic monoid
+with `n` generators.
+
+# Arguments
+- `n::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function plactic_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_plactic_monoid(m)
+end
+
+"""
+ chinese_monoid(n::Integer) -> Presentation
+
+A presentation for the Chinese monoid with `n` generators.
+
+This function returns a monoid presentation defining the Chinese monoid
+with `n` generators.
+
+# Arguments
+- `n::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function chinese_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_chinese_monoid(m)
+end
+
+"""
+ hypo_plactic_monoid(n::Integer) -> Presentation
+
+A presentation for the hypo-plactic monoid with `n` generators.
+
+This function returns a presentation for the hypo-plactic monoid with `n`
+generators. It is a quotient monoid of the plactic monoid.
+
+# Arguments
+- `n::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function hypo_plactic_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_hypo_plactic_monoid(m)
+end
+
+"""
+ stylic_monoid(n::Integer) -> Presentation
+
+A presentation for the stylic monoid with `n` generators.
+
+This function returns a monoid presentation defining the stylic monoid
+with `n` generators.
+
+# Arguments
+- `n::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `n` is negative.
+- `LibsemigroupsError`: if `n < 2`.
+"""
+function stylic_monoid(n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_stylic_monoid(m)
+end
+
+"""
+ special_linear_group_2(q::Integer) -> Presentation
+
+A presentation for the special linear group SL(2, q).
+
+This function returns a presentation for the special linear group
+`SL(2, q)`, where `q` should be an odd prime for the returned
+presentation to define the claimed group.
+
+# Arguments
+- `q::Integer`: the order of the finite field.
+
+# Throws
+- `InexactError`: if `q` is negative.
+- `LibsemigroupsError`: if `q < 3`.
+"""
+function special_linear_group_2(q::Integer)
+ qq = UInt(q)
+ @wrap_libsemigroups_call LibSemigroups.example_special_linear_group_2(qq)
+end
+
+"""
+ fibonacci_semigroup(r::Integer, n::Integer) -> Presentation
+
+A presentation for the Fibonacci semigroup F(r, n).
+
+This function returns a semigroup presentation defining the Fibonacci
+semigroup `F(r, n)`.
+
+# Arguments
+- `r::Integer`: the length of the left-hand sides of the relations.
+- `n::Integer`: the number of generators.
+
+# Throws
+- `InexactError`: if `r` or `n` is negative.
+- `LibsemigroupsError`: if `r < 1`.
+- `LibsemigroupsError`: if `n < 1`.
+"""
+function fibonacci_semigroup(r::Integer, n::Integer)
+ rr = UInt(r)
+ nn = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_fibonacci_semigroup(rr, nn)
+end
+
+"""
+ monogenic_semigroup(m::Integer, r::Integer) -> Presentation
+
+A presentation for the monogenic semigroup with index `m` and period `r`.
+
+This function returns the presentation ``. When
+`m == 0` the result is a monoid presentation; otherwise it is a
+semigroup presentation.
+
+# Arguments
+- `m::Integer`: the index.
+- `r::Integer`: the period.
+
+# Throws
+- `InexactError`: if `m` or `r` is negative.
+- `LibsemigroupsError`: if `r == 0`.
+"""
+function monogenic_semigroup(m::Integer, r::Integer)
+ mm = UInt(m)
+ rr = UInt(r)
+ @wrap_libsemigroups_call LibSemigroups.example_monogenic_semigroup(mm, rr)
+end
+
+"""
+ rectangular_band(m::Integer, n::Integer) -> Presentation
+
+A presentation for the ``m \\times n`` rectangular band.
+
+This function returns a semigroup presentation defining the `m` by `n`
+rectangular band.
+
+# Arguments
+- `m::Integer`: the number of rows.
+- `n::Integer`: the number of columns.
+
+# Throws
+- `InexactError`: if `m` or `n` is negative.
+- `LibsemigroupsError`: if `m == 0`.
+- `LibsemigroupsError`: if `n == 0`.
+"""
+function rectangular_band(m::Integer, n::Integer)
+ mm = UInt(m)
+ nn = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.example_rectangular_band(mm, nn)
+end
+
+"""
+ sigma_plactic_monoid(sigma::AbstractVector{<:Integer}) -> Presentation
+
+Presentation for the ``\\sigma``-plactic monoid with coefficient sequence `sigma`.
+
+This function returns a presentation for the ``\\sigma``-plactic monoid.
+The image of ``\\sigma`` is given by the values in `sigma`, and the
+resulting monoid is the quotient of the plactic monoid by the least
+congruence containing the relations ``a^{\\sigma(a)} = a``.
+
+!!! note "0-based `sigma`"
+ Unlike alphabet letters (which are 1-based in the Julia API), the
+ `sigma` argument here is a vector of libsemigroups-native 0-based
+ coefficient indices - pass it through verbatim.
+
+# Arguments
+- `sigma::AbstractVector{<:Integer}`: the image of ``\\sigma`` in
+ libsemigroups' native 0-based indexing.
+
+# Throws
+- `InexactError`: if any entry of `sigma` is negative.
+- `LibsemigroupsError`: if `sigma` is empty.
+"""
+function sigma_plactic_monoid(sigma::AbstractVector{<:Integer})
+ s = UInt[UInt(x) for x in sigma]
+ @wrap_libsemigroups_call LibSemigroups.example_sigma_plactic_monoid(s)
+end
+
+"""
+ renner_type_B_monoid(l::Integer, q::Integer) -> Presentation
+
+A presentation for the Renner type B monoid of rank `l` over a field of size `q`.
+
+This function returns a presentation for the Renner monoid of type B with
+rank `l` and Iwahori-Hecke deformation `q`.
+
+# Arguments
+- `l::Integer`: the rank.
+- `q::Integer`: the Iwahori-Hecke deformation.
+
+# Throws
+- `InexactError`: if `l` is negative, or if `q` is not representable as
+ `Int32`.
+- `LibsemigroupsError`: if `q` is neither `0` nor `1`.
+"""
+function renner_type_B_monoid(l::Integer, q::Integer)
+ ll = UInt(l)
+ qq = Int32(q)
+ @wrap_libsemigroups_call LibSemigroups.example_renner_type_B_monoid(ll, qq)
+end
+
+"""
+ renner_type_D_monoid(l::Integer, q::Integer) -> Presentation
+
+A presentation for the Renner type D monoid of rank `l` over a field of size `q`.
+
+This function returns a presentation for the Renner monoid of type D with
+rank `l` and Iwahori-Hecke deformation `q`.
+
+# Arguments
+- `l::Integer`: the rank.
+- `q::Integer`: the Iwahori-Hecke deformation.
+
+# Throws
+- `InexactError`: if `l` is negative, or if `q` is not representable as
+ `Int32`.
+- `LibsemigroupsError`: if `q` is neither `0` nor `1`.
+"""
+function renner_type_D_monoid(l::Integer, q::Integer)
+ ll = UInt(l)
+ qq = Int32(q)
+ @wrap_libsemigroups_call LibSemigroups.example_renner_type_D_monoid(ll, qq)
+end
+
+"""
+ not_renner_type_B_monoid(l::Integer, q::Integer) -> Presentation
+
+A presentation (not Renner type B) related to the Renner type B monoid of
+rank `l` over a field of size `q`.
+
+This function returns a presentation that incorrectly claims to define
+the Renner monoid of type B with rank `l` and Iwahori-Hecke deformation
+`q`.
+
+# Arguments
+- `l::Integer`: the rank.
+- `q::Integer`: the Iwahori-Hecke deformation.
+
+# Throws
+- `InexactError`: if `l` is negative, or if `q` is not representable as
+ `Int32`.
+- `LibsemigroupsError`: if `q` is neither `0` nor `1`.
+"""
+function not_renner_type_B_monoid(l::Integer, q::Integer)
+ ll = UInt(l)
+ qq = Int32(q)
+ @wrap_libsemigroups_call LibSemigroups.example_not_renner_type_B_monoid(ll, qq)
+end
+
+"""
+ not_renner_type_D_monoid(l::Integer, q::Integer) -> Presentation
+
+A presentation (not Renner type D) related to the Renner type D monoid of
+rank `l` over a field of size `q`.
+
+This function returns a presentation that incorrectly claims to define
+the Renner monoid of type D with rank `l` and Iwahori-Hecke deformation
+`q`.
+
+# Arguments
+- `l::Integer`: the rank.
+- `q::Integer`: the Iwahori-Hecke deformation.
+
+# Throws
+- `InexactError`: if `l` is negative, or if `q` is not representable as
+ `Int32`.
+- `LibsemigroupsError`: if `q` is neither `0` nor `1`.
+"""
+function not_renner_type_D_monoid(l::Integer, q::Integer)
+ ll = UInt(l)
+ qq = Int32(q)
+ @wrap_libsemigroups_call LibSemigroups.example_not_renner_type_D_monoid(ll, qq)
+end
diff --git a/src/presentation.jl b/src/presentation.jl
new file mode 100644
index 0000000..432b817
--- /dev/null
+++ b/src/presentation.jl
@@ -0,0 +1,1279 @@
+# Copyright (c) 2026, James W. Swent
+#
+# Distributed under the terms of the GPL license version 3.
+
+"""
+presentation.jl - Presentation + InversePresentation wrappers
+"""
+
+"""
+ Presentation() -> Presentation
+ Presentation(other::Presentation) -> Presentation
+
+Type for semigroup or monoid presentations.
+
+This type provides a shallow wrapper around a vector of words (the *rules*
+of the presentation), together with an *alphabet*. It is intended to be
+used as the input to other algorithms in `libsemigroups` (such as
+the Knuth-Bendix, Todd-Coxeter, and Kambites algorithms).
+
+In a valid presentation, rules only consist of letters from within the
+alphabet; however, for performance reasons, it is possible to update both
+the rules and the alphabet independently of each other. For this reason,
+it is possible for the alphabet and the rules to become out of sync.
+[`Presentation`](@ref Semigroups.Presentation) provides some checks that
+the rules define a valid presentation, and some related helper functions
+live as module-level functions in `Semigroups`.
+
+The zero-argument form constructs an empty presentation with no rules and
+no alphabet; the one-argument form copies `other`.
+
+!!! warning "v1 limitation"
+ v1 of Semigroups.jl binds `Presentation` only. Alphabets
+ and rules are expressed as `Vector{Int}` with 1-based letter indices.
+"""
+const Presentation = LibSemigroups.Presentation
+
+"""
+ init!(p::Presentation) -> Presentation
+
+Remove the alphabet and all rules from `p`.
+
+This function clears the alphabet and all rules of `p`, putting it back
+into the state it would be in if it was newly constructed.
+"""
+init!(p::Presentation) = (LibSemigroups.init!(p); p)
+
+"""
+ alphabet(p::Presentation) -> Vector{Int}
+
+Return the alphabet of `p`.
+
+Returns the alphabet of `p` as a `Vector{Int}` of 1-based letter indices.
+
+# Complexity
+Constant.
+"""
+alphabet(p::Presentation) = _word_from_cpp(LibSemigroups.alphabet(p))
+
+# Route `Base.deepcopy` through the C++ copy constructor. Default
+# deepcopy_internal for CxxWrap-wrapped types may shallow-copy the handle.
+Base.deepcopy_internal(p::Presentation, ::IdDict) = Presentation(p)
+
+"""
+ set_alphabet!(p::Presentation, n::Integer) -> Presentation
+
+Set the alphabet of `p` by size.
+
+Sets the alphabet of `p` to be the first `n` positive integers
+`[1, 2, ..., n]`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `n::Integer`: the size of the alphabet.
+
+# Throws
+- `LibsemigroupsError`: if `n` is greater than the maximum number of
+ letters supported.
+
+# Warning
+This function does not verify that the rules in `p` (if any) consist of
+letters belonging to the new alphabet.
+
+# See also
+- [`throw_if_alphabet_has_duplicates`](@ref)
+- [`throw_if_bad_rules`](@ref)
+- [`throw_if_bad_alphabet_or_rules`](@ref)
+"""
+function set_alphabet!(p::Presentation, n::Integer)
+ m = UInt(n)
+ @wrap_libsemigroups_call LibSemigroups.set_alphabet_size!(p, m)
+ return p
+end
+
+"""
+ set_alphabet!(p::Presentation, a::AbstractVector{<:Integer}) -> Presentation
+
+Set the alphabet of `p` to the letters in `a`.
+
+Sets the alphabet of `p` to be the (1-based) letters in `a`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `a::AbstractVector{<:Integer}`: the alphabet.
+
+# Throws
+- `LibsemigroupsError`: if `a` contains duplicate letters.
+
+# Warning
+This function does not verify that the rules in `p` (if any) consist of
+letters belonging to the new alphabet.
+
+# See also
+- [`throw_if_bad_rules`](@ref)
+- [`throw_if_bad_alphabet_or_rules`](@ref)
+"""
+function set_alphabet!(p::Presentation, a::AbstractVector{<:Integer})
+ w = _word_to_cpp(a)
+ @wrap_libsemigroups_call LibSemigroups.set_alphabet!(p, w)
+ return p
+end
+
+"""
+ alphabet_from_rules!(p::Presentation) -> Presentation
+
+Set the alphabet of `p` to be the letters that appear in its rules.
+
+# Complexity
+At most ``O(mn)`` where ``m`` is the number of rules and ``n`` is the
+length of the longest rule.
+
+# See also
+- [`throw_if_bad_rules`](@ref)
+- [`throw_if_bad_alphabet_or_rules`](@ref)
+"""
+alphabet_from_rules!(p::Presentation) = (LibSemigroups.alphabet_from_rules!(p); p)
+
+"""
+ letter(p::Presentation, i::Integer) -> Int
+
+Return a letter in the alphabet of `p` by index.
+
+Returns the letter of the alphabet in position `i` (1-based).
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `i::Integer`: the 1-based index of the letter to return.
+
+# Throws
+- `LibsemigroupsError`: if `i` is not in the range ``[1, n]``, where
+ ``n`` is the length of the alphabet of `p`.
+"""
+function letter(p::Presentation, i::Integer)
+ idx = UInt(i - 1)
+ _letter_from_cpp(@wrap_libsemigroups_call LibSemigroups.letter(p, idx))
+end
+
+"""
+ index_of(p::Presentation, x::Integer) -> Int
+
+Return the index of a letter in the alphabet of `p`.
+
+Returns the 1-based position of the letter `x` in the alphabet of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `x::Integer`: the letter whose index is sought.
+
+# Throws
+- `LibsemigroupsError`: if `x` does not belong to the alphabet of `p`.
+
+# Complexity
+Constant.
+
+# Note
+This function mirrors `Presentation::index` in libsemigroups, renamed to
+`index_of` to avoid clashing with `Base.index`-style conventions in Julia.
+"""
+function index_of(p::Presentation, x::Integer)
+ y = _letter_to_cpp(x)
+ Int(@wrap_libsemigroups_call LibSemigroups.index_of(p, y)) + 1
+end
+
+"""
+ in_alphabet(p::Presentation, x::Integer) -> Bool
+
+Check if a letter belongs to the alphabet of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `x::Integer`: the letter to check.
+
+# Complexity
+Constant on average, worst case linear in the size of the alphabet.
+"""
+in_alphabet(p::Presentation, x::Integer) = LibSemigroups.in_alphabet(p, _letter_to_cpp(x))
+
+"""
+ contains_empty_word(p::Presentation) -> Bool
+
+Return whether the empty word is a valid relation word in `p`.
+
+Returns `true` if the empty word is a valid relation word in `p`, and
+`false` otherwise.
+
+If `p` is not allowed to contain the empty word (according to this
+function), then `p` may still be isomorphic to a monoid, but is not given
+as a quotient of a free monoid.
+
+# Complexity
+Constant.
+"""
+contains_empty_word(p::Presentation) = LibSemigroups.contains_empty_word(p)
+
+"""
+ set_contains_empty_word!(p::Presentation, val::Bool) -> Presentation
+
+Set whether the empty word is a valid relation word in `p`.
+
+Specify whether the empty word should be a valid relation word
+(corresponding to `val` being `true`), or not (corresponding to `val`
+being `false`).
+
+If `p` is not allowed to contain the empty word (according to the value
+specified here), then `p` may still be isomorphic to a monoid, but is not
+given as a quotient of a free monoid.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `val::Bool`: whether `p` can contain the empty word.
+
+# Complexity
+Constant.
+"""
+set_contains_empty_word!(p::Presentation, val::Bool) =
+ (LibSemigroups.set_contains_empty_word!(p, val); p)
+
+"""
+ add_generator!(p::Presentation) -> Int
+ add_generator!(p::Presentation, x::Integer) -> Presentation
+
+Add a generator to `p`.
+
+The zero-argument form adds the first letter not already in the alphabet
+of `p` as a generator and returns this letter (1-based).
+
+The one-argument form adds the letter `x` as a generator of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `x::Integer`: (one-argument form) the letter to add as a generator.
+
+# Throws
+- `LibsemigroupsError`: (zero-argument form) if the alphabet is already
+ of the maximum possible size supported by the letter type.
+- `LibsemigroupsError`: (one-argument form) if `x` is already in the
+ alphabet of `p`.
+"""
+function add_generator!(p::Presentation)
+ x = @wrap_libsemigroups_call LibSemigroups.add_generator_no_arg!(p)
+ return _letter_from_cpp(x)
+end
+
+function add_generator!(p::Presentation, x::Integer)
+ y = _letter_to_cpp(x)
+ @wrap_libsemigroups_call LibSemigroups.add_generator!(p, y)
+ return p
+end
+
+"""
+ remove_generator!(p::Presentation, x::Integer) -> Presentation
+
+Remove the letter `x` as a generator of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `x::Integer`: the letter to remove as a generator.
+
+# Throws
+- `LibsemigroupsError`: if `x` is not in the alphabet of `p`.
+
+# Complexity
+Average case: linear in the length of the alphabet; worst case: quadratic
+in the length of the alphabet.
+"""
+function remove_generator!(p::Presentation, x::Integer)
+ y = _letter_to_cpp(x)
+ @wrap_libsemigroups_call LibSemigroups.remove_generator!(p, y)
+ return p
+end
+
+"""
+ add_rule!(p::Presentation, lhs::AbstractVector{<:Integer}, rhs::AbstractVector{<:Integer}) -> Presentation
+
+Add the rule `lhs = rhs` to `p`, after checking that `lhs` and `rhs`
+only contain letters in the alphabet of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `lhs::AbstractVector{<:Integer}`: the left-hand side of the rule.
+- `rhs::AbstractVector{<:Integer}`: the right-hand side of the rule.
+
+# Throws
+- `LibsemigroupsError`: if `lhs` or `rhs` contains any letters not
+ belonging to the alphabet of `p`.
+- `LibsemigroupsError`: if [`contains_empty_word`](@ref)`(p)` returns
+ `false` and either `lhs` or `rhs` is empty.
+
+# See also
+- [`add_rule_no_checks!`](@ref)
+"""
+function add_rule!(
+ p::Presentation,
+ lhs::AbstractVector{<:Integer},
+ rhs::AbstractVector{<:Integer},
+)
+ l = _word_to_cpp(lhs)
+ r = _word_to_cpp(rhs)
+ @wrap_libsemigroups_call LibSemigroups.add_rule!(p, l, r)
+ return p
+end
+
+"""
+ add_rule_no_checks!(p::Presentation, lhs::AbstractVector{<:Integer}, rhs::AbstractVector{<:Integer}) -> Presentation
+
+Add the rule `lhs = rhs` to `p` without checking the arguments.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `lhs::AbstractVector{<:Integer}`: the left-hand side of the rule.
+- `rhs::AbstractVector{<:Integer}`: the right-hand side of the rule.
+
+# Complexity
+Amortized constant.
+
+# Warning
+No checks that the arguments describe words over the alphabet of `p`
+are performed.
+
+# See also
+- [`add_rule!`](@ref)
+"""
+function add_rule_no_checks!(
+ p::Presentation,
+ lhs::AbstractVector{<:Integer},
+ rhs::AbstractVector{<:Integer},
+)
+ l = _word_to_cpp(lhs)
+ r = _word_to_cpp(rhs)
+ LibSemigroups.add_rule_no_checks!(p, l, r)
+ return p
+end
+
+"""
+ number_of_rules(p::Presentation) -> Int
+
+Return the number of rules in `p`.
+
+The rules of a [`Presentation`](@ref Semigroups.Presentation) are stored
+internally as a vector of words, with each rule occupying two consecutive
+entries (its left-hand and right-hand sides); the number of rules is
+therefore half the length of this vector.
+
+# Complexity
+Constant.
+"""
+number_of_rules(p::Presentation) = Int(LibSemigroups.number_of_rules(p))
+
+"""
+ rule_lhs(p::Presentation, i::Integer) -> Vector{Int}
+
+Return the left-hand side of the `i`-th rule of `p` (1-based rule index).
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `i::Integer`: the 1-based index of the rule.
+
+# Throws
+- `LibsemigroupsError`: if `i` is not in the range ``[1, n]``, where
+ ``n`` is [`number_of_rules`](@ref)`(p)`.
+
+# See also
+- [`rule_rhs`](@ref)
+- [`rules`](@ref)
+"""
+function rule_lhs(p::Presentation, i::Integer)
+ idx = UInt(i - 1)
+ _word_from_cpp(@wrap_libsemigroups_call LibSemigroups.rule_lhs(p, idx))
+end
+
+"""
+ rule_rhs(p::Presentation, i::Integer) -> Vector{Int}
+
+Return the right-hand side of the `i`-th rule of `p` (1-based rule index).
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `i::Integer`: the 1-based index of the rule.
+
+# Throws
+- `LibsemigroupsError`: if `i` is not in the range ``[1, n]``, where
+ ``n`` is [`number_of_rules`](@ref)`(p)`.
+
+# See also
+- [`rule_lhs`](@ref)
+- [`rules`](@ref)
+"""
+function rule_rhs(p::Presentation, i::Integer)
+ idx = UInt(i - 1)
+ _word_from_cpp(@wrap_libsemigroups_call LibSemigroups.rule_rhs(p, idx))
+end
+
+"""
+ rule(p::Presentation, i::Integer) -> Tuple{Vector{Int},Vector{Int}}
+
+Return the `i`-th rule of `p` as a `(lhs, rhs)` tuple (1-based rule index).
+
+Thin wrapper combining [`rule_lhs`](@ref) and [`rule_rhs`](@ref). For bulk
+access prefer [`rules`](@ref), which makes a single C++ call.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `i::Integer`: the 1-based index of the rule.
+
+# Throws
+- `LibsemigroupsError`: if `i` is not in the range ``[1, n]``, where ``n``
+ is [`number_of_rules`](@ref)`(p)`.
+
+# See also
+- [`rule_lhs`](@ref)
+- [`rule_rhs`](@ref)
+- [`rules`](@ref)
+"""
+rule(p::Presentation, i::Integer) = (rule_lhs(p, i), rule_rhs(p, i))
+
+"""
+ rules(p::Presentation) -> Vector{Tuple{Vector{Int},Vector{Int}}}
+
+Return all rules of `p` as a vector of `(lhs, rhs)` tuples.
+
+Mirrors `p.rules` in libsemigroups. This is a single C++ call into
+`LibSemigroups.rules_vector` followed by a Julia-side pairing, so is
+appreciably faster than iterating [`rule_lhs`](@ref) / [`rule_rhs`](@ref)
+for large presentations.
+"""
+function rules(p::Presentation)
+ flat = LibSemigroups.rules_vector(p)
+ n = length(flat)
+ return [(_word_from_cpp(flat[i]), _word_from_cpp(flat[i+1])) for i = 1:2:n]
+end
+
+"""
+ clear_rules!(p::Presentation) -> Presentation
+
+Remove all rules from `p`.
+
+The alphabet of `p` is left untouched.
+"""
+clear_rules!(p::Presentation) = (LibSemigroups.clear_rules!(p); p)
+
+"""
+ throw_if_alphabet_has_duplicates(p::Presentation)
+
+Check if the alphabet of `p` is valid.
+
+# Throws
+- `LibsemigroupsError`: if there are duplicate letters in the alphabet
+ of `p`.
+
+# Complexity
+Linear in the length of the alphabet.
+"""
+throw_if_alphabet_has_duplicates(p::Presentation) =
+ (@wrap_libsemigroups_call LibSemigroups.throw_if_alphabet_has_duplicates(p); nothing)
+
+"""
+ throw_if_letter_not_in_alphabet(p::Presentation, x::Integer)
+
+Check if a letter belongs to the alphabet of `p`.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `x::Integer`: the letter to check.
+
+# Throws
+- `LibsemigroupsError`: if `x` does not belong to the alphabet of `p`.
+
+# Complexity
+Constant on average, worst case linear in the size of the alphabet.
+"""
+function throw_if_letter_not_in_alphabet(p::Presentation, x::Integer)
+ y = _letter_to_cpp(x)
+ @wrap_libsemigroups_call LibSemigroups.throw_if_letter_not_in_alphabet(p, y)
+ return nothing
+end
+
+"""
+ throw_if_bad_rules(p::Presentation)
+
+Check if every word in every rule of `p` consists only of letters
+belonging to the alphabet.
+
+Also checks that there are an even number of words in `p`'s rule list
+(i.e. that every rule has both a left- and right-hand side).
+
+# Throws
+- `LibsemigroupsError`: if any word contains a letter not in the
+ alphabet of `p`.
+- `LibsemigroupsError`: if the number of words in `p`'s rule list is odd.
+
+# Complexity
+Worst case ``O(mnt)`` where ``m`` is the length of the longest word,
+``n`` is the size of the alphabet and ``t`` is the number of rules.
+"""
+throw_if_bad_rules(p::Presentation) =
+ (@wrap_libsemigroups_call LibSemigroups.throw_if_bad_rules(p); nothing)
+
+"""
+ throw_if_bad_alphabet_or_rules(p::Presentation)
+
+Check if the alphabet and rules of `p` are valid.
+
+# Throws
+- `LibsemigroupsError`: if [`throw_if_alphabet_has_duplicates`](@ref) or
+ [`throw_if_bad_rules`](@ref) does.
+
+# Complexity
+Worst case ``O(mnp)`` where ``m`` is the length of the longest word,
+``n`` is the size of the alphabet, and ``p`` is the number of rules.
+"""
+throw_if_bad_alphabet_or_rules(p::Presentation) =
+ (@wrap_libsemigroups_call LibSemigroups.throw_if_bad_alphabet_or_rules(p); nothing)
+
+"""
+ length_of(p::Presentation) -> Int
+
+Return the sum of the lengths of all rule words in `p`.
+
+That is, ``\\sum (|u| + |v|)`` over all rules ``u = v`` of `p`.
+
+This function mirrors `presentation::length` in libsemigroups. It is
+named `length_of` (rather than extending `Base.length`) to avoid ambiguity
+with [`number_of_rules`](@ref), since neither choice is uniformly
+natural.
+"""
+length_of(p::Presentation) = Int(LibSemigroups.length_of(p))
+
+"""
+ longest_rule_length(p::Presentation) -> Int
+
+Return the maximum length of a rule in `p`.
+
+The *length* of a rule is defined to be the sum of the lengths of its
+left-hand and right-hand sides.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+"""
+longest_rule_length(p::Presentation) = Int(LibSemigroups.longest_rule_length(p))
+
+"""
+ shortest_rule_length(p::Presentation) -> Int
+
+Return the minimum length of a rule in `p`.
+
+The *length* of a rule is defined to be the sum of the lengths of its
+left-hand and right-hand sides.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+"""
+shortest_rule_length(p::Presentation) = Int(LibSemigroups.shortest_rule_length(p))
+
+"""
+ is_normalized(p::Presentation) -> Bool
+
+Check if the presentation `p` is normalized.
+
+Returns `true` if the alphabet of `p` is `[1, 2, ..., n]` (where ``n`` is
+the size of the alphabet), and `false` otherwise.
+"""
+is_normalized(p::Presentation) = LibSemigroups.is_normalized(p)
+
+"""
+ are_rules_sorted(p::Presentation) -> Bool
+
+Check if the rules of `p` are sorted in short-lex order.
+
+Returns `true` if the rules ``u_1 = v_1, \\ldots, u_n = v_n`` of `p`
+satisfy ``u_1 v_1 < \\cdots < u_n v_n`` where ``<`` is the short-lex
+order.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+
+# See also
+- [`sort_rules!`](@ref)
+"""
+are_rules_sorted(p::Presentation) = LibSemigroups.are_rules_sorted(p)
+
+"""
+ contains_rule(p::Presentation, lhs::AbstractVector{<:Integer}, rhs::AbstractVector{<:Integer}) -> Bool
+
+Check if `p` contains the rule `lhs = rhs`.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `lhs::AbstractVector{<:Integer}`: the left-hand side of the rule.
+- `rhs::AbstractVector{<:Integer}`: the right-hand side of the rule.
+
+# Complexity
+Linear in [`number_of_rules`](@ref)`(p)`.
+"""
+function contains_rule(
+ p::Presentation,
+ lhs::AbstractVector{<:Integer},
+ rhs::AbstractVector{<:Integer},
+)
+ l = _word_to_cpp(lhs)
+ r = _word_to_cpp(rhs)
+ LibSemigroups.contains_rule(p, l, r)
+end
+
+"""
+ throw_if_odd_number_of_rules(p::Presentation)
+
+Throw if the number of words in the rule list of `p` is odd.
+
+# Throws
+- `LibsemigroupsError`: if the number of words in the rule list of `p`
+ is odd (i.e. there is a dangling left-hand side with no matching
+ right-hand side).
+"""
+throw_if_odd_number_of_rules(p::Presentation) =
+ (@wrap_libsemigroups_call LibSemigroups.throw_if_odd_number_of_rules(p); nothing)
+
+"""
+ normalize_alphabet!(p::Presentation) -> Presentation
+
+Normalize the alphabet of `p` to `[1, ..., n]`.
+
+Modifies `p` in-place so that the alphabet is `[1, ..., n]` (or
+equivalent), rewriting the rules to use this alphabet. If the alphabet is
+already normalized, no changes are made.
+
+# Throws
+- `LibsemigroupsError`: if [`throw_if_bad_alphabet_or_rules`](@ref)
+ throws on `p` before modification.
+"""
+normalize_alphabet!(p::Presentation) =
+ (@wrap_libsemigroups_call LibSemigroups.normalize_alphabet!(p); p)
+
+"""
+ change_alphabet!(p::Presentation, new_alphabet::AbstractVector{<:Integer}) -> Presentation
+
+Change or re-order the alphabet of `p`.
+
+Replaces the alphabet of `p` with `new_alphabet` where possible, and
+re-writes the rules of `p` using the new alphabet.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `new_alphabet::AbstractVector{<:Integer}`: the replacement alphabet.
+
+# Throws
+- `LibsemigroupsError`: if the size of the alphabet of `p` does not
+ equal the length of `new_alphabet`.
+"""
+function change_alphabet!(p::Presentation, new_alphabet::AbstractVector{<:Integer})
+ w = _word_to_cpp(new_alphabet)
+ @wrap_libsemigroups_call LibSemigroups.change_alphabet!(p, w)
+ return p
+end
+
+"""
+ Base.reverse!(p::Presentation) -> Presentation
+
+Reverse every rule of `p`, in place.
+
+Extends `Base.reverse!` so that `reverse!(p)` reverses the left- and
+right-hand sides of every rule of `p`. Binding-level extensions of
+`Base` functions to presentation-like types are documented on the
+`Presentation` doc page.
+"""
+Base.reverse!(p::Presentation) = (LibSemigroups.reverse_rules!(p); p)
+
+"""
+ sort_rules!(p::Presentation) -> Presentation
+
+Sort the rules of `p` in short-lex order.
+
+Sorts the rules ``u_1 = v_1, \\ldots, u_n = v_n`` so that
+``u_1 v_1 < \\cdots < u_n v_n`` where ``<`` is the short-lex order.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+
+# See also
+- [`are_rules_sorted`](@ref)
+"""
+sort_rules!(p::Presentation) = (LibSemigroups.sort_rules!(p); p)
+
+"""
+ sort_each_rule!(p::Presentation) -> Bool
+
+Sort the two sides of each rule in short-lex order.
+
+Reorders each rule ``u = v`` of `p` so that the left-hand side is
+short-lex greater than or equal to the right-hand side (i.e. the longer
+/ lexicographically-larger side is on the left).
+
+Returns `true` if any rule was reordered, and `false` otherwise.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+
+# Complexity
+Linear in the number of rules.
+"""
+sort_each_rule!(p::Presentation) = LibSemigroups.sort_each_rule!(p)
+
+"""
+ add_identity_rules!(p::Presentation, e::Integer) -> Presentation
+
+Add rules for an identity element.
+
+Adds rules of the form ``ae = ea = a`` for every letter ``a`` in the
+alphabet of `p`, where ``e`` is the letter given by the second argument.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `e::Integer`: the identity element.
+
+# Throws
+- `LibsemigroupsError`: if `e` is not a letter in the alphabet of `p`.
+
+# Complexity
+Linear in the number of rules.
+"""
+function add_identity_rules!(p::Presentation, e::Integer)
+ y = _letter_to_cpp(e)
+ @wrap_libsemigroups_call LibSemigroups.add_identity_rules!(p, y)
+ return p
+end
+
+"""
+ add_zero_rules!(p::Presentation, z::Integer) -> Presentation
+
+Add rules for a zero element.
+
+Adds rules of the form ``az = za = z`` for every letter ``a`` in the
+alphabet of `p`, where ``z`` is the letter given by the second argument.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `z::Integer`: the zero element.
+
+# Throws
+- `LibsemigroupsError`: if `z` is not a letter in the alphabet of `p`.
+
+# Complexity
+Linear in the number of rules.
+"""
+function add_zero_rules!(p::Presentation, z::Integer)
+ y = _letter_to_cpp(z)
+ @wrap_libsemigroups_call LibSemigroups.add_zero_rules!(p, y)
+ return p
+end
+
+"""
+ remove_duplicate_rules!(p::Presentation) -> Presentation
+
+Remove duplicate rules from `p`.
+
+Removes all but one instance of any duplicate rules (if any). Note that
+rules of the form ``u = v`` and ``v = u`` (if any) are considered
+duplicates. The rules may be reordered by this function even if there
+are no duplicate rules.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+
+# Complexity
+Linear in the number of rules.
+"""
+remove_duplicate_rules!(p::Presentation) = (LibSemigroups.remove_duplicate_rules!(p); p)
+
+"""
+ remove_trivial_rules!(p::Presentation) -> Presentation
+
+Remove rules consisting of identical words.
+
+Removes all instances of rules (if any) where the left-hand side and
+right-hand side are identical (i.e. rules of the form ``u = u``).
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd.
+
+# Complexity
+Linear in the number of rules.
+"""
+remove_trivial_rules!(p::Presentation) = (LibSemigroups.remove_trivial_rules!(p); p)
+
+"""
+ add_rules!(p::Presentation, q::Presentation) -> Presentation
+
+Add the rules of `q` to `p`.
+
+Each rule of `q` is checked to contain only letters of `alphabet(p)` before
+being added; if the ``n``-th rule would fail this check, the first ``n-1``
+rules are still added to `p`.
+
+Mirrors `libsemigroups::presentation::add_rules`.
+
+# Arguments
+- `p::Presentation`: the presentation to add rules to.
+- `q::Presentation`: the presentation whose rules should be copied into `p`.
+
+# Throws
+- `LibsemigroupsError`: if any rule of `q` contains a letter not in
+ `alphabet(p)`.
+"""
+function add_rules!(p::Presentation, q::Presentation)
+ @wrap_libsemigroups_call LibSemigroups.add_rules!(p, q)
+ return p
+end
+
+"""
+ add_inverse_rules!(p::Presentation, inverses::AbstractVector{<:Integer}) -> Presentation
+ add_inverse_rules!(p::Presentation, inverses::AbstractVector{<:Integer}, e::Integer) -> Presentation
+
+Add rules for inverses.
+
+The letter with index `i` in `inverses` is taken to be the inverse of the
+letter `alphabet(p)[i]`. The rules added are ``a_i b_i = e`` where
+``\\{a_1, \\ldots, a_n\\}`` is `alphabet(p)`, ``\\{b_1, \\ldots, b_n\\}``
+is `inverses`, and `e` is the identity letter. If `e` is omitted, the
+identity is taken to be the empty word.
+
+Mirrors `libsemigroups::presentation::add_inverse_rules`.
+
+# Arguments
+- `p::Presentation`: the presentation to add rules to.
+- `inverses::AbstractVector{<:Integer}`: the inverses of the letters in
+ `alphabet(p)`.
+- `e::Integer`: (3-arg form) the identity letter.
+
+# Throws
+- `LibsemigroupsError`: if `length(inverses) != length(alphabet(p))`, if
+ `inverses` does not contain the same letters as `alphabet(p)`, if
+ ``(a_i^{-1})^{-1} = a_i`` fails for some `i`, or if
+ ``e^{-1} = e`` fails.
+"""
+function add_inverse_rules!(p::Presentation, inverses::AbstractVector{<:Integer})
+ v = _word_to_cpp(inverses)
+ @wrap_libsemigroups_call LibSemigroups.add_inverse_rules!(p, v)
+ return p
+end
+
+function add_inverse_rules!(
+ p::Presentation,
+ inverses::AbstractVector{<:Integer},
+ e::Integer,
+)
+ v = _word_to_cpp(inverses)
+ y = _letter_to_cpp(e)
+ @wrap_libsemigroups_call LibSemigroups.add_inverse_rules_with_identity!(p, v, y)
+ return p
+end
+
+"""
+ replace_subword!(p::Presentation, existing::AbstractVector{<:Integer}, replacement::AbstractVector{<:Integer}) -> Presentation
+
+Replace every non-overlapping occurrence of the word `existing` in every
+rule of `p` with the word `replacement`. `p` is modified in place.
+
+Mirrors `libsemigroups::presentation::replace_subword`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `existing::AbstractVector{<:Integer}`: the subword to replace.
+- `replacement::AbstractVector{<:Integer}`: the replacement word.
+
+# Throws
+- `LibsemigroupsError`: if `existing` is empty.
+
+# See also
+- [`replace_word!`](@ref)
+- [`replace_word_with_new_generator!`](@ref)
+"""
+function replace_subword!(
+ p::Presentation,
+ existing::AbstractVector{<:Integer},
+ replacement::AbstractVector{<:Integer},
+)
+ e = _word_to_cpp(existing)
+ r = _word_to_cpp(replacement)
+ @wrap_libsemigroups_call LibSemigroups.replace_subword!(p, e, r)
+ return p
+end
+
+"""
+ replace_word!(p::Presentation, existing::AbstractVector{<:Integer}, replacement::AbstractVector{<:Integer}) -> Presentation
+
+Replace every instance of the word `existing` that appears as a full side
+of some rule with the word `replacement`. Specifically, every rule of the
+form ``existing = w`` or ``w = existing`` has `existing` replaced by
+`replacement`. `p` is modified in place.
+
+Differs from [`replace_subword!`](@ref), which replaces any non-overlapping
+occurrence of `existing` anywhere inside any rule.
+
+Mirrors `libsemigroups::presentation::replace_word`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `existing::AbstractVector{<:Integer}`: the word to replace.
+- `replacement::AbstractVector{<:Integer}`: the replacement word.
+
+# See also
+- [`replace_subword!`](@ref)
+"""
+function replace_word!(
+ p::Presentation,
+ existing::AbstractVector{<:Integer},
+ replacement::AbstractVector{<:Integer},
+)
+ e = _word_to_cpp(existing)
+ r = _word_to_cpp(replacement)
+ @wrap_libsemigroups_call LibSemigroups.replace_word!(p, e, r)
+ return p
+end
+
+"""
+ replace_word_with_new_generator!(p::Presentation, w::AbstractVector{<:Integer}) -> Int
+
+Replace every non-overlapping (left-to-right) instance of the word `w` in
+every rule of `p` with a new generator `z`, and add the rule ``w = z``.
+The new generator and rule are added even if `w` is not a subword of any
+rule. Returns the new generator `z` as a 1-based letter index.
+
+Mirrors `libsemigroups::presentation::replace_word_with_new_generator`.
+
+# Arguments
+- `p::Presentation`: the presentation to modify.
+- `w::AbstractVector{<:Integer}`: the word to replace.
+
+# Throws
+- `LibsemigroupsError`: if `w` is empty.
+"""
+function replace_word_with_new_generator!(p::Presentation, w::AbstractVector{<:Integer})
+ v = _word_to_cpp(w)
+ z = @wrap_libsemigroups_call LibSemigroups.replace_word_with_new_generator!(p, v)
+ return _letter_from_cpp(z)
+end
+
+"""
+ first_unused_letter(p::Presentation) -> Int
+
+Return the smallest letter not already in the alphabet of `p`.
+
+Mirrors `libsemigroups::presentation::first_unused_letter`.
+
+# Throws
+- `LibsemigroupsError`: if the alphabet of `p` is already of the maximum
+ possible size supported by the underlying letter type.
+"""
+function first_unused_letter(p::Presentation)
+ return _letter_from_cpp(@wrap_libsemigroups_call LibSemigroups.first_unused_letter(p))
+end
+
+"""
+ index_rule(p::Presentation, lhs::AbstractVector{<:Integer}, rhs::AbstractVector{<:Integer}) -> Union{Int, UndefinedType}
+
+Return the 1-based index of the first rule of `p` equal to `lhs = rhs`,
+or [`UNDEFINED`](@ref Semigroups.UNDEFINED) if no such rule exists.
+
+Mirrors `libsemigroups::presentation::index_rule`. The returned index is
+the rule-pair index - the same index accepted by [`rule_lhs`](@ref),
+[`rule_rhs`](@ref), and [`rule`](@ref).
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `lhs::AbstractVector{<:Integer}`: the left-hand side of the rule.
+- `rhs::AbstractVector{<:Integer}`: the right-hand side of the rule.
+
+# Throws
+- `LibsemigroupsError`: if [`throw_if_bad_alphabet_or_rules`](@ref) throws
+ on `p`.
+
+# See also
+- [`is_rule`](@ref)
+"""
+function index_rule(
+ p::Presentation,
+ lhs::AbstractVector{<:Integer},
+ rhs::AbstractVector{<:Integer},
+)
+ l = _word_to_cpp(lhs)
+ r = _word_to_cpp(rhs)
+ i = UInt(@wrap_libsemigroups_call LibSemigroups.index_rule(p, l, r))
+ i == typemax(UInt) && return UNDEFINED
+ return div(Int(i), 2) + 1
+end
+
+"""
+ is_rule(p::Presentation, lhs::AbstractVector{<:Integer}, rhs::AbstractVector{<:Integer}) -> Bool
+
+Return `true` if `lhs = rhs` is a rule of `p`, and `false` otherwise.
+
+Mirrors `libsemigroups::presentation::is_rule`.
+
+# Throws
+- `LibsemigroupsError`: if [`throw_if_bad_alphabet_or_rules`](@ref) throws
+ on `p`.
+
+# See also
+- [`index_rule`](@ref)
+"""
+function is_rule(
+ p::Presentation,
+ lhs::AbstractVector{<:Integer},
+ rhs::AbstractVector{<:Integer},
+)
+ l = _word_to_cpp(lhs)
+ r = _word_to_cpp(rhs)
+ return @wrap_libsemigroups_call LibSemigroups.is_rule(p, l, r)
+end
+
+"""
+ longest_rule_index(p::Presentation) -> Int
+
+Return the 1-based index of the first rule of `p` of maximal length.
+
+The *length* of a rule is the sum of the lengths of its left- and
+right-hand sides. Mirrors `libsemigroups::presentation::longest_rule`,
+returning a rule-pair index instead of the C++ iterator - so the result is
+suitable to pass to [`rule`](@ref), [`rule_lhs`](@ref), or
+[`rule_rhs`](@ref).
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd (which
+ includes the case of no rules).
+
+# See also
+- [`shortest_rule_index`](@ref)
+- [`longest_rule_length`](@ref)
+"""
+function longest_rule_index(p::Presentation)
+ flat = UInt(@wrap_libsemigroups_call LibSemigroups.longest_rule_index(p))
+ return div(Int(flat), 2) + 1
+end
+
+"""
+ shortest_rule_index(p::Presentation) -> Int
+
+Return the 1-based index of the first rule of `p` of minimal length.
+
+The *length* of a rule is the sum of the lengths of its left- and
+right-hand sides. Mirrors `libsemigroups::presentation::shortest_rule`,
+returning a rule-pair index instead of the C++ iterator.
+
+# Throws
+- `LibsemigroupsError`: if the number of rule words in `p` is odd (which
+ includes the case of no rules).
+
+# See also
+- [`longest_rule_index`](@ref)
+- [`shortest_rule_length`](@ref)
+"""
+function shortest_rule_index(p::Presentation)
+ flat = UInt(@wrap_libsemigroups_call LibSemigroups.shortest_rule_index(p))
+ return div(Int(flat), 2) + 1
+end
+
+"""
+ throw_if_bad_inverses(p::Presentation, inverses::AbstractVector{<:Integer})
+
+Throw a `LibsemigroupsError` if `inverses` does not define a valid list of
+semigroup inverses for `alphabet(p)`.
+
+Mirrors `libsemigroups::presentation::throw_if_bad_inverses`. Specifically,
+this function checks that `alphabet(p)` and `inverses` contain the same
+letters, that `inverses` is duplicate-free, and that if `a_i = b_j` (where
+`a` is the alphabet and `b` is `inverses`) then `a_j = b_i` - i.e. taking
+an inverse is an involution on the letters.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `inverses::AbstractVector{<:Integer}`: the proposed inverses.
+
+# Throws
+- `LibsemigroupsError`: if any of the above conditions does not hold.
+"""
+function throw_if_bad_inverses(p::Presentation, inverses::AbstractVector{<:Integer})
+ v = _word_to_cpp(inverses)
+ @wrap_libsemigroups_call LibSemigroups.throw_if_bad_inverses(p, v)
+ return nothing
+end
+
+"""
+ to_gap_string(p::Presentation, var_name::AbstractString = "p") -> String
+
+Return the GAP source code that would construct a presentation with the
+same alphabet and rules as `p`. Presentations in GAP are created by taking
+quotients of free semigroups or monoids.
+
+Mirrors `libsemigroups::presentation::to_gap_string`.
+
+# Arguments
+- `p::Presentation`: the presentation.
+- `var_name::AbstractString`: the name of the GAP variable to assign to
+ (defaults to `"p"`).
+
+# Throws
+- `LibsemigroupsError`: if `p` has more than 49 generators (the cap on
+ GAP's default alphabet).
+"""
+function to_gap_string(p::Presentation, var_name::AbstractString = "p")
+ s = @wrap_libsemigroups_call LibSemigroups.to_gap_string(p, String(var_name))
+ return String(s)
+end
+
+Base.:(==)(a::Presentation, b::Presentation) = LibSemigroups.is_equal(a, b)
+
+"""
+ Base.isempty(p::Presentation) -> Bool
+
+Return `true` iff `p` has an empty alphabet and no rules - the state it
+would be in immediately after [`Presentation`](@ref Semigroups.Presentation)`()`
+or [`init!`](@ref).
+"""
+Base.isempty(p::Presentation) = isempty(alphabet(p)) && number_of_rules(p) == 0
+
+"""
+ Base.hash(p::Presentation, h::UInt) -> UInt
+
+Stable hash suitable for dictionary keys: presentations equal under
+`==` hash to the same value. The hash combines the
+alphabet, the flat rules list, and the `contains_empty_word` flag.
+"""
+function Base.hash(p::Presentation, h::UInt)
+ return hash(
+ (
+ alphabet(p),
+ [_word_from_cpp(w) for w in LibSemigroups.rules_vector(p)],
+ contains_empty_word(p),
+ ),
+ h,
+ )
+end
+
+function Base.show(io::IO, p::Presentation)
+ print(io, LibSemigroups.to_human_readable_repr(p))
+end
+
+# ----------------------------------------------------------------------------
+# InversePresentation
+# ----------------------------------------------------------------------------
+
+"""
+ InversePresentation(p::Presentation) -> InversePresentation
+ InversePresentation(ip::InversePresentation) -> InversePresentation
+
+Type for inverse-semigroup or inverse-monoid presentations.
+
+An [`InversePresentation`](@ref Semigroups.InversePresentation) is a
+[`Presentation`](@ref Semigroups.Presentation) together with a word
+recording a semigroup inverse for each letter of the alphabet. It is a
+subtype of [`Presentation`](@ref Semigroups.Presentation) and is intended
+to be used as the input to other algorithms in `libsemigroups`.
+
+Constructed from a [`Presentation`](@ref Semigroups.Presentation) (with
+inverses initially empty), or as a copy of another
+[`InversePresentation`](@ref Semigroups.InversePresentation).
+
+!!! warning "v1 limitation"
+ v1 of Semigroups.jl binds `InversePresentation` only.
+ Alphabets, rules, and inverses are expressed as `Vector{Int}` with
+ 1-based letter indices.
+"""
+const InversePresentation = LibSemigroups.InversePresentation
+
+"""
+ set_inverses!(ip::InversePresentation, w::AbstractVector{<:Integer}) -> InversePresentation
+
+Set the inverse of each letter in the alphabet of `ip`.
+
+The `i`-th entry of `w` is taken to be the inverse of the `i`-th letter
+of `alphabet(ip)`.
+
+# Arguments
+- `ip::InversePresentation`: the inverse presentation to modify.
+- `w::AbstractVector{<:Integer}`: a word containing the inverses.
+
+# Throws
+- `LibsemigroupsError`: if the alphabet contains duplicate letters, or
+ if `w` does not define a valid semigroup inverse for the alphabet.
+
+# Note
+Although the alphabet is not an explicit argument to this function, the
+alphabet must be checked here since a specification of inverses cannot
+make sense if the alphabet contains duplicate letters.
+
+# See also
+- [`throw_if_alphabet_has_duplicates`](@ref)
+- [`throw_if_bad_alphabet_rules_or_inverses`](@ref)
+"""
+function set_inverses!(ip::InversePresentation, w::AbstractVector{<:Integer})
+ v = _word_to_cpp(w)
+ @wrap_libsemigroups_call LibSemigroups.set_inverses!(ip, v)
+ return ip
+end
+
+"""
+ inverses(ip::InversePresentation) -> Vector{Int}
+
+Return the inverse of each letter in the alphabet of `ip`.
+
+The `i`-th entry of the returned vector is the inverse of the `i`-th
+letter of `alphabet(ip)`.
+"""
+inverses(ip::InversePresentation) = _word_from_cpp(LibSemigroups.inverses(ip))
+
+"""
+ inverse_of(ip::InversePresentation, x::Integer) -> Int
+
+Return the inverse of the letter `x` in `ip`.
+
+# Arguments
+- `ip::InversePresentation`: the inverse presentation.
+- `x::Integer`: the letter whose inverse is sought.
+
+# Throws
+- `LibsemigroupsError`: if no inverses have been set, or if `x` is not
+ in the alphabet of `ip`.
+
+# Note
+This function mirrors `InversePresentation::inverse` in libsemigroups,
+renamed to `inverse_of` to avoid shadowing Julia's `Base.inv`.
+"""
+function inverse_of(ip::InversePresentation, x::Integer)
+ y = _letter_to_cpp(x)
+ _letter_from_cpp(@wrap_libsemigroups_call LibSemigroups.inverse_of(ip, y))
+end
+
+"""
+ throw_if_bad_alphabet_rules_or_inverses(ip::InversePresentation)
+
+Check if `ip` is a valid inverse presentation.
+
+Specifically, checks that the alphabet of `ip` does not contain duplicate
+letters, that all rules only contain letters defined in the alphabet, and
+that the inverses act as semigroup inverses.
+
+# Throws
+- `LibsemigroupsError`: if the alphabet contains duplicate letters.
+- `LibsemigroupsError`: if the rules contain letters not defined in the
+ alphabet.
+- `LibsemigroupsError`: if the inverses do not act as semigroup
+ inverses.
+
+# See also
+- [`throw_if_bad_alphabet_or_rules`](@ref)
+"""
+throw_if_bad_alphabet_rules_or_inverses(ip::InversePresentation) = (
+ @wrap_libsemigroups_call LibSemigroups.throw_if_bad_alphabet_rules_or_inverses(ip);
+ nothing
+)
+
+# Equality that accounts for inverses (not the base `Presentation ==`).
+Base.:(==)(a::InversePresentation, b::InversePresentation) =
+ LibSemigroups.is_equal_inv(a, b)
+
+function Base.show(io::IO, ip::InversePresentation)
+ print(io, LibSemigroups.to_human_readable_repr(ip))
+end
+
+# Extend deepcopy via the copy ctor.
+Base.deepcopy_internal(ip::InversePresentation, ::IdDict) = InversePresentation(ip)
diff --git a/src/transf.jl b/src/transf.jl
index 251f233..0431c87 100644
--- a/src/transf.jl
+++ b/src/transf.jl
@@ -99,7 +99,7 @@ const _PermTypes = Union{Perm1,Perm2,Perm4}
const _PTransfTypes = Union{_TransfTypes,_PPermTypes,_PermTypes}
# ============================================================================
-# Index convention — helpers for crossing the Julia/C++ boundary
+# Index convention - helpers for crossing the Julia/C++ boundary
# ----------------------------------------------------------------------------
# Julia uses 1-based indexing with `UNDEFINED` for missing values.
# C++ (libsemigroups) uses 0-based indexing with `typemax(T)` as the UNDEFINED
@@ -178,7 +178,7 @@ right_one(p::_PPermTypes) = LibSemigroups.right_one(p)
_scalar_type_from_degree(n::Integer) -> Type
Select appropriate unsigned integer type based on degree `n`.
-Returns UInt8 for n ≤ 255, UInt16 for n ≤ 65535, UInt32 otherwise.
+Returns UInt8 for n <= 255, UInt16 for n <= 65535, UInt32 otherwise.
"""
function _scalar_type_from_degree(n::Integer)
# Use <= for typemax: degree n stores 0-based indices 0..n-1, so max index n-1
@@ -475,7 +475,7 @@ Base.copy(t::Transf{T}) where {T} = Transf{T}(copy(t.cxx_obj))
"""
*(t1::Transf, t2::Transf) -> Transf
-Compose two transformations. Returns t1 ∘ t2, i.e., (t1*t2)[i] = t1[t2[i]].
+Compose two transformations. Returns ``t_1 \\circ t_2``, i.e., `(t1*t2)[i] = t1[t2[i]]`.
Both operands must have the same scalar type (use the same underlying C++ type).
# Example
diff --git a/src/word-graph.jl b/src/word-graph.jl
index 054353f..8ac2c76 100644
--- a/src/word-graph.jl
+++ b/src/word-graph.jl
@@ -51,7 +51,7 @@ const WordGraph = LibSemigroups.WordGraph
# ============================================================================
# Index conversion (private, file-local)
# ============================================================================
-# WordGraph — node_type = label_type = uint32_t. Conversion happens
+# WordGraph - node_type = label_type = uint32_t. Conversion happens
# here, not in C++: the C++ binding is pure pass-through per project convention.
_to_cpp(x::Integer) = UInt32(x - 1)
diff --git a/test/runtests.jl b/test/runtests.jl
index f66df62..9682068 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -19,5 +19,7 @@ using Semigroups
include("test_runner.jl")
include("test_transf.jl")
include("test_word_graph.jl")
+ include("test_presentation.jl")
+ include("test_presentation_examples.jl")
include("test_word_range.jl")
end
diff --git a/test/test_constants.jl b/test/test_constants.jl
index 857e282..b713101 100644
--- a/test/test_constants.jl
+++ b/test/test_constants.jl
@@ -9,7 +9,7 @@ test_constants.jl - Tests for libsemigroups constants
"""
@testset "Constants" begin
- # Test UNDEFINED conversions — typemax(T) is the libsemigroups native sentinel
+ # Test UNDEFINED conversions - typemax(T) is the libsemigroups native sentinel
@test convert(UInt8, UNDEFINED) === typemax(UInt8)
@test convert(UInt16, UNDEFINED) === typemax(UInt16)
@test convert(UInt32, UNDEFINED) === typemax(UInt32)
diff --git a/test/test_presentation.jl b/test/test_presentation.jl
new file mode 100644
index 0000000..fc3a929
--- /dev/null
+++ b/test/test_presentation.jl
@@ -0,0 +1,546 @@
+using Test
+using Semigroups
+
+@testset verbose = true "Presentation" begin
+ @testset "scaffolding" begin
+ @test isdefined(Semigroups, :Presentation)
+ @test hasmethod(Presentation, Tuple{})
+ end
+
+ @testset "constructors + init! + deepcopy + alphabet getter" begin
+ p = Presentation()
+ @test p isa Presentation
+
+ q = Presentation(p) # copy constructor
+ @test q isa Presentation
+
+ init!(q)
+ @test alphabet(q) == Int[]
+
+ r = deepcopy(p) # Base.deepcopy via copy ctor
+ @test r isa Presentation
+ @test r !== p # distinct wrapper
+ end
+
+ @testset "alphabet setters" begin
+ p = Presentation()
+ set_alphabet!(p, 3)
+ @test alphabet(p) == [1, 2, 3]
+
+ set_alphabet!(p, [1, 2, 4])
+ @test alphabet(p) == [1, 2, 4]
+
+ # Rejection of duplicates
+ @test_throws LibsemigroupsError set_alphabet!(p, [1, 1])
+
+ # Negative n must surface as InexactError (not wrapped by @wrap_libsemigroups_call)
+ @test_throws InexactError set_alphabet!(p, -1)
+ end
+
+ @testset "alphabet queries" begin
+ p = Presentation()
+ set_alphabet!(p, [3, 1, 4])
+ @test letter(p, 1) == 3
+ @test letter(p, 2) == 1
+ @test letter(p, 3) == 4
+ @test_throws LibsemigroupsError letter(p, 4)
+
+ @test index_of(p, 3) == 1
+ @test index_of(p, 1) == 2
+ @test index_of(p, 4) == 3
+ @test_throws LibsemigroupsError index_of(p, 99)
+
+ @test in_alphabet(p, 3)
+ @test !in_alphabet(p, 99)
+ end
+
+ @testset "contains_empty_word" begin
+ p = Presentation()
+ @test contains_empty_word(p) == false
+ set_contains_empty_word!(p, true)
+ @test contains_empty_word(p) == true
+ set_contains_empty_word!(p, false)
+ @test contains_empty_word(p) == false
+ end
+
+ @testset "generator management" begin
+ p = Presentation()
+ set_alphabet!(p, 2) # alphabet = [1, 2]
+ x = add_generator!(p) # first letter not in alphabet (1-based)
+ @test x == 3
+ @test alphabet(p) == [1, 2, 3]
+
+ add_generator!(p, 7)
+ @test 7 in alphabet(p)
+ @test_throws LibsemigroupsError add_generator!(p, 7) # duplicate
+
+ remove_generator!(p, 7)
+ @test !(7 in alphabet(p))
+ @test_throws LibsemigroupsError remove_generator!(p, 99)
+ end
+
+ @testset "add_rule! / add_rule_no_checks!" begin
+ p = Presentation()
+ set_alphabet!(p, 3)
+
+ add_rule_no_checks!(p, [1, 1, 1], [1])
+ add_rule!(p, [2, 2], [2])
+
+ # Checked form rejects letter outside alphabet
+ @test_throws LibsemigroupsError add_rule!(p, [1, 99], [1])
+
+ # Checked form rejects empty rule when contains_empty_word == false
+ @test_throws LibsemigroupsError add_rule!(p, Int[], [1])
+
+ # With contains_empty_word = true, empty sides are accepted
+ set_contains_empty_word!(p, true)
+ add_rule!(p, Int[], [1])
+ end
+
+ @testset "rule access" begin
+ p = Presentation()
+ set_alphabet!(p, 3)
+ @test number_of_rules(p) == 0
+ @test isempty(rules(p))
+
+ add_rule_no_checks!(p, [1, 1, 1], [1])
+ @test number_of_rules(p) == 1
+ @test rule_lhs(p, 1) == [1, 1, 1]
+ @test rule_rhs(p, 1) == [1]
+ @test rules(p) == [([1, 1, 1], [1])]
+
+ clear_rules!(p)
+ @test number_of_rules(p) == 0
+ end
+
+ @testset "validation throws" begin
+ p = Presentation()
+ set_alphabet!(p, [1, 2, 3])
+ throw_if_alphabet_has_duplicates(p) # no throw
+ throw_if_letter_not_in_alphabet(p, 2) # no throw
+ @test_throws LibsemigroupsError throw_if_letter_not_in_alphabet(p, 99)
+ throw_if_bad_alphabet_or_rules(p)
+
+ q = Presentation()
+ set_alphabet!(q, 2) # [1, 2]
+ add_rule_no_checks!(q, [1, 99], [2]) # illegal letter 99
+ @test_throws LibsemigroupsError throw_if_bad_rules(q)
+ @test_throws LibsemigroupsError throw_if_bad_alphabet_or_rules(q)
+ end
+
+ @testset "helpers: scalar queries" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1]) # lens 2 + 1
+ add_rule!(p, [2, 2], [2]) # lens 2 + 1
+
+ @test length_of(p) == 6 # 2 + 1 + 2 + 1
+ @test longest_rule_length(p) == 3
+ @test shortest_rule_length(p) == 3
+ @test contains_rule(p, [1, 1], [1])
+ @test !contains_rule(p, [1, 2], [1])
+
+ @test are_rules_sorted(p)
+ @test is_normalized(p)
+
+ throw_if_odd_number_of_rules(p) # no throw (even)
+ end
+
+ @testset "helpers: shape mutators" begin
+ # normalize_alphabet!
+ p = Presentation()
+ set_alphabet!(p, [7, 3])
+ add_rule!(p, [7, 3], [7])
+ normalize_alphabet!(p)
+ @test alphabet(p) == [1, 2]
+
+ # change_alphabet!
+ q = Presentation()
+ set_alphabet!(q, [1, 2])
+ add_rule!(q, [1, 2], [1])
+ change_alphabet!(q, [5, 6])
+ @test alphabet(q) == [5, 6]
+ @test rule_lhs(q, 1) == [5, 6]
+
+ # Base.reverse! (not exported; dispatches on ::Presentation)
+ r = Presentation()
+ set_alphabet!(r, 2)
+ add_rule!(r, [1, 2, 1], [2])
+ reverse!(r)
+ @test rule_lhs(r, 1) == [1, 2, 1] # palindrome reverses to itself
+ # adjacent rule with distinct reverse:
+ add_rule!(r, [1, 2], [2, 1])
+ reverse!(r)
+ reverse!(r) # double-reverse is identity
+ @test rule_lhs(r, 2) == [1, 2]
+
+ # sort_rules!, sort_each_rule!
+ s = Presentation()
+ set_alphabet!(s, 2)
+ add_rule!(s, [2, 2], [2])
+ add_rule!(s, [1, 1], [1])
+ sort_rules!(s)
+ @test rule_lhs(s, 1) == [1, 1] # shortlex: smaller lhs first
+
+ t = Presentation()
+ set_alphabet!(t, 2)
+ add_rule!(t, [2], [1, 1, 1]) # lhs < rhs by shortlex
+ sort_each_rule!(t)
+ # After sort_each_rule!: lhs > rhs under shortlex (larger side first).
+ # [1,1,1] has length 3 > [2] length 1, so [1,1,1] becomes lhs.
+ @test rule_lhs(t, 1) == [1, 1, 1]
+ @test rule_rhs(t, 1) == [2]
+ end
+
+ @testset "helpers: rule-set mutators" begin
+ # add_identity_rules!
+ t = Presentation()
+ set_alphabet!(t, 2)
+ set_contains_empty_word!(t, true)
+ add_identity_rules!(t, 1) # make 1 the identity -> 1*a = a*1 = a
+ # 2-letter alphabet: rules are {0,0}={0}, {1,0}={1}, {0,1}={1} (0-indexed)
+ # i.e., 3 rules for 2 letters (2n-1 where n=2)
+ @test number_of_rules(t) == 3
+
+ # add_zero_rules!
+ z = Presentation()
+ set_alphabet!(z, 2)
+ set_contains_empty_word!(z, true)
+ add_zero_rules!(z, 1) # make 1 a zero -> 1*a = a*1 = 1
+ @test number_of_rules(z) == 3
+
+ # remove_duplicate_rules!
+ u = Presentation()
+ set_alphabet!(u, 2)
+ add_rule!(u, [1, 1], [1])
+ add_rule!(u, [1, 1], [1]) # duplicate
+ remove_duplicate_rules!(u)
+ @test number_of_rules(u) == 1
+
+ # remove_trivial_rules!
+ v = Presentation()
+ set_alphabet!(v, 1)
+ add_rule!(v, [1], [1]) # trivial
+ remove_trivial_rules!(v)
+ @test number_of_rules(v) == 0
+ end
+
+ @testset "equality + show" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1])
+
+ q = Presentation()
+ set_alphabet!(q, 2)
+ add_rule!(q, [1, 1], [1])
+
+ @test p == q
+ add_rule!(q, [2], [2])
+ @test p != q
+
+ s = sprint(show, p)
+ @test occursin("presentation", s)
+ end
+
+ @testset "InversePresentation" begin
+ p = Presentation()
+ set_alphabet!(p, [1, 2])
+ ip = InversePresentation(p)
+ @test ip isa InversePresentation
+ @test ip isa Presentation # CxxWrap inheritance chain
+ @test alphabet(ip) == [1, 2]
+
+ # Copy ctor
+ jp = InversePresentation(ip)
+ @test jp isa InversePresentation
+
+ set_inverses!(ip, [2, 1])
+ @test inverses(ip) == [2, 1]
+ @test inverse_of(ip, 1) == 2
+ @test inverse_of(ip, 2) == 1
+
+ # Bad inverses rejected
+ @test_throws LibsemigroupsError set_inverses!(ip, [1, 1])
+
+ throw_if_bad_alphabet_rules_or_inverses(ip)
+
+ # == takes inverses into account
+ kp = InversePresentation(p)
+ set_inverses!(kp, [2, 1])
+ @test ip == kp
+ set_inverses!(kp, [1, 2]) # different inverse map
+ @test ip != kp
+
+ # show produces something containing "presentation" (case-insensitive match)
+ s = sprint(show, ip)
+ @test occursin("presentation", lowercase(s))
+ end
+
+ @testset "end-to-end: construct, validate, render, compare" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1, 1], [1])
+ add_rule!(p, [2, 2], [2])
+ add_rule!(p, [1, 2, 1], [2, 1, 2])
+
+ throw_if_bad_alphabet_or_rules(p)
+ @test number_of_rules(p) == 3
+ @test rules(p) == [([1, 1, 1], [1]), ([2, 2], [2]), ([1, 2, 1], [2, 1, 2])]
+
+ # Round-trip through Base.reverse! and back
+ reverse!(p)
+ reverse!(p)
+ @test rules(p) == [([1, 1, 1], [1]), ([2, 2], [2]), ([1, 2, 1], [2, 1, 2])]
+
+ # Equality against a freshly constructed twin
+ q = Presentation()
+ set_alphabet!(q, 2)
+ add_rule!(q, [1, 1, 1], [1])
+ add_rule!(q, [2, 2], [2])
+ add_rule!(q, [1, 2, 1], [2, 1, 2])
+ @test p == q
+
+ # Promote to InversePresentation
+ ip = InversePresentation(p)
+ set_inverses!(ip, [2, 1])
+ @test inverse_of(ip, 1) == 2
+ throw_if_bad_alphabet_rules_or_inverses(ip)
+ end
+
+ @testset "add_rules!" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1])
+
+ q = Presentation()
+ set_alphabet!(q, 2)
+ add_rule!(q, [2, 2], [2])
+ add_rule!(q, [1, 2], [2, 1])
+
+ @test add_rules!(p, q) === p # chainable
+ @test rules(p) == [([1, 1], [1]), ([2, 2], [2]), ([1, 2], [2, 1])]
+
+ # Bad letter in q's rules -> throws
+ bad = Presentation()
+ set_alphabet!(bad, 1)
+ add_rule_no_checks!(bad, [1, 99], [1]) # 99 not in p's alphabet
+ @test_throws LibsemigroupsError add_rules!(p, bad)
+ end
+
+ @testset "add_inverse_rules!" begin
+ # 2-arg form (no identity): inverses of [1,2] are [2,1]
+ p = Presentation()
+ set_alphabet!(p, [1, 2])
+ set_contains_empty_word!(p, true) # empty identity allowed
+ @test add_inverse_rules!(p, [2, 1]) === p # chainable
+ # The resulting rules should encode a*a^(-1) = empty for each letter.
+ @test number_of_rules(p) >= 2
+
+ # 3-arg form with explicit identity letter.
+ q = Presentation()
+ set_alphabet!(q, [1, 2, 3])
+ @test add_inverse_rules!(q, [2, 1, 3], 3) === q # identity = letter 3
+ @test number_of_rules(q) >= 1
+
+ # Invalid inverses rejected
+ r = Presentation()
+ set_alphabet!(r, [1, 2])
+ @test_throws LibsemigroupsError add_inverse_rules!(r, [1, 1])
+ end
+
+ @testset "replace_subword!" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1, 1], [1]) # a^3 = a
+
+ @test replace_subword!(p, [1, 1], [2]) === p
+ # Every non-overlapping "aa" in the rule "aaa = a" is replaced by "b":
+ # "aaa" -> "ba" (one a leftover); rhs "a" unchanged.
+ @test rules(p) == [([2, 1], [1])]
+
+ @test_throws LibsemigroupsError replace_subword!(p, Int[], [1])
+ end
+
+ @testset "replace_word!" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1, 1]) # a^2 = a^2 (trivial)
+ add_rule!(p, [1, 2], [1, 1]) # ab = a^2
+ # Replace the full-side word [1,1] wherever it appears as a complete side.
+ replace_word!(p, [1, 1], [2])
+ # After: [1,1] fully replaced on matching sides; [1,2]=[1,1] -> [1,2]=[2].
+ @test rules(p) == [([2], [2]), ([1, 2], [2])]
+ end
+
+ @testset "replace_word_with_new_generator!" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 2, 1, 2], [1])
+
+ z = replace_word_with_new_generator!(p, [1, 2])
+ @test z isa Int
+ @test z in alphabet(p) # new generator present
+ # The rule for the new generator (w = z) should be present:
+ @test contains_rule(p, [1, 2], [z])
+
+ @test_throws LibsemigroupsError replace_word_with_new_generator!(p, Int[])
+ end
+
+ @testset "first_unused_letter" begin
+ p = Presentation()
+ set_alphabet!(p, 3)
+ @test first_unused_letter(p) == 4 # 1-based
+
+ q = Presentation()
+ set_alphabet!(q, [1, 3, 5])
+ @test first_unused_letter(q) == 2 # first gap
+ end
+
+ @testset "index_rule + is_rule + UNDEFINED" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1])
+ add_rule!(p, [2, 2], [2])
+ add_rule!(p, [1, 2], [2, 1])
+
+ @test index_rule(p, [1, 1], [1]) == 1
+ @test index_rule(p, [2, 2], [2]) == 2
+ @test index_rule(p, [1, 2], [2, 1]) == 3
+
+ missing_idx = index_rule(p, [1], [2])
+ @test missing_idx === UNDEFINED # singleton, not nothing
+ @test is_undefined(missing_idx)
+
+ @test is_rule(p, [1, 1], [1])
+ @test is_rule(p, [2, 2], [2])
+ @test !is_rule(p, [1], [2])
+ end
+
+ @testset "longest_rule_index + shortest_rule_index" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1], [1]) # len 2
+ add_rule!(p, [1, 1, 1], [2, 2]) # len 5
+ add_rule!(p, [1, 2], [2]) # len 3
+
+ @test longest_rule_index(p) == 2
+ @test shortest_rule_index(p) == 1
+ end
+
+ @testset "throw_if_bad_inverses" begin
+ p = Presentation()
+ set_alphabet!(p, [1, 2])
+ @test throw_if_bad_inverses(p, [2, 1]) === nothing # valid
+
+ # Duplicate inverses -> throws
+ @test_throws LibsemigroupsError throw_if_bad_inverses(p, [1, 1])
+
+ # Wrong length -> throws
+ @test_throws LibsemigroupsError throw_if_bad_inverses(p, [2])
+ end
+
+ @testset "to_gap_string" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1])
+
+ s = to_gap_string(p) # default var_name "p"
+ @test s isa String
+ @test !isempty(s)
+ @test occursin("p", s)
+
+ s2 = to_gap_string(p, "S")
+ @test occursin("S", s2)
+ end
+
+ @testset "rule(p, i) accessor" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ add_rule!(p, [1, 1], [1])
+ add_rule!(p, [2, 2], [2])
+ add_rule!(p, [1, 2], [2, 1])
+
+ for i = 1:3
+ @test rule(p, i) == (rule_lhs(p, i), rule_rhs(p, i))
+ end
+ end
+
+ @testset "rules(p) via rules_vector binding" begin
+ p = Presentation()
+ set_alphabet!(p, 2)
+ @test rules(p) == Tuple{Vector{Int},Vector{Int}}[] # empty
+
+ add_rule!(p, [1, 1], [1])
+ add_rule!(p, [2, 2], [2])
+ @test rules(p) == [([1, 1], [1]), ([2, 2], [2])]
+
+ # Binding-surface: rules_vector is a callable C++ method on Presentation.
+ flat = Semigroups.LibSemigroups.rules_vector(p)
+ @test length(flat) == 2 * number_of_rules(p)
+ end
+
+ @testset "Base.isempty + Base.hash" begin
+ p = Presentation()
+ @test isempty(p) # just constructed
+
+ set_alphabet!(p, 2)
+ @test !isempty(p) # has alphabet
+
+ q = Presentation()
+ set_alphabet!(q, 2)
+ add_rule!(q, [1, 1], [1])
+ @test !isempty(q) # has rules
+
+ init!(q)
+ @test isempty(q) # cleared back
+
+ # hash stability + equality
+ a = Presentation()
+ set_alphabet!(a, 2)
+ add_rule!(a, [1, 1], [1])
+
+ b = deepcopy(a)
+ @test hash(a) isa UInt
+ @test hash(a) == hash(b)
+
+ # Two equal-but-separately-built presentations
+ c = Presentation()
+ set_alphabet!(c, 2)
+ add_rule!(c, [1, 1], [1])
+ @test a == c
+ @test hash(a) == hash(c)
+
+ # Differing presentations hash differently (overwhelmingly likely)
+ add_rule!(c, [2, 2], [2])
+ @test a != c
+ @test hash(a) != hash(c)
+ end
+
+ @testset "binding surface" begin
+ LS = Semigroups.LibSemigroups
+ # Scalar / reference signatures - hasmethod with concrete types works.
+ @test hasmethod(LS.first_unused_letter, Tuple{Presentation})
+ @test hasmethod(LS.longest_rule_index, Tuple{Presentation})
+ @test hasmethod(LS.shortest_rule_index, Tuple{Presentation})
+ @test hasmethod(LS.rules_vector, Tuple{Presentation})
+
+ # Vector-input methods: CxxWrap's ArrayRef maps to a concrete
+ # Julia method signature that is tricky to spell as a `Tuple{...}`, so
+ # use `isdefined` to check the binding name is registered - enough to
+ # catch silent omissions. Call-through correctness is covered above.
+ for name in (
+ :add_rules!,
+ :add_inverse_rules!,
+ :add_inverse_rules_with_identity!,
+ :replace_subword!,
+ :replace_word!,
+ :replace_word_with_new_generator!,
+ :index_rule,
+ :is_rule,
+ :throw_if_bad_inverses,
+ :to_gap_string,
+ )
+ @test isdefined(LS, name)
+ end
+ end
+end
diff --git a/test/test_presentation_examples.jl b/test/test_presentation_examples.jl
new file mode 100644
index 0000000..a9271b4
--- /dev/null
+++ b/test/test_presentation_examples.jl
@@ -0,0 +1,114 @@
+using Test
+using Semigroups
+
+@testset verbose = true "presentation examples" begin
+ @testset "scaffolding" begin
+ p = symmetric_group(3)
+ @test p isa Presentation
+ @test length(alphabet(p)) == 2
+ # Known from libsemigroups tests-presentation-examples-1.cpp:
+ # symmetric_group(3) has 4 rules.
+ @test number_of_rules(p) == 4
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ @testset "batch A (group/transformation)" begin
+ # Smoke: every binding produces a valid presentation
+ for (fn, n) in [
+ (alternating_group, 5),
+ (braid_group, 4),
+ (not_symmetric_group, 4),
+ (full_transformation_monoid, 4),
+ (partial_transformation_monoid, 4),
+ (symmetric_inverse_monoid, 4),
+ (cyclic_inverse_monoid, 4),
+ (order_preserving_monoid, 4),
+ (order_preserving_cyclic_inverse_monoid, 4),
+ (orientation_preserving_monoid, 4),
+ (orientation_preserving_reversing_monoid, 4),
+ ]
+ p = fn(n)
+ @test p isa Presentation
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ # Known-answer check: symmetric_group uses Car56 variant, which has n-1 generators
+ p4 = symmetric_group(4)
+ @test length(alphabet(p4)) == 3 # Car56: n-1 = 3 generators for n=4
+ end
+
+ @testset "batch B (diagram/partition)" begin
+ for (fn, n) in [
+ (partition_monoid, 4),
+ (partial_brauer_monoid, 3),
+ (brauer_monoid, 3),
+ (singular_brauer_monoid, 3),
+ (temperley_lieb_monoid, 4),
+ (motzkin_monoid, 4),
+ (partial_isometries_cycle_graph_monoid, 3),
+ (uniform_block_bijection_monoid, 3),
+ (dual_symmetric_inverse_monoid, 3),
+ (stellar_monoid, 3),
+ (zero_rook_monoid, 3),
+ ]
+ p = fn(n)
+ @test p isa Presentation
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ # abacus_jones_monoid has two args
+ ajm = abacus_jones_monoid(4, 3)
+ @test ajm isa Presentation
+ throw_if_bad_alphabet_or_rules(ajm)
+
+ # Known-answer: temperley_lieb_monoid(4) - n-1 generators
+ tlm = temperley_lieb_monoid(4)
+ @test length(alphabet(tlm)) == 3 # n-1 = 3 generators for n=4
+ end
+
+ @testset "batch C (plactic/misc)" begin
+ for (fn, n) in [
+ (plactic_monoid, 4),
+ (chinese_monoid, 4),
+ (hypo_plactic_monoid, 4),
+ (stylic_monoid, 4),
+ (special_linear_group_2, 3),
+ ]
+ p = fn(n)
+ @test p isa Presentation
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ # 2-arg variants
+ for (fn, a, b) in [
+ (fibonacci_semigroup, 2, 5),
+ (monogenic_semigroup, 3, 2),
+ (rectangular_band, 2, 3),
+ ]
+ p = fn(a, b)
+ @test p isa Presentation
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ # sigma_plactic_monoid - vector input (0-based per libsemigroups)
+ spm = sigma_plactic_monoid([2, 1])
+ @test spm isa Presentation
+ throw_if_bad_alphabet_or_rules(spm)
+
+ # Renner-family (int q)
+ for fn in (
+ renner_type_B_monoid,
+ renner_type_D_monoid,
+ not_renner_type_B_monoid,
+ not_renner_type_D_monoid,
+ )
+ p = fn(3, 1)
+ @test p isa Presentation
+ throw_if_bad_alphabet_or_rules(p)
+ end
+
+ # Known-answer: monogenic_semigroup(m=3, r=2) has a single generator
+ ms = monogenic_semigroup(3, 2)
+ @test length(alphabet(ms)) == 1
+ end
+end
diff --git a/test/test_transf.jl b/test/test_transf.jl
index beb4df7..a0f02db 100644
--- a/test/test_transf.jl
+++ b/test/test_transf.jl
@@ -352,17 +352,17 @@ end
# ========================================================================
function check_parametric_type(T)
- # Auto-selection: small degree → UInt8
+ # Auto-selection: small degree -> UInt8
x8 = T(collect(1:10))
@test x8 isa T{UInt8}
@test degree(x8) == 10
- # Auto-selection: large degree → UInt16
+ # Auto-selection: large degree -> UInt16
x16 = T(collect(1:300))
@test x16 isa T{UInt16}
@test degree(x16) == 300
- # Boundary: 255 → UInt8, 256 → UInt16
+ # Boundary: 255 -> UInt8, 256 -> UInt16
@test T(collect(1:255)) isa T{UInt8}
@test T(collect(1:256)) isa T{UInt16}
@@ -533,18 +533,18 @@ end
@testset "Index conversion helpers" begin
for T in (UInt8, UInt16, UInt32)
- # Julia → C++ (integer)
+ # Julia -> C++ (integer)
@test Semigroups._to_cpp(1, T) === T(0)
@test Semigroups._to_cpp(5, T) === T(4)
- # Julia → C++ (UNDEFINED)
+ # Julia -> C++ (UNDEFINED)
@test Semigroups._to_cpp(UNDEFINED, T) === typemax(T)
- # C++ → Julia (never undefined)
+ # C++ -> Julia (never undefined)
@test Semigroups._from_cpp(T(0)) === 1
@test Semigroups._from_cpp(T(4)) === 5
- # C++ → Julia (PPerm; may be undefined)
+ # C++ -> Julia (PPerm; may be undefined)
@test Semigroups._from_cpp_undef(T(0)) === 1
@test Semigroups._from_cpp_undef(T(4)) === 5
@test Semigroups._from_cpp_undef(typemax(T)) === UNDEFINED
diff --git a/test/test_word_range.jl b/test/test_word_range.jl
index 83a2d23..3afa449 100644
--- a/test/test_word_range.jl
+++ b/test/test_word_range.jl
@@ -47,7 +47,7 @@ using Semigroups
set_alphabet_size!(r, 2)
set_order!(r, ORDER_SHORTLEX)
set_first!(r, [1]) # 0_w in C++
- set_last!(r, [1, 1, 1, 1]) # 0000_w in C++ (length 4 → stops before length 4)
+ set_last!(r, [1, 1, 1, 1]) # 0000_w in C++ (length 4 -> stops before length 4)
# Shortlex over alphabet 2, words of length 1..3: 2 + 4 + 8 = 14.
@test count(r) == 14
@test first_word(r) == [1]