New "Monoids" benchmark

This commit is contained in:
Slava Pestov
2025-07-21 15:29:46 -04:00
parent 858383c71d
commit e6b77812ef
14 changed files with 1635 additions and 0 deletions

View File

@@ -220,12 +220,24 @@ set(SWIFT_BENCH_MODULES
set(SWIFT_MULTISOURCE_SWIFT_BENCHES
multi-source/PrimsSplit
multi-source/Monoids
)
set(PrimsSplit_sources
multi-source/PrimsSplit/Prims.swift
multi-source/PrimsSplit/Prims_main.swift)
set(Monoids_sources
multi-source/Monoids/Automaton.swift
multi-source/Monoids/Benchmark.swift
multi-source/Monoids/Enumeration.swift
multi-source/Monoids/Monoids.swift
multi-source/Monoids/Presentation.swift
multi-source/Monoids/RewritingSystem.swift
multi-source/Monoids/Solver.swift
multi-source/Monoids/Strategy.swift
multi-source/Monoids/Trie.swift)
set(BENCH_DRIVER_LIBRARY_FLAGS)
if (SWIFT_RUNTIME_ENABLE_LEAK_CHECKER)
set(BENCH_DRIVER_LIBRARY_FLAGS -DSWIFT_RUNTIME_ENABLE_LEAK_CHECKER)

View File

@@ -0,0 +1,169 @@
/// This file implements an algorithm to compute the number of elements in a
/// monoid (or determine it is infinite), given a complete presentation.
/// A finite state automaton, given by a set of vertices and edges.
struct Automaton {
var states: [Word] = []
var transitions: [(Word, Symbol, Word)] = []
}
extension Automaton {
var hasStar: Bool {
for state in states {
var visited = Set<Word>()
func rec(_ state: Word) -> Bool {
for (from, _, to) in transitions {
if from == state {
if visited.contains(to) {
return true
} else {
visited.insert(to)
if rec(to) { return true }
visited.remove(to)
}
}
}
return false
}
visited.insert(state)
if rec(state) { return true }
visited.remove(state)
}
return false
}
/// If this automaton is star-free, count the number of unique words accepted.
var countWords: Int {
func R(_ q: Word) -> [Word] {
var result: [Word] = []
for (from, _, to) in transitions {
if to == q {
result.append(from)
}
}
return result
}
func T(_ q: Word, _ p: Word) -> Int {
var letters = Set<Symbol>()
for (from, x, to) in transitions {
if from == q && to == p {
letters.insert(x)
}
}
return letters.count
}
func N(_ q: Word) -> Int {
if q == [] {
return 1
}
var result = 0
for p in R(q) {
result += N(p) * T(p, q)
}
return result
}
var result = 0
for q in states {
result += N(q)
}
return result
}
}
/// Constructs an automaton to recognize the complement of this regular set:
///
/// .*(x1|x2|...).*
///
/// where 'words' is [x1, x2, ...].
///
/// This is Lemma 2.1.3 in:
///
/// String Rewriting Systems, R.V. Book, F. Otto 1993. Springer New York.
func buildAutomaton(_ words: [Word], _ alphabet: Int) -> Automaton {
// Proper prefixes of each word.
var prefixes = Set<Word>()
var result = Automaton()
func isIrreducible(_ word: Word) -> Bool {
for i in 0 ..< word.count {
for other in words {
if i + other.count <= word.count {
if Word(word[i ..< (i + other.count)]) == other {
return false
}
}
}
}
return true
}
prefixes.insert([])
for word in words {
for i in 0 ..< word.count {
let prefix = Word(word[0 ..< i])
prefixes.insert(prefix)
}
}
result.states = prefixes.sorted { compare($0, $1, order: .shortlex) == .lessThan }
for prefix in prefixes {
for x in 0 ..< UInt8(alphabet) {
let word = prefix + [x]
if prefixes.contains(word) {
result.transitions.append((prefix, x, word))
continue
}
if !isIrreducible(word) {
continue
}
for i in 1 ... word.count {
let suffix = Word(word[i...])
if prefixes.contains(suffix) {
result.transitions.append((prefix, x, suffix))
break
}
}
}
}
return result
}
extension Presentation {
/// The Irr(R) automaton.
func automaton(alphabet: Int) -> Automaton {
return buildAutomaton(rules.map { $0.lhs }, alphabet)
}
/// Returns the number of irreducible words in this monoid presentation, or
/// nil if this set is infinite.
///
/// If the presentation is complete, this is the cardinality of the
/// presented monoid. Otherwise, it is an upper bound.
func cardinality(alphabet: Int) -> Int? {
let automaton = automaton(alphabet: alphabet)
if automaton.hasStar {
return nil
}
return automaton.countWords
}
}

View File

@@ -0,0 +1,20 @@
import TestsUtils
import Dispatch
public let benchmarks = [
BenchmarkInfo(
name: "Monoids",
runFunction: run_Monoids,
tags: [.algorithm])
]
func run_Monoids(_ n: Int) {
let semaphore = DispatchSemaphore(value: 0)
for _ in 0 ... n {
Task {
await run(output: false)
semaphore.signal()
}
semaphore.wait()
}
}

View File

@@ -0,0 +1,167 @@
/// This file implements algorithms for enumerating all monoid presentations
/// up to a given length.
func nextSymbol(_ s: inout Symbol, alphabet: Int) -> Bool {
precondition(alphabet > 0)
if s == alphabet - 1 {
s = 0
return true
}
s += 1
return false
}
func nextWord(_ word: inout Word, alphabet: Int) -> Bool {
var carry = true
for i in word.indices.reversed() {
carry = nextSymbol(&word[i], alphabet: alphabet)
if !carry { break }
}
return carry
}
func nextRule(_ rule: inout Rule, alphabet: Int) -> Bool {
if nextWord(&rule.rhs, alphabet: alphabet) {
rule.rhs = Word(repeating: 0, count: rule.rhs.count)
if nextWord(&rule.lhs, alphabet: alphabet) {
rule.lhs = Word(repeating: 0, count: rule.lhs.count)
return true
}
}
return false
}
func nextPresentation(_ p: inout Presentation, alphabet: Int) -> Bool {
precondition(!p.rules.isEmpty)
var carry = true
for i in p.rules.indices.reversed() {
carry = nextRule(&p.rules[i], alphabet: alphabet)
if !carry { break }
}
return carry
}
struct RuleShape {
var lhs: Int
var rhs: Int
var rule: Rule {
return Rule(lhs: Word(repeating: 0, count: lhs),
rhs: Word(repeating: 0, count: rhs))
}
}
struct PresentationShape {
var rules: [RuleShape]
var presentation: Presentation {
return Presentation(rules: rules.map { $0.rule })
}
}
func enumerateAll(alphabet: Int, shapes: [PresentationShape], output: Bool)
-> [Presentation] {
var filteredLHS = 0
var filteredRHS = 0
var filteredSymmetry = 0
var total = 0
var instances: [Presentation] = []
var unique: Set<Presentation> = []
let perms = allPermutations(alphabet)
for shape in shapes {
var p = shape.presentation
var done = false
loop: while !done {
defer {
done = nextPresentation(&p, alphabet: alphabet)
}
total += 1
for n in 0 ..< p.rules.count - 1 {
if compare(p.rules[n].lhs, p.rules[n + 1].lhs, order: .shortlex) != .lessThan {
filteredLHS += 1
continue loop
}
}
for rule in p.rules {
if compare(rule.rhs, rule.lhs, order: .shortlex) != .lessThan {
filteredRHS += 1
continue loop
}
}
if unique.contains(p.sorted(order: .shortlex)) {
filteredSymmetry += 1
continue
}
for perm in perms {
let permuted = p.permuted(perm)
unique.insert(permuted.sorted(order: .shortlex))
}
precondition(p == p.sorted(order: .shortlex))
instances.append(p)
}
}
if output {
print("# Total \(total)")
print("# Discarded lhs:\(filteredLHS),rhs:\(filteredRHS),"
+ "symmetry:\(filteredSymmetry)")
}
return instances
}
func ruleShapes(_ n: Int) -> [RuleShape] {
precondition(n > 0)
var result: [RuleShape] = []
for i in 0 ..< n {
let j = n - i
// Don't generate rules with shorter left-hand side.
if j < i { continue }
result.append(RuleShape(lhs: j, rhs: i))
}
return result
}
func presentationShapes(rules: Int, ofLength n: Int) -> [PresentationShape] {
precondition(n > 0)
precondition(rules > 0)
if rules == 1 {
return ruleShapes(n).map {
PresentationShape(rules: [$0])
}
}
var result: [PresentationShape] = []
for i in 1 ..< n {
let next = presentationShapes(rules: rules - 1, ofLength: i)
for x in ruleShapes(n - i) {
for y in next {
if x.lhs <= y.rules.first!.lhs {
result.append(PresentationShape(rules: [x] + y.rules))
}
}
}
}
return result
}
func presentationShapes(rules: Int, upToLength n: Int) -> [PresentationShape] {
var shapes: [PresentationShape] = []
for i in 1 ... n {
shapes.append(contentsOf: presentationShapes(rules: rules, ofLength: i))
}
return shapes
}

View File

@@ -0,0 +1,26 @@
/// This is the main entry point.
///
/// We enumerate all 2-generator, 2-relation monoids of length up to 10:
///
/// <a, b | u=v, w=x> where |u| + |v| + |w| + |x| <= 10
///
/// We attempt to build an FCRS for each one by trying various strategies,
/// which ultimately succeeds for all but three instances.
func run(output: Bool) async {
let shapes = presentationShapes(rules: 2, upToLength: 10)
let alphabet = 2
let instances = enumerateAll(alphabet: alphabet, shapes: shapes, output: output)
var solver = Solver(alphabet: alphabet, instances: instances, output: output)
await solver.solve()
// These we know we can't solve.
let expect: [Presentation] = ["bab=aaa,bbbb=1", "aaaa=1,abbba=b", "aaa=a,abba=bb"]
// Make sure everything else was solved.
let unsolved = solver.subset.map { instances[$0] }
if unsolved != expect {
fatalError("Expected \(expect), but got \(unsolved)")
}
}

View File

@@ -0,0 +1,372 @@
/// This file defines the data types we use for words, rules, and
/// monoid presentations. Also, a few other fundamental algorithms
/// for working with permutations and reduction orders.
typealias Symbol = UInt8
let symbols: [Character] = ["a", "b", "c"]
func printSymbol(_ s: Symbol) -> Character {
return symbols[Int(s)]
}
func parseSymbol(_ c: Character) -> Symbol {
return Symbol(symbols.firstIndex(of: c)!)
}
typealias Word = [Symbol]
func printWord(_ word: Word) -> String {
if word.isEmpty { return "1" }
return String(word.map { printSymbol($0) })
}
func parseWord(_ str: String) -> Word {
if str == "" || str == "1" { return [] }
return str.map { parseSymbol($0) }
}
struct Rule: Hashable {
var lhs: Word
var rhs: Word
}
extension Rule: ExpressibleByStringLiteral, CustomStringConvertible {
init(_ str: String) {
let pair = str.split(separator: "=")
precondition(pair.count == 2)
self.lhs = parseWord(String(pair[0]))
self.rhs = parseWord(String(pair[1]))
}
init(stringLiteral: String) {
self.init(stringLiteral)
}
var description: String {
return "\(printWord(lhs))=\(printWord(rhs))"
}
}
struct Presentation: Hashable {
var rules: [Rule]
var alphabet: Int {
var result: Int = 0
for rule in rules {
for s in rule.lhs { result = max(result, Int(s)) }
for s in rule.rhs { result = max(result, Int(s)) }
}
return result + 1
}
var longestRule: Int {
var result = 0
for rule in rules {
result = max(result, rule.lhs.count)
result = max(result, rule.rhs.count)
}
return result
}
}
extension Presentation: ExpressibleByStringLiteral, CustomStringConvertible {
init(_ str: String) {
self.rules = str.split(separator: ",").map { Rule(String($0)) }
}
init(stringLiteral: String) {
self.init(stringLiteral)
}
var description: String {
if rules.isEmpty { return "," }
return rules.map { $0.description }.joined(separator: ",")
}
}
typealias Permutation = [Int]
func identityPermutation(_ n: Int) -> Permutation {
return Permutation(0 ..< n)
}
func inversePermutation(_ perm: Permutation) -> Permutation {
var result = Permutation(repeating: 0, count: perm.count)
for (i, j) in perm.enumerated() {
result[j] = i
}
return result
}
// TAOCP 4A 7.2.1.2 Algorithm L
func nextPermutation(_ perm: inout Permutation) -> Bool {
var j = perm.count - 2
while j >= 0 && perm[j] >= perm[j + 1] {
j -= 1
}
if j < 0 { return true }
var l = perm.count - 1
while perm[j] >= perm[l] { l -= 1 }
perm.swapAt(l, j)
l = perm.count - 1
var k = j + 1
while k < l {
perm.swapAt(k, l)
k += 1
l -= 1
}
return false
}
func allPermutations(_ alphabet: Int) -> [Permutation] {
var perm = identityPermutation(alphabet)
var perms: [Permutation] = []
repeat {
perms.append(perm)
} while !nextPermutation(&perm)
return perms
}
extension Word {
func permuted(_ perm: Permutation) -> Word {
return map { Symbol(perm[Int($0)]) }
}
}
extension Rule {
func permuted(_ perm: Permutation) -> Rule {
return Rule(lhs: lhs.permuted(perm), rhs: rhs.permuted(perm))
}
}
extension Presentation {
func permuted(_ perm: Permutation) -> Presentation {
return Presentation(rules: rules.map { $0.permuted(perm) })
}
}
enum CompareResult {
case lessThan
case equal
case greaterThan
}
enum Order: Hashable {
case shortlex
case permutation(Permutation)
case wreath([Int], Permutation)
var simplified: Order {
switch self {
case .shortlex:
return self
case .permutation(let perm):
// shortlex with identity permutation avoids some indirection
if perm == identityPermutation(perm.count) {
return .shortlex
}
return self
case .wreath(_, _):
return self
}
}
func removeGenerator(_ a: Symbol) -> Order {
func updatePermutation(_ perm: Permutation, removing i: Int) -> Permutation {
return Permutation(perm[0 ..< i] + perm[(i + 1)...]).map {
return $0 > perm[i] ? $0 - 1 : $0
}
}
switch self {
case .shortlex:
return self
case .permutation(let perm):
return .permutation(updatePermutation(perm, removing: Int(a)))
case .wreath(let degrees, let perm):
var newDegrees = Array(degrees[0 ..< Int(a)] + degrees[(Int(a) + 1)...])
let oldDegree = degrees[Int(a)]
if newDegrees.firstIndex(of: oldDegree) == nil {
newDegrees = newDegrees.map { $0 > oldDegree ? $0 - 1 : $0 }
}
let newPerm = updatePermutation(perm, removing: Int(a))
if newDegrees.max()! == 0 { return .permutation(newPerm) }
return .wreath(newDegrees, newPerm)
}
}
}
func shortlex(_ lhs: Word, _ lhsFrom: Int, _ lhsTo: Int,
_ rhs: Word, _ rhsFrom: Int, _ rhsTo: Int,
perm: Permutation) -> CompareResult {
let lhsCount = (lhsTo - lhsFrom)
let rhsCount = (rhsTo - rhsFrom)
if lhsCount != rhsCount {
return lhsCount < rhsCount ? .lessThan : .greaterThan
}
for i in 0 ..< lhsCount {
let x = lhs[lhsFrom + i]
let y = rhs[rhsFrom + i]
if x != y {
return perm[Int(x)] < perm[Int(y)] ? .lessThan : .greaterThan
}
}
return .equal
}
// The "wreath product" or "recursive path" order:
//
// Sims, C. C. (1994). Computation with Finitely Presented Groups.
// Cambridge: Cambridge University Press.
//
func wreath(_ lhs: Word, _ lhsFrom: Int, _ lhsTo: Int,
_ rhs: Word, _ rhsFrom: Int, _ rhsTo: Int,
degrees: [Int], perm: Permutation) -> CompareResult {
var i = lhsFrom
var j = rhsFrom
while true {
if i == lhsTo {
if j == rhsTo { return .equal }
return .lessThan
} else if j == rhsTo {
return .greaterThan
}
if lhs[i] != rhs[j] { break }
i += 1
j += 1
}
func maxDegree(_ word: Word, _ from: Int, _ to: Int)
-> (degree: Int, count: Int, symbol: Symbol?) {
var degree = -1, count = 0
var symbol: Symbol? = nil
for s in word[from ..< to] {
if degrees[Int(s)] > degree {
degree = degrees[Int(s)]
count = 1
symbol = s
} else if degrees[Int(s)] == degree {
count += 1
if symbol != s { symbol = nil }
}
}
return (degree, count, symbol)
}
let (lhsMaxDegree, lhsCount, lhsHeadSymbol) = maxDegree(lhs, i, lhsTo)
let (rhsMaxDegree, rhsCount, rhsHeadSymbol) = maxDegree(rhs, j, rhsTo)
if lhsMaxDegree < rhsMaxDegree {
return .lessThan
} else if lhsMaxDegree > rhsMaxDegree {
return .greaterThan
} else if lhsCount < rhsCount {
return .lessThan
} else if lhsCount > rhsCount {
return .greaterThan
}
if lhsHeadSymbol != nil && rhsHeadSymbol != nil {
if lhsHeadSymbol != rhsHeadSymbol {
return perm[Int(lhsHeadSymbol!)] < perm[Int(rhsHeadSymbol!)]
? .lessThan : .greaterThan
}
} else {
if lhsMaxDegree == 0 {
return shortlex(lhs, i, lhsTo, rhs, j, rhsTo, perm: perm)
} else {
let lhsHeadWord = lhs[i ..< lhsTo].filter { degrees[Int($0)] == lhsMaxDegree }
let rhsHeadWord = rhs[j ..< rhsTo].filter { degrees[Int($0)] == rhsMaxDegree }
let result = shortlex(lhsHeadWord, 0, lhsHeadWord.count,
rhsHeadWord, 0, rhsHeadWord.count,
perm: perm)
if result != .equal { return result }
}
}
if lhsMaxDegree == 0 { return .equal }
var ii = i, jj = j
while i < lhsTo {
while i < lhsTo && degrees[Int(lhs[i])] != lhsMaxDegree { i += 1 }
while j < rhsTo && degrees[Int(rhs[j])] != rhsMaxDegree { j += 1 }
let result = wreath(lhs, ii, i, rhs, jj, j, degrees: degrees, perm: perm)
if result != .equal { return result }
i += 1; ii = i
j += 1; jj = j
}
precondition(j == rhsTo)
return .equal
}
func compare(_ lhs: Word, _ rhs: Word, order: Order) -> CompareResult {
switch order {
case .shortlex:
if lhs.count != rhs.count {
return lhs.count < rhs.count ? .lessThan : .greaterThan
}
for i in lhs.indices {
let x = lhs[i]
let y = rhs[i]
if x != y {
return x < y ? .lessThan : .greaterThan
}
}
return .equal
case .permutation(let perm):
return shortlex(lhs, 0, lhs.count, rhs, 0, rhs.count, perm: perm)
case .wreath(let degrees, let perm):
return wreath(lhs, 0, lhs.count, rhs, 0, rhs.count,
degrees: degrees, perm: perm)
}
}
func compare(_ lhs: Rule, _ rhs: Rule, order: Order) -> CompareResult {
let result = compare(lhs.lhs, rhs.lhs, order: order)
if result != .equal {
return result
}
return compare(lhs.rhs, rhs.rhs, order: order)
}
extension Rule {
func oriented(order: Order) -> Rule? {
switch compare(lhs, rhs, order: order) {
case .equal:
return nil
case .lessThan:
return Rule(lhs: rhs, rhs: lhs)
case .greaterThan:
return self
}
}
}
extension Presentation {
func sorted(order: Order) -> Presentation {
let sortedRules =
rules.map { $0.oriented(order: order)! }
.sorted { compare($0, $1, order: order) == .lessThan }
return Presentation(rules: sortedRules)
}
}

View File

@@ -0,0 +1,24 @@
# Monoids Benchmark
This benchmark solves the "word problem" in a bunch of monoids simultaneously, using Swift concurrency (or really, just `Task`). It exercises the standard library data structures heavily. You can also run "sh compile.sh" inside the source directory to build a standalone binary separately from the benchmark harness. The standalone binary prints results to standard output.
More specifically, this program enumerates two-generator two-relation monoid presentations up to length 10, and then applies the Knuth-Bendix algorithm to each one:
<a,b|u=v,w=x> where |u| + |v| + |w| + |x| <= 10
This takes a few seconds to finish and solves all but three instances. The three it cannot solve are:
<a,b|aaa=a,abba=bb>
<a,b|bab=aaa,bbbb=1>
<a,b|aaaa=1,abbba=b>
In addition to Knuth-Bendix completion, there are some other interesting algorithms here as well:
- Shortlex order with arbitrary permutation of alphabet
- Wreath product order (also known as recursive path order) with arbitrary degree mapping
- Enumerating all words, permutations, monoid presentations
- Inverse of a permutation
- Computing number of irreducible words in complete presentation (= cardinality of presented monoid) with finite state automata
The Knuth-Bendix implementation is pretty fast. It uses a trie to speed up reduction and finding overlaps.
The main "entry point" is `func run()` in Monoids.swift.

View File

@@ -0,0 +1,293 @@
/// This file implements Knuth-Bendix completion and the normal form algorithm.
enum RewritingError: Error {
case tooManyRounds
case tooManyRules
case tooManyNodes
case ruleTooLong
case tooManySteps
case reducedWordTooLong
}
let debug = false
func log(_ str: @autoclosure () -> String) {
if debug {
print(str())
}
}
struct RewritingSystem {
var state: State = .initial
enum State {
case initial
case complete
case failed
}
var alphabet: Int
var rules: [Rule] = []
var trie: Trie
// Limits for completion
struct Limits: Hashable {
var maxRounds = 100
var maxRules = 180
var maxLength = 100
var maxReductionLength = 100
var maxReductionSteps = 1 << 24
}
var limits: Limits
var checkedRulesUpTo = 0 // Completion progress
var reducedRules: [UInt32] = [] // Bitmap of reduced rules
typealias CriticalPair = (i: Int, from: Int, j: Int)
var criticalPairs: [CriticalPair] = [] // Temporary array for completion
var stats = Stats()
struct Stats {
var numRounds = 0
var numRulesRemaining = 0 // Number of rules that were not reduced away
var numReductionSteps = 0
}
init(alphabet: Int, limits: Limits) {
self.alphabet = alphabet
self.trie = Trie(alphabet: self.alphabet)
self.limits = limits
criticalPairs.reserveCapacity(128)
}
mutating func addRules(_ rules: [Rule], order: Order)
throws(RewritingError) {
for var rule in rules {
_ = try addRule(&rule, order: order)
}
}
func reduceOne(_ word: Word, excluding: Int? = nil) -> (Int, Int)? {
var from = 0
while from < word.count {
if let n = trie.lookup(word, from) {
if n != excluding { return (from, n) }
}
from += 1
}
return nil
}
func reduce(_ word: inout Word, stats: inout Stats) throws(RewritingError) {
var count = 0
repeat {
guard let (from, n) = reduceOne(word) else { return }
let index = word.startIndex + from
word.replaceSubrange(index ..< index + rules[n].lhs.count,
with: rules[n].rhs)
stats.numReductionSteps += (from + rules[n].lhs.count)
if stats.numReductionSteps > limits.maxReductionSteps { throw .tooManySteps }
if count > limits.maxReductionLength { throw .tooManySteps }
// FIXME: Load bearing
if word.count > limits.maxLength { throw .reducedWordTooLong }
count += 1
} while true
}
mutating func addOrientedRule(_ rule: Rule) throws(RewritingError) {
let longestSide = max(rule.lhs.count, rule.rhs.count)
if longestSide > limits.maxLength { throw .ruleTooLong }
if stats.numRulesRemaining == limits.maxRules { throw .tooManyRules }
log("Adding rule \(rules.count) = \(rule)")
try trie.insert(rule.lhs, rules.count)
rules.append(rule)
stats.numRulesRemaining += 1
}
mutating func addRule(_ rule: inout Rule, order: Order)
throws(RewritingError) -> Bool {
try reduce(&rule.lhs, stats: &stats)
try reduce(&rule.rhs, stats: &stats)
switch compare(rule.lhs, rule.rhs, order: order) {
case .equal:
return false
case .lessThan:
swap(&rule.lhs, &rule.rhs)
fallthrough
case .greaterThan:
try addOrientedRule(rule)
return true
}
}
mutating func resolveOverlap(i: Int, from: Int, j: Int, order: Order)
throws(RewritingError) -> Bool {
let lhs = rules[i]
let rhs = rules[j]
log("Critical pair: \(i) vs \(j) at \(from)")
log("\(printWord(rules[i].lhs))")
log("\(String(repeating: " ", count: from))\(printWord(rules[j].lhs))")
var rule = Rule(lhs: [], rhs: [])
let end = lhs.lhs.count
if from + rhs.lhs.count < end {
rule.lhs = lhs.rhs
rule.rhs.reserveCapacity(lhs.lhs.count - rhs.lhs.count + rhs.rhs.count)
rule.rhs.append(contentsOf: lhs.lhs[0 ..< from])
rule.rhs.append(contentsOf: rhs.rhs)
rule.rhs.append(contentsOf: lhs.lhs[(from + rhs.lhs.count)...])
} else {
rule.lhs.reserveCapacity(lhs.rhs.count + rhs.lhs.count - lhs.lhs.count + from)
rule.lhs.append(contentsOf: lhs.rhs)
rule.lhs.append(contentsOf: rhs.lhs[(lhs.lhs.count - from)...])
rule.rhs.reserveCapacity(from + rhs.rhs.count)
rule.rhs.append(contentsOf: lhs.lhs[..<from])
rule.rhs.append(contentsOf: rhs.rhs)
}
return try addRule(&rule, order: order)
}
mutating func processRule(_ i: Int) {
if isReduced(i) { return }
let lhs = rules[i]
var from = 0
while from < lhs.lhs.count {
trie.visitOverlaps(lhs.lhs, from) { j in
precondition(!isReduced(j))
if i < checkedRulesUpTo && j < checkedRulesUpTo { return }
if from == 0 {
if i == j { return }
if rules[j].lhs.count > lhs.lhs.count { return }
}
criticalPairs.append((i: i, from: from, j: j))
}
from += 1
}
}
mutating func completeOne(order: Order) throws(RewritingError) -> Bool {
precondition(state == .initial)
precondition(criticalPairs.isEmpty)
for i in rules.indices {
processRule(i)
}
checkedRulesUpTo = rules.count
stats.numRounds += 1
reduceLeft()
var confluent = true
do {
log("Resolving critical pairs...")
for (i, from, j) in criticalPairs {
if try resolveOverlap(i: i, from: from, j: j, order: order) {
confluent = false
}
}
criticalPairs.removeAll(keepingCapacity: true)
log("All critical pairs resolved")
try reduceRight()
} catch let e {
state = .failed
throw e
}
if confluent {
state = .complete
return true
}
if stats.numRounds > limits.maxRounds {
state = .failed
throw .tooManyRounds
}
return false
}
mutating func complete(order: Order) throws(RewritingError) {
while try !completeOne(order: order) {}
}
func isReduced(_ rule: Int) -> Bool {
let i = (rule >> 5)
let j = (rule & 31)
if i >= reducedRules.count { return false }
return (reducedRules[i] & (1 << j)) != 0
}
mutating func setReduced(_ rule: Int) {
let i = (rule >> 5)
let j = (rule & 31)
while i >= reducedRules.count { reducedRules.append(0) }
reducedRules[i] |= (1 << j)
}
mutating func reduceLeft() {
if rules.isEmpty { return }
log("Reducing left-hand sides...")
for (n, rule) in rules.enumerated() {
if !isReduced(n) && reduceOne(rule.lhs, excluding: n) != nil {
log("Reduced \(n) = \(rule)")
setReduced(n)
trie.remove(rule.lhs, n)
stats.numRulesRemaining -= 1
continue
}
}
precondition(stats.numRulesRemaining > 0)
}
mutating func reduceRight() throws(RewritingError) {
for n in rules.indices {
if !isReduced(n) {
try reduce(&rules[n].rhs, stats: &stats)
}
}
}
/// Returns a complete presentation once the rewriting system is complete.
var presentation: Presentation {
var result: [Rule] = []
for (n, rule) in rules.enumerated() {
if !isReduced(n) {
result.append(rule)
}
}
return Presentation(rules: result)
}
}

View File

@@ -0,0 +1,283 @@
/// This file implements the driver loop which attempts Knuth-Bendix completion
/// with various strategies on all instances in parallel.
struct Dispatcher {
let subset: [Int]
let strategies: [Strategy]
var currentStrategy = 0
var currentInstance = 0
mutating func next() -> (instance: Int, strategy: Int)? {
if subset.isEmpty || currentStrategy == strategies.count { return nil }
defer {
currentInstance += 1
if currentInstance == subset.count {
currentInstance = 0
currentStrategy += 1
}
}
return (instance: subset[currentInstance],
strategy: currentStrategy)
}
}
struct Solver {
let alphabet: Int
let instances: [Presentation]
// This is a list of indices of all remaining unsolved instances.
var subset: [Int]
var factors: [Order: [Int: [Word]]] = [:]
var maxFactors: [Int] = []
var output: Bool
init(alphabet: Int, instances: [Presentation], output: Bool) {
self.alphabet = alphabet
self.instances = instances
self.subset = Array(instances.indices)
self.output = output
}
mutating func solve() async {
if output {
print("# Remaining \(subset.count)")
print("# n:\tpresentation:\tcardinality:\tcomplete presentation:\tstrategy:")
}
// The shortlex order with identity permutation of generators solves
// almost everything.
await attempt([Strategy()])
if output {
print("# Remaining \(subset.count)")
print("# Attempting more reduction orders")
}
var orderMix: [Int: [Order]] = [:]
for i in [0, 1] {
orderMix[alphabet + i] = getExhaustiveOrderMix(alphabet, i)
}
do {
var strategies: [Strategy] = []
let strategy = Strategy()
let orders = orderMix[alphabet]!
// We already did the first one.
for order in orders[1...] {
strategies.append(strategy.withOrder(order))
}
await attempt(strategies)
}
if output {
print("# Remaining \(subset.count)")
print("# Attempting to add a generator")
}
do {
collectFactors(orderMix[alphabet]!)
var strategies: [Strategy] = []
for frequency in [0, 1] {
for factorLength in 2 ..< maxFactors.count {
let strategy = Strategy(factorLength: factorLength,
frequency: frequency)
for order in orderMix[alphabet + 1]! {
strategies.append(strategy.withOrder(order))
}
}
}
await attempt(strategies)
}
if output {
print("# Remaining \(subset.count)")
for n in subset {
let instance = instances[n]
print("\(n)\t\(instance)\thard")
}
}
}
mutating func collectFactors(_ orders: [Order]) {
for order in orders {
for n in subset {
factors[order, default: [:]][n] =
collectFactors(n, instances[n], order: order.simplified)
}
}
}
mutating func collectFactors(_ n: Int, _ p: Presentation, order: Order) -> [Word] {
// FIXME: The 2 is a magic number
let words = p.collectFactors(order: order, upToLength: p.longestRule + 2)
if !words.isEmpty {
let longestFactor = words.map { $0.count }.max()!
for i in 2 ... longestFactor {
let factorsOfLength = words.filter { $0.count == i }
while maxFactors.count <= i {
maxFactors.append(0)
}
maxFactors[i] = max(maxFactors[i], factorsOfLength.count)
}
}
return words
}
func prepare(_ instance: Int, _ strategy: Strategy) -> Strategy? {
var strategy = strategy
var factorsOfLength: [Word] = []
if let length = strategy.factorLength {
let order = strategy.order.removeGenerator(Symbol(alphabet))
factorsOfLength = (factors[order]!)[instance]!
let factorsOfLength = factorsOfLength.filter { $0.count == length }
if strategy.frequency >= factorsOfLength.count { return nil }
// Add a new generator 'c' and a rule 'c=x' for a magic factor 'x'.
let newFactor = factorsOfLength[strategy.frequency]
let newGenerator = [Symbol(alphabet)]
// If 'c' is just going to reduce to 'x' there's no point in
// considering it further.
if compare(newFactor, newGenerator, order: strategy.order) == .lessThan {
return nil
}
strategy.extra = [Rule(lhs: newFactor, rhs: newGenerator)]
}
strategy.order = strategy.order.simplified
return strategy
}
mutating func attempt(_ strategies: [Strategy]) async {
if subset.isEmpty { return }
let solved = await withTaskGroup(of: (Int, Int, Solution?).self) { group in
var dispatcher = Dispatcher(subset: subset,
strategies: strategies)
var solved: [Int: (Int, Solution)] = [:]
var pending: [Int: Int] = [:]
func startTask() {
while true {
guard let (instance, strategyIndex) = dispatcher.next() else {
return
}
if solved[instance] != nil {
continue
}
guard let strategy = prepare(instance, strategies[strategyIndex]) else {
continue
}
pending[strategyIndex, default: 0] += 1
let p = instances[instance]
group.addTask { () -> (Int, Int, Solution?) in
if let solution = try? p.complete(strategy) {
return (instance, strategyIndex, solution)
}
return (instance, strategyIndex, nil)
}
return
}
}
func completeTask(_ instance: Int, _ strategyIndex: Int, _ solution: Solution?) {
pending[strategyIndex, default: 0] -= 1
precondition(pending[strategyIndex]! >= 0)
if let solution {
// The lowest-numbered strategy is the 'official' solution for the instance.
var betterStrategy = true
if let (oldStrategyIndex, _) = solved[instance] {
if oldStrategyIndex < strategyIndex {
betterStrategy = false
}
}
if betterStrategy {
solved[instance] = (strategyIndex, solution)
}
}
retireStrategies()
}
func retireStrategies() {
var retired: [Int: [Int]] = [:]
let pendingInOrder = pending.sorted { $0.key < $1.key }
for (strategyIndex, numTasks) in pendingInOrder {
precondition(strategyIndex <= dispatcher.currentStrategy)
if dispatcher.currentStrategy == strategyIndex { break }
if numTasks > 0 { break }
pending[strategyIndex] = nil
retired[strategyIndex] = []
}
if retired.isEmpty { return }
// If we are done dispatching a strategy, look at all instances solved
// by that strategy and print them out.
for n in subset {
if let (strategyIndex, solution) = solved[n] {
if retired[strategyIndex] != nil {
retired[strategyIndex]!.append(n)
// Print the instance and solution.
var str = "\(n)\t\(instances[n])\t"
if let cardinality = solution.cardinality {
str += "finite:\(cardinality)"
} else {
str += "infinite"
}
str += "\tfcrs:\(solution.presentation)"
// Print the extra generators that were added, if any.
if !solution.extra.isEmpty {
str += "\t\(Presentation(rules: solution.extra))"
}
if output {
print(str)
}
}
}
}
}
// We run 32 tasks at a time.
for _ in 0 ..< 32 {
startTask()
}
for await (instance, strategyIndex, solution) in group {
startTask()
completeTask(instance, strategyIndex, solution)
}
return solved
}
subset = subset.filter { solved[$0] == nil }
}
}

View File

@@ -0,0 +1,13 @@
// Only generate main entry point if we're not being built as part of the
// benchmark harness. In this case you get a binary that runs the same
// workload, except it also prints results to standard output.
#if !canImport(TestsUtils)
@main struct Main {
static func main() async {
await run(output: true)
}
}
#endif

View File

@@ -0,0 +1,117 @@
/// Generate a bunch of reduction orders.
func getExhaustiveOrderMix(_ alphabet: Int, _ extraAlphabet: Int) -> [Order] {
var result: [Order] = []
let permutations = allPermutations(alphabet + extraAlphabet)
for perm in permutations {
result.append(.permutation(perm))
}
for perm in permutations {
let degrees = perm.map { Int($0) }
result.append(.wreath(degrees, perm))
}
return result
}
/// Parameters for completion.
struct Strategy: Sendable, Hashable {
var extra: [Rule] = []
var factorLength: Int? = nil
var frequency: Int = 0
var order: Order = .shortlex
var limits = RewritingSystem.Limits()
func withOrder(_ order: Order) -> Self {
var result = self
result.order = order
return result
}
}
extension Presentation {
func collectFactors(order: Order, upToLength: Int) -> [Word] {
let strategy = Strategy()
var rws = RewritingSystem(alphabet: alphabet,
limits: strategy.limits)
do {
try rws.addRules(rules, order: strategy.order)
for _ in [0, 1] {
if try rws.completeOne(order: strategy.order) { break }
}
} catch {}
return rws.collectFactors(upToLength: upToLength, order: strategy.order)
}
}
extension Word {
func collectFactors(_ table: inout [Word: Int], length: Int) {
precondition(length >= 2)
if length > count { return }
for i in 0 ... count - length {
table[Word(self[i ..< i + length]), default: 0] += 1
}
}
}
extension RewritingSystem {
func collectFactors(upToLength: Int, order: Order) -> [Word] {
var table: [Word: Int] = [:]
for n in rules.indices {
if isReduced(n) { continue }
let lhs = rules[n].lhs
for length in 2 ... upToLength {
lhs.collectFactors(&table, length: length)
}
}
return table.sorted {
$0.1 > $1.1 || ($0.1 == $1.1 &&
compare($0.0, $1.0, order: order) == .greaterThan)
}.map { $0.key }
}
}
struct Solution {
/// Total number of generators.
let alphabet: Int
/// Extra generators added by morphocompletion.
let extra: [Rule]
/// The cardinality of the presented monoid.
let cardinality: Int?
/// A complete presentation.
let presentation: Presentation
}
extension RewritingSystem {
func formSolution(_ strategy: Strategy) -> Solution {
let p = presentation.sorted(order: strategy.order)
let cardinality = p.cardinality(alphabet: alphabet)
return Solution(alphabet: alphabet, extra: strategy.extra,
cardinality: cardinality, presentation: p)
}
}
extension Presentation {
func complete(_ strategy: Strategy) throws(RewritingError) -> Solution {
let alphabet = 2 + strategy.extra.count
var rws = RewritingSystem(alphabet: alphabet,
limits: strategy.limits)
try rws.addRules(rules, order: strategy.order)
try rws.addRules(strategy.extra, order: strategy.order)
try rws.complete(order: strategy.order)
return rws.formSolution(strategy)
}
}

View File

@@ -0,0 +1,136 @@
struct Trie {
typealias Node = Int16
var values: [Node] = []
var children: [Node] = []
var freeList: [Node] = []
let emptyNode: [Node]
init(alphabet: Int) {
self.emptyNode = Array(repeating: -1, count: alphabet)
_ = try! createNode() // The root node
}
mutating func createNode() throws(RewritingError) -> Node {
if !freeList.isEmpty {
let result = Int(freeList.removeLast())
values.replaceSubrange(result ..< result + emptyNode.count, with: emptyNode)
children.replaceSubrange(result ..< result + emptyNode.count, with: emptyNode)
return Node(result)
}
let result = values.count
if result + emptyNode.count >= 32000 {
throw RewritingError.tooManyNodes
}
values.append(contentsOf: emptyNode)
children.append(contentsOf: emptyNode)
precondition(values.count == children.count)
precondition(values.count % emptyNode.count == 0)
return Node(result)
}
mutating func reclaimNode(_ node: Node) {
freeList.append(node)
}
mutating func insert(_ key: Word, _ value: Int) throws(RewritingError) {
var node = 0
for i in 0 ..< key.count - 1 {
let s = key[i]
if children[node + Int(s)] == -1 {
children[node + Int(s)] = try createNode()
}
node = Int(children[node + Int(s)])
}
values[node + Int(key.last!)] = Node(value)
}
func lookup(_ key: Word, _ i: Int) -> Int? {
var node = 0
for s in key[i ..< key.count] {
let n = Int(values[node + Int(s)])
if n != -1 { return n }
node = Int(children[node + Int(s)])
if node == -1 { return nil }
}
return nil
}
// Visits all keys that are equal to a prefix of key[i ...], as well as
// all keys whose prefix is equal to key[i ...].
func visitOverlaps(_ key: Word, _ from: Int, callback: (Int) -> ()) {
var node = 0
for s in key[from...] {
let n = Int(values[node + Int(s)])
if n != -1 { callback(n) }
node = Int(children[node + Int(s)])
if node == -1 { return }
}
if node == 0 { return }
var stack: [Int] = [node]
repeat {
let node = stack.removeLast()
for s in (0 ..< emptyNode.count) {
let n = Int(values[node + s])
if n != -1 { callback(n) }
let child = Int(children[node + s])
if child != -1 { stack.append(child) }
}
} while !stack.isEmpty
}
func isEmptyNode(_ node: Int) -> Bool {
for i in 0 ..< emptyNode.count {
if values[node + i] != -1 ||
children[node + i] != -1 {
return false
}
}
return true
}
mutating func remove(_ key: Word, _ value: Int) {
var node = 0
var stack: [Int] = [] // path to current node from root
for i in 0 ..< key.count - 1 {
let s = key[i]
precondition(children[node + Int(s)] != -1)
stack.append(node)
node = Int(children[node + Int(s)])
}
let j = node + Int(key.last!)
precondition(values[j] == value)
values[j] = -1
// Remove any newly-empty nodes, up to the root.
repeat {
if !isEmptyNode(node) { return }
reclaimNode(Node(node))
let parent = stack.removeLast()
var sawThis = false
for i in 0 ..< emptyNode.count {
if Int(children[parent + i]) == node {
children[parent + i] = -1
sawThis = true
break
}
}
precondition(sawThis)
node = parent
} while !stack.isEmpty
}
}

View File

@@ -0,0 +1 @@
xcrun swiftc -O Automaton.swift Enumeration.swift Monoids.swift Presentation.swift RewritingSystem.swift Solver.swift Standalone.swift Strategy.swift Trie.swift -O -swift-version 6 -g -wmo -parse-as-library -o Monoids $@

View File

@@ -117,6 +117,7 @@ import LuhnAlgoLazy
import MapReduce
import Memset
import MirrorTest
import Monoids
import MonteCarloE
import MonteCarloPi
import NaiveRangeReplaceableCollectionConformance
@@ -317,6 +318,7 @@ register(LuhnAlgoLazy.benchmarks)
register(MapReduce.benchmarks)
register(Memset.benchmarks)
register(MirrorTest.benchmarks)
register(Monoids.benchmarks)
register(MonteCarloE.benchmarks)
register(MonteCarloPi.benchmarks)
register(NaiveRangeReplaceableCollectionConformance.benchmarks)