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,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Codegen
|
|
5
|
+
class Table < Base
|
|
6
|
+
def generate(state: :initial)
|
|
7
|
+
machine = compiled.machines.fetch(state)
|
|
8
|
+
dfa = machine.dfa
|
|
9
|
+
{
|
|
10
|
+
ec: dfa.ec,
|
|
11
|
+
nxt: dfa.transitions.flatten.map { |value| value || -1 },
|
|
12
|
+
acc: dfa.accepts.map { |rules| rules.first&.rule_index || -1 },
|
|
13
|
+
start: dfa.start,
|
|
14
|
+
states: dfa.states,
|
|
15
|
+
classes: dfa.class_count
|
|
16
|
+
}.freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def header(source: nil, backend: :table)
|
|
20
|
+
stat = stats.fetch(:initial, {})
|
|
21
|
+
[
|
|
22
|
+
"# Generated by flexr. DO NOT EDIT.",
|
|
23
|
+
"# source: #{source}",
|
|
24
|
+
"# backend: #{backend} / states: #{stat[:states]} / byte-classes: #{stat[:classes]}"
|
|
25
|
+
].join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def source(indent: " ")
|
|
29
|
+
lines = []
|
|
30
|
+
lines << "#{indent}def scan_one"
|
|
31
|
+
lines << "#{indent} return Flexr::Runtime::Interpreter.new(self).scan unless __flexr_generated_fast_path?"
|
|
32
|
+
lines << "#{indent} machine = self.class.__flexr_compiled.machines.fetch(state)"
|
|
33
|
+
lines << "#{indent} dfa = machine.dfa"
|
|
34
|
+
lines << "#{indent} position = byte_pos"
|
|
35
|
+
lines << "#{indent} return nil unless valid_utf8_at?(position)"
|
|
36
|
+
lines << "#{indent} cursor = position"
|
|
37
|
+
lines << "#{indent} current = dfa.start"
|
|
38
|
+
lines << "#{indent} direct = dfa.direct"
|
|
39
|
+
lines << "#{indent} source = buffer.source"
|
|
40
|
+
simple = if simple_fast_path?
|
|
41
|
+
lines << "#{indent} best = nil"
|
|
42
|
+
true
|
|
43
|
+
else
|
|
44
|
+
lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, position, nil)"
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
lines << "#{indent} accelerate = self.class.__flexr_config.options.fetch(:accel, :auto) != :none && !utf8_input?"
|
|
48
|
+
lines << "#{indent} while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)"
|
|
49
|
+
lines << "#{indent} if accelerate"
|
|
50
|
+
lines << "#{indent} region = __flexr_generated_acceleration_region(dfa, current)"
|
|
51
|
+
lines << "#{indent} accelerated_end = __flexr_generated_accelerate(region, cursor) if region"
|
|
52
|
+
lines << "#{indent} if accelerated_end && accelerated_end > cursor"
|
|
53
|
+
lines << "#{indent} cursor = accelerated_end"
|
|
54
|
+
lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
|
|
55
|
+
lines << "#{indent} next"
|
|
56
|
+
lines << "#{indent} end"
|
|
57
|
+
lines << "#{indent} end"
|
|
58
|
+
lines << "#{indent} byte = source.getbyte(cursor)"
|
|
59
|
+
lines << "#{indent} current = if direct"
|
|
60
|
+
lines << "#{indent} class_id = dfa.ec[byte]"
|
|
61
|
+
lines << "#{indent} value = direct[:nxt][(current * direct[:classes]) + class_id]"
|
|
62
|
+
lines << "#{indent} value >= 0 ? value : nil"
|
|
63
|
+
lines << "#{indent} else"
|
|
64
|
+
lines << "#{indent} dfa.transition(current, byte)"
|
|
65
|
+
lines << "#{indent} end"
|
|
66
|
+
lines << "#{indent} break unless current"
|
|
67
|
+
lines << "#{indent} cursor += 1"
|
|
68
|
+
if simple
|
|
69
|
+
lines << "#{indent} acceptance = dfa.accepts[current].first"
|
|
70
|
+
lines << "#{indent} if acceptance"
|
|
71
|
+
lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
|
|
72
|
+
lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
|
|
73
|
+
lines << "#{indent} best.rule = rule"
|
|
74
|
+
lines << "#{indent} best.start_pos = position"
|
|
75
|
+
lines << "#{indent} best.end_pos = cursor"
|
|
76
|
+
lines << "#{indent} best.total_end_pos = cursor"
|
|
77
|
+
lines << "#{indent} end"
|
|
78
|
+
else
|
|
79
|
+
lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
|
|
80
|
+
end
|
|
81
|
+
lines << "#{indent} end"
|
|
82
|
+
lines << "#{indent} best"
|
|
83
|
+
lines << "#{indent}end"
|
|
84
|
+
lines << "#{indent}def __flexr_generated_acceleration_region(dfa, state)"
|
|
85
|
+
lines << "#{indent} @__flexr_generated_accel_regions ||= {}"
|
|
86
|
+
lines << "#{indent} regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })"
|
|
87
|
+
lines << "#{indent} region = regions[state]"
|
|
88
|
+
lines << "#{indent} return unless region"
|
|
89
|
+
lines << "#{indent} return if dfa.accepts[state].any? do |acceptance|"
|
|
90
|
+
lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
|
|
91
|
+
lines << "#{indent} acceptance.bol_only || acceptance.end_anchor || rule.trailing"
|
|
92
|
+
lines << "#{indent} end"
|
|
93
|
+
lines << "#{indent} region"
|
|
94
|
+
lines << "#{indent}end"
|
|
95
|
+
lines << "#{indent}def __flexr_generated_accelerate(region, position)"
|
|
96
|
+
lines << "#{indent} binary = buffer.source.b"
|
|
97
|
+
lines << "#{indent} mode = self.class.__flexr_config.options.fetch(:accel, :auto)"
|
|
98
|
+
lines << "#{indent} match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)"
|
|
99
|
+
lines << "#{indent} scanner = StringScanner.new(binary)"
|
|
100
|
+
lines << "#{indent} scanner.pos = position"
|
|
101
|
+
lines << "#{indent} length = scanner.skip(region.regexp)"
|
|
102
|
+
lines << "#{indent} length && scanner.pos"
|
|
103
|
+
lines << "#{indent} else"
|
|
104
|
+
lines << "#{indent} match = region.regexp.match(binary, position)"
|
|
105
|
+
lines << "#{indent} match && match.begin(0) == position ? match.end(0) : nil"
|
|
106
|
+
lines << "#{indent} end"
|
|
107
|
+
lines << "#{indent} return match_end if match_end && match_end < buffer.bytesize"
|
|
108
|
+
lines << "#{indent} return match_end if match_end && buffer.eof_loaded?"
|
|
109
|
+
lines << "#{indent} return unless buffer.ensure_available?(buffer.bytesize + 1)"
|
|
110
|
+
lines << "#{indent} __flexr_generated_accelerate(region, position)"
|
|
111
|
+
lines << "#{indent}rescue ArgumentError"
|
|
112
|
+
lines << "#{indent} nil"
|
|
113
|
+
lines << "#{indent}end"
|
|
114
|
+
lines << "#{indent}def __flexr_generated_fast_path?"
|
|
115
|
+
lines << "#{indent} return @__flexr_generated_fast_path if defined?(@__flexr_generated_fast_path)"
|
|
116
|
+
lines << "#{indent} @__flexr_generated_fast_path = if self.class.__flexr_config.backend == :firstmatch ||"
|
|
117
|
+
lines << "#{indent} self.class.__flexr_config.options[:allow_empty_match] == true"
|
|
118
|
+
lines << "#{indent} false"
|
|
119
|
+
lines << "#{indent} else"
|
|
120
|
+
lines << "#{indent} self.class.__flexr_rules.none? do |rule|"
|
|
121
|
+
lines << "#{indent} !rule.trailing.nil? || rule.patterns.any? do |pattern|"
|
|
122
|
+
lines << "#{indent} next false unless pattern.is_a?(Regexp)"
|
|
123
|
+
lines << "#{indent} next true if pattern.source.match?(/\\\\[pP]\\\\{/) || pattern.source.match?(/\\[:(?:\\^)?[a-z]+:\\]/)"
|
|
124
|
+
lines << "#{indent} self.class.__flexr_config.options[:unicode] == true && utf8_input? && pattern.source.match?(/\\\\[dDwWsS]/)"
|
|
125
|
+
lines << "#{indent} end"
|
|
126
|
+
lines << "#{indent} end"
|
|
127
|
+
lines << "#{indent} end"
|
|
128
|
+
lines << "#{indent}end"
|
|
129
|
+
lines << "#{indent}def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)"
|
|
130
|
+
lines << "#{indent} dfa.accepts[state].each do |acceptance|"
|
|
131
|
+
lines << "#{indent} next if acceptance.bol_only && !beginning_of_line?"
|
|
132
|
+
lines << "#{indent} next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)"
|
|
133
|
+
lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
|
|
134
|
+
lines << "#{indent} defer_token_size_check!(cursor - start_position)"
|
|
135
|
+
lines << "#{indent} next if best && cursor < best.total_end_pos"
|
|
136
|
+
lines << "#{indent} next if best && cursor == best.total_end_pos &&"
|
|
137
|
+
lines << "#{indent} rule.index > best.rule.index"
|
|
138
|
+
lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
|
|
139
|
+
lines << "#{indent} best.rule = rule"
|
|
140
|
+
lines << "#{indent} best.start_pos = start_position"
|
|
141
|
+
lines << "#{indent} best.end_pos = cursor"
|
|
142
|
+
lines << "#{indent} best.total_end_pos = cursor"
|
|
143
|
+
lines << "#{indent} end"
|
|
144
|
+
lines << "#{indent} best"
|
|
145
|
+
lines << "#{indent}end"
|
|
146
|
+
"#{lines.join("\n")}\n"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def simple_fast_path?
|
|
150
|
+
compiled.rules.all? do |rule|
|
|
151
|
+
rule.trailing.nil? && rule.pattern_conditions.all? do |condition|
|
|
152
|
+
condition && !condition.bol_only && !condition.end_anchor
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Codegen
|
|
5
|
+
module TablePacker
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def pack(rows, compression: :rows)
|
|
9
|
+
full = compression.to_sym == :full
|
|
10
|
+
base = []
|
|
11
|
+
default = rows.map { |row| row.tally.max_by { |_value, count| count }&.first }
|
|
12
|
+
fallback = full ? Array.new(rows.length) : nil
|
|
13
|
+
next_table = []
|
|
14
|
+
check = []
|
|
15
|
+
occupied = {}
|
|
16
|
+
rows.each_with_index do |row, state|
|
|
17
|
+
if full
|
|
18
|
+
candidate, matches = rows.each_index.take(state).map do |other_state|
|
|
19
|
+
[other_state, row.each_index.count { |class_id| row[class_id] == rows[other_state][class_id] }]
|
|
20
|
+
end.max_by(&:last)
|
|
21
|
+
fallback[state] = candidate if candidate && matches > row.count { |value| value == default[state] }
|
|
22
|
+
end
|
|
23
|
+
inherited = fallback && fallback[state] ? rows.fetch(fallback.fetch(state)) : nil
|
|
24
|
+
entries = row.each_index.reject do |class_id|
|
|
25
|
+
row[class_id] == (inherited ? inherited[class_id] : default[state])
|
|
26
|
+
end
|
|
27
|
+
offset = 0
|
|
28
|
+
while entries.any? { |class_id| occupied.key?(offset + class_id) }
|
|
29
|
+
offset += 1
|
|
30
|
+
end
|
|
31
|
+
base[state] = offset
|
|
32
|
+
entries.each do |class_id|
|
|
33
|
+
index = offset + class_id
|
|
34
|
+
next_table[index] = row[class_id]
|
|
35
|
+
check[index] = state
|
|
36
|
+
occupied[index] = true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
result = { base: base.freeze, default: default.freeze, next: next_table.freeze, check: check.freeze }
|
|
40
|
+
result[:fallback] = fallback.freeze if fallback
|
|
41
|
+
result.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def encode(packed)
|
|
45
|
+
result = {
|
|
46
|
+
encoding: :base64,
|
|
47
|
+
base: encode_array(packed.fetch(:base)),
|
|
48
|
+
default: encode_array(packed.fetch(:default), nil_value: -1),
|
|
49
|
+
next: encode_array(packed.fetch(:next), nil_value: -1),
|
|
50
|
+
check: encode_array(packed.fetch(:check), nil_value: -1)
|
|
51
|
+
}
|
|
52
|
+
result[:fallback] = encode_array(packed.fetch(:fallback), nil_value: -1) if packed[:fallback]
|
|
53
|
+
result.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def encode_array(values, nil_value: 0)
|
|
57
|
+
[values.map { |value| value.nil? ? nil_value : value }.pack("l<*")].pack("m0")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
Diagnostic = Struct.new(:code, :severity, :message, :location, :help, :note, keyword_init: true) do
|
|
5
|
+
def error?
|
|
6
|
+
severity == :error
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def warning?
|
|
10
|
+
severity == :warning
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
{
|
|
15
|
+
code: code,
|
|
16
|
+
severity: severity,
|
|
17
|
+
message: message,
|
|
18
|
+
location: location,
|
|
19
|
+
help: help,
|
|
20
|
+
note: note
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class DiagnosticSet
|
|
26
|
+
include Enumerable
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@items = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def each(&)
|
|
33
|
+
@items.each(&)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def <<(diagnostic)
|
|
37
|
+
@items << diagnostic
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def any_error?
|
|
42
|
+
@items.any?(&:error?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty?
|
|
46
|
+
@items.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_a
|
|
50
|
+
@items.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render(format: :human, color: :auto)
|
|
54
|
+
return JSON.generate(@items.map(&:to_h)) if format.to_sym == :json
|
|
55
|
+
|
|
56
|
+
@items.map { |item| render_one(item, color: color_enabled?(color)) }.join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def render_one(item, color:)
|
|
62
|
+
prefix = "#{item.severity}[#{item.code}]: #{item.message}"
|
|
63
|
+
prefix = "\e[31m#{prefix}\e[0m" if [true, :always].include?(color)
|
|
64
|
+
lines = [prefix]
|
|
65
|
+
lines << " help: #{item.help}" if item.help
|
|
66
|
+
lines << " note: #{item.note}" if item.note
|
|
67
|
+
lines.join("\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def color_enabled?(color)
|
|
71
|
+
return false if ENV.key?("NO_COLOR") || color == :never || color == false
|
|
72
|
+
return true if [:always, true].include?(color)
|
|
73
|
+
|
|
74
|
+
$stderr.tty?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
module Diagnostics
|
|
79
|
+
module_function
|
|
80
|
+
|
|
81
|
+
def error(code, message, location: nil, help: nil, note: nil)
|
|
82
|
+
Diagnostic.new(code: code, severity: :error, message: message, location: location, help: help, note: note)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def warning(code, message, location: nil, help: nil, note: nil)
|
|
86
|
+
Diagnostic.new(code: code, severity: :warning, message: message, location: location, help: help, note: note)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def raise!(diagnostic)
|
|
90
|
+
klass = diagnostic.code == "FLEXR-E014" ? UnsupportedRegexpError : CompileError
|
|
91
|
+
raise klass.new(diagnostic.message, diagnostic: diagnostic)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/flexr/dsl.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module DSL
|
|
5
|
+
DSL_METHODS = %i[rule state all_states on_eof emits backend token_kind encoding option accel].freeze
|
|
6
|
+
AUTO_DIRECT_CELL_THRESHOLD = 100_000
|
|
7
|
+
|
|
8
|
+
def inherited(child)
|
|
9
|
+
super
|
|
10
|
+
child.__flexr_reset!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def __flexr_reset!
|
|
14
|
+
@__flexr_rules = []
|
|
15
|
+
@__flexr_states = { initial: IR::State.new(name: :initial, inclusive: true, id: 0) }
|
|
16
|
+
@__flexr_state_stack = []
|
|
17
|
+
@__flexr_eof_rules = {}
|
|
18
|
+
@__flexr_config = IR::Config.new(
|
|
19
|
+
backend: :table, token_kind: :array, encoding: Encoding::UTF_8,
|
|
20
|
+
options: {}, declared_tokens: [], states: @__flexr_states
|
|
21
|
+
)
|
|
22
|
+
@__flexr_compiled = nil
|
|
23
|
+
@__flexr_generated = false
|
|
24
|
+
@__flexr_compile_mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rule(pattern, skip: false, emit: nil, followed_by: nil, &action)
|
|
28
|
+
patterns = normalize_patterns(pattern)
|
|
29
|
+
rule_action = if skip
|
|
30
|
+
:skip
|
|
31
|
+
elsif emit
|
|
32
|
+
[:emit, emit.to_sym]
|
|
33
|
+
else
|
|
34
|
+
action || proc { emit(nil, text) }
|
|
35
|
+
end
|
|
36
|
+
states = @__flexr_state_stack.empty? ? [:initial] : @__flexr_state_stack.dup
|
|
37
|
+
@__flexr_rules << IR::Rule.new(
|
|
38
|
+
index: @__flexr_rules.length, patterns: patterns, trailing: normalize_trailing(followed_by),
|
|
39
|
+
action: rule_action, states: states, bol_only: false, end_anchor: nil
|
|
40
|
+
)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def state(*names, inclusive: false, &block)
|
|
45
|
+
raise ArgumentError, "state requires a block" unless block
|
|
46
|
+
raise ArgumentError, "state requires a name" if names.empty?
|
|
47
|
+
|
|
48
|
+
names.each do |name|
|
|
49
|
+
symbol = name.to_sym
|
|
50
|
+
@__flexr_states[symbol] ||= IR::State.new(name: symbol, inclusive: inclusive, id: @__flexr_states.length)
|
|
51
|
+
end
|
|
52
|
+
@__flexr_state_stack.concat(names.map(&:to_sym))
|
|
53
|
+
class_eval(&block)
|
|
54
|
+
ensure
|
|
55
|
+
names&.length&.times { @__flexr_state_stack.pop }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def all_states(&)
|
|
59
|
+
state(*@__flexr_states.keys, &)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_eof(&action)
|
|
63
|
+
state_name = @__flexr_state_stack.last || :initial
|
|
64
|
+
@__flexr_eof_rules[state_name] = action
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emits(*tokens)
|
|
68
|
+
@__flexr_config.declared_tokens.concat(tokens.flatten.map(&:to_sym)).uniq!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def backend(name)
|
|
72
|
+
@__flexr_config.backend = name.to_sym
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def token_kind(name)
|
|
76
|
+
value = name.to_sym
|
|
77
|
+
raise ArgumentError, "unsupported token_kind: #{name}" unless %i[array struct yield].include?(value)
|
|
78
|
+
|
|
79
|
+
@__flexr_config.token_kind = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def encoding(value)
|
|
83
|
+
encoding = value.is_a?(Encoding) ? value : Encoding.find(value.to_s)
|
|
84
|
+
unless [Encoding::UTF_8, Encoding::BINARY].include?(encoding)
|
|
85
|
+
diagnostic = Diagnostics.error("FLEXR-E011", "flexr supports UTF-8 and BINARY only")
|
|
86
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@__flexr_config.encoding = encoding
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def option(*values)
|
|
93
|
+
values.each { |value| @__flexr_config.options[value.to_sym] = true }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def accel(value)
|
|
97
|
+
@__flexr_config.options[:accel] = value.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def compile!
|
|
101
|
+
@__flexr_compile_mutex.synchronize do
|
|
102
|
+
# The ivar is part of the generated/runtime class contract.
|
|
103
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
|
104
|
+
@__flexr_compiled ||= begin
|
|
105
|
+
compiled = Automaton::Compiler.new(__flexr_spec).compile
|
|
106
|
+
@__flexr_config.backend = auto_direct?(compiled) ? :direct : :table if @__flexr_config.backend == :auto
|
|
107
|
+
compiled
|
|
108
|
+
end
|
|
109
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dfa
|
|
114
|
+
compile!
|
|
115
|
+
__flexr_compiled
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def __flexr_spec
|
|
119
|
+
IR::Spec.new(
|
|
120
|
+
class_name: name,
|
|
121
|
+
superclass: superclass&.name,
|
|
122
|
+
backend: @__flexr_config.backend,
|
|
123
|
+
token_kind: @__flexr_config.token_kind,
|
|
124
|
+
encoding: @__flexr_config.encoding,
|
|
125
|
+
options: @__flexr_config.options,
|
|
126
|
+
declared_tokens: @__flexr_config.declared_tokens,
|
|
127
|
+
states: @__flexr_states,
|
|
128
|
+
rules: @__flexr_rules,
|
|
129
|
+
eof_rules: @__flexr_eof_rules,
|
|
130
|
+
verbatim: nil
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
attr_reader :__flexr_rules, :__flexr_states, :__flexr_config, :__flexr_compiled
|
|
135
|
+
|
|
136
|
+
def __flexr_add_generated_eof(state, action)
|
|
137
|
+
@__flexr_eof_rules[state.to_sym] = action
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def __flexr_set_compiled!(compiled)
|
|
141
|
+
@__flexr_compiled = compiled
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def __flexr_mark_generated!
|
|
145
|
+
@__flexr_generated = true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def __flexr_generated?
|
|
149
|
+
@__flexr_generated == true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def normalize_patterns(pattern)
|
|
155
|
+
values = pattern.is_a?(Array) ? pattern : [pattern]
|
|
156
|
+
values.each do |value|
|
|
157
|
+
unless value.is_a?(::Regexp) || value.is_a?(String)
|
|
158
|
+
diagnostic = Diagnostics.error("FLEXR-E018", "rule pattern must be a Regexp, String, or Array")
|
|
159
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
values
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def normalize_trailing(value)
|
|
166
|
+
case value
|
|
167
|
+
when nil, ::Regexp
|
|
168
|
+
value
|
|
169
|
+
when String
|
|
170
|
+
::Regexp.new(::Regexp.escape(value))
|
|
171
|
+
else
|
|
172
|
+
diagnostic = Diagnostics.error("FLEXR-E018", "followed_by must be a Regexp or String")
|
|
173
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def auto_direct?(compiled)
|
|
178
|
+
cells = compiled.stats.values.map { |stats| stats[:states] * stats[:classes] }.max.to_i
|
|
179
|
+
cells > AUTO_DIRECT_CELL_THRESHOLD
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/flexr/errors.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :diagnostic
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, diagnostic: nil)
|
|
8
|
+
@diagnostic = diagnostic
|
|
9
|
+
super(message || diagnostic&.message)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class LexError < Error
|
|
14
|
+
attr_reader :filename, :byte_pos, :line, :text
|
|
15
|
+
|
|
16
|
+
def initialize(message, filename: nil, byte_pos: nil, line: nil, text: nil, diagnostic: nil)
|
|
17
|
+
@filename = filename
|
|
18
|
+
@byte_pos = byte_pos
|
|
19
|
+
@line = line
|
|
20
|
+
@text = text
|
|
21
|
+
super(message, diagnostic: diagnostic)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class CompileError < Error; end
|
|
26
|
+
class UnsupportedRegexpError < CompileError; end
|
|
27
|
+
class StaticResolutionError < CompileError; end
|
|
28
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Generated
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def install!(klass, payload)
|
|
8
|
+
rules = payload.fetch(:rules)
|
|
9
|
+
config = payload
|
|
10
|
+
klass.__flexr_reset!
|
|
11
|
+
klass.backend(config.fetch(:backend, :table))
|
|
12
|
+
klass.token_kind(config.fetch(:token_kind, :array))
|
|
13
|
+
klass.encoding(config.fetch(:encoding, Encoding::UTF_8))
|
|
14
|
+
Array(config[:declared_tokens]).each { |token| klass.emits(token) }
|
|
15
|
+
config.fetch(:options, {}).each do |option, value|
|
|
16
|
+
if option == :accel
|
|
17
|
+
klass.accel(value)
|
|
18
|
+
elsif value == true
|
|
19
|
+
klass.option(option)
|
|
20
|
+
else
|
|
21
|
+
klass.__flexr_config.options[option] = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
Array(config[:states]).each do |state|
|
|
25
|
+
klass.state(state, inclusive: config.fetch(:inclusive_states, {}).fetch(state.to_sym, false)) { nil }
|
|
26
|
+
end
|
|
27
|
+
rules.each do |definition|
|
|
28
|
+
klass.__flexr_add_generated_rule(definition)
|
|
29
|
+
end
|
|
30
|
+
config.fetch(:eof_rules, {}).each do |state, action|
|
|
31
|
+
klass.__flexr_add_generated_eof(state, action)
|
|
32
|
+
end
|
|
33
|
+
klass
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_compiled!(klass, payload)
|
|
37
|
+
install!(klass, payload)
|
|
38
|
+
machines = payload.fetch(:compiled).fetch(:machines).transform_values do |machine|
|
|
39
|
+
dfa_data = machine.fetch(:dfa)
|
|
40
|
+
packed = decode_packed(dfa_data[:packed])
|
|
41
|
+
dfa = Automaton::DFA.new(
|
|
42
|
+
transitions: dfa_data[:transitions] || inflate_packed(packed, dfa_data.fetch(:state_count),
|
|
43
|
+
dfa_data.fetch(:class_count)),
|
|
44
|
+
accepts: dfa_data.fetch(:accepts),
|
|
45
|
+
ec: dfa_data.fetch(:ec),
|
|
46
|
+
class_count: dfa_data.fetch(:class_count),
|
|
47
|
+
start: dfa_data.fetch(:start),
|
|
48
|
+
rule_ids: dfa_data.fetch(:rule_ids),
|
|
49
|
+
packed: packed,
|
|
50
|
+
direct: dfa_data[:direct]
|
|
51
|
+
)
|
|
52
|
+
Automaton::Machine.new(dfa: dfa, state_name: machine.fetch(:state_name).to_sym)
|
|
53
|
+
end
|
|
54
|
+
compiled = Automaton::CompiledSpec.new(
|
|
55
|
+
machines: machines,
|
|
56
|
+
rules: klass.__flexr_rules,
|
|
57
|
+
states: payload.fetch(:compiled).fetch(:states).map(&:to_sym),
|
|
58
|
+
stats: payload.fetch(:compiled).fetch(:stats),
|
|
59
|
+
diagnostics: payload.fetch(:compiled).fetch(:diagnostics, []).map do |diagnostic|
|
|
60
|
+
Diagnostic.new(**diagnostic.transform_keys(&:to_sym))
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
klass.__flexr_set_compiled!(compiled)
|
|
64
|
+
klass.__flexr_mark_generated!
|
|
65
|
+
klass
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def decode_packed(packed)
|
|
69
|
+
return packed unless packed.is_a?(Hash) && packed[:encoding]&.to_sym == :base64
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
base: decode_array(packed.fetch(:base)),
|
|
73
|
+
default: decode_array(packed.fetch(:default), nil_value: -1),
|
|
74
|
+
next: decode_array(packed.fetch(:next), nil_value: -1),
|
|
75
|
+
check: decode_array(packed.fetch(:check), nil_value: -1),
|
|
76
|
+
fallback: packed.key?(:fallback) ? decode_array(packed.fetch(:fallback), nil_value: -1) : nil
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def decode_array(encoded, nil_value: nil)
|
|
81
|
+
values = encoded.unpack1("m0").unpack("l<*")
|
|
82
|
+
return values unless nil_value
|
|
83
|
+
|
|
84
|
+
values.map { |value| value == nil_value ? nil : value }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def inflate_packed(packed, state_count, class_count)
|
|
88
|
+
return nil unless packed
|
|
89
|
+
|
|
90
|
+
Array.new(state_count) do |state|
|
|
91
|
+
Array.new(class_count) do |class_id|
|
|
92
|
+
cursor = state
|
|
93
|
+
loop do
|
|
94
|
+
index = packed.fetch(:base).fetch(cursor) + class_id
|
|
95
|
+
break packed.fetch(:next).fetch(index) if packed.fetch(:check)[index] == cursor
|
|
96
|
+
|
|
97
|
+
fallback = packed[:fallback]&.fetch(cursor)
|
|
98
|
+
break packed.fetch(:default).fetch(cursor) unless fallback
|
|
99
|
+
|
|
100
|
+
cursor = fallback
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module DSL
|
|
108
|
+
def __flexr_add_generated_rule(definition)
|
|
109
|
+
action = definition.fetch(:action)
|
|
110
|
+
@__flexr_rules << IR::Rule.new(
|
|
111
|
+
index: definition.fetch(:index), patterns: Array(definition.fetch(:patterns)),
|
|
112
|
+
trailing: normalize_trailing(definition[:trailing]), action: action,
|
|
113
|
+
states: Array(definition.fetch(:states)).map(&:to_sym),
|
|
114
|
+
bol_only: definition.fetch(:bol_only, false), end_anchor: definition[:end_anchor],
|
|
115
|
+
location: definition[:span] || definition[:location],
|
|
116
|
+
pattern_conditions: Array(definition[:pattern_conditions]).map do |condition|
|
|
117
|
+
next unless condition
|
|
118
|
+
|
|
119
|
+
Automaton::Acceptance.new(rule_index: condition[0], pattern_index: condition[1],
|
|
120
|
+
bol_only: condition[2], end_anchor: condition[3])
|
|
121
|
+
end
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|