const MaxPatterns, MaxPatternLen
Bounds on what New will build, so an on-chain caller cannot be handed an automaton whose size it did not choose. At most MaxPatterns*MaxPatternLen+1 nodes exist, and the build is linear in the total pattern length.
Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.
Package ahocorasick implements Aho-Corasick multi-pattern search as a pure, reusable package.
gno.land/p/moul/x/daily/ahocorasick/v0Aho-Corasick multi-pattern search over strings, every overlapping match in one pass: New, FindAll, FindFirst, Contains, Patterns, Nodes.
1import "gno.land/p/moul/x/daily/ahocorasick/v0"
2
3m := ahocorasick.New([]string{"he", "she", "his", "hers"})
4m.FindAll("ushers") // she@1, he@2, hers@2
5m.Contains("ushers") // true
6m.Nodes() // 10, one per distinct pattern prefix plus the root
Searching for k patterns by running a single-pattern scan k times costs O(k*n): every extra word in the dictionary is another full pass over the text. Aho-Corasick builds one automaton from the whole set, a trie of the patterns plus a failure link on every node pointing at the longest proper suffix that is also a prefix of some pattern, and then reads the text once. Building the trie and the links costs O(total pattern length), scanning costs O(n + matches), and k does not appear in either.
The text cursor never moves backwards, and that is why it belongs on chain. The worst
case equals the best case, so a dictionary and a text chosen by an attacker cost what a
friendly pair costs. A naive k-pass scan is one input away from quadratic, and on chain a
pathological input is an attack rather than bad luck. It is
p/moul/x/daily/kmp
generalised: KMP's failure table is this automaton for a one-pattern set.
Four behaviours worth knowing before you use it:
FindAll is ordered by
end offset, and at one end offset the longest match comes first, because the scan reports
the node it reached before the shorter suffixes hanging off its failure chain. A caller
taking the first match at each end offset gets the greedy one, and a caller wanting only
non-overlapping matches filters on Start >= lastEnd itself.Match.Index is the caller's slot, not a node number, so a match maps back to whatever
was stored alongside the pattern. A duplicated pattern is two slots and reports twice:
deduplicating here would silently drop the second caller's payload.nil. MaxPatterns is 256 and MaxPatternLen
is 256, so an on-chain caller cannot be handed an automaton whose size it did not choose.
Empty patterns are dropped, since the empty string matches at every offset and that is a
different question. A nil matcher answers every method, so the result of New can go
straight into a scan without a check.Each node stores the patterns matching there, merged from its failure target at build
time, rather than a link to be walked at every offset. That is a departure from the paper
and it is a trade: a reported match costs O(1) instead of a walk up the failure chain, and
the build pays memory in exactly one shape, a set whose patterns nest (a, aa, aaa, and
so on), where the merged lists are quadratic in the number of patterns rather than linear.
MaxPatterns is what bounds that, and nothing else does. A dictionary of distinct words is
nowhere near it: the lists are one entry per node.
Node lookup is a binary search over the edges a node actually has, kept sorted by label, rather than a 256-entry row per node. A dictionary of words touches a few dozen distinct bytes, and paying 1 KiB a node for the rest of the alphabet is how this structure gets expensive in the one place a realm cannot afford it.
Live demo: r/moul/x/daily/ahocorasickdemo
· render it at /r/moul/x/daily/ahocorasickdemo/v0.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Package ahocorasick implements Aho-Corasick multi-pattern search as a pure, reusable package.
Searching for k patterns by running a single-pattern scan k times costs O(k*n): every extra word in the dictionary is another full pass over the text. Aho-Corasick builds one automaton from the whole set, a trie of the patterns plus a failure link on every node pointing at the longest proper suffix that is also a prefix of some pattern, and then reads the text once. Building the trie and the failure links costs O(total pattern length) and scanning costs O(n + matches), whatever k is, and the text cursor never moves backwards.
One deliberate departure from the paper: each node stores the full list of patterns matching there, merged from its failure target at build time, rather than a link to be walked at every offset. That makes a reported match O(1) instead of a walk up the failure chain, and it costs build memory in exactly one shape, a set whose patterns nest ("a", "aa", "aaa", ...), where the merged lists are quadratic in k rather than linear. MaxPatterns is what bounds that, and nothing else does.
That last property is why it is worth having on chain: the worst case equals the best case, so a dictionary and a text chosen by an attacker cost the same as a friendly pair. A naive k-pass scan is an input away from quadratic.
It is p/moul/x/daily/kmp(/p/moul/x/daily/kmp/v0) generalised: KMP's failure table is this automaton for a one-pattern set.
Operates on BYTES, not runes. gno strings are UTF-8 and a match offset is a byte offset, which is the right unit for slicing. UTF-8 is self-synchronizing, so a pattern that is itself valid UTF-8 can only match on a rune boundary of valid UTF-8 text; a pattern that is a bare continuation byte can and will match inside a rune, and slicing on that offset splits it.
A live demo of this package is at r/moul/x/daily/ahocorasickdemo(/r/moul/x/daily/ahocorasickdemo/v0).
Bounds on what New will build, so an on-chain caller cannot be handed an automaton whose size it did not choose. At most MaxPatterns*MaxPatternLen+1 nodes exist, and the build is linear in the total pattern length.
New builds the automaton for patterns. Empty patterns are dropped: the empty string matches at every offset, which is a different question from the one this package answers.
Returns nil when there is nothing to build (no pattern survives the drop) or when the set exceeds the bounds above. A nil Matcher answers every method, so a caller may hand the result straight to a scan without a check.
Match is one occurrence of one pattern. Start and End are byte offsets into the text, half-open, so text[Start:End] is the matched run. Index is the pattern's slot in the slice given to New, which is how a caller maps a match back to whatever it stored alongside the pattern.
Matcher is a built automaton. It is immutable once New returns, so one Matcher can serve any number of scans.
Contains reports whether any pattern occurs in text.
FindAll returns every occurrence of every pattern, including overlapping and nested ones, ordered by end offset. At one end offset the longest match comes first, because the scan reports the node it reached before the shorter suffixes hanging off its failure chain, so a caller taking the first match at each offset gets the greedy one.
FindFirst returns the first match FindAll would return, and stops the scan there. The one to reach for when the question is "does this text contain anything from the list, and where".
Nodes returns the size of the automaton: one node per distinct pattern prefix, plus the root. The number a caller sizing a dictionary wants, since it is what the build actually costs.
Patterns returns the accepted patterns, in the caller's order, as a copy.
Imports nothing.