Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ pkginclude_HEADERS += include/libsemigroups/word-graph-helpers-deprecated.hpp
pkginclude_HEADERS += include/libsemigroups/word-graph-helpers-deprecated.tpp
pkginclude_HEADERS += include/libsemigroups/word-graph-helpers.hpp
pkginclude_HEADERS += include/libsemigroups/word-graph-helpers.tpp
pkginclude_HEADERS += include/libsemigroups/word-graph-io.hpp
pkginclude_HEADERS += include/libsemigroups/word-graph-io.tpp
pkginclude_HEADERS += include/libsemigroups/word-graph-view-class.hpp
pkginclude_HEADERS += include/libsemigroups/word-graph-view-class.tpp
pkginclude_HEADERS += include/libsemigroups/word-graph-view-helpers-deprecated.hpp
Expand Down Expand Up @@ -211,6 +213,7 @@ detailinclude_HEADERS += include/libsemigroups/detail/iterator.hpp
detailinclude_HEADERS += include/libsemigroups/detail/kambites-nf.hpp
detailinclude_HEADERS += include/libsemigroups/detail/kbe.hpp
detailinclude_HEADERS += include/libsemigroups/detail/kbe.tpp
detailinclude_HEADERS += include/libsemigroups/detail/kbmag.hpp
detailinclude_HEADERS += include/libsemigroups/detail/ke.hpp
detailinclude_HEADERS += include/libsemigroups/detail/knuth-bendix-impl.hpp
detailinclude_HEADERS += include/libsemigroups/detail/knuth-bendix-impl.tpp
Expand Down Expand Up @@ -1130,6 +1133,7 @@ libsemigroups_la_SOURCES += src/words-helpers.cpp
libsemigroups_la_SOURCES += src/detail/aho-corasick-impl.cpp
libsemigroups_la_SOURCES += src/detail/cong-common-class.cpp
libsemigroups_la_SOURCES += src/detail/felsch-tree.cpp
libsemigroups_la_SOURCES += src/detail/kbmag.cpp
libsemigroups_la_SOURCES += src/detail/ke.cpp
libsemigroups_la_SOURCES += src/detail/knuth-bendix-impl.cpp
libsemigroups_la_SOURCES += src/detail/print.cpp
Expand Down Expand Up @@ -1268,6 +1272,7 @@ EXTRA_PROGRAMS += test_types
EXTRA_PROGRAMS += test_uf
EXTRA_PROGRAMS += test_ukkonen
EXTRA_PROGRAMS += test_word_graph
EXTRA_PROGRAMS += test_word_graph_io
EXTRA_PROGRAMS += test_word_graph_view
EXTRA_PROGRAMS += test_word_range

Expand Down Expand Up @@ -1383,6 +1388,7 @@ test_all_SOURCES += tests/test-types.cpp
test_all_SOURCES += tests/test-uf.cpp
test_all_SOURCES += tests/test-ukkonen.cpp
test_all_SOURCES += tests/test-word-graph.cpp
test_all_SOURCES += tests/test-word-graph-io.cpp
test_all_SOURCES += tests/test-word-graph-view.cpp
test_all_SOURCES += tests/test-word-range.cpp
test_all_LDADD = $(LDADD) $(CATCH2_LIBS) $(CATCH2MAIN_LIBS)
Expand Down Expand Up @@ -1777,6 +1783,11 @@ test_word_graph_SOURCES += tests/test-main.cpp
test_word_graph_SOURCES += $(CATCH2_SOURCES)
test_word_graph_LDADD = $(LDADD) $(CATCH2_LIBS) $(CATCH2MAIN_LIBS)

test_word_graph_io_SOURCES = tests/test-word-graph-io.cpp
test_word_graph_io_SOURCES += tests/test-main.cpp
test_word_graph_io_SOURCES += $(CATCH2_SOURCES)
test_word_graph_io_LDADD = $(LDADD) $(CATCH2_LIBS) $(CATCH2MAIN_LIBS)

test_word_graph_view_SOURCES = tests/test-word-graph-view.cpp
test_word_graph_view_SOURCES += tests/test-main.cpp
test_word_graph_view_SOURCES += $(CATCH2_SOURCES)
Expand Down
102 changes: 102 additions & 0 deletions include/libsemigroups/detail/kbmag.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2026 James D. Mitchell
//
// 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 <http://www.gnu.org/licenses/>.
//

// Private streaming parser and buffered writer for KBMAG records.

#ifndef LIBSEMIGROUPS_DETAIL_KBMAG_HPP_
#define LIBSEMIGROUPS_DETAIL_KBMAG_HPP_

#include <cstddef> // for size_t
#include <cstdint> // for uint64_t
#include <iosfwd> // for istream, ostream
#include <string> // for string
#include <string_view> // for string_view
#include <unordered_set> // for unordered_set
#include <vector> // for vector

