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,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,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
|