kleene 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +117 -0
- data/LICENSE +21 -0
- data/README.md +21 -0
- data/Rakefile +8 -0
- data/build.ops +63 -0
- data/kleene.gemspec +39 -0
- data/lib/kleene/dfa.rb +258 -0
- data/lib/kleene/dsl.rb +263 -0
- data/lib/kleene/kleene.rb +88 -0
- data/lib/kleene/multi_match_dfa.rb +308 -0
- data/lib/kleene/nfa.rb +304 -0
- data/lib/kleene/patches.rb +23 -0
- data/lib/kleene/version.rb +3 -0
- data/lib/kleene.rb +17 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0c062445eeb37aa9a123c09e8d682b3c6514be3842a4f63b821ab320daee958b
|
4
|
+
data.tar.gz: 902a5fcc8d767bbb0c3b97e931dba5b72e07f9ef0ec8540c2420675ecd7bc0f7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 54b677033bbae4ced31b75bf3b3130a123017982fb9959a957360a9d8a10a4b50d03533bf27e3fbfafc7052574388e2adba0eeae63beee9dc5c7c6cabed429c0
|
7
|
+
data.tar.gz: 9d0b6d4715254e3f544e4f2ed220103f9d092063ab58a7781688ce035810662f6a441a899660ff9496776a5052548251174aeab3107515530dbe106486d27175
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
kleene (0.1.0)
|
5
|
+
activesupport (~> 7.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (7.1.1)
|
11
|
+
base64
|
12
|
+
bigdecimal
|
13
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
+
connection_pool (>= 2.2.5)
|
15
|
+
drb
|
16
|
+
i18n (>= 1.6, < 2)
|
17
|
+
minitest (>= 5.1)
|
18
|
+
mutex_m
|
19
|
+
tzinfo (~> 2.0)
|
20
|
+
ast (2.4.2)
|
21
|
+
backport (1.2.0)
|
22
|
+
base64 (0.1.1)
|
23
|
+
benchmark (0.2.1)
|
24
|
+
bigdecimal (3.1.4)
|
25
|
+
concurrent-ruby (1.2.2)
|
26
|
+
connection_pool (2.4.1)
|
27
|
+
diff-lcs (1.5.0)
|
28
|
+
drb (2.1.1)
|
29
|
+
ruby2_keywords
|
30
|
+
e2mmap (0.1.0)
|
31
|
+
i18n (1.14.1)
|
32
|
+
concurrent-ruby (~> 1.0)
|
33
|
+
jaro_winkler (1.5.6)
|
34
|
+
json (2.6.3)
|
35
|
+
kramdown (2.4.0)
|
36
|
+
rexml
|
37
|
+
kramdown-parser-gfm (1.1.0)
|
38
|
+
kramdown (~> 2.0)
|
39
|
+
language_server-protocol (3.17.0.3)
|
40
|
+
minitest (5.20.0)
|
41
|
+
mutex_m (0.1.2)
|
42
|
+
nokogiri (1.15.4-x86_64-linux)
|
43
|
+
racc (~> 1.4)
|
44
|
+
parallel (1.23.0)
|
45
|
+
parser (3.2.2.4)
|
46
|
+
ast (~> 2.4.1)
|
47
|
+
racc
|
48
|
+
racc (1.7.2)
|
49
|
+
rainbow (3.1.1)
|
50
|
+
rake (13.1.0)
|
51
|
+
rbs (2.8.4)
|
52
|
+
regexp_parser (2.8.2)
|
53
|
+
reverse_markdown (2.1.1)
|
54
|
+
nokogiri
|
55
|
+
rexml (3.2.6)
|
56
|
+
rspec (3.12.0)
|
57
|
+
rspec-core (~> 3.12.0)
|
58
|
+
rspec-expectations (~> 3.12.0)
|
59
|
+
rspec-mocks (~> 3.12.0)
|
60
|
+
rspec-core (3.12.2)
|
61
|
+
rspec-support (~> 3.12.0)
|
62
|
+
rspec-expectations (3.12.3)
|
63
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
+
rspec-support (~> 3.12.0)
|
65
|
+
rspec-mocks (3.12.6)
|
66
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
67
|
+
rspec-support (~> 3.12.0)
|
68
|
+
rspec-support (3.12.1)
|
69
|
+
rubocop (1.57.2)
|
70
|
+
json (~> 2.3)
|
71
|
+
language_server-protocol (>= 3.17.0)
|
72
|
+
parallel (~> 1.10)
|
73
|
+
parser (>= 3.2.2.4)
|
74
|
+
rainbow (>= 2.2.2, < 4.0)
|
75
|
+
regexp_parser (>= 1.8, < 3.0)
|
76
|
+
rexml (>= 3.2.5, < 4.0)
|
77
|
+
rubocop-ast (>= 1.28.1, < 2.0)
|
78
|
+
ruby-progressbar (~> 1.7)
|
79
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
80
|
+
rubocop-ast (1.30.0)
|
81
|
+
parser (>= 3.2.1.0)
|
82
|
+
ruby-progressbar (1.13.0)
|
83
|
+
ruby2_keywords (0.0.5)
|
84
|
+
solargraph (0.49.0)
|
85
|
+
backport (~> 1.2)
|
86
|
+
benchmark
|
87
|
+
bundler (~> 2.0)
|
88
|
+
diff-lcs (~> 1.4)
|
89
|
+
e2mmap
|
90
|
+
jaro_winkler (~> 1.5)
|
91
|
+
kramdown (~> 2.3)
|
92
|
+
kramdown-parser-gfm (~> 1.1)
|
93
|
+
parser (~> 3.0)
|
94
|
+
rbs (~> 2.0)
|
95
|
+
reverse_markdown (~> 2.0)
|
96
|
+
rubocop (~> 1.38)
|
97
|
+
thor (~> 1.0)
|
98
|
+
tilt (~> 2.0)
|
99
|
+
yard (~> 0.9, >= 0.9.24)
|
100
|
+
thor (1.3.0)
|
101
|
+
tilt (2.3.0)
|
102
|
+
tzinfo (2.0.6)
|
103
|
+
concurrent-ruby (~> 1.0)
|
104
|
+
unicode-display_width (2.5.0)
|
105
|
+
yard (0.9.34)
|
106
|
+
|
107
|
+
PLATFORMS
|
108
|
+
x86_64-linux
|
109
|
+
|
110
|
+
DEPENDENCIES
|
111
|
+
kleene!
|
112
|
+
rake (~> 13.0)
|
113
|
+
rspec (~> 3.0)
|
114
|
+
solargraph
|
115
|
+
|
116
|
+
BUNDLED WITH
|
117
|
+
2.4.10
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 David Ellis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# kleene
|
2
|
+
|
3
|
+
kleene is a library for building regular expression recognition automata - nfas, dfas, and some specialty structures.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Install the gem and add to the application's Gemfile by executing:
|
9
|
+
|
10
|
+
$ bundle add kleene
|
11
|
+
|
12
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
13
|
+
|
14
|
+
$ gem install kleene
|
15
|
+
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require "kleene"
|
21
|
+
```
|
data/Rakefile
ADDED
data/build.ops
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
params:
|
2
|
+
version: string
|
3
|
+
|
4
|
+
imports:
|
5
|
+
core: "opswalrus/core"
|
6
|
+
|
7
|
+
...
|
8
|
+
|
9
|
+
# when you run this script, it should do something like:
|
10
|
+
# ~/sync/projects/kleene-rb
|
11
|
+
# ❯ ops run build.ops version:1.0.0
|
12
|
+
# Write version.rb for version 1.0.0
|
13
|
+
# [localhost] Build gem: gem build opswalrus.gemspec
|
14
|
+
# [localhost] Check whether Bitwarden is locked or not: bw status
|
15
|
+
# [localhost] Get Rubygems OTP: bw get totp Rubygems
|
16
|
+
# [localhost] Push gem: gem push opswalrus-1.0.0.gem
|
17
|
+
# [localhost] Build docker image: docker build -t opswalrus/ops:1.0.0 .
|
18
|
+
|
19
|
+
# ~/sync/projects/ops/opswalrus on main via 💎 v3.2.2 took 44s
|
20
|
+
|
21
|
+
|
22
|
+
version = params.version
|
23
|
+
|
24
|
+
exit 1, "version parameter must be specified" unless version
|
25
|
+
|
26
|
+
template = <<TEMPLATE
|
27
|
+
module Kleene
|
28
|
+
VERSION = "{{ version }}"
|
29
|
+
end
|
30
|
+
TEMPLATE
|
31
|
+
|
32
|
+
puts "Write version.rb for version #{version}"
|
33
|
+
core.template.write template: template,
|
34
|
+
variables: {version: version},
|
35
|
+
to: "./lib/kleene/version.rb"
|
36
|
+
|
37
|
+
sh("Build gem") { 'gem build kleene.gemspec' }
|
38
|
+
|
39
|
+
sh("Commit Gemfile.lock and version.rb and git push changes") { 'git commit -am "gem {{ version }}" && git push' }
|
40
|
+
|
41
|
+
# bw_status_output = sh("Check whether Bitwarden is locked or not") { 'bw status' }
|
42
|
+
is_unlocked = sh? "Check whether Bitwarden is locked or not",
|
43
|
+
'rbw unlocked'
|
44
|
+
# the `bw status`` command currently exhibits an error in which it emits 'mac failed.' some number of times, so we need to filter that out
|
45
|
+
# see:
|
46
|
+
# - https://community.bitwarden.com/t/what-does-mac-failed-mean-exactly/29208
|
47
|
+
# - https://github.com/bitwarden/cli/issues/88
|
48
|
+
# - https://github.com/vwxyzjn/portwarden/issues/22
|
49
|
+
# ❯ bw status
|
50
|
+
# mac failed.
|
51
|
+
# {"serverUrl":"...","lastSync":"2023-08-17T19:14:09.384Z","userEmail":"...","userId":"...","status":"locked"}
|
52
|
+
# bw_status_output = bw_status_output.gsub('mac failed.', '').strip
|
53
|
+
# bw_status_json = bw_status_output.parse_json
|
54
|
+
|
55
|
+
# if bw_status_json['status'] != 'unlocked'
|
56
|
+
# exit 1, "Bitwarden is not unlocked. Please unlock bitwarden with: bw unlock"
|
57
|
+
# end
|
58
|
+
exit 1, "Bitwarden is not unlocked. Please unlock bitwarden with: rbw unlock" unless is_unlocked
|
59
|
+
|
60
|
+
# totp = sh("Get Rubygems OTP") { 'bw get totp Rubygems' }
|
61
|
+
totp = sh "Get Rubygems OTP",
|
62
|
+
'rbw get -f totp Rubygems'
|
63
|
+
sh("Push gem", input: {/You have enabled multi-factor authentication. Please enter OTP code./ => "#{totp}\n"}) { 'gem push kleene-{{ version }}.gem' }
|
data/kleene.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/kleene/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "kleene"
|
7
|
+
spec.version = Kleene::VERSION
|
8
|
+
spec.authors = ["David Ellis"]
|
9
|
+
spec.email = ["david@conquerthelawn.com"]
|
10
|
+
|
11
|
+
spec.summary = "kleene is a library for building regular expression recognition automata"
|
12
|
+
spec.description = "kleene is a library for building regular expression recognition automata - nfas, dfas, and some specialty structures."
|
13
|
+
spec.homepage = "https://github.com/davidkellis/kleene-rb"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/davidkellis/kleene-rb"
|
21
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
# Uncomment to register a new dependency of your gem
|
35
|
+
spec.add_dependency "activesupport", "~> 7.1"
|
36
|
+
|
37
|
+
# For more information and examples about making a new gem, check out our
|
38
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
39
|
+
end
|
data/lib/kleene/dfa.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
module Kleene
|
2
|
+
class DFATransition
|
3
|
+
attr_accessor :token # : Char
|
4
|
+
attr_accessor :from # : State
|
5
|
+
attr_accessor :to # : State
|
6
|
+
|
7
|
+
def initialize(token, from_state, to_state)
|
8
|
+
@token = token
|
9
|
+
@from = from_state
|
10
|
+
@to = to_state
|
11
|
+
end
|
12
|
+
|
13
|
+
def accept?(input)
|
14
|
+
@token == input
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# ->(transition : DFATransition, token : Char, token_index : Int32) : Nil { ... }
|
19
|
+
# alias DFATransitionCallback = Proc(DFATransition, Char, Int32, Nil)
|
20
|
+
|
21
|
+
class DFA
|
22
|
+
attr_accessor :alphabet # : Set(Char)
|
23
|
+
attr_accessor :states # : Set(State)
|
24
|
+
attr_accessor :start_state # : State
|
25
|
+
attr_accessor :current_state # : State
|
26
|
+
attr_accessor :transitions # : Hash(State, Hash(Char, DFATransition))
|
27
|
+
attr_accessor :final_states # : Set(State)
|
28
|
+
attr_accessor :dfa_state_to_nfa_state_sets # : Hash(State, Set(State)) # this map contains (dfa_state => nfa_state_set) pairs
|
29
|
+
attr_accessor :nfa_state_to_dfa_state_sets # : Hash(State, Set(State)) # this map contains (nfa_state => dfa_state_set) pairs
|
30
|
+
attr_accessor :transition_callbacks # : Hash(DFATransition, DFATransitionCallback)
|
31
|
+
attr_accessor :transition_callbacks_per_destination_state # : Hash(State, DFATransitionCallback)
|
32
|
+
# @origin_nfa : NFA?
|
33
|
+
# @error_states : Set(State)?
|
34
|
+
# @regex_pattern : String?
|
35
|
+
|
36
|
+
def initialize(start_state, alphabet = DEFAULT_ALPHABET, transitions = Hash.new, dfa_state_to_nfa_state_sets = Hash.new, transition_callbacks = nil, origin_nfa: nil)
|
37
|
+
@start_state = start_state
|
38
|
+
@current_state = start_state
|
39
|
+
@transitions = transitions
|
40
|
+
@dfa_state_to_nfa_state_sets = dfa_state_to_nfa_state_sets
|
41
|
+
|
42
|
+
@alphabet = alphabet + all_transitions.map(&:token)
|
43
|
+
|
44
|
+
@states = reachable_states(@start_state)
|
45
|
+
@final_states = Set.new
|
46
|
+
|
47
|
+
@nfa_state_to_dfa_state_sets = Hash.new
|
48
|
+
@dfa_state_to_nfa_state_sets.each do |dfa_state, nfa_state_set|
|
49
|
+
nfa_state_set.each do |nfa_state|
|
50
|
+
dfa_state_set = @nfa_state_to_dfa_state_sets[nfa_state] ||= Set.new
|
51
|
+
dfa_state_set << dfa_state
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@transition_callbacks = transition_callbacks || Hash.new
|
56
|
+
@transition_callbacks_per_destination_state = Hash.new
|
57
|
+
|
58
|
+
@origin_nfa = origin_nfa
|
59
|
+
|
60
|
+
update_final_states
|
61
|
+
reset_current_state
|
62
|
+
end
|
63
|
+
|
64
|
+
def origin_nfa
|
65
|
+
@origin_nfa || raise("This DFA was not created from an NFA, therefore it has no origin_nfa.")
|
66
|
+
end
|
67
|
+
|
68
|
+
def error_states
|
69
|
+
@error_states ||= @states.select {|s| s.error? }.to_set
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear_error_states
|
73
|
+
@error_states = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def all_transitions() # : Array(DFATransition)
|
77
|
+
transitions.flat_map {|state, char_transition_map| char_transition_map.values }
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_transition(transition, &blk)
|
81
|
+
@transition_callbacks[transition] = blk
|
82
|
+
end
|
83
|
+
|
84
|
+
def on_transition_to(state, &blk)
|
85
|
+
@transition_callbacks_per_destination_state[state] = blk
|
86
|
+
end
|
87
|
+
|
88
|
+
def shallow_clone
|
89
|
+
DFA.new(start_state, alphabet, transitions, dfa_state_to_nfa_state_sets, transition_callbacks, origin_nfa: origin_nfa).set_regex_pattern(regex_pattern)
|
90
|
+
end
|
91
|
+
|
92
|
+
# transition callbacks are not copied beacuse it is assumed that the state transition callbacks may be stateful and reference structures or states that only exist in `self`, but not the cloned copy.
|
93
|
+
def deep_clone
|
94
|
+
old_states = @states.to_a
|
95
|
+
new_states = old_states.map(&:dup)
|
96
|
+
state_mapping = old_states.zip(new_states).to_h
|
97
|
+
transition_mapping = Hash.new
|
98
|
+
new_transitions = transitions.map do |state, char_transition_map|
|
99
|
+
[
|
100
|
+
state_mapping[state],
|
101
|
+
char_transition_map.map do |char, old_transition|
|
102
|
+
new_transition = DFATransition.new(old_transition.token, state_mapping[old_transition.from], state_mapping[old_transition.to])
|
103
|
+
transition_mapping[old_transition] = new_transition
|
104
|
+
[char, new_transition]
|
105
|
+
end.to_h
|
106
|
+
]
|
107
|
+
end.to_h
|
108
|
+
# new_transition_callbacks = transition_callbacks.map do |transition, callback|
|
109
|
+
# {
|
110
|
+
# transition_mapping[transition],
|
111
|
+
# callback
|
112
|
+
# }
|
113
|
+
# end.to_h
|
114
|
+
|
115
|
+
new_dfa_state_to_nfa_state_sets = dfa_state_to_nfa_state_sets.map {|dfa_state, nfa_state_set| [state_mapping[dfa_state], nfa_state_set] }.to_h
|
116
|
+
|
117
|
+
DFA.new(state_mapping[@start_state], @alphabet.clone, new_transitions, new_dfa_state_to_nfa_state_sets, origin_nfa: origin_nfa).set_regex_pattern(regex_pattern)
|
118
|
+
end
|
119
|
+
|
120
|
+
def update_final_states
|
121
|
+
@final_states = @states.select {|s| s.final? }.to_set
|
122
|
+
end
|
123
|
+
|
124
|
+
def reset_current_state
|
125
|
+
@current_state = @start_state
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_transition(token, from_state, to_state)
|
129
|
+
@alphabet << token # alphabet is a set, so there will be no duplications
|
130
|
+
@states << to_state # states is a set, so there will be no duplications (to_state should be the only new state)
|
131
|
+
new_transition = DFATransition.new(token, from_state, to_state)
|
132
|
+
@transitions[from_state][token] = new_transition
|
133
|
+
new_transition
|
134
|
+
end
|
135
|
+
|
136
|
+
def match?(input)
|
137
|
+
reset_current_state
|
138
|
+
|
139
|
+
input.each_char.with_index do |char, index|
|
140
|
+
handle_token!(char, index)
|
141
|
+
end
|
142
|
+
|
143
|
+
if accept?
|
144
|
+
MatchRef.new(input, 0...input.size)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns an array of matches found in the input string, each of which begins at the offset input_start_offset
|
149
|
+
def matches_at_offset(input, input_start_offset)
|
150
|
+
reset_current_state
|
151
|
+
|
152
|
+
matches = []
|
153
|
+
(input_start_offset...input.size).each do |offset|
|
154
|
+
token = input[offset]
|
155
|
+
handle_token!(token, offset)
|
156
|
+
if accept?
|
157
|
+
matches << MatchRef.new(input, input_start_offset..offset)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
matches
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns an array of matches found anywhere in the input string
|
164
|
+
def matches(input)
|
165
|
+
(0...input.size).reduce([]) do |memo, offset|
|
166
|
+
memo + matches_at_offset(input, offset)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# accept an input token and transition to the next state in the state machine
|
171
|
+
def handle_token!(input_token, token_index)
|
172
|
+
@current_state = next_state(@current_state, input_token, token_index)
|
173
|
+
end
|
174
|
+
|
175
|
+
def accept?
|
176
|
+
@current_state.final?
|
177
|
+
end
|
178
|
+
|
179
|
+
def error?
|
180
|
+
@current_state.error?
|
181
|
+
end
|
182
|
+
|
183
|
+
# def terminal?
|
184
|
+
# accept? || error?
|
185
|
+
# end
|
186
|
+
|
187
|
+
# if the DFA is currently in a final state, then we look up the associated NFA states that were also final, and return them
|
188
|
+
# def accepting_nfa_states : Set(State)
|
189
|
+
# if accept?
|
190
|
+
# dfa_state_to_nfa_state_sets[@current_state].select(&:final?).to_set
|
191
|
+
# else
|
192
|
+
# Set.new
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
|
196
|
+
# this function transitions from state to state on an input token
|
197
|
+
def next_state(from_state, input_token, token_index)
|
198
|
+
transition = @transitions[from_state][input_token] || raise("No DFA transition found. Input token #{input_token} not in DFA alphabet.")
|
199
|
+
|
200
|
+
# invoke the relevant transition callback function
|
201
|
+
transition_callbacks[transition].try {|callback_fn| callback_fn.call(transition, input_token, token_index) }
|
202
|
+
transition_callbacks_per_destination_state[transition.to].try {|callback_fn| callback_fn.call(transition, input_token, token_index) }
|
203
|
+
|
204
|
+
transition.to
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns a set of State objects which are reachable through any transition path from the DFA's start_state.
|
208
|
+
def reachable_states(start_state)
|
209
|
+
visited_states = Set.new()
|
210
|
+
unvisited_states = Set[start_state]
|
211
|
+
while !unvisited_states.empty?
|
212
|
+
outbound_transitions = unvisited_states.flat_map {|state| @transitions[state].try(&:values) || Array.new }
|
213
|
+
destination_states = outbound_transitions.map(&:to).to_set
|
214
|
+
visited_states.merge(unvisited_states) # add the unvisited states to the visited_states
|
215
|
+
unvisited_states = destination_states - visited_states
|
216
|
+
end
|
217
|
+
visited_states
|
218
|
+
end
|
219
|
+
|
220
|
+
# this is currently broken
|
221
|
+
# def to_nfa
|
222
|
+
# dfa = self.deep_clone
|
223
|
+
# NFA.new(dfa.start_state, dfa.alphabet.clone, dfa.transitions)
|
224
|
+
# # todo: add all of this machine's transitions to the new machine
|
225
|
+
# # @transitions.each {|t| nfa.add_transition(t.token, t.from, t.to) }
|
226
|
+
# # nfa
|
227
|
+
# end
|
228
|
+
|
229
|
+
def to_s(verbose = false)
|
230
|
+
if verbose
|
231
|
+
retval = states.map(&:to_s).join("\n")
|
232
|
+
retval += "\n"
|
233
|
+
all_transitions.each do |t|
|
234
|
+
retval += "#{t.from.id} -> #{t.token} -> #{t.to.id}\n"
|
235
|
+
end
|
236
|
+
retval
|
237
|
+
else
|
238
|
+
regex_pattern
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# This is an implementation of the "Reducing a DFA to a Minimal DFA" algorithm presented here: http://web.cecs.pdx.edu/~harry/compilers/slides/LexicalPart4.pdf
|
243
|
+
# This implements Hopcroft's algorithm as presented on page 142 of the first edition of the dragon book.
|
244
|
+
def minimize!
|
245
|
+
# todo: I'll implement this when I need it
|
246
|
+
end
|
247
|
+
|
248
|
+
def set_regex_pattern(pattern)
|
249
|
+
@regex_pattern = pattern
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
253
|
+
def regex_pattern
|
254
|
+
@regex_pattern || "<<empty>>"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|