namespace libsemigroups {
namespace detail {
class KbmagReader {
public:
explicit KbmagReader(std::istream& input);

uint64_t number_of_nodes() const {
return _number_of_nodes;
}
uint64_t out_degree() const {
return _out_degree;
}
std::vector<uint64_t> const& read_row();
void finish();
[[noreturn]] void fail(std::string const& reason) const;

private:
enum class kind { identifier, number, string, punctuation, end };

int get();
int peek();
void next();
bool is(std::string_view value) const;
bool accept(std::string_view value);
void expect(std::string_view value);
uint64_t number(std::string_view description,
bool allow_negative = false);
std::string string();
std::string field(std::unordered_set<std::string>& seen);
void skip_value();
uint64_t read_set(std::string const& name);
void read_flags();
void read_initial();
void read_accepting();
void read_table_field(std::string const& name);
void require_header() const;

std::istream& _input;
size_t _line = 1;
size_t _column = 1;
size_t _token_line = 1;
size_t _token_column = 1;
kind _kind = kind::end;
std::string _token;
std::unordered_set<std::string> _fields;
std::unordered_set<std::string> _table_fields;
uint64_t _number_of_nodes = 0;
uint64_t _out_degree = 0;
uint64_t _source = 0;
uint64_t _default_target = 0;
uint64_t _expected_transitions = 0;
uint64_t _transitions = 0;
bool _sparse = false;
std::vector<uint64_t> _row;
std::vector<bool> _seen;
};

class KbmagWriter {
public:
explicit KbmagWriter(std::ostream& output);
void write(std::string_view text);
void write(uint64_t value);
void flush();

private:
std::ostream& _output;
std::string _buffer;
};
} // namespace detail
} // namespace libsemigroups
#endif // LIBSEMIGROUPS_DETAIL_KBMAG_HPP_
2 changes: 2 additions & 0 deletions include/libsemigroups/libsemigroups.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
#include "word-graph-class.hpp"
#include "word-graph-helpers-deprecated.hpp"
#include "word-graph-helpers.hpp"
#include "word-graph-io.hpp"
#include "word-graph-view-class.hpp"
#include "word-graph-view-helpers-deprecated.hpp"
#include "word-graph-view-helpers.hpp"
Expand All @@ -126,6 +127,7 @@
#include "detail/iterator.hpp"
#include "detail/kambites-nf.hpp"
#include "detail/kbe.hpp"
#include "detail/kbmag.hpp"
#include "detail/ke.hpp"
#include "detail/knuth-bendix-impl.hpp"
#include "detail/knuth-bendix-nf.hpp"
Expand Down
190 changes: 190 additions & 0 deletions include/libsemigroups/word-graph-io.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2026 James D. Mitchell
//
// 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 <http://www.gnu.org/licenses/>.
//

// KBMAG serialization of word graphs.

#ifndef LIBSEMIGROUPS_WORD_GRAPH_IO_HPP_
#define LIBSEMIGROUPS_WORD_GRAPH_IO_HPP_

#include <iosfwd> // for istream, ostream
#include <string> // for string
#include <type_traits> // for enable_if_t

#include "is_specialization_of.hpp" // for is_specialization_of_v
#include "word-graph-class.hpp" // for WordGraph

