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.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +33 -0
  3. data/CONTRIBUTING.md +39 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +116 -0
  6. data/Rakefile +468 -0
  7. data/benchmark/baselines/json.json +34 -0
  8. data/benchmark/baselines/json_handwritten.rb +43 -0
  9. data/benchmark/baselines/json_rexical.rex +25 -0
  10. data/benchmark/corpora/README.md +11 -0
  11. data/benchmark/corpora/generate_json.rb +26 -0
  12. data/benchmark/golden/calculator_lexer.sha256 +1 -0
  13. data/benchmark/golden/json_lexer.sha256 +1 -0
  14. data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
  15. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
  16. data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
  17. data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
  18. data/benchmark/golden/with_racc_lexer.sha256 +1 -0
  19. data/benchmark/run.rb +254 -0
  20. data/docs/README.md +64 -0
  21. data/docs/RELEASING.md +30 -0
  22. data/docs/adr/0001-byte-level-dfa.md +5 -0
  23. data/docs/adr/0003-leftmost-longest.md +4 -0
  24. data/docs/adr/0006-accel-not-scanner.md +4 -0
  25. data/docs/adr/0008-what-pure-ruby-means.md +5 -0
  26. data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
  27. data/docs/adr/0017-static-analysis-by-default.md +5 -0
  28. data/docs/adr/0018-prism-for-generator-only.md +4 -0
  29. data/docs/adr/0019-measured-performance-floor.md +26 -0
  30. data/docs/adr/0020-vendored-unicode-contract.md +21 -0
  31. data/docs/explanation/backends.md +33 -0
  32. data/docs/explanation/matching-semantics.md +20 -0
  33. data/docs/explanation/runtime-vs-generated.md +22 -0
  34. data/docs/explanation/security-model.md +18 -0
  35. data/docs/explanation/unicode-and-encoding.md +20 -0
  36. data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
  37. data/docs/how-to/generate-a-lexer.md +39 -0
  38. data/docs/how-to/handle-errors.md +32 -0
  39. data/docs/how-to/integrate-with-lrama.md +21 -0
  40. data/docs/how-to/integrate-with-racc.md +25 -0
  41. data/docs/how-to/migrate-from-flex.md +21 -0
  42. data/docs/how-to/migrate-from-rexical.md +23 -0
  43. data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
  44. data/docs/how-to/track-token-locations.md +27 -0
  45. data/docs/how-to/tune-performance.md +23 -0
  46. data/docs/how-to/use-states.md +36 -0
  47. data/docs/how-to/use-trailing-context.md +22 -0
  48. data/docs/internals/README.md +14 -0
  49. data/docs/perf-log.md +56 -0
  50. data/docs/reference/README.md +23 -0
  51. data/docs/reference/actions.md +47 -0
  52. data/docs/reference/cli.md +80 -0
  53. data/docs/reference/compatibility.md +38 -0
  54. data/docs/reference/diagnostics.md +41 -0
  55. data/docs/reference/dsl.md +81 -0
  56. data/docs/reference/errors.md +27 -0
  57. data/docs/reference/generated-artifacts.md +50 -0
  58. data/docs/reference/public-api.md +42 -0
  59. data/docs/reference/regexp.md +39 -0
  60. data/docs/reference/runtime.md +49 -0
  61. data/docs/reference/tokens-and-locations.md +33 -0
  62. data/docs/tutorial/build-a-calculator-lexer.md +96 -0
  63. data/examples/calculator/README.md +27 -0
  64. data/examples/calculator/lexer.flexr.rb +17 -0
  65. data/examples/json/README.md +30 -0
  66. data/examples/json/lexer.flexr.rb +24 -0
  67. data/examples/ruby_subset/README.md +17 -0
  68. data/examples/ruby_subset/lexer.flexr.rb +22 -0
  69. data/examples/toy_lang/README.md +17 -0
  70. data/examples/toy_lang/lexer.flexr.rb +18 -0
  71. data/examples/with_lrama/README.md +17 -0
  72. data/examples/with_lrama/lexer.flexr.rb +13 -0
  73. data/examples/with_racc/README.md +17 -0
  74. data/examples/with_racc/lexer.flexr.rb +13 -0
  75. data/exe/flexr +7 -0
  76. data/lib/flexr/automaton/accel.rb +39 -0
  77. data/lib/flexr/automaton/analysis.rb +38 -0
  78. data/lib/flexr/automaton/byte_class_set.rb +29 -0
  79. data/lib/flexr/automaton/compiler.rb +413 -0
  80. data/lib/flexr/automaton/dfa.rb +103 -0
  81. data/lib/flexr/automaton/minimizer.rb +70 -0
  82. data/lib/flexr/automaton/nfa.rb +92 -0
  83. data/lib/flexr/cli.rb +342 -0
  84. data/lib/flexr/codegen/base.rb +17 -0
  85. data/lib/flexr/codegen/direct.rb +52 -0
  86. data/lib/flexr/codegen/firstmatch.rb +17 -0
  87. data/lib/flexr/codegen/table.rb +158 -0
  88. data/lib/flexr/codegen/table_packer.rb +61 -0
  89. data/lib/flexr/diagnostics.rb +94 -0
  90. data/lib/flexr/dsl.rb +182 -0
  91. data/lib/flexr/errors.rb +28 -0
  92. data/lib/flexr/generated.rb +125 -0
  93. data/lib/flexr/generator.rb +400 -0
  94. data/lib/flexr/importer.rb +560 -0
  95. data/lib/flexr/ir.rb +36 -0
  96. data/lib/flexr/lexer.rb +10 -0
  97. data/lib/flexr/options.rb +47 -0
  98. data/lib/flexr/rake_task.rb +27 -0
  99. data/lib/flexr/regexp/ast.rb +45 -0
  100. data/lib/flexr/regexp/char_class.rb +7 -0
  101. data/lib/flexr/regexp/normalizer.rb +117 -0
  102. data/lib/flexr/regexp/parser.rb +517 -0
  103. data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
  104. data/lib/flexr/regexp/tokenizer.rb +168 -0
  105. data/lib/flexr/regexp/unsupported.rb +7 -0
  106. data/lib/flexr/runtime/buffer.rb +112 -0
  107. data/lib/flexr/runtime/core.rb +388 -0
  108. data/lib/flexr/runtime/errors.rb +22 -0
  109. data/lib/flexr/runtime/interpreter.rb +505 -0
  110. data/lib/flexr/runtime/location.rb +26 -0
  111. data/lib/flexr/runtime/token.rb +7 -0
  112. data/lib/flexr/source/passthrough.rb +31 -0
  113. data/lib/flexr/source/prism_reader.rb +283 -0
  114. data/lib/flexr/source/static_eval.rb +145 -0
  115. data/lib/flexr/unicode/case_fold.rb +45 -0
  116. data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
  117. data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
  118. data/lib/flexr/unicode/data/case_folding.rb +9 -0
  119. data/lib/flexr/unicode/data/properties.rb +10 -0
  120. data/lib/flexr/unicode/property.rb +107 -0
  121. data/lib/flexr/unicode/reference_regexp.rb +102 -0
  122. data/lib/flexr/unicode/utf8_splitter.rb +109 -0
  123. data/lib/flexr/version.rb +5 -0
  124. data/lib/flexr.rb +81 -0
  125. data/site/README.md +22 -0
  126. data/site/astro.config.mjs +57 -0
  127. data/site/package.json +19 -0
  128. data/site/pnpm-lock.yaml +5029 -0
  129. data/site/pnpm-workspace.yaml +6 -0
  130. data/site/public/playground.js +189 -0
  131. data/site/scripts/verify-site.mjs +42 -0
  132. data/site/src/content/docs/benchmarks.md +8 -0
  133. data/site/src/content/docs/concepts/matching-semantics.md +15 -0
  134. data/site/src/content/docs/concepts/regexp-model.md +18 -0
  135. data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
  136. data/site/src/content/docs/concepts/security-model.md +15 -0
  137. data/site/src/content/docs/examples.md +17 -0
  138. data/site/src/content/docs/learn/generation.md +29 -0
  139. data/site/src/content/docs/learn/getting-started.md +56 -0
  140. data/site/src/content/docs/learn/parser-integration.md +27 -0
  141. data/site/src/content/docs/learn/runtime-mode.md +32 -0
  142. data/site/src/content/docs/reference/action-context.md +20 -0
  143. data/site/src/content/docs/reference/cli.md +22 -0
  144. data/site/src/content/docs/reference/diagnostics.md +16 -0
  145. data/site/src/content/docs/reference/dsl.md +19 -0
  146. data/site/src/content/docs/reference/public-api.md +18 -0
  147. data/site/src/content/docs/reference/regexp.md +16 -0
  148. data/site/src/content/docs/reference/runtime.md +16 -0
  149. data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
  150. data/site/src/content.config.ts +12 -0
  151. data/site/src/env.d.ts +1 -0
  152. data/site/src/layouts/SiteLayout.astro +39 -0
  153. data/site/src/pages/index.astro +174 -0
  154. data/site/src/pages/playground.astro +64 -0
  155. data/site/src/styles/custom.css +711 -0
  156. data/site/tsconfig.json +5 -0
  157. data/tools/coverage.rb +32 -0
  158. data/tools/docs_verify.rb +116 -0
  159. data/tools/gen_unicode_tables.rb +202 -0
  160. data/tools/regexp_tokenizer_reference.rb +60 -0
  161. metadata +205 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Automaton
