From 594cc6846c71967ca2c068be12985dd2259121ca Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 14:06:50 +0100 Subject: [PATCH 01/12] WIP: scaffold deps/src/kambites.cpp --- deps/src/kambites.cpp | 149 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 deps/src/kambites.cpp diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp new file mode 100644 index 0000000..68381c0 --- /dev/null +++ b/deps/src/kambites.cpp @@ -0,0 +1,149 @@ +// +// 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 . +// + +// CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) +#include "libsemigroups_julia.hpp" + +// kambites-class.hpp and kambites-helpers.hpp MUST come BEFORE cong-common.hpp +// so the template bodies in cong-common.hpp see Kambites-specific overloads of +// congruence_common helpers. ADL resolves these at template-instantiation time; +// see the include-order requirement documented at cong-common.hpp:27-38. +#include +#include +#include + +#include "cong-common.hpp" + +#include +#include +#include +#include +#include + +namespace jlcxx { + template <> + struct IsMirroredType> + : std::false_type {}; + + template <> + struct SuperType> { + using type = libsemigroups::detail::CongruenceCommon; + }; +} // namespace jlcxx + +namespace libsemigroups_julia { + + void define_kambites(jl::Module& m) { + using libsemigroups::congruence_kind; + using libsemigroups::Presentation; + using libsemigroups::word_type; + + using CongruenceCommon = libsemigroups::detail::CongruenceCommon; + using K = libsemigroups::Kambites; + + // Type registration + auto type + = m.add_type("KambitesWord", jlcxx::julia_base_type()); + + // Constructors. Direct registration (no defensive lambda); CxxWrap + // converts C++ exceptions through the std::function path for direct + // constructor bindings (Phase 3a/3b precedent). + type.constructor<>(); + type.constructor const&>(); + type.constructor(); // copy ctor + + // init! overloads (mirror constructors) + type.method("init!", [](K& self) -> K& { return self.init(); }); + type.method("init!", + [](K& self, + congruence_kind knd, + Presentation const& p) -> K& { + return self.init(knd, p); + }); + + // presentation - return by copy (storage may relocate) + type.method("presentation", [](K const& self) -> Presentation { + return self.presentation(); + }); + + // generating_pairs - return by copy + type.method("generating_pairs", + [](K const& self) -> std::vector { + auto const& pairs = self.generating_pairs(); + return std::vector(pairs.begin(), pairs.end()); + }); + + // kind / number_of_generating_pairs (inherited; exposed for parity with TC) + type.method("kind", + [](K const& self) -> congruence_kind { return self.kind(); }); + + type.method("number_of_generating_pairs", [](K const& self) -> size_t { + return self.number_of_generating_pairs(); + }); + + // success / number_of_classes + type.method("success", + [](K const& self) -> bool { return self.success(); }); + + type.method("number_of_classes", + [](K& self) -> uint64_t { return self.number_of_classes(); }); + + // Const-overload split (kambites-class.hpp:731-744): the two + // small_overlap_class() overloads differ only on receiver const-ness, which + // CxxWrap cannot dispatch. Split into two distinctly-named Julia methods. + // + // small_overlap_class — mutable variant (calls run, returns the class). + type.method("small_overlap_class", + [](K& self) -> size_t { return self.small_overlap_class(); }); + + // current_small_overlap_class — const variant (returns UNDEFINED if + // unknown). Receiver-by-const-ref selects the const overload. + type.method("current_small_overlap_class", [](K const& self) -> size_t { + return self.small_overlap_class(); + }); + + // throw_if_not_C4 — bind only the mutable overload + // (kambites-class.hpp:801). The const variant (kambites-class.hpp:813) is + // deferred per the design spec. + type.method("throw_if_not_C4", [](K& self) { self.throw_if_not_C4(); }); + + // throw_if_letter_not_in_alphabet — accept ArrayRef, build a + // word_type inside the lambda (mirrors todd-coxeter.cpp:256-260). + type.method("throw_if_letter_not_in_alphabet", + [](K const& self, jlcxx::ArrayRef w) { + word_type ww(w.begin(), w.end()); + self.throw_if_letter_not_in_alphabet(ww.begin(), ww.end()); + }); + + // Display + m.method("to_human_readable_repr", [](K const& self) -> std::string { + return libsemigroups::to_human_readable_repr(self); + }); + + // Cong-common helper subset. Do NOT call define_cong_common_helpers (the + // aggregator) — kambites-helpers.hpp:128-133 documents that + // non_trivial_classes(Kambites, Kambites) is intentionally undefined + // upstream because both Kambites instances always represent infinite-class + // congruences, so the construction does not generalize. ADL would silently + // bind the generic congruence_common::non_trivial_classes here, producing + // nonsense at runtime. The Julia wrapper provides a throwing override. + define_cong_common_word_helpers(m); + define_cong_common_normal_forms(m); + } + +} // namespace libsemigroups_julia From f6722acf75412cc55437966daf206972128ca579 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 14:16:30 +0100 Subject: [PATCH 02/12] fix: to_human_readable_repr on type --- deps/src/kambites.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp index 68381c0..ed7af33 100644 --- a/deps/src/kambites.cpp +++ b/deps/src/kambites.cpp @@ -25,7 +25,6 @@ // see the include-order requirement documented at cong-common.hpp:27-38. #include #include -#include #include "cong-common.hpp" @@ -96,10 +95,7 @@ namespace libsemigroups_julia { return self.number_of_generating_pairs(); }); - // success / number_of_classes - type.method("success", - [](K const& self) -> bool { return self.success(); }); - + // number_of_classes type.method("number_of_classes", [](K& self) -> uint64_t { return self.number_of_classes(); }); @@ -131,7 +127,7 @@ namespace libsemigroups_julia { }); // Display - m.method("to_human_readable_repr", [](K const& self) -> std::string { + type.method("to_human_readable_repr", [](K const& self) -> std::string { return libsemigroups::to_human_readable_repr(self); }); From 121a109670ae2143f8b688b7b204e70538b97b4b Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 14:30:54 +0100 Subject: [PATCH 03/12] feat: bind Kambites C++ glue --- deps/src/CMakeLists.txt | 1 + deps/src/kambites.cpp | 7 +++++++ deps/src/libsemigroups_julia.cpp | 1 + deps/src/libsemigroups_julia.hpp | 1 + 4 files changed, 10 insertions(+) diff --git a/deps/src/CMakeLists.txt b/deps/src/CMakeLists.txt index 4f33dda..791fcb6 100644 --- a/deps/src/CMakeLists.txt +++ b/deps/src/CMakeLists.txt @@ -60,6 +60,7 @@ add_library(libsemigroups_julia SHARED presentation-examples.cpp knuth-bendix.cpp todd-coxeter.cpp + kambites.cpp ) # Include directories diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp index ed7af33..183e681 100644 --- a/deps/src/kambites.cpp +++ b/deps/src/kambites.cpp @@ -26,6 +26,13 @@ #include #include +// Required for KambitesNormalFormRange::init to instantiate +// libsemigroups::to(Kambites&); the template definition lives in +// to-froidure-pin.tpp (transitively included by to-froidure-pin.hpp). Without +// this, define_cong_common_normal_forms> compiles but +// fails to link with an undefined `libsemigroups::to` symbol. +#include + #include "cong-common.hpp" #include diff --git a/deps/src/libsemigroups_julia.cpp b/deps/src/libsemigroups_julia.cpp index 94b2d52..e17d477 100644 --- a/deps/src/libsemigroups_julia.cpp +++ b/deps/src/libsemigroups_julia.cpp @@ -55,6 +55,7 @@ namespace libsemigroups_julia { define_presentation_examples(mod); define_knuth_bendix(mod); define_todd_coxeter(mod); + define_kambites(mod); } } // namespace libsemigroups_julia diff --git a/deps/src/libsemigroups_julia.hpp b/deps/src/libsemigroups_julia.hpp index 85b92e1..522f7ec 100644 --- a/deps/src/libsemigroups_julia.hpp +++ b/deps/src/libsemigroups_julia.hpp @@ -70,6 +70,7 @@ namespace libsemigroups_julia { void define_presentation_examples(jl::Module& mod); void define_knuth_bendix(jl::Module& mod); void define_todd_coxeter(jl::Module& mod); + void define_kambites(jl::Module& mod); } // namespace libsemigroups_julia From a2ce4d89bdec96ef1971b41bcfbbe61245cf7180 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 16:36:12 +0100 Subject: [PATCH 04/12] test: add Kambites tests for tdd --- test/runtests.jl | 1 + test/test_kambites.jl | 375 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 test/test_kambites.jl diff --git a/test/runtests.jl b/test/runtests.jl index 7d4a1ee..4aadc90 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -27,4 +27,5 @@ using Semigroups include("test_knuth_bendix_1.jl") include("test_knuth_bendix_6.jl") include("test_todd_coxeter.jl") + include("test_kambites.jl") end diff --git a/test/test_kambites.jl b/test/test_kambites.jl new file mode 100644 index 0000000..ac7714c --- /dev/null +++ b/test/test_kambites.jl @@ -0,0 +1,375 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +test_kambites.jl - Tests for Kambites (Phase 3c of the v1 design). + +Ports a focused subset of [quick] cases from +libsemigroups/tests/test-kambites.cpp, plus binding-surface and high-level +integration tests. Tests that depend on Ukkonen helpers +(`ukkonen::number_of_pieces`, `number_of_distinct_subwords`), FroidurePin +conversion, or long random multi-character string presentations are +deferred or substituted with smaller `word_type`-native equivalents (see +the design spec at +docs/superpowers/specs/2026-04-27-kambites-phase-3c-design.md). + +The binding-surface tests may pass for inherited cong-common methods as +soon as the C++ glue compiles, but the `isdefined(Semigroups, :Kambites)` +gate and all correctness/integration tests will fail until Task 4 lands +the Julia wrapper at `src/kambites.jl`. +""" + +using Test +using Semigroups + +# Phase 3a/3b precedent: alias the low-level CxxWrap type to the public +# Julia name. Until Task 4 adds `const Kambites = LibSemigroups.KambitesWord` +# to `src/Semigroups.jl`, the surface-level `isdefined(Semigroups, :Kambites)` +# assertion below is the RED signal. +const Kambites = Semigroups.LibSemigroups.KambitesWord + +# Build a 1-based Julia word from 0-based libsemigroups indices. +# `_test_kambites_cword(0, 0, 1)` -> `[1, 1, 2]`. Long file-prefixed name +# avoids shadowing in shared test scope. +_test_kambites_cword(xs::Integer...) = [Int(x) + 1 for x in xs] + +@testset "Kambites binding surface" begin + # This is the primary RED gate: the alias only exists once Task 4 adds + # `const Kambites = LibSemigroups.KambitesWord` to `src/Semigroups.jl`. + @test isdefined(Semigroups, :Kambites) + + # Constructors (3 forms): default; (kind, presentation); copy. + @test hasmethod(Kambites, Tuple{}) + @test hasmethod(Kambites, Tuple{congruence_kind,Presentation}) + @test hasmethod(Kambites, Tuple{Kambites}) + + # init! overloads (2 forms): zero-arg and (kind, presentation). + @test hasmethod(init!, Tuple{Kambites}) + @test hasmethod(init!, Tuple{Kambites,congruence_kind,Presentation}) + + # Accessors + @test hasmethod(presentation, Tuple{Kambites}) + @test hasmethod(generating_pairs, Tuple{Kambites}) + @test hasmethod(kind, Tuple{Kambites}) + @test hasmethod(number_of_generating_pairs, Tuple{Kambites}) + @test hasmethod(number_of_classes, Tuple{Kambites}) + + # Runner-inherited + @test hasmethod(success, Tuple{Semigroups.Runner}) + + # small_overlap_class const-overload split + @test hasmethod(small_overlap_class, Tuple{Kambites}) + @test hasmethod(current_small_overlap_class, Tuple{Kambites}) + + # Validators + @test hasmethod(throw_if_not_C4, Tuple{Kambites}) + @test hasmethod( + throw_if_letter_not_in_alphabet, + Tuple{Kambites,AbstractVector{<:Integer}}, + ) + + # Cong-common helpers reachable via CongruenceCommon dispatch. + # `contains` and `reduce` shadow Base, so they stay module-qualified. + @test hasmethod( + add_generating_pair!, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod( + currently_contains, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod( + Semigroups.contains, + Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, + ) + @test hasmethod(Semigroups.reduce, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) + @test hasmethod(reduce_no_run, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) + @test hasmethod(normal_forms, Tuple{CongruenceCommon}) + @test hasmethod( + partition, + Tuple{CongruenceCommon,AbstractVector{<:AbstractVector{<:Integer}}}, + ) + + # Base.* overloads + @test hasmethod(Base.show, Tuple{IO,Kambites}) + @test hasmethod(Base.copy, Tuple{Kambites}) + + # Negative assertion: design-spec deviation from Phase 3a/3b precedent. + # `Base.length` is intentionally NOT defined for Kambites because + # `number_of_classes` is always-infinite-or-throws; defining `length` as + # an alias would silently misbehave with `for i in 1:length(k)`. + @test !hasmethod(Base.length, Tuple{Kambites}) +end + +# ============================================================================ +# correctness tests inspired by test-kambites.cpp +# ============================================================================ + +@testset "Kambites 000 - MT test 4" begin + # Port of libsemigroups Kambites 000 (test-kambites.cpp:148-190). + # Alphabet "abcdefg" -> 1-based [a=1, b=2, c=3, d=4, e=5, f=6, g=7]. + # Rules: abcd = aaaeaa; ef = dg. + # The to(k) and non_trivial_classes(k, StringRange) parts of + # the upstream test are out of scope for Phase 3c. + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!( + p, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + + k = Kambites(twosided, p) + + @test Semigroups.contains( + k, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + @test Semigroups.contains( + k, + _test_kambites_cword(4, 5), + _test_kambites_cword(3, 6), + ) + @test Semigroups.contains( + k, + _test_kambites_cword(0, 0, 0, 0, 0, 4, 5), + _test_kambites_cword(0, 0, 0, 0, 0, 3, 6), + ) + @test Semigroups.contains( + k, + _test_kambites_cword(4, 5, 0, 1, 0, 1, 0), + _test_kambites_cword(3, 6, 0, 1, 0, 1, 0), + ) +end + +@testset "Kambites 002 - small_overlap_class parametric loop" begin + # Port of libsemigroups Kambites 002 (test-kambites.cpp:251-276). + # For i = 4..19, build: + # lhs(i) = concat over b=1..i of (a, b copies of b) + # rhs(i) = concat over b=i+1..2i of (a, b copies of b) + # over alphabet "ab" (1-based: a=1, b=2). + # Assert k.small_overlap_class() == i. + # + # The upstream test also asserts ukkonen::number_of_pieces; that + # part is deferred to v1.1 with the Ukkonen binding. + for i = 4:19 + lhs = Int[] + for b = 1:i + push!(lhs, 1) + for _ = 1:b + push!(lhs, 2) + end + end + rhs = Int[] + for b = (i+1):(2*i) + push!(rhs, 1) + for _ = 1:b + push!(rhs, 2) + end + end + + p = Presentation() + set_alphabet!(p, 2) + add_rule_no_checks!(p, lhs, rhs) + + k = Kambites(twosided, p) + @test small_overlap_class(k) == i + end +end + +@testset "Kambites 005 - smalloverlap/gap/test.gi:85" begin + # Port of libsemigroups Kambites 005 (test-kambites.cpp:476-503), reduced + # to the parts that don't depend on StringRange / number_of_words. + # Alphabet "cab" -> 1-based [c=1, a=2, b=3]. Rule: aabc = acba -> + # [2,2,3,1] = [2,1,3,2]. + p = Presentation() + set_alphabet!(p, 3) + add_rule_no_checks!( + p, + _test_kambites_cword(1, 1, 2, 0), # aabc with c=0,a=1,b=2 0-based + _test_kambites_cword(1, 0, 2, 1), # acba + ) + + k = Kambites(twosided, p) + + @test !Semigroups.contains(k, _test_kambites_cword(1), _test_kambites_cword(2)) + @test Semigroups.contains( + k, + _test_kambites_cword(1, 1, 2, 0, 1, 2, 0), # aabcabc + _test_kambites_cword(1, 1, 2, 0, 0, 2, 1), # aabccba + ) + + @test number_of_classes(k) == POSITIVE_INFINITY +end + +@testset "Kambites 006 - free semigroup" begin + # Port of libsemigroups Kambites 006 (test-kambites.cpp:507-522). + # Empty rule set over alphabet "cab" -> small_overlap_class is + # POSITIVE_INFINITY (i.e. the free semigroup trivially satisfies every + # small overlap condition). + p = Presentation() + set_alphabet!(p, 3) + k = Kambites(twosided, p) + @test small_overlap_class(k) == POSITIVE_INFINITY + + # And again with the smallest non-empty alphabet. + p2 = Presentation() + set_alphabet!(p2, 1) + k2 = Kambites(twosided, p2) + @test small_overlap_class(k2) == POSITIVE_INFINITY +end + +@testset "Kambites 011 - code coverage (negated containment)" begin + # Port of libsemigroups Kambites 011 (test-kambites.cpp:717-715). + # Alphabet "abcde" -> 1-based [a=1, b=2, c=3, d=4, e=5]. + # Rule: cadeca = baedba -> [3,1,4,5,3,1] = [2,1,5,4,2,1]. + p = Presentation() + set_alphabet!(p, 5) + add_rule_no_checks!( + p, + _test_kambites_cword(2, 0, 3, 4, 2, 0), # cadeca + _test_kambites_cword(1, 0, 4, 3, 1, 0), # baedba + ) + + k = Kambites(twosided, p) + + # Upstream: REQUIRE(!contains(k, "cadece", "baedce")). + # cadece = c,a,d,e,c,e -> [3,1,4,5,3,5]; baedce = b,a,e,d,c,e -> [2,1,5,4,3,5]. + @test !Semigroups.contains( + k, + _test_kambites_cword(2, 0, 3, 4, 2, 4), # cadece + _test_kambites_cword(1, 0, 4, 3, 2, 4), # baedce + ) +end + +# ============================================================================ +# high-level integration tests +# ============================================================================ + +@testset "Kambites - end-to-end smoke (run!, finished, success, contains, reduce)" begin + # Reuse the MT4 presentation: small_overlap_class >= 4, so the algorithm + # makes progress and `success` should be true after a run. + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!( + p, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + + k = Kambites(twosided, p) + run!(k) + @test finished(k) + @test success(k) + + @test Semigroups.contains( + k, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + + r = Semigroups.reduce(k, _test_kambites_cword(0, 1, 2, 3)) + @test r isa Vector{Int} + @test Semigroups.contains(k, r, _test_kambites_cword(0, 1, 2, 3)) +end + +@testset "Kambites - normal_forms finite-prefix iteration" begin + # The set of normal forms is infinite for any C(>=4) presentation, so + # the integration test only takes a finite prefix. + # + # NOTE (RED phase, blocks Task 4): the shared + # `normal_forms(::CongruenceCommon)` in `src/cong-common.jl` + # materializes the underlying rx-range eagerly (via the C++ helper at + # `deps/src/cong-common.hpp:125-133`), which would hang forever on an + # infinite Kambites congruence. Task 4 must therefore introduce a + # Kambites-specific lazy `normal_forms` wrapper (or otherwise expose + # the underlying range so `Iterators.take` can short-circuit). The + # assertions below are gated behind `@test_skip` for now so the suite + # does not hang during the RED → GREEN transition; flip to live + # `@test`s once Task 4 lands the lazy wrapper. + @test_skip false # placeholder: flip to actual lazy-iteration assertions in Task 4 +end + +@testset "Kambites - copy round-trip" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!( + p, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + + k = Kambites(twosided, p) + k2 = copy(k) + + # k2 is independent: running k does not change k2's small_overlap_class, + # and the kind / presentation surface remains stable. + @test kind(k2) == twosided + @test small_overlap_class(k2) == small_overlap_class(k) + + run!(k) + @test kind(k2) == twosided +end + +@testset "Kambites - Base.show non-empty" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + k = Kambites(twosided, p) + + s = sprint(show, k) + @test s isa String + @test !isempty(s) +end + +@testset "Kambites - negative cases" begin + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!( + p, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + + # (1) Kambites only accepts twosided congruences upstream. + @test_throws LibsemigroupsError Kambites(Semigroups.onesided, p) + + # (2) non_trivial_classes(k1, k2) is intentionally not provided upstream + # for two Kambites instances because both represent infinite-class + # congruences (kambites-helpers.hpp:128-133). Task 4 adds an + # `ArgumentError`-throwing override on the (Kambites, Kambites) signature; + # until then this falls through to the generic CongruenceCommon dispatch + # and computes nonsense (or errors with something other than ArgumentError), + # which is the RED signal. + k1 = Kambites(twosided, p) + k2 = Kambites(twosided, p) + @test_throws ArgumentError non_trivial_classes(k1, k2) + + # (3) throw_if_not_C4 throws when small_overlap_class < 4. The presentation + # `{aa = b}` over a 2-letter alphabet has small_overlap_class < 4 + # (the rule's left side `aa` is short enough that pieces collapse). + p_low = Presentation() + set_alphabet!(p_low, 2) + add_rule_no_checks!(p_low, _test_kambites_cword(0, 0), _test_kambites_cword(1)) + k_low = Kambites(twosided, p_low) + # First confirm the precondition: the algorithm computes a small overlap + # class strictly below 4 for this presentation. + @test small_overlap_class(k_low) < 4 + @test_throws LibsemigroupsError throw_if_not_C4(k_low) +end + +# ============================================================================ +# TODO - port deferred test-kambites.cpp test cases when their dependencies land: +# - Tests 001, 002 (number_of_pieces parts), 003 (random/long-string presentations) +# depend on the Ukkonen binding (deferred to v1.1). +# - Tests 004, 007-014, ... that exercise to(k) depend on +# Phase 2b / Phase 5 conversions and are deferred. +# - Tests over std::string presentations are deferred to v1.1's +# string-presentation track. +# ============================================================================ From 73e26cc00c3849eed189db20d197521858471217 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 17:05:23 +0100 Subject: [PATCH 05/12] feat: bind Kambites Julia wrapper --- deps/src/kambites.cpp | 22 ++- src/Semigroups.jl | 5 + src/kambites.jl | 333 ++++++++++++++++++++++++++++++++++++++++++ test/test_kambites.jl | 35 +++-- 4 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 src/kambites.jl diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp index 183e681..3fa6b02 100644 --- a/deps/src/kambites.cpp +++ b/deps/src/kambites.cpp @@ -145,8 +145,28 @@ namespace libsemigroups_julia { // congruences, so the construction does not generalize. ADL would silently // bind the generic congruence_common::non_trivial_classes here, producing // nonsense at runtime. The Julia wrapper provides a throwing override. + // + // We also do NOT call define_cong_common_normal_forms(m) — the eager + // template at cong-common.hpp:125-133 drains the entire range, which + // hangs forever on Kambites's infinite KambitesNormalFormRange. The + // Kambites-specific bounded binding `kambites_normal_forms_take` below + // materializes only the first `n` normal forms. define_cong_common_word_helpers(m); - define_cong_common_normal_forms(m); + + // Bounded normal_forms binding (Kambites-specific). Mirrors the cong-common + // normal_forms template but caps iteration at n elements so callers can + // safely take a finite prefix of the infinite normal-form range. + m.method("kambites_normal_forms_take", + [](K& self, size_t n) -> std::vector { + std::vector result; + result.reserve(n); + auto range = libsemigroups::congruence_common::normal_forms(self); + for (size_t i = 0; i < n && !range.at_end(); ++i) { + result.push_back(range.get()); + range.next(); + } + return result; + }); } } // namespace libsemigroups_julia diff --git a/src/Semigroups.jl b/src/Semigroups.jl index 9d143e7..1d79ada 100644 --- a/src/Semigroups.jl +++ b/src/Semigroups.jl @@ -83,6 +83,7 @@ include("presentation-examples.jl") include("cong-common.jl") include("knuth-bendix.jl") include("todd-coxeter.jl") +include("kambites.jl") # High-level element types include("bmat8.jl") @@ -375,6 +376,10 @@ export standardize!, is_standardized, current_word_graph, word_graph export current_index_of, word_of, current_word_of export is_non_trivial, tc_redundant_rule +# Kambites +export Kambites +export small_overlap_class, current_small_overlap_class, throw_if_not_C4 + # Transformation types and functions export Transf, PPerm, Perm export degree, rank, image, domain, inverse diff --git a/src/kambites.jl b/src/kambites.jl new file mode 100644 index 0000000..0007706 --- /dev/null +++ b/src/kambites.jl @@ -0,0 +1,333 @@ +# Copyright (c) 2026, James W. Swent +# +# Distributed under the terms of the GPL license version 3. + +""" +kambites.jl - Kambites wrapper (Layer 2 + 3) +""" + +# ============================================================================ +# Type alias +# ============================================================================ + +""" + Kambites + +Type implementing Kambites's algorithm for computing the word problem in +small overlap monoids - finitely presented monoids whose presentation +satisfies the small overlap condition `C(n)` for `n >= 4` (Kambites, +2009). + +A `Kambites` object is constructed from a +[`congruence_kind`](@ref Semigroups.congruence_kind) (must be +[`twosided`](@ref Semigroups.twosided)) and a +[`Presentation`](@ref Semigroups.Presentation), or copied from another +`Kambites`. + +`Kambites` is a subtype of [`CongruenceCommon`](@ref +Semigroups.CongruenceCommon) (and hence of [`Runner`](@ref +Semigroups.Runner)), so all runner methods (`run!`, `run_for!`, +`finished`, `timed_out`, etc.) and most common congruence helpers +(`reduce`, `contains`, `currently_contains`, `add_generating_pair!`, +`partition`) work on `Kambites` objects. + +Two structural deviations from [`KnuthBendix`](@ref Semigroups.KnuthBendix) +and [`ToddCoxeter`](@ref Semigroups.ToddCoxeter) precedent: + +- `Base.length` is intentionally **not** defined for `Kambites`. The + number of classes is always `POSITIVE_INFINITY` (or throws), so an + alias would silently misbehave with `for i in 1:length(k)`. Use + [`number_of_classes`](@ref Semigroups.number_of_classes) explicitly. +- `non_trivial_classes(::Kambites, ::Kambites)` throws `ArgumentError` + (upstream rationale: both Kambites instances always represent + infinite-class congruences, so the construction does not generalize). +- `normal_forms(k::Kambites)` (no-arg) throws `ArgumentError` because + the underlying normal-form range is infinite. Use the bounded form + `normal_forms(k, n)` to materialize the first `n` normal forms. + +# Constructors + + Kambites() -> Kambites + Kambites(kind::congruence_kind, p::Presentation) -> Kambites + Kambites(other::Kambites) -> Kambites + +# Throws + +- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). + +!!! warning "v1 limitation" + v1 of Semigroups.jl binds `Kambites{word_type}` only. String-alphabet + presentations are deferred to v1.1. +""" +const Kambites = LibSemigroups.KambitesWord + +# ============================================================================ +# Initialization +# ============================================================================ + +""" + Kambites(kind::congruence_kind, p::Presentation) -> Kambites + +Construct a `Kambites` from a congruence kind and a presentation. + +This Julia wrapper builds a default `Kambites` and then calls +[`init!`](@ref Semigroups.init!) so that exceptions raised by +libsemigroups surface as [`LibsemigroupsError`](@ref +Semigroups.LibsemigroupsError) (the direct CxxWrap-bound constructor +would surface them as `Base.ErrorException`). + +# Throws + +- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). +""" +function Kambites(kind::congruence_kind, p::Presentation) + k = LibSemigroups.KambitesWord() + init!(k, kind, p) + return k +end + +""" + init!(k::Kambites) -> Kambites + init!(k::Kambites, kind::congruence_kind, p::Presentation) -> Kambites + +Re-initialize `k` so that it is in the state it would have been in +immediately after the corresponding constructor. + +The one-argument form clears the underlying state, putting `k` back +into the same state as a newly default-constructed +[`Kambites`](@ref Semigroups.Kambites). + +The three-argument form reinitializes `k` from `(kind, p)`. + +Returns `k` for chaining. + +# Throws + +- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). +""" +function init!(k::Kambites) + @wrap_libsemigroups_call LibSemigroups.init!(k) + return k +end + +function init!(k::Kambites, kind::congruence_kind, p::Presentation) + @wrap_libsemigroups_call LibSemigroups.init!(k, kind, p) + return k +end + +# ============================================================================ +# Accessors +# ============================================================================ + +""" + presentation(k::Kambites) -> Presentation + +Return a copy of the presentation used to construct `k`. +""" +presentation(k::Kambites) = LibSemigroups.presentation(k) + +""" + generating_pairs(k::Kambites) -> Vector{Vector{Int}} + +Return the generating pairs of `k` as a flat vector of 1-based words. + +Pairs are returned as a flat `Vector` of words, with consecutive entries +forming a pair: `[u1, v1, u2, v2, ...]`. The total length equals +`2 * number_of_generating_pairs(k)`. Words are 1-based `Vector{Int}` +letter indices. +""" +function generating_pairs(k::Kambites) + flat = LibSemigroups.generating_pairs(k) + return [_word_from_cpp(w) for w in flat] +end + +""" + kind(k::Kambites) -> congruence_kind + +Return the kind of congruence represented by `k`. Always +[`twosided`](@ref Semigroups.twosided) for `Kambites`. +""" +kind(k::Kambites) = LibSemigroups.kind(k) + +""" + number_of_generating_pairs(k::Kambites) -> Int + +Return the number of generating pairs added to `k`. +""" +number_of_generating_pairs(k::Kambites) = Int(LibSemigroups.number_of_generating_pairs(k)) + +""" + number_of_classes(k::Kambites) -> UInt64 + +Return the number of congruence classes of `k`. Returns +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) when +[`small_overlap_class`](@ref Semigroups.small_overlap_class) is at least +4. + +# Throws + +- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +""" +number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_of_classes(k) + +# ============================================================================ +# small_overlap_class (const-overload split) +# ============================================================================ + +""" + small_overlap_class(k::Kambites) -> UInt64 + +Return the small overlap class of the presentation underlying `k`. + +This is the greatest positive integer `n` such that the presentation +satisfies the condition `C(n)`, or +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) if no word +occurring in a relation can be written as a product of pieces. + +The return type is `UInt64` (rather than `Int`) because +`POSITIVE_INFINITY` is encoded as `typemax(UInt64) - 1` on the wire and +would not round-trip through `Int`. Use +[`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or +direct comparison `result == POSITIVE_INFINITY`) to detect the infinite +case. This function may trigger computation. + +# See also + +[`current_small_overlap_class`](@ref Semigroups.current_small_overlap_class) +""" +small_overlap_class(k::Kambites) = LibSemigroups.small_overlap_class(k) + +""" + current_small_overlap_class(k::Kambites) -> Union{UInt64, UndefinedType} + +Return the small overlap class of `k` if it is currently known, or +[`UNDEFINED`](@ref Semigroups.UNDEFINED) otherwise. + +This function does not trigger any computation. The known value is +returned as `UInt64` (see [`small_overlap_class`](@ref +Semigroups.small_overlap_class) for the rationale). + +# See also + +[`small_overlap_class`](@ref Semigroups.small_overlap_class) +""" +function current_small_overlap_class(k::Kambites) + val = LibSemigroups.current_small_overlap_class(k) + return val == typemax(UInt) ? UNDEFINED : val +end + +# ============================================================================ +# Validators +# ============================================================================ + +""" + throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) + +Check that every letter in `w` belongs to the alphabet of the underlying +presentation of `k`. + +# Throws + +- `LibsemigroupsError` if any letter in `w` is not in the alphabet. +""" +function throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) + cpp_w = _word_to_cpp(w) + @wrap_libsemigroups_call LibSemigroups.throw_if_letter_not_in_alphabet(k, cpp_w) + return nothing +end + +""" + throw_if_not_C4(k::Kambites) + +Throw a `LibsemigroupsError` unless the small overlap class of `k` is +at least 4. + +# Throws + +- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +""" +function throw_if_not_C4(k::Kambites) + @wrap_libsemigroups_call LibSemigroups.throw_if_not_C4(k) + return nothing +end + +# ============================================================================ +# Base.* overloads +# ============================================================================ + +""" + Base.show(io::IO, k::Kambites) + +Print a human-readable representation of `k`. +""" +function Base.show(io::IO, k::Kambites) + print(io, LibSemigroups.to_human_readable_repr(k)) +end + +""" + Base.copy(k::Kambites) -> Kambites + +Create an independent copy of `k`. +""" +Base.copy(k::Kambites) = LibSemigroups.KambitesWord(k) + +Base.deepcopy_internal(k::Kambites, ::IdDict) = LibSemigroups.KambitesWord(k) + +# ============================================================================ +# non_trivial_classes override (throws) +# ============================================================================ + +""" + non_trivial_classes(k1::Kambites, k2::Kambites) + +Always throws `ArgumentError` for `Kambites` arguments. + +`non_trivial_classes(Kambites, Kambites)` is intentionally not provided +upstream (see `kambites-helpers.hpp:128-133`) because both Kambites +instances always represent infinite-class congruences, so the +construction does not generalize. +""" +function non_trivial_classes(::Kambites, ::Kambites) + throw(ArgumentError( + "non_trivial_classes(::Kambites, ::Kambites) is intentionally not " * + "supported: both Kambites instances always represent infinite-class " * + "congruences, so the construction does not generalize " * + "(see kambites-helpers.hpp:128-133).", + )) +end + +# ============================================================================ +# normal_forms (bounded; the no-arg form throws) +# ============================================================================ + +""" + normal_forms(k::Kambites, n::Integer) -> Vector{Vector{Int}} + +Return the first `n` normal forms of `k`, as 1-based `Vector{Int}` words. + +The set of normal forms is infinite for any C(>=4) presentation, so the +caller must specify the bound `n`. + +# Throws + +- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +""" +function normal_forms(k::Kambites, n::Integer) + nf = @wrap_libsemigroups_call LibSemigroups.kambites_normal_forms_take(k, UInt(n)) + return [_word_from_cpp(w) for w in nf] +end + +""" + normal_forms(k::Kambites) + +Always throws `ArgumentError`. The set of normal forms of a `Kambites` +is infinite, so the no-argument form is unsafe; use the bounded form +[`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) +instead. +""" +function normal_forms(k::Kambites) + throw(ArgumentError( + "Kambites has infinitely many normal forms; use normal_forms(k, n) " * + "to take the first n.", + )) +end diff --git a/test/test_kambites.jl b/test/test_kambites.jl index ac7714c..224ad7a 100644 --- a/test/test_kambites.jl +++ b/test/test_kambites.jl @@ -279,19 +279,28 @@ end @testset "Kambites - normal_forms finite-prefix iteration" begin # The set of normal forms is infinite for any C(>=4) presentation, so - # the integration test only takes a finite prefix. - # - # NOTE (RED phase, blocks Task 4): the shared - # `normal_forms(::CongruenceCommon)` in `src/cong-common.jl` - # materializes the underlying rx-range eagerly (via the C++ helper at - # `deps/src/cong-common.hpp:125-133`), which would hang forever on an - # infinite Kambites congruence. Task 4 must therefore introduce a - # Kambites-specific lazy `normal_forms` wrapper (or otherwise expose - # the underlying range so `Iterators.take` can short-circuit). The - # assertions below are gated behind `@test_skip` for now so the suite - # does not hang during the RED → GREEN transition; flip to live - # `@test`s once Task 4 lands the lazy wrapper. - @test_skip false # placeholder: flip to actual lazy-iteration assertions in Task 4 + # callers must specify a bound. Task 4 added the Kambites-specific + # `normal_forms(k, n)` binding (a lazy take from the underlying rx + # range) and made the no-arg `normal_forms(k)` form throw + # `ArgumentError` so the inherited eager + # `normal_forms(::CongruenceCommon)` cannot accidentally hang. + p = Presentation() + set_alphabet!(p, 7) + add_rule_no_checks!( + p, + _test_kambites_cword(0, 1, 2, 3), + _test_kambites_cword(0, 0, 0, 4, 0, 0), + ) + add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + + k = Kambites(twosided, p) + + nfs = normal_forms(k, 20) + @test length(nfs) == 20 + @test all(w -> w isa Vector{Int}, nfs) + + # The no-arg form must throw, not hang on the infinite range. + @test_throws ArgumentError normal_forms(k) end @testset "Kambites - copy round-trip" begin From aa9f6ff97cbb5e68221a798c51bb9f77ae5fe641 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 17:28:54 +0100 Subject: [PATCH 06/12] feat: add to kambites julia api --- src/kambites.jl | 150 +++++++++++++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 58 deletions(-) diff --git a/src/kambites.jl b/src/kambites.jl index 0007706..b0efddf 100644 --- a/src/kambites.jl +++ b/src/kambites.jl @@ -14,50 +14,61 @@ kambites.jl - Kambites wrapper (Layer 2 + 3) Kambites Type implementing Kambites's algorithm for computing the word problem in -small overlap monoids - finitely presented monoids whose presentation +small overlap monoids — finitely presented monoids whose presentation satisfies the small overlap condition `C(n)` for `n >= 4` (Kambites, 2009). A `Kambites` object is constructed from a -[`congruence_kind`](@ref Semigroups.congruence_kind) (must be +[`congruence_kind`](@ref Semigroups.congruence_kind) (which must be [`twosided`](@ref Semigroups.twosided)) and a [`Presentation`](@ref Semigroups.Presentation), or copied from another `Kambites`. -`Kambites` is a subtype of [`CongruenceCommon`](@ref -Semigroups.CongruenceCommon) (and hence of [`Runner`](@ref -Semigroups.Runner)), so all runner methods (`run!`, `run_for!`, -`finished`, `timed_out`, etc.) and most common congruence helpers -(`reduce`, `contains`, `currently_contains`, `add_generating_pair!`, -`partition`) work on `Kambites` objects. +`Kambites` is a subtype of +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) (and hence of +[`Runner`](@ref Semigroups.Runner)), so all runner methods (`run!`, +`run_for!`, `finished`, `timed_out`, etc.) and most common congruence +helpers ([`reduce`](@ref Semigroups.reduce), +[`contains`](@ref Semigroups.contains), +[`currently_contains`](@ref Semigroups.currently_contains), +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!), +[`partition`](@ref Semigroups.partition)) work on `Kambites` objects. -Two structural deviations from [`KnuthBendix`](@ref Semigroups.KnuthBendix) -and [`ToddCoxeter`](@ref Semigroups.ToddCoxeter) precedent: +# Structural deviations from `KnuthBendix` / `ToddCoxeter` precedent - `Base.length` is intentionally **not** defined for `Kambites`. The - number of classes is always `POSITIVE_INFINITY` (or throws), so an - alias would silently misbehave with `for i in 1:length(k)`. Use - [`number_of_classes`](@ref Semigroups.number_of_classes) explicitly. -- `non_trivial_classes(::Kambites, ::Kambites)` throws `ArgumentError` - (upstream rationale: both Kambites instances always represent - infinite-class congruences, so the construction does not generalize). -- `normal_forms(k::Kambites)` (no-arg) throws `ArgumentError` because - the underlying normal-form range is infinite. Use the bounded form - `normal_forms(k, n)` to materialize the first `n` normal forms. + number of classes is always + [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) (or throws), + so an alias would silently misbehave with `for i in 1:length(k)`. + Use [`number_of_classes`](@ref Semigroups.number_of_classes) + explicitly. +- [`non_trivial_classes`](@ref Semigroups.non_trivial_classes)`(k1::Kambites, k2::Kambites)` + throws `ArgumentError` (upstream rationale: both `Kambites` instances + always represent infinite-class congruences, so the construction does + not generalize; cf. `kambites-helpers.hpp:128-133`). +- [`normal_forms`](@ref Semigroups.normal_forms)`(k::Kambites)` (no-arg) + throws `ArgumentError` because the underlying normal-form range is + infinite. Use the bounded form `normal_forms(k, n)` to materialize + the first `n` normal forms. +- The `ukkonen()` accessor (and the `Ukkonen` type) is deferred to a + later release; see the v1 design spec + (`docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`). # Constructors Kambites() -> Kambites Kambites(kind::congruence_kind, p::Presentation) -> Kambites - Kambites(other::Kambites) -> Kambites + Kambites(k::Kambites) -> Kambites # Throws -- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided). !!! warning "v1 limitation" v1 of Semigroups.jl binds `Kambites{word_type}` only. String-alphabet - presentations are deferred to v1.1. + presentations are deferred to v1.1. See the v1 design spec at + `docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`. """ const Kambites = LibSemigroups.KambitesWord @@ -68,17 +79,19 @@ const Kambites = LibSemigroups.KambitesWord """ Kambites(kind::congruence_kind, p::Presentation) -> Kambites -Construct a `Kambites` from a congruence kind and a presentation. +Construct a [`Kambites`](@ref Semigroups.Kambites) from a congruence kind +and a presentation. This Julia wrapper builds a default `Kambites` and then calls [`init!`](@ref Semigroups.init!) so that exceptions raised by -libsemigroups surface as [`LibsemigroupsError`](@ref -Semigroups.LibsemigroupsError) (the direct CxxWrap-bound constructor -would surface them as `Base.ErrorException`). +libsemigroups surface as +[`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) (the direct +CxxWrap-bound constructor would surface them as `Base.ErrorException`). # Throws -- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided). """ function Kambites(kind::congruence_kind, p::Presentation) k = LibSemigroups.KambitesWord() @@ -103,7 +116,8 @@ Returns `k` for chaining. # Throws -- `LibsemigroupsError` if `kind` is not [`twosided`](@ref Semigroups.twosided). +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` + is not [`twosided`](@ref Semigroups.twosided). """ function init!(k::Kambites) @wrap_libsemigroups_call LibSemigroups.init!(k) @@ -122,37 +136,45 @@ end """ presentation(k::Kambites) -> Presentation -Return a copy of the presentation used to construct `k`. +Return a copy of the [`Presentation`](@ref Semigroups.Presentation) used +to construct `k`. """ presentation(k::Kambites) = LibSemigroups.presentation(k) """ - generating_pairs(k::Kambites) -> Vector{Vector{Int}} + generating_pairs(k::Kambites) -> Vector{Tuple{Vector{Int}, Vector{Int}}} -Return the generating pairs of `k` as a flat vector of 1-based words. +Return the generating pairs of `k` as a vector of 1-based word pairs. -Pairs are returned as a flat `Vector` of words, with consecutive entries -forming a pair: `[u1, v1, u2, v2, ...]`. The total length equals -`2 * number_of_generating_pairs(k)`. Words are 1-based `Vector{Int}` -letter indices. +These are the pairs added via +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!). Each pair +`(u, v)` is returned as a 2-tuple of 1-based `Vector{Int}` letter +indices. The length of the returned vector equals +[`number_of_generating_pairs`](@ref Semigroups.number_of_generating_pairs). """ function generating_pairs(k::Kambites) flat = LibSemigroups.generating_pairs(k) - return [_word_from_cpp(w) for w in flat] + result = Tuple{Vector{Int},Vector{Int}}[] + for i = 1:2:length(flat) + push!(result, (_word_from_cpp(flat[i]), _word_from_cpp(flat[i+1]))) + end + return result end """ kind(k::Kambites) -> congruence_kind Return the kind of congruence represented by `k`. Always -[`twosided`](@ref Semigroups.twosided) for `Kambites`. +[`twosided`](@ref Semigroups.twosided) for [`Kambites`](@ref +Semigroups.Kambites). """ kind(k::Kambites) = LibSemigroups.kind(k) """ number_of_generating_pairs(k::Kambites) -> Int -Return the number of generating pairs added to `k`. +Return the number of generating pairs added to `k`. Equals the length of +[`generating_pairs`](@ref Semigroups.generating_pairs)`(k)`. """ number_of_generating_pairs(k::Kambites) = Int(LibSemigroups.number_of_generating_pairs(k)) @@ -162,11 +184,14 @@ number_of_generating_pairs(k::Kambites) = Int(LibSemigroups.number_of_generating Return the number of congruence classes of `k`. Returns [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) when [`small_overlap_class`](@ref Semigroups.small_overlap_class) is at least -4. +4. Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or +direct comparison `result == POSITIVE_INFINITY`) to detect the infinite +case. # Throws -- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if + `small_overlap_class(k) < 4`. """ number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_of_classes(k) @@ -177,7 +202,8 @@ number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_o """ small_overlap_class(k::Kambites) -> UInt64 -Return the small overlap class of the presentation underlying `k`. +Return the small overlap class of the [`Presentation`](@ref +Semigroups.Presentation) underlying `k`. This is the greatest positive integer `n` such that the presentation satisfies the condition `C(n)`, or @@ -185,11 +211,11 @@ satisfies the condition `C(n)`, or occurring in a relation can be written as a product of pieces. The return type is `UInt64` (rather than `Int`) because -`POSITIVE_INFINITY` is encoded as `typemax(UInt64) - 1` on the wire and -would not round-trip through `Int`. Use -[`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or -direct comparison `result == POSITIVE_INFINITY`) to detect the infinite -case. This function may trigger computation. +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) is encoded as +`typemax(UInt64) - 1` on the wire and would not round-trip through +`Int`. Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) +(or direct comparison `result == POSITIVE_INFINITY`) to detect the +infinite case. This function may trigger computation. # See also @@ -224,11 +250,13 @@ end throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) Check that every letter in `w` belongs to the alphabet of the underlying -presentation of `k`. +[`Presentation`](@ref Semigroups.Presentation) of `k`. Letters in `w` +are 1-based indices. # Throws -- `LibsemigroupsError` if any letter in `w` is not in the alphabet. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if any + letter in `w` is not in the alphabet. """ function throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) cpp_w = _word_to_cpp(w) @@ -239,12 +267,14 @@ end """ throw_if_not_C4(k::Kambites) -Throw a `LibsemigroupsError` unless the small overlap class of `k` is -at least 4. +Throw a [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) unless +the [`small_overlap_class`](@ref Semigroups.small_overlap_class) of `k` +is at least 4. # Throws -- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if + `small_overlap_class(k) < 4`. """ function throw_if_not_C4(k::Kambites) @wrap_libsemigroups_call LibSemigroups.throw_if_not_C4(k) @@ -267,7 +297,7 @@ end """ Base.copy(k::Kambites) -> Kambites -Create an independent copy of `k`. +Create an independent copy of `k` via the C++ copy constructor. """ Base.copy(k::Kambites) = LibSemigroups.KambitesWord(k) @@ -280,10 +310,11 @@ Base.deepcopy_internal(k::Kambites, ::IdDict) = LibSemigroups.KambitesWord(k) """ non_trivial_classes(k1::Kambites, k2::Kambites) -Always throws `ArgumentError` for `Kambites` arguments. +Always throws `ArgumentError` for [`Kambites`](@ref Semigroups.Kambites) +arguments. `non_trivial_classes(Kambites, Kambites)` is intentionally not provided -upstream (see `kambites-helpers.hpp:128-133`) because both Kambites +upstream (see `kambites-helpers.hpp:128-133`) because both `Kambites` instances always represent infinite-class congruences, so the construction does not generalize. """ @@ -305,15 +336,18 @@ end Return the first `n` normal forms of `k`, as 1-based `Vector{Int}` words. -The set of normal forms is infinite for any C(>=4) presentation, so the -caller must specify the bound `n`. +The set of normal forms is infinite for any `C(>=4)` presentation, so +the caller must specify the bound `n`. # Throws -- `LibsemigroupsError` if `small_overlap_class(k) < 4`. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if + `small_overlap_class(k) < 4`. +- `InexactError` if `n` is negative. """ function normal_forms(k::Kambites, n::Integer) - nf = @wrap_libsemigroups_call LibSemigroups.kambites_normal_forms_take(k, UInt(n)) + cpp_n = UInt(n) + nf = @wrap_libsemigroups_call LibSemigroups.kambites_normal_forms_take(k, cpp_n) return [_word_from_cpp(w) for w in nf] end From 08fb90c4854fd87b582c0942af47b6e5415e3c6a Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 17:55:07 +0100 Subject: [PATCH 07/12] docs: add Kambites Documenter.jl page --- docs/make.jl | 4 + docs/src/main-algorithms/kambites/index.md | 62 +++++++ docs/src/main-algorithms/kambites/kambites.md | 154 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 docs/src/main-algorithms/kambites/index.md create mode 100644 docs/src/main-algorithms/kambites/kambites.md diff --git a/docs/make.jl b/docs/make.jl index 04fb924..6226c81 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -78,6 +78,10 @@ makedocs(; "The KnuthBendix type" => "main-algorithms/knuth-bendix/knuth-bendix.md", "Helper functions" => "main-algorithms/knuth-bendix/helpers.md", ], + "Kambites" => [ + "Overview" => "main-algorithms/kambites/index.md", + "The Kambites type" => "main-algorithms/kambites/kambites.md", + ], ], ], warnonly = [:missing_docs, :linkcheck, :cross_references], diff --git a/docs/src/main-algorithms/kambites/index.md b/docs/src/main-algorithms/kambites/index.md new file mode 100644 index 0000000..9cf04fd --- /dev/null +++ b/docs/src/main-algorithms/kambites/index.md @@ -0,0 +1,62 @@ +# Kambites + +This section contains documentation related to the implementation of +Kambites's algorithm for the word problem in small overlap monoids in +Semigroups.jl. + +Kambites's algorithm decides the word problem in a finitely presented +monoid whose presentation satisfies the small overlap condition `C(n)` +for some `n >= 4` (Kambites, 2009). For such presentations the +congruence has infinitely many classes, and a normal form for any input +word can be computed in linear time. + +!!! warning "v1 limitation" + Semigroups.jl v1 binds `Kambites{word_type}` only. String-alphabet + presentations are deferred to v1.1. Letter indices are 1-based + `Int` values throughout the Julia API. + +## Deviations from the Knuth-Bendix / Todd-Coxeter precedent + +The [`Kambites`](@ref Semigroups.Kambites) binding intentionally +deviates from the [`KnuthBendix`](@ref Semigroups.KnuthBendix) and +`ToddCoxeter` precedent in four places. The motivation in every case is +that a `C(>=4)` presentation has infinitely many congruence classes +(and infinitely many normal forms), so APIs that implicitly assume a +finite class count are unsafe. + +- `Base.length` is intentionally **not** defined for `Kambites`. The + number of classes is always + [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) (or throws), + so an alias for `length(k)` would silently misbehave with + `for i in 1:length(k)`. Use + [`number_of_classes`](@ref Semigroups.number_of_classes) explicitly. +- [`non_trivial_classes`](@ref Semigroups.non_trivial_classes)`(::Kambites, ::Kambites)` + throws `ArgumentError` (mirroring upstream libsemigroups, which + declines to provide this overload because both arguments always + represent infinite-class congruences). +- [`normal_forms`](@ref Semigroups.normal_forms)`(::Kambites)` (no-arg) + throws `ArgumentError` because the underlying normal-form range is + infinite. Use the bounded form + [`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) + to materialize the first `n` normal forms. +- The `ukkonen()` accessor (and the `Ukkonen` suffix-tree type itself) + is deferred to a later release; see the v1 design spec + (`docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`). + +## Contents + +| Page | Description | +| ---- | ----------- | +| [The Kambites type](kambites.md) | The main [`Kambites`](@ref Semigroups.Kambites) type: construction, queries, small-overlap-class accessors, validators, and bounded normal forms. | + +There is no Kambites-specific helper namespace in Semigroups.jl. The +shared word-operation and class-enumeration helpers +([`reduce`](@ref Semigroups.reduce(::CongruenceCommon, ::AbstractVector{<:Integer})), +[`contains`](@ref Semigroups.contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`currently_contains`](@ref Semigroups.currently_contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`partition`](@ref Semigroups.partition(::CongruenceCommon, ::AbstractVector{<:AbstractVector{<:Integer}}))) +are documented on the +[Common congruence helpers](../cong-common-helpers.md) page and apply +uniformly to `Kambites` because it is a subtype of +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon). diff --git a/docs/src/main-algorithms/kambites/kambites.md b/docs/src/main-algorithms/kambites/kambites.md new file mode 100644 index 0000000..8389a95 --- /dev/null +++ b/docs/src/main-algorithms/kambites/kambites.md @@ -0,0 +1,154 @@ +# The Kambites type + +This page documents the [`Kambites`](@ref Semigroups.Kambites) type, +which implements Kambites's algorithm for the word problem in small +overlap monoids — finitely presented monoids whose presentation +satisfies the small overlap condition `C(n)` for some `n >= 4`. + +`Kambites` is a subtype of +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) (and hence of +[`Runner`](@ref Semigroups.Runner)), so all runner methods +([`run!`](@ref), [`run_for!`](@ref), [`finished`](@ref), etc.) and the +shared word-operation helpers ([`reduce`](@ref Semigroups.reduce(::CongruenceCommon, ::AbstractVector{<:Integer})), +[`contains`](@ref Semigroups.contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`currently_contains`](@ref Semigroups.currently_contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), +[`partition`](@ref Semigroups.partition(::CongruenceCommon, ::AbstractVector{<:AbstractVector{<:Integer}}))) +are available. + +## Table of contents + +| Section | Description | +| ------- | ----------- | +| [Construction and re-initialization](#Construction-and-re-initialization) | Constructors and `init!`. | +| [Queries](#Queries) | Class count and number of generating pairs. | +| [Presentation and generating pairs](#Presentation-and-generating-pairs) | Access the underlying presentation and extra generating pairs. | +| [Small overlap class](#Small-overlap-class) | Compute or read the cached small overlap class `C(n)` of the underlying presentation. | +| [Validators](#Validators) | Throw on invalid letters or insufficient small overlap class. | +| [Normal forms](#Normal-forms) | Bounded enumeration of normal forms (the unbounded form throws). | +| [Non-trivial classes (always throws)](#Non-trivial-classes-always-throws) | Why `non_trivial_classes(::Kambites, ::Kambites)` is intentionally not provided. | +| [Display and copy](#Display-and-copy) | `show`, `copy`. | + +```@docs +Semigroups.Kambites +``` + +## Construction and re-initialization + +| Function | Description | +| -------- | ----------- | +| `Kambites()` | Construct a default `Kambites`; throws on subsequent use until reinitialized via [`init!`](@ref). | +| `Kambites(kind, p)` | Construct from a [`congruence_kind`](@ref Semigroups.congruence_kind) (must be [`twosided`](@ref Semigroups.twosided)) and a [`Presentation`](@ref Semigroups.Presentation). | +| `Kambites(other)` | Copy an existing `Kambites`. | +| [`init!(k)`](@ref Semigroups.init!(::Kambites)) | Reset to default-constructed state, or reinitialize from a new kind and presentation. | + +```@docs +Semigroups.init!(::Kambites) +``` + +## Queries + +| Function | Description | +| -------- | ----------- | +| [`number_of_classes(k)`](@ref Semigroups.number_of_classes(::Kambites)) | Number of congruence classes; returns [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) when `small_overlap_class(k) >= 4`. | +| [`number_of_generating_pairs(k)`](@ref Semigroups.number_of_generating_pairs(::Kambites)) | Number of extra generating pairs. | + +```@docs +Semigroups.number_of_classes(::Kambites) +Semigroups.number_of_generating_pairs(::Kambites) +``` + +## Presentation and generating pairs + +| Function | Description | +| -------- | ----------- | +| [`kind(k)`](@ref Semigroups.kind(::Kambites)) | Congruence kind (always `twosided`). | +| [`presentation(k)`](@ref Semigroups.presentation(::Kambites)) | Copy of the underlying presentation. | +| [`generating_pairs(k)`](@ref Semigroups.generating_pairs(::Kambites)) | Extra generating pairs as 1-based word-tuple pairs. | + +```@docs +Semigroups.kind(::Kambites) +Semigroups.presentation(::Kambites) +Semigroups.generating_pairs(::Kambites) +``` + +## Small overlap class + +The small overlap class of the underlying presentation is the largest +`n` such that the presentation satisfies the condition `C(n)`. Kambites's +algorithm decides the word problem when this class is at least `4`. + +| Function | Description | +| -------- | ----------- | +| [`small_overlap_class(k)`](@ref Semigroups.small_overlap_class(::Kambites)) | Compute the small overlap class (may trigger work). | +| [`current_small_overlap_class(k)`](@ref Semigroups.current_small_overlap_class(::Kambites)) | Return the cached value, or [`UNDEFINED`](@ref Semigroups.UNDEFINED) if not yet computed. | + +```@docs +Semigroups.small_overlap_class(::Kambites) +Semigroups.current_small_overlap_class(::Kambites) +``` + +## Validators + +| Function | Description | +| -------- | ----------- | +| [`throw_if_not_C4(k)`](@ref Semigroups.throw_if_not_C4(::Kambites)) | Throw if the small overlap class is less than `4`. | +| [`throw_if_letter_not_in_alphabet(k, w)`](@ref Semigroups.throw_if_letter_not_in_alphabet(::Kambites, ::AbstractVector{<:Integer})) | Throw if `w` contains any letter that is not in the alphabet of `k`'s presentation. | + +```@docs +Semigroups.throw_if_not_C4(::Kambites) +Semigroups.throw_if_letter_not_in_alphabet(::Kambites, ::AbstractVector{<:Integer}) +``` + +## Normal forms + +For `Kambites`, the set of normal forms is infinite, so only the +bounded form `normal_forms(k, n)` is provided. The no-argument form +[`normal_forms(k)`](@ref Semigroups.normal_forms(::Kambites)) throws an +`ArgumentError` to prevent accidental infinite enumeration. + +| Function | Description | +| -------- | ----------- | +| [`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) | Return the first `n` normal forms as 1-based `Vector{Int}` words. | +| [`normal_forms(k)`](@ref Semigroups.normal_forms(::Kambites)) | Always throws `ArgumentError`. | + +```@docs +Semigroups.normal_forms(::Kambites, ::Integer) +Semigroups.normal_forms(::Kambites) +``` + +## Non-trivial classes (always throws) + +```@docs +Semigroups.non_trivial_classes(::Kambites, ::Kambites) +``` + +## Word operations + +These functions are defined on +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) and work on all +congruence types, including `Kambites`. Words are given and returned +as 1-based `Vector{Int}` letter indices. See the +[Common congruence helpers](../cong-common-helpers.md) page for the +full API. + +!!! note + `Semigroups.reduce` and `Semigroups.contains` are not exported to + avoid shadowing `Base.reduce` and `Base.contains`. Use the + module-qualified form: `Semigroups.reduce(k, w)`, + `Semigroups.contains(k, u, v)`. + +| Function | Description | +| -------- | ----------- | +| `Semigroups.reduce(k, w)` | Reduce a word to normal form (triggers a full run). | +| `Semigroups.contains(k, u, v)` | Test if two words are congruent (triggers a full run). | +| `currently_contains(k, u, v)` | Test containment using current state; returns [`tril`](@ref Semigroups.tril). | +| `add_generating_pair!(k, u, v)` | Add an extra generating pair. | +| `partition(k, ws)` | Partition a list of words into congruence classes. | + +## Display and copy + +```@docs +Base.show(::IO, ::Kambites) +Base.copy(::Kambites) +``` From 7d0718857c5e005e53db54ae65d32b3b3cb5aaca Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 19:35:30 +0100 Subject: [PATCH 08/12] fix: minor fixes to kambites --- deps/src/kambites.cpp | 12 +++++++----- src/kambites.jl | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp index 3fa6b02..765c2c1 100644 --- a/deps/src/kambites.cpp +++ b/deps/src/kambites.cpp @@ -26,11 +26,13 @@ #include #include -// Required for KambitesNormalFormRange::init to instantiate -// libsemigroups::to(Kambites&); the template definition lives in -// to-froidure-pin.tpp (transitively included by to-froidure-pin.hpp). Without -// this, define_cong_common_normal_forms> compiles but -// fails to link with an undefined `libsemigroups::to` symbol. +// Required for `congruence_common::normal_forms(Kambites&)` (used by the +// `kambites_normal_forms_take` binding below). The returned +// `KambitesNormalFormRange::init` calls `libsemigroups::to(k)`, +// whose template definition lives in to-froidure-pin.tpp (transitively +// included by to-froidure-pin.hpp). Without this header, the binding compiles +// but fails to link with an undefined `libsemigroups::to` +// symbol. #include #include "cong-common.hpp" diff --git a/src/kambites.jl b/src/kambites.jl index b0efddf..fe2ad69 100644 --- a/src/kambites.jl +++ b/src/kambites.jl @@ -239,7 +239,7 @@ Semigroups.small_overlap_class) for the rationale). """ function current_small_overlap_class(k::Kambites) val = LibSemigroups.current_small_overlap_class(k) - return val == typemax(UInt) ? UNDEFINED : val + return val == convert(UInt, UNDEFINED) ? UNDEFINED : val end # ============================================================================ From c5905a43151b83ea52bcdd6aeec38796c6c6cc1e Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 21:47:18 +0100 Subject: [PATCH 09/12] chore: remove dev tests --- test/test_kambites.jl | 325 +++++++++--------------------------------- 1 file changed, 66 insertions(+), 259 deletions(-) diff --git a/test/test_kambites.jl b/test/test_kambites.jl index 224ad7a..ddea463 100644 --- a/test/test_kambites.jl +++ b/test/test_kambites.jl @@ -3,172 +3,50 @@ # Distributed under the terms of the GPL license version 3. """ -test_kambites.jl - Tests for Kambites (Phase 3c of the v1 design). - -Ports a focused subset of [quick] cases from -libsemigroups/tests/test-kambites.cpp, plus binding-surface and high-level -integration tests. Tests that depend on Ukkonen helpers -(`ukkonen::number_of_pieces`, `number_of_distinct_subwords`), FroidurePin -conversion, or long random multi-character string presentations are -deferred or substituted with smaller `word_type`-native equivalents (see -the design spec at -docs/superpowers/specs/2026-04-27-kambites-phase-3c-design.md). - -The binding-surface tests may pass for inherited cong-common methods as -soon as the C++ glue compiles, but the `isdefined(Semigroups, :Kambites)` -gate and all correctness/integration tests will fail until Task 4 lands -the Julia wrapper at `src/kambites.jl`. +test_kambites.jl - Tests for the Kambites Julia API. """ +# TODO: Lots of improvements should be made to the +# comprehensiveness of these tests. This was made to +# validate the development process only. + using Test using Semigroups -# Phase 3a/3b precedent: alias the low-level CxxWrap type to the public -# Julia name. Until Task 4 adds `const Kambites = LibSemigroups.KambitesWord` -# to `src/Semigroups.jl`, the surface-level `isdefined(Semigroups, :Kambites)` -# assertion below is the RED signal. -const Kambites = Semigroups.LibSemigroups.KambitesWord - -# Build a 1-based Julia word from 0-based libsemigroups indices. -# `_test_kambites_cword(0, 0, 1)` -> `[1, 1, 2]`. Long file-prefixed name -# avoids shadowing in shared test scope. -_test_kambites_cword(xs::Integer...) = [Int(x) + 1 for x in xs] - -@testset "Kambites binding surface" begin - # This is the primary RED gate: the alias only exists once Task 4 adds - # `const Kambites = LibSemigroups.KambitesWord` to `src/Semigroups.jl`. - @test isdefined(Semigroups, :Kambites) - - # Constructors (3 forms): default; (kind, presentation); copy. - @test hasmethod(Kambites, Tuple{}) - @test hasmethod(Kambites, Tuple{congruence_kind,Presentation}) - @test hasmethod(Kambites, Tuple{Kambites}) - - # init! overloads (2 forms): zero-arg and (kind, presentation). - @test hasmethod(init!, Tuple{Kambites}) - @test hasmethod(init!, Tuple{Kambites,congruence_kind,Presentation}) - - # Accessors - @test hasmethod(presentation, Tuple{Kambites}) - @test hasmethod(generating_pairs, Tuple{Kambites}) - @test hasmethod(kind, Tuple{Kambites}) - @test hasmethod(number_of_generating_pairs, Tuple{Kambites}) - @test hasmethod(number_of_classes, Tuple{Kambites}) - - # Runner-inherited - @test hasmethod(success, Tuple{Semigroups.Runner}) - - # small_overlap_class const-overload split - @test hasmethod(small_overlap_class, Tuple{Kambites}) - @test hasmethod(current_small_overlap_class, Tuple{Kambites}) - - # Validators - @test hasmethod(throw_if_not_C4, Tuple{Kambites}) - @test hasmethod( - throw_if_letter_not_in_alphabet, - Tuple{Kambites,AbstractVector{<:Integer}}, - ) - - # Cong-common helpers reachable via CongruenceCommon dispatch. - # `contains` and `reduce` shadow Base, so they stay module-qualified. - @test hasmethod( - add_generating_pair!, - Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, - ) - @test hasmethod( - currently_contains, - Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, - ) - @test hasmethod( - Semigroups.contains, - Tuple{CongruenceCommon,AbstractVector{<:Integer},AbstractVector{<:Integer}}, - ) - @test hasmethod(Semigroups.reduce, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) - @test hasmethod(reduce_no_run, Tuple{CongruenceCommon,AbstractVector{<:Integer}}) - @test hasmethod(normal_forms, Tuple{CongruenceCommon}) - @test hasmethod( - partition, - Tuple{CongruenceCommon,AbstractVector{<:AbstractVector{<:Integer}}}, - ) - - # Base.* overloads - @test hasmethod(Base.show, Tuple{IO,Kambites}) - @test hasmethod(Base.copy, Tuple{Kambites}) - - # Negative assertion: design-spec deviation from Phase 3a/3b precedent. - # `Base.length` is intentionally NOT defined for Kambites because - # `number_of_classes` is always-infinite-or-throws; defining `length` as - # an alias would silently misbehave with `for i in 1:length(k)`. - @test !hasmethod(Base.length, Tuple{Kambites}) -end - -# ============================================================================ -# correctness tests inspired by test-kambites.cpp -# ============================================================================ - -@testset "Kambites 000 - MT test 4" begin - # Port of libsemigroups Kambites 000 (test-kambites.cpp:148-190). - # Alphabet "abcdefg" -> 1-based [a=1, b=2, c=3, d=4, e=5, f=6, g=7]. - # Rules: abcd = aaaeaa; ef = dg. - # The to(k) and non_trivial_classes(k, StringRange) parts of - # the upstream test are out of scope for Phase 3c. +@testset "Kambites - construction and small-overlap-class on MT4" begin + # Alphabet "abcdefg" = 1..7. Rules: abcd = aaaeaa; ef = dg. p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!( - p, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) k = Kambites(twosided, p) - @test Semigroups.contains( - k, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - @test Semigroups.contains( - k, - _test_kambites_cword(4, 5), - _test_kambites_cword(3, 6), - ) - @test Semigroups.contains( - k, - _test_kambites_cword(0, 0, 0, 0, 0, 4, 5), - _test_kambites_cword(0, 0, 0, 0, 0, 3, 6), - ) - @test Semigroups.contains( - k, - _test_kambites_cword(4, 5, 0, 1, 0, 1, 0), - _test_kambites_cword(3, 6, 0, 1, 0, 1, 0), - ) + @test kind(k) == twosided + @test small_overlap_class(k) >= 4 + @test number_of_classes(k) == POSITIVE_INFINITY + + @test Semigroups.contains(k, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + @test Semigroups.contains(k, [5, 6], [4, 7]) + @test Semigroups.contains(k, [1, 1, 1, 1, 1, 5, 6], [1, 1, 1, 1, 1, 4, 7]) + @test Semigroups.contains(k, [5, 6, 1, 2, 1, 2, 1], [4, 7, 1, 2, 1, 2, 1]) end -@testset "Kambites 002 - small_overlap_class parametric loop" begin - # Port of libsemigroups Kambites 002 (test-kambites.cpp:251-276). - # For i = 4..19, build: - # lhs(i) = concat over b=1..i of (a, b copies of b) - # rhs(i) = concat over b=i+1..2i of (a, b copies of b) - # over alphabet "ab" (1-based: a=1, b=2). - # Assert k.small_overlap_class() == i. - # - # The upstream test also asserts ukkonen::number_of_pieces; that - # part is deferred to v1.1 with the Ukkonen binding. +@testset "Kambites - small_overlap_class parametric loop" begin + # For i = 4..19, build over alphabet "ab" = [1, 2]: + # lhs(i) = concat over b = 1..i of (1, b copies of 2) + # rhs(i) = concat over b = i+1..2i of (1, b copies of 2) + # The single rule lhs(i) = rhs(i) yields small_overlap_class == i. for i = 4:19 lhs = Int[] for b = 1:i push!(lhs, 1) - for _ = 1:b - push!(lhs, 2) - end + append!(lhs, fill(2, b)) end rhs = Int[] for b = (i+1):(2*i) push!(rhs, 1) - for _ = 1:b - push!(rhs, 2) - end + append!(rhs, fill(2, b)) end p = Presentation() @@ -180,118 +58,67 @@ end end end -@testset "Kambites 005 - smalloverlap/gap/test.gi:85" begin - # Port of libsemigroups Kambites 005 (test-kambites.cpp:476-503), reduced - # to the parts that don't depend on StringRange / number_of_words. - # Alphabet "cab" -> 1-based [c=1, a=2, b=3]. Rule: aabc = acba -> - # [2,2,3,1] = [2,1,3,2]. +@testset "Kambites - aabc = acba over a 3-letter alphabet" begin + # Alphabet [c=1, a=2, b=3]; rule aabc = acba -> [2,2,3,1] = [2,1,3,2]. p = Presentation() set_alphabet!(p, 3) - add_rule_no_checks!( - p, - _test_kambites_cword(1, 1, 2, 0), # aabc with c=0,a=1,b=2 0-based - _test_kambites_cword(1, 0, 2, 1), # acba - ) + add_rule_no_checks!(p, [2, 2, 3, 1], [2, 1, 3, 2]) k = Kambites(twosided, p) - @test !Semigroups.contains(k, _test_kambites_cword(1), _test_kambites_cword(2)) - @test Semigroups.contains( - k, - _test_kambites_cword(1, 1, 2, 0, 1, 2, 0), # aabcabc - _test_kambites_cword(1, 1, 2, 0, 0, 2, 1), # aabccba - ) - + @test !Semigroups.contains(k, [2], [3]) + @test Semigroups.contains(k, [2, 2, 3, 1, 2, 3, 1], [2, 2, 3, 1, 1, 3, 2]) @test number_of_classes(k) == POSITIVE_INFINITY end -@testset "Kambites 006 - free semigroup" begin - # Port of libsemigroups Kambites 006 (test-kambites.cpp:507-522). - # Empty rule set over alphabet "cab" -> small_overlap_class is - # POSITIVE_INFINITY (i.e. the free semigroup trivially satisfies every - # small overlap condition). +@testset "Kambites - free semigroup" begin + # Empty rule set -> small_overlap_class is POSITIVE_INFINITY. p = Presentation() set_alphabet!(p, 3) k = Kambites(twosided, p) @test small_overlap_class(k) == POSITIVE_INFINITY - # And again with the smallest non-empty alphabet. p2 = Presentation() set_alphabet!(p2, 1) k2 = Kambites(twosided, p2) @test small_overlap_class(k2) == POSITIVE_INFINITY end -@testset "Kambites 011 - code coverage (negated containment)" begin - # Port of libsemigroups Kambites 011 (test-kambites.cpp:717-715). - # Alphabet "abcde" -> 1-based [a=1, b=2, c=3, d=4, e=5]. - # Rule: cadeca = baedba -> [3,1,4,5,3,1] = [2,1,5,4,2,1]. +@testset "Kambites - negated containment" begin + # Alphabet [a=1, b=2, c=3, d=4, e=5]; rule cadeca = baedba. p = Presentation() set_alphabet!(p, 5) - add_rule_no_checks!( - p, - _test_kambites_cword(2, 0, 3, 4, 2, 0), # cadeca - _test_kambites_cword(1, 0, 4, 3, 1, 0), # baedba - ) + add_rule_no_checks!(p, [3, 1, 4, 5, 3, 1], [2, 1, 5, 4, 2, 1]) k = Kambites(twosided, p) - # Upstream: REQUIRE(!contains(k, "cadece", "baedce")). - # cadece = c,a,d,e,c,e -> [3,1,4,5,3,5]; baedce = b,a,e,d,c,e -> [2,1,5,4,3,5]. - @test !Semigroups.contains( - k, - _test_kambites_cword(2, 0, 3, 4, 2, 4), # cadece - _test_kambites_cword(1, 0, 4, 3, 2, 4), # baedce - ) + # cadece (= [3,1,4,5,3,5]) is not congruent to baedce (= [2,1,5,4,3,5]). + @test !Semigroups.contains(k, [3, 1, 4, 5, 3, 5], [2, 1, 5, 4, 3, 5]) end -# ============================================================================ -# high-level integration tests -# ============================================================================ - @testset "Kambites - end-to-end smoke (run!, finished, success, contains, reduce)" begin - # Reuse the MT4 presentation: small_overlap_class >= 4, so the algorithm - # makes progress and `success` should be true after a run. p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!( - p, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) k = Kambites(twosided, p) run!(k) @test finished(k) @test success(k) - @test Semigroups.contains( - k, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) + @test Semigroups.contains(k, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) - r = Semigroups.reduce(k, _test_kambites_cword(0, 1, 2, 3)) + r = Semigroups.reduce(k, [1, 2, 3, 4]) @test r isa Vector{Int} - @test Semigroups.contains(k, r, _test_kambites_cword(0, 1, 2, 3)) + @test Semigroups.contains(k, r, [1, 2, 3, 4]) end -@testset "Kambites - normal_forms finite-prefix iteration" begin - # The set of normal forms is infinite for any C(>=4) presentation, so - # callers must specify a bound. Task 4 added the Kambites-specific - # `normal_forms(k, n)` binding (a lazy take from the underlying rx - # range) and made the no-arg `normal_forms(k)` form throw - # `ArgumentError` so the inherited eager - # `normal_forms(::CongruenceCommon)` cannot accidentally hang. +@testset "Kambites - bounded normal_forms" begin p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!( - p, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) k = Kambites(twosided, p) @@ -299,25 +126,19 @@ end @test length(nfs) == 20 @test all(w -> w isa Vector{Int}, nfs) - # The no-arg form must throw, not hang on the infinite range. + # The no-arg form must throw rather than hang on the infinite range. @test_throws ArgumentError normal_forms(k) end @testset "Kambites - copy round-trip" begin p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!( - p, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) k = Kambites(twosided, p) k2 = copy(k) - # k2 is independent: running k does not change k2's small_overlap_class, - # and the kind / presentation surface remains stable. @test kind(k2) == twosided @test small_overlap_class(k2) == small_overlap_class(k) @@ -325,10 +146,10 @@ end @test kind(k2) == twosided end -@testset "Kambites - Base.show non-empty" begin +@testset "Kambites - show" begin p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) + add_rule_no_checks!(p, [5, 6], [4, 7]) k = Kambites(twosided, p) s = sprint(show, k) @@ -336,49 +157,35 @@ end @test !isempty(s) end -@testset "Kambites - negative cases" begin +@testset "Kambites - error paths" begin p = Presentation() set_alphabet!(p, 7) - add_rule_no_checks!( - p, - _test_kambites_cword(0, 1, 2, 3), - _test_kambites_cword(0, 0, 0, 4, 0, 0), - ) - add_rule_no_checks!(p, _test_kambites_cword(4, 5), _test_kambites_cword(3, 6)) - - # (1) Kambites only accepts twosided congruences upstream. + add_rule_no_checks!(p, [1, 2, 3, 4], [1, 1, 1, 5, 1, 1]) + add_rule_no_checks!(p, [5, 6], [4, 7]) + + # Kambites accepts only twosided congruences. @test_throws LibsemigroupsError Kambites(Semigroups.onesided, p) - # (2) non_trivial_classes(k1, k2) is intentionally not provided upstream - # for two Kambites instances because both represent infinite-class - # congruences (kambites-helpers.hpp:128-133). Task 4 adds an - # `ArgumentError`-throwing override on the (Kambites, Kambites) signature; - # until then this falls through to the generic CongruenceCommon dispatch - # and computes nonsense (or errors with something other than ArgumentError), - # which is the RED signal. + # `non_trivial_classes(::Kambites, ::Kambites)` is intentionally + # unsupported (both arguments always represent infinite-class + # congruences); the override throws ArgumentError. k1 = Kambites(twosided, p) k2 = Kambites(twosided, p) @test_throws ArgumentError non_trivial_classes(k1, k2) - # (3) throw_if_not_C4 throws when small_overlap_class < 4. The presentation - # `{aa = b}` over a 2-letter alphabet has small_overlap_class < 4 - # (the rule's left side `aa` is short enough that pieces collapse). + # throw_if_not_C4 throws when small_overlap_class < 4. The presentation + # `{aa = b}` over a 2-letter alphabet has small_overlap_class < 4. p_low = Presentation() set_alphabet!(p_low, 2) - add_rule_no_checks!(p_low, _test_kambites_cword(0, 0), _test_kambites_cword(1)) + add_rule_no_checks!(p_low, [1, 1], [2]) k_low = Kambites(twosided, p_low) - # First confirm the precondition: the algorithm computes a small overlap - # class strictly below 4 for this presentation. @test small_overlap_class(k_low) < 4 @test_throws LibsemigroupsError throw_if_not_C4(k_low) end -# ============================================================================ -# TODO - port deferred test-kambites.cpp test cases when their dependencies land: -# - Tests 001, 002 (number_of_pieces parts), 003 (random/long-string presentations) -# depend on the Ukkonen binding (deferred to v1.1). -# - Tests 004, 007-014, ... that exercise to(k) depend on -# Phase 2b / Phase 5 conversions and are deferred. -# - Tests over std::string presentations are deferred to v1.1's -# string-presentation track. -# ============================================================================ +@testset "Kambites - design constraint: no Base.length" begin + # `Base.length` is intentionally NOT defined for Kambites because + # `number_of_classes` is always-infinite-or-throws; defining `length` + # as an alias would silently misbehave with `for i in 1:length(k)`. + @test !hasmethod(Base.length, Tuple{Kambites}) +end From 2c7954263cbbfbde79ebc4b9b90e7ddef8f04391 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 21:48:56 +0100 Subject: [PATCH 10/12] docs: improve kambites documentation --- deps/src/kambites.cpp | 55 +++++++----- src/kambites.jl | 192 ++++++++++++++++++++++++++++++------------ 2 files changed, 169 insertions(+), 78 deletions(-) diff --git a/deps/src/kambites.cpp b/deps/src/kambites.cpp index 765c2c1..289e78b 100644 --- a/deps/src/kambites.cpp +++ b/deps/src/kambites.cpp @@ -19,6 +19,8 @@ // CRITICAL: libsemigroups_julia.hpp MUST be included first (fmt consteval fix) #include "libsemigroups_julia.hpp" +#include + // kambites-class.hpp and kambites-helpers.hpp MUST come BEFORE cong-common.hpp // so the template bodies in cong-common.hpp see Kambites-specific overloads of // congruence_common helpers. ADL resolves these at template-instantiation time; @@ -65,12 +67,12 @@ namespace libsemigroups_julia { using K = libsemigroups::Kambites; // Type registration - auto type - = m.add_type("KambitesWord", jlcxx::julia_base_type()); + auto type = m.add_type("KambitesWord", + jlcxx::julia_base_type()); // Constructors. Direct registration (no defensive lambda); CxxWrap // converts C++ exceptions through the std::function path for direct - // constructor bindings (Phase 3a/3b precedent). + // constructor bindings. type.constructor<>(); type.constructor const&>(); type.constructor(); // copy ctor @@ -112,22 +114,23 @@ namespace libsemigroups_julia { // small_overlap_class() overloads differ only on receiver const-ness, which // CxxWrap cannot dispatch. Split into two distinctly-named Julia methods. // - // small_overlap_class — mutable variant (calls run, returns the class). + // small_overlap_class -- mutable variant (calls run, returns the class). type.method("small_overlap_class", [](K& self) -> size_t { return self.small_overlap_class(); }); - // current_small_overlap_class — const variant (returns UNDEFINED if + // current_small_overlap_class -- const variant (returns UNDEFINED if // unknown). Receiver-by-const-ref selects the const overload. type.method("current_small_overlap_class", [](K const& self) -> size_t { return self.small_overlap_class(); }); - // throw_if_not_C4 — bind only the mutable overload - // (kambites-class.hpp:801). The const variant (kambites-class.hpp:813) is - // deferred per the design spec. + // throw_if_not_C4 -- bind only the mutable overload, const deferred type.method("throw_if_not_C4", [](K& self) { self.throw_if_not_C4(); }); - // throw_if_letter_not_in_alphabet — accept ArrayRef, build a + // TODO: ukkonen() is intentionally NOT bound: its return type is the + // Ukkonen suffix-tree class, which is currently not bound + + // throw_if_letter_not_in_alphabet -- accept ArrayRef, build a // word_type inside the lambda (mirrors todd-coxeter.cpp:256-260). type.method("throw_if_letter_not_in_alphabet", [](K const& self, jlcxx::ArrayRef w) { @@ -140,21 +143,26 @@ namespace libsemigroups_julia { return libsemigroups::to_human_readable_repr(self); }); - // Cong-common helper subset. Do NOT call define_cong_common_helpers (the - // aggregator) — kambites-helpers.hpp:128-133 documents that - // non_trivial_classes(Kambites, Kambites) is intentionally undefined - // upstream because both Kambites instances always represent infinite-class - // congruences, so the construction does not generalize. ADL would silently - // bind the generic congruence_common::non_trivial_classes here, producing - // nonsense at runtime. The Julia wrapper provides a throwing override. - // - // We also do NOT call define_cong_common_normal_forms(m) — the eager - // template at cong-common.hpp:125-133 drains the entire range, which - // hangs forever on Kambites's infinite KambitesNormalFormRange. The - // Kambites-specific bounded binding `kambites_normal_forms_take` below - // materializes only the first `n` normal forms. + // Cong common helper subset - DO NOT call aggregator define_cong_common_word_helpers(m); + // Defense: register a throwing `cong_common_normal_forms` for + // Kambites so the abstract-supertype dispatch path in + // `src/cong-common.jl::normal_forms(::CongruenceCommon)` raises a clear + // LibsemigroupsError if it is ever reached on a Kambites value (rather + // than CxxWrap's opaque method-not-found error). The user-facing + // `normal_forms(::Kambites)` override in `src/kambites.jl` already + // throws ArgumentError for direct calls; this guards the indirect path. + m.method("cong_common_normal_forms", [](K&) -> std::vector { + throw libsemigroups::LibsemigroupsException( + __FILE__, + __LINE__, + __func__, + "Kambites has infinitely many normal forms; use " + "kambites_normal_forms_take (or normal_forms(k, n) on " + "the Julia side) to materialize a finite prefix."); + }); + // Bounded normal_forms binding (Kambites-specific). Mirrors the cong-common // normal_forms template but caps iteration at n elements so callers can // safely take a finite prefix of the infinite normal-form range. @@ -162,7 +170,8 @@ namespace libsemigroups_julia { [](K& self, size_t n) -> std::vector { std::vector result; result.reserve(n); - auto range = libsemigroups::congruence_common::normal_forms(self); + auto range + = libsemigroups::congruence_common::normal_forms(self); for (size_t i = 0; i < n && !range.at_end(); ++i) { result.push_back(range.get()); range.next(); diff --git a/src/kambites.jl b/src/kambites.jl index fe2ad69..1d5e794 100644 --- a/src/kambites.jl +++ b/src/kambites.jl @@ -13,11 +13,20 @@ kambites.jl - Kambites wrapper (Layer 2 + 3) """ Kambites -Type implementing Kambites's algorithm for computing the word problem in -small overlap monoids — finitely presented monoids whose presentation +Type implementing small overlap class, equality, and normal forms for +small overlap monoids -- finitely presented monoids whose presentation satisfies the small overlap condition `C(n)` for `n >= 4` (Kambites, 2009). +A [`Kambites`](@ref Semigroups.Kambites) instance represents a +congruence on the free monoid or semigroup containing the rules of the +[`Presentation`](@ref Semigroups.Presentation) used to construct the +instance, together with the +[`generating_pairs`](@ref Semigroups.generating_pairs) added via +[`add_generating_pair!`](@ref Semigroups.add_generating_pair!). As such, +generating pairs and presentation rules are interchangeable in the +context of `Kambites` objects. + A `Kambites` object is constructed from a [`congruence_kind`](@ref Semigroups.congruence_kind) (which must be [`twosided`](@ref Semigroups.twosided)) and a @@ -51,8 +60,7 @@ helpers ([`reduce`](@ref Semigroups.reduce), infinite. Use the bounded form `normal_forms(k, n)` to materialize the first `n` normal forms. - The `ukkonen()` accessor (and the `Ukkonen` type) is deferred to a - later release; see the v1 design spec - (`docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`). + later release # Constructors @@ -67,8 +75,7 @@ helpers ([`reduce`](@ref Semigroups.reduce), !!! warning "v1 limitation" v1 of Semigroups.jl binds `Kambites{word_type}` only. String-alphabet - presentations are deferred to v1.1. See the v1 design spec at - `docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`. + presentations are deferred to later versions. """ const Kambites = LibSemigroups.KambitesWord @@ -79,8 +86,15 @@ const Kambites = LibSemigroups.KambitesWord """ Kambites(kind::congruence_kind, p::Presentation) -> Kambites -Construct a [`Kambites`](@ref Semigroups.Kambites) from a congruence kind -and a presentation. +Construct a [`Kambites`](@ref Semigroups.Kambites) instance representing +a congruence of kind `kind` over the semigroup or monoid defined by the +presentation `p`. + +`Kambites` instances can only be used to compute two-sided congruences, +so `kind` must always be [`twosided`](@ref Semigroups.twosided). The +parameter is included for uniformity of interface with +[`KnuthBendix`](@ref Semigroups.KnuthBendix), `ToddCoxeter`, and +`Congruence`. This Julia wrapper builds a default `Kambites` and then calls [`init!`](@ref Semigroups.init!) so that exceptions raised by @@ -90,6 +104,8 @@ CxxWrap-bound constructor would surface them as `Base.ErrorException`). # Throws +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `p` is + not valid. - [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` is not [`twosided`](@ref Semigroups.twosided). """ @@ -106,18 +122,26 @@ end Re-initialize `k` so that it is in the state it would have been in immediately after the corresponding constructor. -The one-argument form clears the underlying state, putting `k` back -into the same state as a newly default-constructed -[`Kambites`](@ref Semigroups.Kambites). +The one-argument form puts `k` back into the same state as a newly +default-constructed [`Kambites`](@ref Semigroups.Kambites). -The three-argument form reinitializes `k` from `(kind, p)`. +The three-argument form puts `k` back into the state it would have been +in if it had just been newly constructed from `kind` and `p`. +`Kambites` instances can only be used to compute two-sided congruences, +so `kind` must always be [`twosided`](@ref Semigroups.twosided); the +parameter is included for uniformity of interface with +[`KnuthBendix`](@ref Semigroups.KnuthBendix), `ToddCoxeter`, and +`Congruence`. Returns `k` for chaining. # Throws +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `p` is + not valid (three-argument form only). - [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if `kind` - is not [`twosided`](@ref Semigroups.twosided). + is not [`twosided`](@ref Semigroups.twosided) (three-argument form + only). """ function init!(k::Kambites) @wrap_libsemigroups_call LibSemigroups.init!(k) @@ -137,7 +161,12 @@ end presentation(k::Kambites) -> Presentation Return a copy of the [`Presentation`](@ref Semigroups.Presentation) used -to construct `k`. +to construct or initialize `k` (if any). + +If `k` was constructed or initialized using a presentation, that +presentation is returned. The Julia binding returns by value (an +independent copy) rather than by reference; mutating the returned +object does not affect `k`. """ presentation(k::Kambites) = LibSemigroups.presentation(k) @@ -164,34 +193,45 @@ end """ kind(k::Kambites) -> congruence_kind -Return the kind of congruence represented by `k`. Always -[`twosided`](@ref Semigroups.twosided) for [`Kambites`](@ref -Semigroups.Kambites). +Return the kind of congruence (one- or two-sided) represented by `k`; +see [`congruence_kind`](@ref Semigroups.congruence_kind) for details. +For [`Kambites`](@ref Semigroups.Kambites) this is always +[`twosided`](@ref Semigroups.twosided), since the constructor enforces +that constraint. + +Complexity: constant. """ kind(k::Kambites) = LibSemigroups.kind(k) """ number_of_generating_pairs(k::Kambites) -> Int -Return the number of generating pairs added to `k`. Equals the length of -[`generating_pairs`](@ref Semigroups.generating_pairs)`(k)`. +Return the number of generating pairs added to `k`. Equals the length +of [`generating_pairs`](@ref Semigroups.generating_pairs)`(k)`. + +Complexity: constant. """ number_of_generating_pairs(k::Kambites) = Int(LibSemigroups.number_of_generating_pairs(k)) """ number_of_classes(k::Kambites) -> UInt64 -Return the number of congruence classes of `k`. Returns -[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) when -[`small_overlap_class`](@ref Semigroups.small_overlap_class) is at least -4. Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or +Compute the number of congruence classes of `k`. + +`Kambites` instances can only compute the number of classes when +[`small_overlap_class`](@ref Semigroups.small_overlap_class) is at +least 4, and in that case the number is always +[`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY); otherwise an +exception is thrown. Use +[`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or direct comparison `result == POSITIVE_INFINITY`) to detect the infinite case. # Throws -- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if - `small_overlap_class(k) < 4`. +- [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if it is + not possible to compute the number of classes because the small + overlap class is too small (`small_overlap_class(k) < 4`). """ number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_of_classes(k) @@ -202,20 +242,39 @@ number_of_classes(k::Kambites) = @wrap_libsemigroups_call LibSemigroups.number_o """ small_overlap_class(k::Kambites) -> UInt64 -Return the small overlap class of the [`Presentation`](@ref +Get the small overlap class of the [`Presentation`](@ref Semigroups.Presentation) underlying `k`. -This is the greatest positive integer `n` such that the presentation -satisfies the condition `C(n)`, or +If ``S`` is a finitely presented semigroup with generating set ``A``, +then a word ``w`` over ``A`` is a *piece* if ``w`` occurs as a factor +in at least two of the relations defining ``S``, or if it occurs as a +factor of one relation in two different positions (possibly +overlapping). A finitely presented semigroup ``S`` satisfies the +condition ``C(n)`` for a positive integer ``n`` if the minimum number +of pieces in any factorisation of a word occurring as the left or +right hand side of a relation is at least ``n``. + +This function returns the greatest positive integer `n` such that the +finitely presented semigroup represented by `k` satisfies the +condition ``C(n)``, or [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) if no word -occurring in a relation can be written as a product of pieces. +occurring in a relation can be written as a product of pieces. It may +trigger computation. The return type is `UInt64` (rather than `Int`) because [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) is encoded as `typemax(UInt64) - 1` on the wire and would not round-trip through `Int`. Use [`is_positive_infinity`](@ref Semigroups.is_positive_infinity) (or direct comparison `result == POSITIVE_INFINITY`) to detect the -infinite case. This function may trigger computation. +infinite case. + +Complexity: ``O(m^3)``, where ``m`` is the sum of the lengths of the +words occurring in the relations of the semigroup. + +!!! warning + [`Semigroups.contains`](@ref Semigroups.contains) and + [`Semigroups.reduce`](@ref Semigroups.reduce) only work if the + return value of this function is at least 4. # See also @@ -226,12 +285,16 @@ small_overlap_class(k::Kambites) = LibSemigroups.small_overlap_class(k) """ current_small_overlap_class(k::Kambites) -> Union{UInt64, UndefinedType} -Return the small overlap class of `k` if it is currently known, or -[`UNDEFINED`](@ref Semigroups.UNDEFINED) otherwise. +Get the current value of the small overlap class of `k`, if known. + +Returns the small overlap class if it has already been computed, or +[`UNDEFINED`](@ref Semigroups.UNDEFINED) otherwise. This function does +not trigger any computation. The known value is returned as `UInt64` +(see [`small_overlap_class`](@ref Semigroups.small_overlap_class) for +the rationale). -This function does not trigger any computation. The known value is -returned as `UInt64` (see [`small_overlap_class`](@ref -Semigroups.small_overlap_class) for the rationale). +See [`small_overlap_class`](@ref Semigroups.small_overlap_class) for +more details on what the small overlap class is. # See also @@ -249,9 +312,18 @@ end """ throw_if_letter_not_in_alphabet(k::Kambites, w::AbstractVector{<:Integer}) -Check that every letter in `w` belongs to the alphabet of the underlying -[`Presentation`](@ref Semigroups.Presentation) of `k`. Letters in `w` -are 1-based indices. +Throw if any letter in `w` is out of bounds. + +This function throws a +[`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) if any +letter of `w` is out of bounds -- i.e. does not belong to the alphabet +of the [`Presentation`](@ref Semigroups.Presentation) used to +construct `k`. Letters in `w` are 1-based indices. + +# Arguments + +- `k::Kambites`: the [`Kambites`](@ref Semigroups.Kambites) instance. +- `w::AbstractVector{<:Integer}`: the word to check. # Throws @@ -267,9 +339,11 @@ end """ throw_if_not_C4(k::Kambites) -Throw a [`LibsemigroupsError`](@ref Semigroups.LibsemigroupsError) unless -the [`small_overlap_class`](@ref Semigroups.small_overlap_class) of `k` -is at least 4. +Throw if the [`small_overlap_class`](@ref Semigroups.small_overlap_class) +of `k` is not at least 4. + +This function throws an exception if the small overlap class of `k` is +not at least 4 (and computes it if necessary). # Throws @@ -288,7 +362,8 @@ end """ Base.show(io::IO, k::Kambites) -Print a human-readable representation of `k`. +Print a human-readable representation of `k`. Delegates to +libsemigroups' `to_human_readable_repr`. """ function Base.show(io::IO, k::Kambites) print(io, LibSemigroups.to_human_readable_repr(k)) @@ -319,12 +394,14 @@ instances always represent infinite-class congruences, so the construction does not generalize. """ function non_trivial_classes(::Kambites, ::Kambites) - throw(ArgumentError( - "non_trivial_classes(::Kambites, ::Kambites) is intentionally not " * - "supported: both Kambites instances always represent infinite-class " * - "congruences, so the construction does not generalize " * - "(see kambites-helpers.hpp:128-133).", - )) + throw( + ArgumentError( + "non_trivial_classes(::Kambites, ::Kambites) is intentionally not " * + "supported: both Kambites instances always represent infinite-class " * + "congruences, so the construction does not generalize " * + "(see kambites-helpers.hpp:128-133).", + ), + ) end # ============================================================================ @@ -334,10 +411,13 @@ end """ normal_forms(k::Kambites, n::Integer) -> Vector{Vector{Int}} -Return the first `n` normal forms of `k`, as 1-based `Vector{Int}` words. +Return the first `n` short-lex normal forms of the classes of the +congruence represented by `k`, as 1-based `Vector{Int}` words. -The set of normal forms is infinite for any `C(>=4)` presentation, so -the caller must specify the bound `n`. +The underlying range of normal forms is always infinite (one per +congruence class, and a `C(>=4)` presentation has infinitely many +classes), so the caller must specify the bound `n`. The bounded form +materializes only the first `n` words and is safe to call. # Throws @@ -360,8 +440,10 @@ is infinite, so the no-argument form is unsafe; use the bounded form instead. """ function normal_forms(k::Kambites) - throw(ArgumentError( - "Kambites has infinitely many normal forms; use normal_forms(k, n) " * - "to take the first n.", - )) + throw( + ArgumentError( + "Kambites has infinitely many normal forms; use normal_forms(k, n) " * + "to take the first n.", + ), + ) end From 4c9ec9706a5a2460700d04926e9795f6398bb90c Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 21:56:34 +0100 Subject: [PATCH 11/12] doc: add congruence_kind to constants docs --- docs/src/data-structures/constants/index.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/data-structures/constants/index.md b/docs/src/data-structures/constants/index.md index 31cdce2..9188591 100644 --- a/docs/src/data-structures/constants/index.md +++ b/docs/src/data-structures/constants/index.md @@ -29,3 +29,11 @@ Semigroups.tril_FALSE Semigroups.tril_unknown Semigroups.tril_to_bool ``` + +## Congruence Kind + +```@docs +Semigroups.congruence_kind +Semigroups.onesided +Semigroups.twosided +``` From ed1ac1472072228df98105013f42dc364f068786 Mon Sep 17 00:00:00 2001 From: James Swent Date: Mon, 27 Apr 2026 22:02:33 +0100 Subject: [PATCH 12/12] docs: kambites doc improvements --- docs/src/main-algorithms/kambites/index.md | 70 ++++--------------- docs/src/main-algorithms/kambites/kambites.md | 2 +- 2 files changed, 15 insertions(+), 57 deletions(-) diff --git a/docs/src/main-algorithms/kambites/index.md b/docs/src/main-algorithms/kambites/index.md index 9cf04fd..3d4fefd 100644 --- a/docs/src/main-algorithms/kambites/index.md +++ b/docs/src/main-algorithms/kambites/index.md @@ -1,62 +1,20 @@ # Kambites -This section contains documentation related to the implementation of -Kambites's algorithm for the word problem in small overlap monoids in -Semigroups.jl. - -Kambites's algorithm decides the word problem in a finitely presented -monoid whose presentation satisfies the small overlap condition `C(n)` -for some `n >= 4` (Kambites, 2009). For such presentations the -congruence has infinitely many classes, and a normal form for any input -word can be computed in linear time. - -!!! warning "v1 limitation" - Semigroups.jl v1 binds `Kambites{word_type}` only. String-alphabet - presentations are deferred to v1.1. Letter indices are 1-based - `Int` values throughout the Julia API. - -## Deviations from the Knuth-Bendix / Todd-Coxeter precedent - -The [`Kambites`](@ref Semigroups.Kambites) binding intentionally -deviates from the [`KnuthBendix`](@ref Semigroups.KnuthBendix) and -`ToddCoxeter` precedent in four places. The motivation in every case is -that a `C(>=4)` presentation has infinitely many congruence classes -(and infinitely many normal forms), so APIs that implicitly assume a -finite class count are unsafe. - -- `Base.length` is intentionally **not** defined for `Kambites`. The - number of classes is always - [`POSITIVE_INFINITY`](@ref Semigroups.POSITIVE_INFINITY) (or throws), - so an alias for `length(k)` would silently misbehave with - `for i in 1:length(k)`. Use - [`number_of_classes`](@ref Semigroups.number_of_classes) explicitly. -- [`non_trivial_classes`](@ref Semigroups.non_trivial_classes)`(::Kambites, ::Kambites)` - throws `ArgumentError` (mirroring upstream libsemigroups, which - declines to provide this overload because both arguments always - represent infinite-class congruences). -- [`normal_forms`](@ref Semigroups.normal_forms)`(::Kambites)` (no-arg) - throws `ArgumentError` because the underlying normal-form range is - infinite. Use the bounded form - [`normal_forms(k, n)`](@ref Semigroups.normal_forms(::Kambites, ::Integer)) - to materialize the first `n` normal forms. -- The `ukkonen()` accessor (and the `Ukkonen` suffix-tree type itself) - is deferred to a later release; see the v1 design spec - (`docs/superpowers/specs/2026-04-19-semigroups-jl-v1-design.md`). - -## Contents +This section links to the documentation for the algorithms in +Semigroups.jl for small overlap monoids by Mark Kambites and the authors +of libsemigroups. | Page | Description | | ---- | ----------- | -| [The Kambites type](kambites.md) | The main [`Kambites`](@ref Semigroups.Kambites) type: construction, queries, small-overlap-class accessors, validators, and bounded normal forms. | +| [The Kambites type](kambites.md) | The [`Kambites`](@ref Semigroups.Kambites) type: construction, queries, the small-overlap-class accessors, validators, and bounded normal forms. | + +Helper functions for [`Kambites`](@ref Semigroups.Kambites) are +documented on the [Common congruence helpers](../cong-common-helpers.md) +page. There are currently no helper functions specific to `Kambites` +beyond those that apply to every +[`CongruenceCommon`](@ref Semigroups.CongruenceCommon) subtype. -There is no Kambites-specific helper namespace in Semigroups.jl. The -shared word-operation and class-enumeration helpers -([`reduce`](@ref Semigroups.reduce(::CongruenceCommon, ::AbstractVector{<:Integer})), -[`contains`](@ref Semigroups.contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), -[`currently_contains`](@ref Semigroups.currently_contains(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), -[`add_generating_pair!`](@ref Semigroups.add_generating_pair!(::CongruenceCommon, ::AbstractVector{<:Integer}, ::AbstractVector{<:Integer})), -[`partition`](@ref Semigroups.partition(::CongruenceCommon, ::AbstractVector{<:AbstractVector{<:Integer}}))) -are documented on the -[Common congruence helpers](../cong-common-helpers.md) page and apply -uniformly to `Kambites` because it is a subtype of -[`CongruenceCommon`](@ref Semigroups.CongruenceCommon). +!!! warning "v1 limitation" + Semigroups.jl v1 binds `Kambites{word_type}` only. String-alphabet + presentations are deferred to a later release. Letter indices are + 1-based `Int` values throughout the Julia API. diff --git a/docs/src/main-algorithms/kambites/kambites.md b/docs/src/main-algorithms/kambites/kambites.md index 8389a95..77de927 100644 --- a/docs/src/main-algorithms/kambites/kambites.md +++ b/docs/src/main-algorithms/kambites/kambites.md @@ -2,7 +2,7 @@ This page documents the [`Kambites`](@ref Semigroups.Kambites) type, which implements Kambites's algorithm for the word problem in small -overlap monoids — finitely presented monoids whose presentation +overlap monoids -- finitely presented monoids whose presentation satisfies the small overlap condition `C(n)` for some `n >= 4`. `Kambites` is a subtype of