namespace libsemigroups {
namespace word_graph {
//! \ingroup word_graph_group
//! \brief The representation of a KBMAG transition table.
enum class kbmagformat {
dense, //!< One target per label, with zero for undefined transitions.
sparse //!< A list of (label, target) pairs for each source node.
};

//! \ingroup word_graph_group
//! \brief Writes a word graph as a KBMAG finite-state automaton record.
//!
//! Writes a GAP assignment `word_graph := rec(...);`. States and alphabet
//! letters have type `"simple"`. Node and label numbers are increased by
//! one; dense tables use zero for undefined transitions, and sparse tables
//! omit them. The alphabet size records the out-degree even when some
//! labels have no transitions. Different labels with the same target are
//! preserved. The initial list is `[1]`, or `[]` for a graph with no nodes,
//! and the accepting list is always `[]`.
//!
//! The algorithm counts the defined transitions, then visits nodes and
//! labels in their original order and writes the targets directly, using
//! a bounded output buffer. Including `numTransitions` allows KBMAG tools
//! to read sparse output using sparse storage. The algorithm does
//! not sort edges or copy the graph. Output is independent of the stream's
//! numeric formatting flags and locale.
//!
//! \tparam Node the node type of the word graph.
//! \param output the destination stream.
//! \param wg the word graph to encode.
//! \param format the dense or sparse table representation (defaults to
//! \ref kbmagformat::dense).
//!
//! \throws LibsemigroupsException if a target is out of bounds, the
//! dimensions exceed the node type's range, the format is invalid, or
//! writing fails without the stream throwing its own exception.
//! \throws std::ios_base::failure if enabled by the stream.
//!
//! \complexity
//! \f$O(n(d+1)+L)\f$ time and constant additional space, where \f$n\f$
//! is the number of nodes, \f$d\f$ is the out-degree, and \f$L\f$ is the
//! output length.
//!
//! \note On failure the stream may contain a partial record.
//!
//! \sa make(std::istream&), to_kbmag_string(), and the
//! <a
//! href="https://github.com/gap-packages/kbmag/blob/master/standalone/doc/fsa_format">
//! KBMAG/GASP finite-state automaton specification</a>.
template <typename Node>
void write_kbmag(std::ostream& output,
WordGraph<Node> const& wg,
kbmagformat format = kbmagformat::dense);

//! \ingroup word_graph_group
//! \brief Returns a KBMAG representation of a word graph.
//!
//! Uses the same encoding and conventions as write_kbmag(). For large
//! graphs, write_kbmag() avoids constructing the complete output string.
//!
//! \par Example
//! \code
//! WordGraph<uint32_t> graph(2, 2);
//! graph.target(0, 0, 1);
//! graph.target(0, 1, 1);
//! auto text = word_graph::to_kbmag_string(graph);
//! auto copy = make<WordGraph<uint32_t>>(text);
//! copy == graph; // returns true
//! \endcode
//!
//! \tparam Node the node type of the word graph.
//! \param wg the word graph to encode.
//! \param format the dense or sparse table representation (defaults to
//! \ref kbmagformat::dense).
//! \returns The KBMAG record as a string.
//!
//! \throws LibsemigroupsException if write_kbmag() throws.
//!
//! \complexity
//! \f$O(n(d+1)+L)\f$ time and \f$O(L)\f$ additional space, with the same
//! notation as write_kbmag().
template <typename Node>
[[nodiscard]] std::string to_kbmag_string(WordGraph<Node> const& wg,
kbmagformat format
= kbmagformat::dense);
} // namespace word_graph

//! \ingroup make_word_graph_group
//! \brief Constructs a word graph from a KBMAG input stream.
//!
//! Reads a KBMAG/GASP record, optionally preceded by a GAP variable
//! assignment and followed by a semicolon. Both `"dense deterministic"`
//! and `"sparse"` tables are supported. States and alphabet must have type
//! `"simple"`, and flags must include `"DFA"`. The initial list must be
//! `[1]` (or `[]` for no nodes), and the accepting list must be empty.
//! Nonempty accepting lists and other initial states are rejected rather
//! than silently discarded.
//!
//! The parser accepts whitespace, comments beginning with `#`, backslash
//! line continuations, and additional record fields containing balanced
//! GAP expressions. Additional fields are skipped without evaluating GAP
//! code. Required metadata must precede `table`, and `format` and any
//! `defaultTarget` must precede `transitions`. External transition files
//! and nondeterministic tables are not supported.
//!
//! Dense rows have exactly the alphabet size entries: a blank or nonpositive
//! integer denotes an undefined transition. Sparse rows contain distinct
//! positive labels and positive targets. A sparse `defaultTarget` supplies
//! omitted transitions. Repeated targets under different labels are valid.
//! The declared dimensions, including isolated nodes and unused labels,
//! are preserved. The parser checks one row at a time, converts one-based
//! indices to zero-based indices, and places targets at their original
//! labels. It never constructs a second complete transition table.
//!
//! \tparam Return the return type, a specialization of WordGraph.
//! \param input the source stream, consumed through the end of input.
//! \returns The decoded word graph.
//!
//! \throws LibsemigroupsException for invalid or unsupported input, including
//! dimensions too large for the requested node type or transition storage.
//! Messages include the line and column, and the invalid value or field.
//! \throws std::ios_base::failure if enabled by the stream.
//!
//! \complexity
//! \f$O(L+n(d+1))\f$ time and \f$O(d+t+h)\f$ additional space, excluding
//! the result, where \f$L\f$ is the input length, \f$n\f$ the node count,
//! \f$d\f$ the out-degree, \f$t\f$ the longest token, and \f$h\f$ the
//! size of additional metadata and its nesting depth.
//!
//! \note An empty graph is encoded with an empty initial list because
//! there is no node zero. On failure the input may have been partly consumed.
//!
//! \sa word_graph::write_kbmag(), word_graph::to_kbmag_string().
template <typename Return>
[[nodiscard]] std::enable_if_t<is_specialization_of_v<Return, WordGraph>,
Return>
make(std::istream& input);

//! \ingroup make_word_graph_group
//! \brief Constructs a word graph from a KBMAG string.
//!
//! Uses the same parser and conventions as make(std::istream&).
//!
//! \tparam Return the return type, a specialization of WordGraph.
//! \param input the KBMAG record to decode.
//! \returns The decoded word graph.
//!
//! \throws LibsemigroupsException if make(std::istream&) throws.
//!
//! \complexity
//! As for make(std::istream&), with an additional \f$O(L)\f$ space for
//! the input stream's copy of \p input.
template <typename Return>
[[nodiscard]] std::enable_if_t<is_specialization_of_v<Return, WordGraph>,
Return>
make(std::string const& input);
} // namespace libsemigroups

#include "word-graph-io.tpp"
#endif // LIBSEMIGROUPS_WORD_GRAPH_IO_HPP_
Loading
Loading