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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in kleene.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem 'solargraph', group: :development
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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