5
+ module Minimizer
6
+ module_function
7
+
8
+ def minimize(dfa)
9
+ partitions = initial_partitions(dfa)
10
+ loop do
11
+ groups = group_ids(partitions)
12
+ refined = partitions.flat_map do |partition|
13
+ partition.group_by do |state|
14
+ [dfa.accepts[state], dfa.transitions[state].map { |destination| destination.nil? ? -1 : groups[destination] }]
15
+ end.values
16
+ end
17
+ break if refined == partitions
18
+
19
+ partitions = refined
20
+ end
21
+ rebuild(dfa, partitions)
22
+ end
23
+
24
+ def initial_partitions(dfa)
25
+ (0...dfa.states).group_by { |state| dfa.accepts[state] }.values
26
+ end
27
+
28
+ def group_ids(partitions)
29
+ ids = {}
30
+ partitions.each_with_index { |partition, id| partition.each { |state| ids[state] = id } }
31
+ ids
32
+ end
33
+
34
+ def rebuild(dfa, partitions)
35
+ groups = group_ids(partitions)
36
+ start_group = groups.fetch(dfa.start)
37
+ order = bfs_groups(dfa, groups, start_group)
38
+ index = order.each_with_index.to_h
39
+ transitions = order.map do |group|
40
+ representative = partitions[group].first
41
+ dfa.transitions[representative].map { |destination| destination.nil? ? nil : index[groups[destination]] }
42
+ end
43
+ accepts = order.map { |group| dfa.accepts[partitions[group].first] }
44
+ rule_ids = accepts.flatten.map(&:rule_index).uniq.sort
45
+ DFA.new(transitions: transitions, accepts: accepts, ec: dfa.ec, class_count: dfa.class_count,
46
+ start: 0, rule_ids: rule_ids)
47
+ end
48
+
49
+ def bfs_groups(dfa, groups, start_group)
50
+ result = []
51
+ queue = [start_group]
52
+ seen = {}
53
+ until queue.empty?
54
+ group = queue.shift
55
+ next if seen[group]
56
+
57
+ seen[group] = true
58
+ result << group
59
+ representative = dfa.transitions[group_members(dfa, groups, group).first]
60
+ representative.compact.each { |destination| queue << groups[destination] }
61
+ end
62
+ result
63
+ end
64
+
65
+ def group_members(dfa, groups, group)
66
+ (0...dfa.states).select { |state| groups[state] == group }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Automaton
5
+ NFAState = Struct.new(:epsilon, :transitions, :accepts, keyword_init: true)
6
+ NFATransition = Struct.new(:lo, :hi, :to, keyword_init: true)
7
+ Acceptance = Struct.new(:rule_index, :pattern_index, :bol_only, :end_anchor, keyword_init: true) do
8
+ def inspect
9
+ [rule_index, pattern_index, bol_only, end_anchor].inspect
10
+ end
11
+ end
12
+
13
+ class NFA
14
+ attr_reader :states, :start, :byte_classes
15
+
16
+ def initialize
17
+ @states = []
18
+ @start = new_state
19
+ @byte_classes = ByteClassSet.new
20
+ end
21
+
22
+ def new_state
23
+ id = @states.length
24
+ @states << NFAState.new(epsilon: [], transitions: [], accepts: [])
25
+ id
26
+ end
27
+
28
+ def epsilon(from, to)
29
+ @states[from].epsilon << to
30
+ end
31
+
32
+ def transition(from, lo, hi, to)
33
+ @states[from].transitions << NFATransition.new(lo: lo, hi: hi, to: to)
34
+ @byte_classes.add_range(lo, hi)
35
+ end
36
+ end
37
+
38
+ class NFABuilder
39
+ def initialize
40
+ @nfa = NFA.new
41
+ end
42
+
43
+ def build(patterns)
44
+ patterns.each do |pattern, acceptance|
45
+ start, finish = fragment(pattern)
46
+ @nfa.epsilon(@nfa.start, start)
47
+ @nfa.states[finish].accepts << acceptance
48
+ end
49
+ @nfa
50
+ end
51
+
52
+ private
53
+
54
+ def fragment(node)
55
+ case node
56
+ when Regexp::AST::Empty
57
+ state = @nfa.new_state
58
+ [state, state]
59
+ when Regexp::AST::ByteRange
60
+ from = @nfa.new_state
61
+ to = @nfa.new_state
62
+ @nfa.transition(from, node.lo, node.hi, to)
63
+ [from, to]
64
+ when Regexp::AST::Seq
65
+ fragments = node.children.map { |child| fragment(child) }
66
+ fragments.each_cons(2) { |(_, end_state), (start_state, _)| @nfa.epsilon(end_state, start_state) }
67
+ [fragments.first.first, fragments.last.last]
68
+ when Regexp::AST::Alt
69
+ from = @nfa.new_state
70
+ to = @nfa.new_state
71
+ node.children.each do |child|
72
+ child_start, child_end = fragment(child)
73
+ @nfa.epsilon(from, child_start)
74
+ @nfa.epsilon(child_end, to)
75
+ end
76
+ [from, to]
77
+ when Regexp::AST::Star
78
+ from = @nfa.new_state
79
+ to = @nfa.new_state
80
+ child_start, child_end = fragment(node.child)
81
+ @nfa.epsilon(from, to)
82
+ @nfa.epsilon(from, child_start)
83
+ @nfa.epsilon(child_end, child_start)
84
+ @nfa.epsilon(child_end, to)
85
+ [from, to]
86
+ else
87
+ raise CompileError, "cannot build NFA from #{node.class}"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/flexr/cli.rb ADDED
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module CLI
5
+ COMMANDS = %w[check stats tokens dot explain trace bench import].freeze
6
+ EXIT_OK = 0
7
+ EXIT_FAILURE = 1
8
+ EXIT_USAGE = 2
9
+
10
+ module_function
11
+
12
+ def run(argv, out: $stdout, err: $stderr)
13
+ args = argv.dup
14
+ return usage(out) if args.include?("--help") || args.include?("-h")
15
+ return version(out) if args.delete("--version")
16
+
17
+ command = COMMANDS.include?(args.first) ? args.shift.to_sym : :generate
18
+ options, output, rule_number, benchmark_args, positionals = parse(args, command)
19
+ spec = positionals.shift
20
+ raise ArgumentError, "a SPEC.rb path is required" unless spec
21
+ raise ArgumentError, "unexpected argument: #{positionals.first}" unless positionals.empty?
22
+
23
+ execute(command, spec, options, output, rule_number, benchmark_args, out, err)
24
+ rescue ArgumentError => e
25
+ err.puts "error: #{e.message}"
26
+ usage(err, status: EXIT_USAGE)
27
+ rescue Flexr::Error, Errno::ENOENT, Errno::EACCES, SyntaxError => e
28
+ err.puts render_error(e, options: options)
29
+ EXIT_FAILURE
30
+ rescue StandardError => e
31
+ err.puts "error: #{e.class}: #{e.message}"
32
+ EXIT_FAILURE
33
+ end
34
+
35
+ def parse(args, command)
36
+ options = Options.default
37
+ output = nil
38
+ rule_number = nil
39
+ benchmark_args = []
40
+ positionals = []
41
+ parsing_options = true
42
+
43
+ until args.empty?
44
+ argument = args.shift
45
+ if parsing_options && argument == "--"
46
+ parsing_options = false
47
+ next
48
+ end
49
+ unless parsing_options && argument.start_with?("-")
50
+ positionals << argument
51
+ next
52
+ end
53
+
54
+ case argument
55
+ when "-o", "--output"
56
+ output = required_argument!(args, argument)
57
+ when "-b", "--backend"
58
+ options.set(:backend, required_argument!(args, argument).to_sym)
59
+ when "--token-kind"
60
+ options.set(:token_kind, required_argument!(args, argument).to_sym)
61
+ when "--accel"
62
+ options.set(:accel, required_argument!(args, argument).to_sym)
63
+ when "--standalone"
64
+ options.set(:standalone, true)
65
+ when "--eval"
66
+ options.eval_mode = true
67
+ when "--table-compression"
68
+ options.set(:table_compression, required_argument!(args, argument).to_sym)
69
+ when "--table-format"
70
+ options.set(:table_format, required_argument!(args, argument).to_sym)
71
+ when "--max-dfa-states"
72
+ options.set(:max_dfa_states, Integer(required_argument!(args, argument), 10))
73
+ when "-W", "--warn"
74
+ options.set(:warn_level, required_argument!(args, argument).to_sym)
75
+ when "--warn-as-error"
76
+ options.warn_as_error = true
77
+ when "--color"
78
+ options.color = required_argument!(args, argument).to_sym
79
+ when "--format"
80
+ options.format = required_argument!(args, argument).to_sym
81
+ when "--rule"
82
+ rule_number = Integer(required_argument!(args, argument), 10)
83
+ raise ArgumentError, "--rule must be non-negative" if rule_number.negative?
84
+ when "--input-file", "--baseline", "--iterations"
85
+ raise ArgumentError, "#{argument} is only valid for the bench command" unless command == :bench
86
+ benchmark_args.push(argument, required_argument!(args, argument))
87
+ else
88
+ raise ArgumentError, "unknown option: #{argument}"
89
+ end
90
+ end
91
+
92
+ options.validate!
93
+ [options, output, rule_number, benchmark_args, positionals]
94
+ rescue ArgumentError => e
95
+ raise e if e.message.start_with?("unsupported ", "--rule")
96
+
97
+ raise ArgumentError, "invalid option value: #{e.message}"
98
+ end
99
+
100
+ def execute(command, spec, options, output, rule_number, benchmark_args, out, err)
101
+ case command
102
+ when :check
103
+ check(spec, options, out)
104
+ when :stats
105
+ print_stats(spec, options, out)
106
+ when :tokens
107
+ parsed = read_spec(spec)
108
+ out.puts Array(parsed.config[:declared_tokens]).join(" ")
109
+ EXIT_OK
110
+ when :dot
111
+ print_dot(spec, options, out)
112
+ when :explain
113
+ print_explanation(spec, rule_number, out)
114
+ when :trace
115
+ print_trace(spec, options, out)
116
+ when :bench
117
+ run_benchmark(spec, options, benchmark_args, out)
118
+ when :import
119
+ result = Importer.import(spec)
120
+ result.warnings.each { |warning| err.puts "warning: #{warning}" }
121
+ return EXIT_FAILURE unless result.complete?
122
+
123
+ if output
124
+ File.binwrite(output, result.source)
125
+ else
126
+ out.write(result.source)
127
+ end
128
+ EXIT_OK
129
+ when :generate
130
+ target = output || spec.sub(/\.flexr\.rb\z/, ".rb")
131
+ Generator.new(spec, output: target, eval_mode: options.eval_mode,
132
+ options: options.generator_options).generate
133
+ EXIT_OK
134
+ end
135
+ end
136
+
137
+ def check(spec, options, out)
138
+ compiled = if options.eval_mode
139
+ generator = Generator.new(spec, eval_mode: true, options: options.generator_options)
140
+ generator.generate
141
+ generator.diagnostics
142
+ else
143
+ compile(read_spec(spec), overrides: options.overrides)
144
+ end
145
+ diagnostics = compiled.is_a?(Array) ? compiled : Array(compiled&.diagnostics)
146
+ diagnostics.select! do |diagnostic|
147
+ options.warn_level == :all || (options.warn_level == :default && diagnostic.code != "FLEXR-W016")
148
+ end
149
+ set = DiagnosticSet.new
150
+ diagnostics.each { |diagnostic| set << diagnostic }
151
+ if options.format == :json
152
+ out.puts set.render(format: :json)
153
+ elsif !diagnostics.empty?
154
+ out.puts set.render(format: :human, color: options.color)
155
+ end
156
+ return EXIT_FAILURE if options.warn_as_error && diagnostics.any?(&:warning?)
157
+
158
+ EXIT_OK
159
+ end
160
+
161
+ def print_stats(spec, options, out)
162
+ compiled = compile(read_spec(spec), overrides: options.overrides)
163
+ stats = compiled.stats.transform_keys(&:to_s).transform_values do |stat|
164
+ stat.merge(table_cells: stat[:states] * stat[:classes])
165
+ end
166
+ compiled.machines.each do |state_name, machine|
167
+ dfa = machine.dfa
168
+ stat = stats.fetch(state_name.to_s)
169
+ stat[:table_entries] = dfa.transitions.sum { |row| row.compact.length }
170
+ stat[:acceleration_regions] = Automaton::Accel.extract(dfa).length
171
+ end
172
+ stats[:diagnostics] = Array(compiled.diagnostics).map(&:to_h) if options.format == :json
173
+ out.puts JSON.pretty_generate(stats)
174
+ EXIT_OK
175
+ end
176
+
177
+ def print_dot(spec, options, out)
178
+ compiled = compile(read_spec(spec), overrides: options.overrides)
179
+ out.puts "digraph flexr {"
180
+ compiled.machines.each do |state_name, machine|
181
+ dfa = machine.dfa
182
+ accelerated_states = Automaton::Accel.extract(dfa).to_h { |region| [region.state, true] }
183
+ dfa.transitions.each_index do |state|
184
+ node_name = "#{state_name}_#{state}"
185
+ node = dot_quote(node_name)
186
+ accepting = !dfa.accepts[state].empty?
187
+ label = dfa.accepts[state].map(&:rule_index).uniq.join(",")
188
+ label = "#{node_name}\naccept=#{label}" unless label.empty?
189
+ attributes = dot_node_attributes(label, accepting: accepting, accelerated: accelerated_states[state])
190
+ out.puts " #{node} [#{attributes}];"
191
+ dfa.transitions[state].compact.uniq.each do |destination|
192
+ out.puts " #{node} -> #{dot_quote("#{state_name}_#{destination}")};"
193
+ end
194
+ end
195
+ end
196
+ out.puts "}"
197
+ EXIT_OK
198
+ end
199
+
200
+ def dot_node_attributes(label, accepting:, accelerated:)
201
+ attributes = { shape: accepting ? "doublecircle" : "circle", label: dot_quote(label) }
202
+ if accepting
203
+ attributes[:color] = dot_quote("#2E7D32")
204
+ attributes[:penwidth] = 2
205
+ end
206
+ if accelerated
207
+ attributes[:color] = dot_quote("#D97706")
208
+ attributes[:style] = "filled"
209
+ attributes[:fillcolor] = dot_quote("#FEF3C7")
210
+ end
211
+ attributes.map { |name, value| "#{name}=#{value}" }.join(", ")
212
+ end
213
+
214
+ def dot_quote(value)
215
+ escaped = value.to_s.each_char.with_object(+'') do |character, result|
216
+ result << case character
217
+ when "\\" then "\\\\"
218
+ when '"' then '\\"'
219
+ when "\n" then "\\n"
220
+ when "\r" then "\\r"
221
+ else character
222
+ end
223
+ end
224
+ "\"#{escaped}\""
225
+ end
226
+
227
+ def print_trace(spec, options, out)
228
+ compiled = compile(read_spec(spec), overrides: options.overrides)
229
+ compiled.machines.each do |state_name, machine|
230
+ dfa = machine.dfa
231
+ out.puts "state #{state_name} start=#{dfa.start} classes=#{dfa.class_count}"
232
+ dfa.transitions.each_index do |state|
233
+ accepts = dfa.accepts[state].map do |acceptance|
234
+ [acceptance.rule_index, acceptance.pattern_index, acceptance.bol_only, acceptance.end_anchor]
235
+ end
236
+ transitions = dfa.transitions[state].each_with_index.filter_map do |destination, class_id|
237
+ destination && [class_id, destination]
238
+ end
239
+ out.puts " #{state}: accepts=#{accepts.inspect} transitions=#{transitions.inspect}"
240
+ end
241
+ end
242
+ EXIT_OK
243
+ end
244
+
245
+ def print_explanation(spec, rule_number, out)
246
+ rules = read_spec(spec).rules
247
+ rules = rules.select { |rule| rule.index == rule_number } if rule_number
248
+ raise ArgumentError, "rule not found: #{rule_number}" if rules.empty? && rule_number
249
+
250
+ out.puts(rules.map { |rule| "rule #{rule.index}: #{rule.patterns.inspect}" })
251
+ EXIT_OK
252
+ end
253
+
254
+ def run_benchmark(spec, options, benchmark_args, out)
255
+ require_relative "../../benchmark/run"
256
+ args = ["--spec", spec, *benchmark_args]
257
+ args << "--json" if options.format == :json
258
+ Flexr::Benchmarking.run(args, out: out, err: $stderr)
259
+ end
260
+
261
+ def generate(spec, options)
262
+ Generator.new(spec, eval_mode: options.eval_mode, options: options.generator_options).generate
263
+ end
264
+
265
+ def read_spec(spec)
266
+ source = File.binread(spec).force_encoding(Encoding::UTF_8)
267
+ Source::PrismReader.new(source, path: spec).read
268
+ end
269
+
270
+ def compile(parsed, overrides: {})
271
+ klass = Class.new(Flexr::Lexer)
272
+ config = parsed.config
273
+ klass.backend(overrides.fetch(:backend, config.fetch(:backend, :table)))
274
+ klass.token_kind(overrides.fetch(:token_kind, config.fetch(:token_kind, :array)))
275
+ klass.encoding(config.fetch(:encoding, Encoding::UTF_8))
276
+ Array(config[:declared_tokens]).each { |token| klass.emits(token) }
277
+ config_options = config.fetch(:options, {}).merge(overrides.slice(:experimental, :allow_empty_match))
278
+ config_options.each do |name, value|
279
+ value ? klass.option(name) : nil
280
+ end
281
+ klass.__flexr_config.options[:max_dfa_states] = overrides[:max_dfa_states] if overrides[:max_dfa_states]
282
+ accel = overrides.fetch(:accel, config_options[:accel])
283
+ klass.accel(accel) if accel
284
+ parsed.states.each do |name, value|
285
+ next if name.to_sym == :initial
286
+
287
+ klass.state(name, inclusive: value[:inclusive]) { nil }
288
+ end
289
+ parsed.rules.each { |rule| klass.__flexr_add_generated_rule(rule.to_h) }
290
+ klass.compile!
291
+ end
292
+
293
+ def required_argument!(args, option)
294
+ value = args.shift
295
+ raise ArgumentError, "#{option} requires a value" if value.nil? || value.start_with?("-")
296
+
297
+ value
298
+ end
299
+
300
+ def render_error(error, options: nil)
301
+ if error.respond_to?(:diagnostic) && error.diagnostic
302
+ return DiagnosticSet.new.tap { |set| set << error.diagnostic }.render(
303
+ format: options&.format || :human, color: options&.color || :auto
304
+ )
305
+ end
306
+
307
+ if options&.format == :json
308
+ JSON.generate([{ code: "FLEXR-E000", severity: "error", message: error.message }])
309
+ else
310
+ "error: #{error.message}"
311
+ end
312
+ end
313
+
314
+ def version(out)
315
+ out.puts Flexr::VERSION
316
+ EXIT_OK
317
+ end
318
+
319
+ def usage(out, status: EXIT_OK)
320
+ out.puts <<~USAGE
321
+ Usage: flexr [COMMAND] SPEC.rb [options]
322
+
323
+ Commands: check, stats, tokens, dot, explain, trace, bench, import
324
+ Options:
325
+ -o, --output PATH generated output path
326
+ -b, --backend NAME table | direct | firstmatch | auto
327
+ --token-kind KIND array | struct | yield
328
+ --accel MODE auto | strscan | regexp | none
329
+ --standalone
330
+ --eval
331
+ --table-compression VALUE none | rows | full
332
+ --table-format VALUE literal | packed
333
+ --max-dfa-states N
334
+ -W, --warn LEVEL all | default | none
335
+ --warn-as-error
336
+ --color WHEN auto | always | never
337
+ --format FMT human | json
338
+ USAGE
339
+ status
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Codegen
5
+ class Base
6
+ attr_reader :compiled
7
+
8
+ def initialize(compiled)
9
+ @compiled = compiled
10
+ end
11
+
12
+ def stats
13
+ compiled.stats
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Codegen
5
+ class Direct < Table
6
+ def generate(state: :initial)
7
+ table = super
8
+ table.merge(dispatch: :case).freeze
9
+ end
10
+
11
+ def source(indent: " ")
12
+ lines = []
13
+ lines << "#{indent}def self.__flexr_generated_direct_transition(state_name, state, byte)"
14
+ lines << "#{indent} case state_name"
15
+ compiled.machines.each do |state_name, machine|
16
+ lines << "#{indent} when #{state_name.inspect}"
17
+ lines << "#{indent} class_id = case byte"
18
+ byte_classes(machine.dfa.ec).each do |first, last, class_id|
19
+ selector = first == last ? first.to_s : "#{first}..#{last}"
20
+ lines << "#{indent} when #{selector} then #{class_id}"
21
+ end
22
+ lines << "#{indent} end"
23
+ lines << "#{indent} case state"
24
+ machine.dfa.transitions.each_with_index do |row, state|
25
+ lines << "#{indent} when #{state}"
26
+ lines << "#{indent} case class_id"
27
+ row.each_with_index do |destination, class_id|
28
+ value = destination.nil? ? "nil" : destination
29
+ lines << "#{indent} when #{class_id} then #{value}"
30
+ end
31
+ lines << "#{indent} else nil"
32
+ lines << "#{indent} end"
33
+ end
34
+ lines << "#{indent} else nil"
35
+ lines << "#{indent} end"
36
+ end
37
+ lines << "#{indent} else nil"
38
+ lines << "#{indent} end"
39
+ lines << "#{indent}end"
40
+ "#{lines.join("\n")}\n"
41
+ end
42
+
43
+ private
44
+
45
+ def byte_classes(ec)
46
+ ec.each_with_index.chunk_while { |left, right| right[0] == left[0] }.map do |run|
47
+ [run.first[1], run.last[1], run.first[0]]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Codegen
5
+ class Firstmatch < Base
6
+ def initialize(compiled, experimental: false)
7
+ raise CompileError, "firstmatch requires option :experimental" unless experimental
8
+
9
+ super(compiled)
10
+ end
11
+
12
+ def generate
13
+ compiled.rules.map { |rule| rule.patterns }.flatten.freeze
14
+ end
15
+ end
16
+ end
17
+ end