flexr 1.0.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/.rubocop.yml +33 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +468 -0
- data/benchmark/baselines/json.json +34 -0
- data/benchmark/baselines/json_handwritten.rb +43 -0
- data/benchmark/baselines/json_rexical.rex +25 -0
- data/benchmark/corpora/README.md +11 -0
- data/benchmark/corpora/generate_json.rb +26 -0
- data/benchmark/golden/calculator_lexer.sha256 +1 -0
- data/benchmark/golden/json_lexer.sha256 +1 -0
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
- data/benchmark/golden/with_racc_lexer.sha256 +1 -0
- data/benchmark/run.rb +254 -0
- data/docs/README.md +64 -0
- data/docs/RELEASING.md +30 -0
- data/docs/adr/0001-byte-level-dfa.md +5 -0
- data/docs/adr/0003-leftmost-longest.md +4 -0
- data/docs/adr/0006-accel-not-scanner.md +4 -0
- data/docs/adr/0008-what-pure-ruby-means.md +5 -0
- data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
- data/docs/adr/0017-static-analysis-by-default.md +5 -0
- data/docs/adr/0018-prism-for-generator-only.md +4 -0
- data/docs/adr/0019-measured-performance-floor.md +26 -0
- data/docs/adr/0020-vendored-unicode-contract.md +21 -0
- data/docs/explanation/backends.md +33 -0
- data/docs/explanation/matching-semantics.md +20 -0
- data/docs/explanation/runtime-vs-generated.md +22 -0
- data/docs/explanation/security-model.md +18 -0
- data/docs/explanation/unicode-and-encoding.md +20 -0
- data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
- data/docs/how-to/generate-a-lexer.md +39 -0
- data/docs/how-to/handle-errors.md +32 -0
- data/docs/how-to/integrate-with-lrama.md +21 -0
- data/docs/how-to/integrate-with-racc.md +25 -0
- data/docs/how-to/migrate-from-flex.md +21 -0
- data/docs/how-to/migrate-from-rexical.md +23 -0
- data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
- data/docs/how-to/track-token-locations.md +27 -0
- data/docs/how-to/tune-performance.md +23 -0
- data/docs/how-to/use-states.md +36 -0
- data/docs/how-to/use-trailing-context.md +22 -0
- data/docs/internals/README.md +14 -0
- data/docs/perf-log.md +56 -0
- data/docs/reference/README.md +23 -0
- data/docs/reference/actions.md +47 -0
- data/docs/reference/cli.md +80 -0
- data/docs/reference/compatibility.md +38 -0
- data/docs/reference/diagnostics.md +41 -0
- data/docs/reference/dsl.md +81 -0
- data/docs/reference/errors.md +27 -0
- data/docs/reference/generated-artifacts.md +50 -0
- data/docs/reference/public-api.md +42 -0
- data/docs/reference/regexp.md +39 -0
- data/docs/reference/runtime.md +49 -0
- data/docs/reference/tokens-and-locations.md +33 -0
- data/docs/tutorial/build-a-calculator-lexer.md +96 -0
- data/examples/calculator/README.md +27 -0
- data/examples/calculator/lexer.flexr.rb +17 -0
- data/examples/json/README.md +30 -0
- data/examples/json/lexer.flexr.rb +24 -0
- data/examples/ruby_subset/README.md +17 -0
- data/examples/ruby_subset/lexer.flexr.rb +22 -0
- data/examples/toy_lang/README.md +17 -0
- data/examples/toy_lang/lexer.flexr.rb +18 -0
- data/examples/with_lrama/README.md +17 -0
- data/examples/with_lrama/lexer.flexr.rb +13 -0
- data/examples/with_racc/README.md +17 -0
- data/examples/with_racc/lexer.flexr.rb +13 -0
- data/exe/flexr +7 -0
- data/lib/flexr/automaton/accel.rb +39 -0
- data/lib/flexr/automaton/analysis.rb +38 -0
- data/lib/flexr/automaton/byte_class_set.rb +29 -0
- data/lib/flexr/automaton/compiler.rb +413 -0
- data/lib/flexr/automaton/dfa.rb +103 -0
- data/lib/flexr/automaton/minimizer.rb +70 -0
- data/lib/flexr/automaton/nfa.rb +92 -0
- data/lib/flexr/cli.rb +342 -0
- data/lib/flexr/codegen/base.rb +17 -0
- data/lib/flexr/codegen/direct.rb +52 -0
- data/lib/flexr/codegen/firstmatch.rb +17 -0
- data/lib/flexr/codegen/table.rb +158 -0
- data/lib/flexr/codegen/table_packer.rb +61 -0
- data/lib/flexr/diagnostics.rb +94 -0
- data/lib/flexr/dsl.rb +182 -0
- data/lib/flexr/errors.rb +28 -0
- data/lib/flexr/generated.rb +125 -0
- data/lib/flexr/generator.rb +400 -0
- data/lib/flexr/importer.rb +560 -0
- data/lib/flexr/ir.rb +36 -0
- data/lib/flexr/lexer.rb +10 -0
- data/lib/flexr/options.rb +47 -0
- data/lib/flexr/rake_task.rb +27 -0
- data/lib/flexr/regexp/ast.rb +45 -0
- data/lib/flexr/regexp/char_class.rb +7 -0
- data/lib/flexr/regexp/normalizer.rb +117 -0
- data/lib/flexr/regexp/parser.rb +517 -0
- data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
- data/lib/flexr/regexp/tokenizer.rb +168 -0
- data/lib/flexr/regexp/unsupported.rb +7 -0
- data/lib/flexr/runtime/buffer.rb +112 -0
- data/lib/flexr/runtime/core.rb +388 -0
- data/lib/flexr/runtime/errors.rb +22 -0
- data/lib/flexr/runtime/interpreter.rb +505 -0
- data/lib/flexr/runtime/location.rb +26 -0
- data/lib/flexr/runtime/token.rb +7 -0
- data/lib/flexr/source/passthrough.rb +31 -0
- data/lib/flexr/source/prism_reader.rb +283 -0
- data/lib/flexr/source/static_eval.rb +145 -0
- data/lib/flexr/unicode/case_fold.rb +45 -0
- data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
- data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
- data/lib/flexr/unicode/data/case_folding.rb +9 -0
- data/lib/flexr/unicode/data/properties.rb +10 -0
- data/lib/flexr/unicode/property.rb +107 -0
- data/lib/flexr/unicode/reference_regexp.rb +102 -0
- data/lib/flexr/unicode/utf8_splitter.rb +109 -0
- data/lib/flexr/version.rb +5 -0
- data/lib/flexr.rb +81 -0
- data/site/README.md +22 -0
- data/site/astro.config.mjs +57 -0
- data/site/package.json +19 -0
- data/site/pnpm-lock.yaml +5029 -0
- data/site/pnpm-workspace.yaml +6 -0
- data/site/public/playground.js +189 -0
- data/site/scripts/verify-site.mjs +42 -0
- data/site/src/content/docs/benchmarks.md +8 -0
- data/site/src/content/docs/concepts/matching-semantics.md +15 -0
- data/site/src/content/docs/concepts/regexp-model.md +18 -0
- data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
- data/site/src/content/docs/concepts/security-model.md +15 -0
- data/site/src/content/docs/examples.md +17 -0
- data/site/src/content/docs/learn/generation.md +29 -0
- data/site/src/content/docs/learn/getting-started.md +56 -0
- data/site/src/content/docs/learn/parser-integration.md +27 -0
- data/site/src/content/docs/learn/runtime-mode.md +32 -0
- data/site/src/content/docs/reference/action-context.md +20 -0
- data/site/src/content/docs/reference/cli.md +22 -0
- data/site/src/content/docs/reference/diagnostics.md +16 -0
- data/site/src/content/docs/reference/dsl.md +19 -0
- data/site/src/content/docs/reference/public-api.md +18 -0
- data/site/src/content/docs/reference/regexp.md +16 -0
- data/site/src/content/docs/reference/runtime.md +16 -0
- data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
- data/site/src/content.config.ts +12 -0
- data/site/src/env.d.ts +1 -0
- data/site/src/layouts/SiteLayout.astro +39 -0
- data/site/src/pages/index.astro +174 -0
- data/site/src/pages/playground.astro +64 -0
- data/site/src/styles/custom.css +711 -0
- data/site/tsconfig.json +5 -0
- data/tools/coverage.rb +32 -0
- data/tools/docs_verify.rb +116 -0
- data/tools/gen_unicode_tables.rb +202 -0
- data/tools/regexp_tokenizer_reference.rb +60 -0
- metadata +205 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
class Importer
|
|
5
|
+
Result = Struct.new(:source, :warnings, :complete?, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
class UnsupportedFormatError < CompileError; end
|
|
8
|
+
|
|
9
|
+
def self.import(path)
|
|
10
|
+
new(path, File.binread(path)).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(path, source)
|
|
14
|
+
@path = path
|
|
15
|
+
@source = source.force_encoding(Encoding::UTF_8)
|
|
16
|
+
@warnings = []
|
|
17
|
+
@complete = true
|
|
18
|
+
@macros = {}
|
|
19
|
+
@states = {}
|
|
20
|
+
@rules = []
|
|
21
|
+
@eof_rules = []
|
|
22
|
+
@last_action = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
case File.extname(@path).downcase
|
|
27
|
+
when ".l", ".lex"
|
|
28
|
+
parse_flex
|
|
29
|
+
when ".rex"
|
|
30
|
+
parse_rexical
|
|
31
|
+
else
|
|
32
|
+
raise UnsupportedFormatError, "import expects a flex .l or rexical .rex file"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Result.new(source: render, warnings: @warnings.freeze, complete?: @complete)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse_flex
|
|
41
|
+
sections = @source.split(/^%%\s*$/)
|
|
42
|
+
raise CompileError, "flex specification must contain a %% rule separator" if sections.length < 2
|
|
43
|
+
|
|
44
|
+
declarations = sections.shift
|
|
45
|
+
rules = sections.shift
|
|
46
|
+
footer = sections.join("%%\n")
|
|
47
|
+
parse_declarations(declarations)
|
|
48
|
+
parse_rules(rules)
|
|
49
|
+
add_comment_block(footer, "footer") unless footer.strip.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_rexical
|
|
53
|
+
@warnings << "Rexical uses first-match semantics; imported rules will use flexr longest-match semantics"
|
|
54
|
+
@source.scan(/^\s*macro\s*\n(.*?)^\s*end\s*$/m) do |body|
|
|
55
|
+
body.first.each_line do |line|
|
|
56
|
+
name, expression = line.strip.split(/\s+/, 2)
|
|
57
|
+
@macros[name] = expression if name && expression
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
match = @source.match(/^\s*rule\s*\n(.*?)^\s*end\s*$/m)
|
|
61
|
+
raise CompileError, "rexical specification has no rule section" unless match
|
|
62
|
+
|
|
63
|
+
parse_rules(match[1].lines.map { |line| normalize_rexical_line(line) }.join)
|
|
64
|
+
warn_rexical_semantic_differences
|
|
65
|
+
header = @source.lines.take_while { |line| !line.match?(/^\s*(?:macro|rule)\b/) }.join
|
|
66
|
+
add_comment_block(header, "rexical header") unless header.strip.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_declarations(text)
|
|
70
|
+
text.each_line do |line|
|
|
71
|
+
stripped = line.strip
|
|
72
|
+
case stripped
|
|
73
|
+
when "", /^%\{/
|
|
74
|
+
next
|
|
75
|
+
when /^%x\s+(.+)/
|
|
76
|
+
::Regexp.last_match(1).split.each { |name| @states[name] = false }
|
|
77
|
+
when /^%s\s+(.+)/
|
|
78
|
+
::Regexp.last_match(1).split.each { |name| @states[name] = true }
|
|
79
|
+
when /^%token\s+(.+)/
|
|
80
|
+
@declared_tokens ||= []
|
|
81
|
+
@declared_tokens.concat(::Regexp.last_match(1).split.map(&:to_sym))
|
|
82
|
+
when /^%option\s+(.+)/
|
|
83
|
+
parse_flex_options(::Regexp.last_match(1))
|
|
84
|
+
when /^%[A-Za-z]/
|
|
85
|
+
warn_incomplete("unsupported flex declaration: #{stripped}")
|
|
86
|
+
when /^([A-Za-z_]\w*)\s+(.+)/
|
|
87
|
+
@macros[::Regexp.last_match(1)] = ::Regexp.last_match(2).strip
|
|
88
|
+
else
|
|
89
|
+
add_comment_block(line, "declaration")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_rules(text)
|
|
95
|
+
lines = text.each_line.to_a
|
|
96
|
+
index = 0
|
|
97
|
+
pending_pattern = nil
|
|
98
|
+
|
|
99
|
+
while index < lines.length
|
|
100
|
+
line = lines[index]
|
|
101
|
+
index += 1
|
|
102
|
+
next if line.strip.empty? || line.lstrip.start_with?("/*")
|
|
103
|
+
|
|
104
|
+
pattern, action = split_rule_line(line)
|
|
105
|
+
if pattern && action
|
|
106
|
+
action, index = collect_action(action, lines, index)
|
|
107
|
+
elsif pattern
|
|
108
|
+
pending_pattern = pattern
|
|
109
|
+
next
|
|
110
|
+
elsif pending_pattern
|
|
111
|
+
action = line.strip
|
|
112
|
+
action, index = collect_action(action, lines, index)
|
|
113
|
+
pattern = pending_pattern
|
|
114
|
+
pending_pattern = nil
|
|
115
|
+
else
|
|
116
|
+
warn_incomplete("could not parse flex rule: #{line.strip}")
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
add_rule(pattern, action)
|
|
121
|
+
end
|
|
122
|
+
warn_incomplete("rule has no action: #{pending_pattern}") if pending_pattern
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def collect_action(action, lines, index)
|
|
126
|
+
return [action, index] unless action.lstrip.start_with?("{")
|
|
127
|
+
|
|
128
|
+
depth = brace_delta(action)
|
|
129
|
+
while depth.positive? && index < lines.length
|
|
130
|
+
action = "#{action}#{lines[index]}"
|
|
131
|
+
depth += brace_delta(lines[index])
|
|
132
|
+
index += 1
|
|
133
|
+
end
|
|
134
|
+
warn_incomplete("unterminated flex action") if depth.positive?
|
|
135
|
+
[action, index]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def add_rule(pattern, action)
|
|
139
|
+
if pattern.strip == "<<EOF>>"
|
|
140
|
+
action_expression, complete = translate_action(action)
|
|
141
|
+
@complete = false unless complete
|
|
142
|
+
@eof_rules << [[], action_expression]
|
|
143
|
+
return
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
states, pattern = extract_states(pattern)
|
|
147
|
+
if pattern == "<<EOF>>"
|
|
148
|
+
action_expression, complete = translate_action(action)
|
|
149
|
+
@complete = false unless complete
|
|
150
|
+
@eof_rules << [states, action_expression]
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
expanded = expand_macros(pattern)
|
|
155
|
+
expression = normalize_pattern(expanded)
|
|
156
|
+
action_expression, complete = translate_action(action)
|
|
157
|
+
@complete = false unless complete
|
|
158
|
+
@rules << { states: states, pattern: expression, raw_pattern: expanded, action: action_expression }
|
|
159
|
+
@last_action = action_expression
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def extract_states(pattern)
|
|
163
|
+
match = pattern.match(/\A<([^>]+)>/)
|
|
164
|
+
return [[], pattern] unless match
|
|
165
|
+
|
|
166
|
+
names = match[1].split(",").map(&:strip)
|
|
167
|
+
names = @states.keys if names.include?("*")
|
|
168
|
+
names = names.map { |name| name == "INITIAL" ? :initial : name.to_sym }
|
|
169
|
+
[names, pattern.delete_prefix(match[0]).strip]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def expand_macros(pattern)
|
|
173
|
+
result = pattern.dup
|
|
174
|
+
10.times do
|
|
175
|
+
before = result
|
|
176
|
+
@macros.sort_by { |name, _| -name.length }.each do |name, expression|
|
|
177
|
+
result = result.gsub("{#{name}}", "(?:#{expression})")
|
|
178
|
+
end
|
|
179
|
+
break if before == result
|
|
180
|
+
end
|
|
181
|
+
warn_incomplete("unresolved flex macro in #{pattern}") if result.match?(/\{[A-Za-z_]\w*\}/)
|
|
182
|
+
result
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def normalize_pattern(pattern)
|
|
186
|
+
return "" if pattern == ""
|
|
187
|
+
|
|
188
|
+
result = +""
|
|
189
|
+
index = 0
|
|
190
|
+
class_depth = 0
|
|
191
|
+
while index < pattern.length
|
|
192
|
+
if pattern[index] == '"' && class_depth.zero?
|
|
193
|
+
finish = index + 1
|
|
194
|
+
escaped = false
|
|
195
|
+
while finish < pattern.length
|
|
196
|
+
char = pattern[finish]
|
|
197
|
+
break if char == '"' && !escaped
|
|
198
|
+
|
|
199
|
+
escaped = char == "\\" && !escaped
|
|
200
|
+
escaped = false unless char == "\\"
|
|
201
|
+
finish += 1
|
|
202
|
+
end
|
|
203
|
+
if finish >= pattern.length
|
|
204
|
+
warn_incomplete("unterminated quoted flex pattern: #{pattern}")
|
|
205
|
+
return pattern.inspect
|
|
206
|
+
end
|
|
207
|
+
result << ::Regexp.escape(unescape_flex(pattern[(index + 1)...finish]))
|
|
208
|
+
index = finish + 1
|
|
209
|
+
else
|
|
210
|
+
class_depth += 1 if pattern[index] == "["
|
|
211
|
+
class_depth -= 1 if pattern[index] == "]" && class_depth.positive?
|
|
212
|
+
result << pattern[index]
|
|
213
|
+
index += 1
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
result.inspect
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def translate_action(action)
|
|
220
|
+
value = action.to_s.strip
|
|
221
|
+
return ["skip: true", true] if value.empty? || value == ";"
|
|
222
|
+
return [@last_action || "skip: true", !@last_action.nil?] if value == "|"
|
|
223
|
+
|
|
224
|
+
body = if value.start_with?("{") && value.end_with?("}")
|
|
225
|
+
value[1...-1]
|
|
226
|
+
else
|
|
227
|
+
value
|
|
228
|
+
end
|
|
229
|
+
translated = body.dup
|
|
230
|
+
translated.gsub!(/\byytext\b/, "text")
|
|
231
|
+
translated.gsub!(/\byyleng\b/, "text.bytesize")
|
|
232
|
+
translated.gsub!(/\byylineno\b/, "lineno")
|
|
233
|
+
translated.gsub!(/\bBEGIN\s*\(\s*([A-Za-z_]\w*)\s*\)/, 'begin_state :\1')
|
|
234
|
+
translated.gsub!(/\byyless\s*\(\s*([^)]*)\)/, 'less(\1)')
|
|
235
|
+
translated.gsub!(/\byymore\s*\(\s*\)/, "more")
|
|
236
|
+
translated.gsub!(/\bECHO\b/, "echo")
|
|
237
|
+
|
|
238
|
+
returns = translated.scan(/\breturn\s+([^;]+);?/)
|
|
239
|
+
translated.gsub!(/\breturn\s+([^;]+);?\s*/) { "emit #{token_expression(::Regexp.last_match(1))}\n" }
|
|
240
|
+
translated.gsub!(/\breturn\s*;/, "skip\n")
|
|
241
|
+
|
|
242
|
+
complete = true
|
|
243
|
+
unless translated.match?(/\A\s*(?:emit\b|skip\b|echo\b|more\b|less\b|begin_state\b|[A-Za-z_]\w*\s*=|#|\z)/)
|
|
244
|
+
warn_incomplete("FLEXR-TODO: manual action translation required: #{body.strip}")
|
|
245
|
+
translated = "# FLEXR-TODO: translate imported action: #{body.strip.inspect}\n skip"
|
|
246
|
+
complete = false
|
|
247
|
+
end
|
|
248
|
+
if returns.any? && translated.scan(/\bemit\b/).empty?
|
|
249
|
+
warn_incomplete("FLEXR-TODO: could not translate flex return action: #{body.strip}")
|
|
250
|
+
translated = "# FLEXR-TODO: translate imported action: #{body.strip.inspect}\n skip"
|
|
251
|
+
complete = false
|
|
252
|
+
end
|
|
253
|
+
["do\n #{translated.strip}\nend", complete]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def render
|
|
257
|
+
lines = ["# Generated by flexr import. Review semantic warnings before use.", "require \"flexr\"", "", "class Lexer < Flexr::Lexer"]
|
|
258
|
+
lines.concat(render_comments)
|
|
259
|
+
lines << " emits #{@declared_tokens.map(&:inspect).join(', ')}" if @declared_tokens&.any?
|
|
260
|
+
@states.each do |name, inclusive|
|
|
261
|
+
lines << " state #{name.inspect}, inclusive: #{inclusive} do" unless @rules.any? { |rule| rule[:states].include?(name.to_sym) }
|
|
262
|
+
lines << " end" unless @rules.any? { |rule| rule[:states].include?(name.to_sym) }
|
|
263
|
+
end
|
|
264
|
+
@rules.each do |rule|
|
|
265
|
+
lines.concat(render_rule(rule))
|
|
266
|
+
end
|
|
267
|
+
@eof_rules.each do |states, action|
|
|
268
|
+
states = [:initial] if states.empty?
|
|
269
|
+
states.each do |state|
|
|
270
|
+
if state == :initial
|
|
271
|
+
lines << " on_eof #{action}"
|
|
272
|
+
else
|
|
273
|
+
lines << " state #{state.inspect} do"
|
|
274
|
+
lines << " on_eof #{action}"
|
|
275
|
+
lines << " end"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
lines << "end"
|
|
280
|
+
lines << ""
|
|
281
|
+
lines.concat(@comments_after_class || [])
|
|
282
|
+
"#{lines.join("\n")}\n"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def render_rule(rule)
|
|
286
|
+
action = rule[:action]
|
|
287
|
+
states = rule[:states]
|
|
288
|
+
if states.empty? || states == [:initial]
|
|
289
|
+
[render_rule_call(" ", rule[:pattern], action)]
|
|
290
|
+
else
|
|
291
|
+
states.map do |state|
|
|
292
|
+
if state == :initial
|
|
293
|
+
render_rule_call(" ", rule[:pattern], action)
|
|
294
|
+
else
|
|
295
|
+
inclusive = @states[state.to_s] == true ? ", inclusive: true" : ""
|
|
296
|
+
" state #{state.inspect}#{inclusive} do\n#{render_rule_call(' ', rule[:pattern], action)}\n end"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def render_rule_call(indent, pattern, action)
|
|
303
|
+
prefix = "#{indent}rule(Regexp.new(#{pattern})"
|
|
304
|
+
return "#{prefix}, #{action})" unless action.start_with?("do\n")
|
|
305
|
+
|
|
306
|
+
lines = action.lines.map(&:chomp)
|
|
307
|
+
body = lines.drop(1).map { |line| "#{indent}#{line}" }
|
|
308
|
+
"#{prefix}) #{([lines.first] + body).join("\n")}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def token_expression(value)
|
|
312
|
+
value = value.strip
|
|
313
|
+
value.match?(/\A[A-Za-z_]\w*\z/) ? ":#{value}" : value
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def render_comments
|
|
317
|
+
@comments || []
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def add_comment_block(text, label)
|
|
321
|
+
@comments ||= []
|
|
322
|
+
@comments << " # Imported #{label}:"
|
|
323
|
+
text.each_line { |line| @comments << " # #{line.chomp}" }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def warn_incomplete(message)
|
|
327
|
+
@warnings << message
|
|
328
|
+
@complete = false
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def parse_flex_options(text)
|
|
332
|
+
text.split(/[\s,]+/).reject(&:empty?).each do |option|
|
|
333
|
+
next if option == "yylineno"
|
|
334
|
+
|
|
335
|
+
warn_incomplete("unsupported flex option #{option}")
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def warn_rexical_semantic_differences
|
|
340
|
+
@rules.each_index do |first_index|
|
|
341
|
+
((first_index + 1)...@rules.length).each do |second_index|
|
|
342
|
+
first = @rules[first_index]
|
|
343
|
+
second = @rules[second_index]
|
|
344
|
+
next unless rexical_rules_overlap?(first, second)
|
|
345
|
+
|
|
346
|
+
counterexample = rexical_counterexample(first[:raw_pattern], second[:raw_pattern])
|
|
347
|
+
next unless counterexample
|
|
348
|
+
|
|
349
|
+
input, first_length, second_length = counterexample
|
|
350
|
+
@warnings << <<~WARNING.chomp
|
|
351
|
+
Rexical rules #{first_index} and #{second_index} differ under first-match vs longest-match: #{input.inspect} is a counterexample; first-match selects rule #{first_index} (#{first_length} bytes), while longest-match selects rule #{second_index} (#{second_length} bytes)
|
|
352
|
+
WARNING
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def rexical_rules_overlap?(first, second)
|
|
358
|
+
first_states = first[:states].empty? ? [:initial] : first[:states]
|
|
359
|
+
second_states = second[:states].empty? ? [:initial] : second[:states]
|
|
360
|
+
first_states.intersect?(second_states)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def rexical_counterexample(first_pattern, second_pattern)
|
|
364
|
+
regexps = [first_pattern, second_pattern].map { |pattern| ::Regexp.new(pattern) }
|
|
365
|
+
if (automata = regexps.map { |regexp| rexical_dfa(regexp) }) && automata.all?
|
|
366
|
+
candidate = rexical_dfa_counterexample(regexps, automata)
|
|
367
|
+
return candidate if candidate
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
rexical_candidate_inputs([first_pattern, second_pattern]).each do |input|
|
|
371
|
+
lengths = rexical_match_lengths(regexps, input)
|
|
372
|
+
return lengths if lengths
|
|
373
|
+
end
|
|
374
|
+
nil
|
|
375
|
+
rescue RegexpError, ArgumentError
|
|
376
|
+
nil
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def rexical_dfa(regexp)
|
|
380
|
+
dfa = Flexr.compile_pattern(regexp)
|
|
381
|
+
return unless dfa.respond_to?(:accepts) && dfa.respond_to?(:transition)
|
|
382
|
+
|
|
383
|
+
dfa
|
|
384
|
+
rescue Flexr::Error, RegexpError, ArgumentError
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def rexical_dfa_counterexample(regexps, automata)
|
|
389
|
+
first, second = automata
|
|
390
|
+
queue = [[first.start, second.start, false, +"".b]]
|
|
391
|
+
visited = { [first.start, second.start, false] => true }
|
|
392
|
+
bytes = rexical_byte_representatives(first, second)
|
|
393
|
+
|
|
394
|
+
until queue.empty?
|
|
395
|
+
first_state, second_state, first_seen, input = queue.shift
|
|
396
|
+
bytes.each do |byte|
|
|
397
|
+
next_first = first_state && first.transition(first_state, byte)
|
|
398
|
+
next_second = second.transition(second_state, byte)
|
|
399
|
+
next unless next_second
|
|
400
|
+
|
|
401
|
+
next_input = input + byte.chr(Encoding::BINARY)
|
|
402
|
+
next_first_seen = first_seen || rexical_accepting?(first, next_first)
|
|
403
|
+
if next_first_seen && rexical_accepting?(second, next_second) &&
|
|
404
|
+
!rexical_accepting?(first, next_first)
|
|
405
|
+
lengths = rexical_match_lengths(regexps, next_input)
|
|
406
|
+
return lengths if lengths
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
key = [next_first, next_second, next_first_seen]
|
|
410
|
+
next if visited[key]
|
|
411
|
+
|
|
412
|
+
visited[key] = true
|
|
413
|
+
queue << [next_first, next_second, next_first_seen, next_input]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def rexical_byte_representatives(first, second)
|
|
420
|
+
representatives = {}
|
|
421
|
+
256.times do |byte|
|
|
422
|
+
key = [first.ec[byte], second.ec[byte]]
|
|
423
|
+
representatives[key] ||= byte
|
|
424
|
+
end
|
|
425
|
+
representatives.values.sort_by { |byte| [byte.between?(32, 126) ? 0 : 1, byte] }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def rexical_accepting?(dfa, state)
|
|
429
|
+
state && !dfa.accepts.fetch(state).empty?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def rexical_match_lengths(regexps, input)
|
|
433
|
+
matches = regexps.map do |regexp|
|
|
434
|
+
regexp.match(input.dup.force_encoding(regexp.encoding))
|
|
435
|
+
end
|
|
436
|
+
return unless matches.all? { |match| match&.begin(0)&.zero? && !match[0].empty? }
|
|
437
|
+
|
|
438
|
+
lengths = matches.map { |match| match[0].bytesize }
|
|
439
|
+
lengths[0] < lengths[1] ? [input, *lengths] : nil
|
|
440
|
+
rescue ArgumentError, EncodingError
|
|
441
|
+
nil
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def rexical_candidate_inputs(patterns)
|
|
445
|
+
characters = (%w[x a b 0 1 _] + patterns.flat_map { |pattern| rexical_candidate_characters(pattern) }).uniq.first(24)
|
|
446
|
+
fragments = patterns.flat_map { |pattern| rexical_literal_fragments(pattern) }
|
|
447
|
+
.reject(&:empty?).uniq.first(24)
|
|
448
|
+
seeds = (fragments + characters).uniq
|
|
449
|
+
candidates = []
|
|
450
|
+
|
|
451
|
+
seeds.each do |seed|
|
|
452
|
+
candidates.push(seed, seed * 2, seed * 3)
|
|
453
|
+
end
|
|
454
|
+
fragments.each do |fragment|
|
|
455
|
+
characters.each do |character|
|
|
456
|
+
candidates << "#{fragment}#{character}"
|
|
457
|
+
candidates << "#{character}#{fragment}"
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
fragments.combination(2) { |left, right| candidates << "#{left}#{right}" }
|
|
461
|
+
|
|
462
|
+
candidates.uniq.each_with_index
|
|
463
|
+
.sort_by { |candidate, index| [candidate.bytesize, index] }.map(&:first)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def rexical_candidate_characters(pattern)
|
|
467
|
+
escaped = pattern.scan(/\\x([0-9A-Fa-f]{2})|\\(.)/).filter_map do |hex, character|
|
|
468
|
+
if hex
|
|
469
|
+
byte = hex.to_i(16)
|
|
470
|
+
[byte].pack("C").force_encoding(Encoding::UTF_8) if byte < 0x80
|
|
471
|
+
else
|
|
472
|
+
{ "n" => "\n", "r" => "\r", "t" => "\t", "f" => "\f", "v" => "\v" }[character] ||
|
|
473
|
+
(character unless %w[d D w W s S A Z b B p P G K].include?(character))
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
pattern.each_char.grep(/[A-Za-z0-9_]/) + escaped
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def rexical_literal_fragments(pattern)
|
|
480
|
+
fragments = []
|
|
481
|
+
current = +""
|
|
482
|
+
in_class = false
|
|
483
|
+
escaped = false
|
|
484
|
+
flush = lambda do
|
|
485
|
+
fragments << current unless current.empty?
|
|
486
|
+
current = +""
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
pattern.each_char do |character|
|
|
490
|
+
if escaped
|
|
491
|
+
if { "n" => "\n", "r" => "\r", "t" => "\t", "f" => "\f", "v" => "\v" }.key?(character)
|
|
492
|
+
current << { "n" => "\n", "r" => "\r", "t" => "\t", "f" => "\f", "v" => "\v" }.fetch(character)
|
|
493
|
+
elsif character.match?(/[dDwWsSAZbBpPGK]/)
|
|
494
|
+
flush.call
|
|
495
|
+
else
|
|
496
|
+
current << character
|
|
497
|
+
end
|
|
498
|
+
escaped = false
|
|
499
|
+
elsif character == "\\"
|
|
500
|
+
escaped = true
|
|
501
|
+
elsif in_class
|
|
502
|
+
in_class = false if character == "]"
|
|
503
|
+
elsif character == "["
|
|
504
|
+
flush.call
|
|
505
|
+
in_class = true
|
|
506
|
+
elsif character.match?(/[A-Za-z0-9_]/)
|
|
507
|
+
current << character
|
|
508
|
+
else
|
|
509
|
+
flush.call
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
flush.call unless current.empty?
|
|
513
|
+
fragments
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def split_rule_line(line)
|
|
517
|
+
source = line.strip
|
|
518
|
+
return [nil, nil] if source.empty?
|
|
519
|
+
|
|
520
|
+
quote = nil
|
|
521
|
+
class_depth = 0
|
|
522
|
+
escaped = false
|
|
523
|
+
source.each_char.with_index do |char, index|
|
|
524
|
+
if quote
|
|
525
|
+
quote = nil if char == quote && !escaped
|
|
526
|
+
elsif char == '"' && class_depth.zero?
|
|
527
|
+
quote = char
|
|
528
|
+
elsif char == "["
|
|
529
|
+
class_depth += 1
|
|
530
|
+
elsif char == "]" && class_depth.positive?
|
|
531
|
+
class_depth -= 1
|
|
532
|
+
elsif char.match?(/\s/) && class_depth.zero?
|
|
533
|
+
return [source[0...index], source[index..].strip]
|
|
534
|
+
end
|
|
535
|
+
escaped = char == "\\" && !escaped
|
|
536
|
+
escaped = false unless char == "\\"
|
|
537
|
+
end
|
|
538
|
+
[source, nil]
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def brace_delta(text)
|
|
542
|
+
text.count("{") - text.count("}")
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def unescape_flex(text)
|
|
546
|
+
text.gsub(/\\([\\"])/, '\\1')
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def normalize_rexical_line(line)
|
|
550
|
+
stripped = line.strip
|
|
551
|
+
if stripped.match?(%r{\A/}) && (finish = stripped.rindex("/")) && finish.positive?
|
|
552
|
+
pattern = stripped[1...finish]
|
|
553
|
+
rest = stripped[(finish + 1)..].to_s
|
|
554
|
+
"#{pattern} #{rest}\n"
|
|
555
|
+
else
|
|
556
|
+
line
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
end
|
data/lib/flexr/ir.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module IR
|
|
5
|
+
Rule = Struct.new(
|
|
6
|
+
:index, :patterns, :trailing, :action, :states, :bol_only, :end_anchor, :location,
|
|
7
|
+
:pattern_conditions,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
) do
|
|
10
|
+
def skip?
|
|
11
|
+
action == :skip
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def emit?
|
|
15
|
+
action.is_a?(Array) && action.first == :emit
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
State = Struct.new(:name, :inclusive, :id, keyword_init: true)
|
|
20
|
+
|
|
21
|
+
Spec = Struct.new(
|
|
22
|
+
:class_name, :superclass, :backend, :token_kind, :encoding, :options,
|
|
23
|
+
:declared_tokens, :states, :rules, :eof_rules, :verbatim,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
) do
|
|
26
|
+
def initial_state
|
|
27
|
+
:initial
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Config = Struct.new(
|
|
32
|
+
:backend, :token_kind, :encoding, :options, :declared_tokens, :states,
|
|
33
|
+
keyword_init: true
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/flexr/lexer.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
Options = Struct.new(
|
|
5
|
+
:backend, :token_kind, :accel, :standalone, :eval_mode, :table_compression,
|
|
6
|
+
:table_format, :max_dfa_states, :warn_level, :warn_as_error, :color, :format, :overrides,
|
|
7
|
+
keyword_init: true
|
|
8
|
+
) do
|
|
9
|
+
def self.default
|
|
10
|
+
new(backend: :table, token_kind: :array, accel: :auto, standalone: false,
|
|
11
|
+
eval_mode: false, table_compression: :rows, table_format: :literal,
|
|
12
|
+
max_dfa_states: 100_000, warn_level: :default, warn_as_error: false,
|
|
13
|
+
color: :auto, format: :human, overrides: {})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set(name, value)
|
|
17
|
+
self[name] = value
|
|
18
|
+
self.overrides ||= {}
|
|
19
|
+
overrides[name] = value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate!
|
|
23
|
+
validate_value!(backend, %i[table direct firstmatch auto], :backend)
|
|
24
|
+
validate_value!(token_kind, %i[array struct yield], :token_kind)
|
|
25
|
+
validate_value!(accel, %i[auto strscan regexp none], :accel)
|
|
26
|
+
validate_value!(table_compression, %i[none rows full], :table_compression)
|
|
27
|
+
validate_value!(table_format, %i[literal packed], :table_format)
|
|
28
|
+
validate_value!(warn_level, %i[all default none], :warn_level)
|
|
29
|
+
validate_value!(color, %i[auto always never], :color)
|
|
30
|
+
validate_value!(format, %i[human json], :format)
|
|
31
|
+
raise ArgumentError, "unsupported max_dfa_states: #{max_dfa_states.inspect}" unless max_dfa_states.is_a?(Integer) && max_dfa_states.positive?
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generator_options
|
|
36
|
+
(overrides || {}).merge(warn_as_error: warn_as_error, color: color)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def validate_value!(value, allowed, name)
|
|
42
|
+
return if allowed.include?(value)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, "unsupported #{name}: #{value.inspect}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
class RakeTask
|
|
5
|
+
attr_accessor :spec, :output, :warn_as_error
|
|
6
|
+
|
|
7
|
+
def initialize(name = :flexr)
|
|
8
|
+
@name = name
|
|
9
|
+
@warn_as_error = false
|
|
10
|
+
yield self if block_given?
|
|
11
|
+
define_task
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def define_task
|
|
17
|
+
require "rake"
|
|
18
|
+
raise ArgumentError, "spec is required" unless @spec
|
|
19
|
+
|
|
20
|
+
output = @output || @spec.sub(/\.flexr\.rb\z/, ".rb")
|
|
21
|
+
Rake::FileTask.define_task(output => @spec) do
|
|
22
|
+
Generator.new(@spec, output: output, options: { warn_as_error: @warn_as_error }).generate
|
|
23
|
+
end
|
|
24
|
+
Rake::Task.define_task(@name => output)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|