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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Regexp
|
|
5
|
+
module AST
|
|
6
|
+
Empty = Struct.new(:loc, keyword_init: true)
|
|
7
|
+
ByteRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
|
|
8
|
+
CodepointRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
|
|
9
|
+
Seq = Struct.new(:children, :loc, keyword_init: true)
|
|
10
|
+
Alt = Struct.new(:children, :loc, keyword_init: true)
|
|
11
|
+
Star = Struct.new(:child, :loc, keyword_init: true)
|
|
12
|
+
Anchor = Struct.new(:kind, :loc, keyword_init: true)
|
|
13
|
+
TrailMark = Struct.new(:rule_id, :loc, keyword_init: true)
|
|
14
|
+
CharClass = Struct.new(:ranges, :negated, :loc, keyword_init: true)
|
|
15
|
+
Property = Module.new
|
|
16
|
+
|
|
17
|
+
module Formatting
|
|
18
|
+
def to_s
|
|
19
|
+
name = self.class.name.split("::").last
|
|
20
|
+
case name
|
|
21
|
+
when "Empty" then "Empty"
|
|
22
|
+
when "ByteRange" then "ByteRange(#{lo}..#{hi})"
|
|
23
|
+
when "CodepointRange" then "CodepointRange(#{lo}..#{hi})"
|
|
24
|
+
when "Anchor" then "Anchor(#{kind})"
|
|
25
|
+
when "Star" then "Star(#{child})"
|
|
26
|
+
when "Seq", "Alt" then "#{name}(#{children.join(', ')})"
|
|
27
|
+
else super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
[Empty, ByteRange, CodepointRange, Seq, Alt, Star, Anchor, TrailMark, CharClass].each do |node_class|
|
|
33
|
+
node_class.include(Formatting)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Node
|
|
37
|
+
attr_reader :loc
|
|
38
|
+
|
|
39
|
+
def initialize(loc = nil)
|
|
40
|
+
@loc = loc
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Regexp
|
|
5
|
+
class Normalizer
|
|
6
|
+
def initialize(ast, encoding: Encoding::UTF_8, options: 0)
|
|
7
|
+
@ast = ast
|
|
8
|
+
@encoding = encoding
|
|
9
|
+
@options = options
|
|
10
|
+
@byte_mode = [Encoding::BINARY, Encoding::US_ASCII].include?(encoding)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def normalize
|
|
14
|
+
normalize_node(@ast)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_node(node)
|
|
20
|
+
case node
|
|
21
|
+
when AST::Empty, AST::ByteRange, AST::CodepointRange, AST::Anchor
|
|
22
|
+
normalize_leaf(node)
|
|
23
|
+
when AST::CharClass
|
|
24
|
+
char_class(node)
|
|
25
|
+
when AST::Seq
|
|
26
|
+
sequence(node.children.map { |child| normalize_node(child) })
|
|
27
|
+
when AST::Alt
|
|
28
|
+
alternatives(node.children.map { |child| normalize_node(child) })
|
|
29
|
+
when AST::Star
|
|
30
|
+
AST::Star.new(child: normalize_node(node.child), loc: node.loc)
|
|
31
|
+
else
|
|
32
|
+
raise CompileError, "unknown regexp AST node: #{node.class}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize_leaf(node)
|
|
37
|
+
case node
|
|
38
|
+
when AST::CodepointRange
|
|
39
|
+
byte_sequences_for_ranges(casefold_ranges(node.lo, node.hi))
|
|
40
|
+
else
|
|
41
|
+
node
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def char_class(node)
|
|
46
|
+
ranges = node.ranges.flat_map do |range|
|
|
47
|
+
if range.first == AST::Property
|
|
48
|
+
Unicode::Property.ranges(range.last, negate: range[1])
|
|
49
|
+
else
|
|
50
|
+
casefold_ranges(*range)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
ranges = complement(ranges) if node.negated
|
|
54
|
+
byte_sequences_for_ranges(ranges)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def byte_sequences_for_ranges(ranges)
|
|
58
|
+
if @byte_mode
|
|
59
|
+
byte_ranges = ranges.filter_map do |lo, hi|
|
|
60
|
+
next if lo > 255
|
|
61
|
+
|
|
62
|
+
[lo, [hi, 255].min]
|
|
63
|
+
end
|
|
64
|
+
return AST::Empty.new(loc: nil) if byte_ranges.empty?
|
|
65
|
+
|
|
66
|
+
return alternatives(byte_ranges.map { |lo, hi| AST::ByteRange.new(lo: lo, hi: hi, loc: nil) })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sequences = ranges.flat_map { |lo, hi| Unicode::Utf8Splitter.split(lo, hi) }
|
|
70
|
+
trie(sequences, 0)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def trie(sequences, index)
|
|
74
|
+
return AST::Empty.new(loc: nil) if sequences.empty?
|
|
75
|
+
return AST::Empty.new(loc: nil) if sequences.first.length == index
|
|
76
|
+
|
|
77
|
+
groups = sequences.group_by { |sequence| sequence[index] }
|
|
78
|
+
alternatives(groups.map do |(lo, hi), group|
|
|
79
|
+
child = trie(group, index + 1)
|
|
80
|
+
AST::Seq.new(children: [AST::ByteRange.new(lo: lo, hi: hi, loc: nil), child], loc: nil)
|
|
81
|
+
end)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sequence(children)
|
|
85
|
+
flattened = children.flat_map { |child| child.is_a?(AST::Seq) ? child.children : [child] }
|
|
86
|
+
return AST::Empty.new(loc: nil) if flattened.empty?
|
|
87
|
+
return flattened.first if flattened.length == 1
|
|
88
|
+
|
|
89
|
+
AST::Seq.new(children: flattened, loc: nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def alternatives(children)
|
|
93
|
+
flattened = children.flat_map { |child| child.is_a?(AST::Alt) ? child.children : [child] }
|
|
94
|
+
return AST::Empty.new(loc: nil) if flattened.empty?
|
|
95
|
+
return flattened.first if flattened.length == 1
|
|
96
|
+
|
|
97
|
+
AST::Alt.new(children: flattened, loc: nil)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def complement(ranges)
|
|
101
|
+
result = []
|
|
102
|
+
cursor = 0
|
|
103
|
+
ranges.sort_by(&:first).each do |lo, hi|
|
|
104
|
+
result << [cursor, lo - 1] if cursor < lo
|
|
105
|
+
cursor = [cursor, hi + 1].max
|
|
106
|
+
end
|
|
107
|
+
result << [cursor, 0x10ffff] if cursor <= 0x10ffff
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def casefold_ranges(lo, hi)
|
|
112
|
+
return [[lo, hi]] if @options.nobits?(::Regexp::IGNORECASE)
|
|
113
|
+
Unicode::CaseFold.ranges(lo, hi)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Regexp
|
|
5
|
+
class Parser
|
|
6
|
+
ESCAPES = {
|
|
7
|
+
"n" => 0x0a, "t" => 0x09, "r" => 0x0d, "f" => 0x0c,
|
|
8
|
+
"v" => 0x0b, "a" => 0x07, "e" => 0x1b, "0" => 0
|
|
9
|
+
}.freeze
|
|
10
|
+
PROPERTY_ALIASES = {
|
|
11
|
+
"digit" => "Nd", "alpha" => "Alphabetic", "alnum" => "Alnum",
|
|
12
|
+
"word" => "Word", "space" => "Space"
|
|
13
|
+
}.freeze
|
|
14
|
+
POSIX_CLASSES = %w[alnum alpha blank cntrl digit graph lower print punct space upper xdigit].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :source
|
|
17
|
+
|
|
18
|
+
def initialize(source, options: 0, encoding: Encoding::UTF_8, unicode: false)
|
|
19
|
+
@source = source
|
|
20
|
+
@options = options
|
|
21
|
+
@encoding = encoding
|
|
22
|
+
@unicode = unicode
|
|
23
|
+
@index = 0
|
|
24
|
+
@class_depth = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse
|
|
28
|
+
node = parse_expression
|
|
29
|
+
raise_syntax("unexpected `#{current}`") unless eof?
|
|
30
|
+
validate_anchor_positions(node)
|
|
31
|
+
node
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def parse_expression
|
|
37
|
+
branches = [parse_sequence]
|
|
38
|
+
branches << parse_sequence while consume?("|")
|
|
39
|
+
return branches.first if branches.length == 1
|
|
40
|
+
|
|
41
|
+
AST::Alt.new(children: branches, loc: nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_sequence
|
|
45
|
+
children = []
|
|
46
|
+
children << parse_quantified until eof? || [")", "|"].include?(current)
|
|
47
|
+
return AST::Empty.new(loc: nil) if children.empty?
|
|
48
|
+
return children.first if children.length == 1
|
|
49
|
+
|
|
50
|
+
AST::Seq.new(children: children, loc: nil)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_quantified
|
|
54
|
+
atom = parse_atom
|
|
55
|
+
return atom unless ["*", "+", "?", "{"].include?(current)
|
|
56
|
+
|
|
57
|
+
if consume?("*")
|
|
58
|
+
reject_postfix_quantifier
|
|
59
|
+
return AST::Star.new(child: atom, loc: nil)
|
|
60
|
+
end
|
|
61
|
+
if consume?("+")
|
|
62
|
+
reject_postfix_quantifier
|
|
63
|
+
return AST::Seq.new(children: [atom, AST::Star.new(child: atom, loc: nil)], loc: nil)
|
|
64
|
+
end
|
|
65
|
+
if consume?("?")
|
|
66
|
+
reject_postfix_quantifier
|
|
67
|
+
return AST::Alt.new(children: [atom, AST::Empty.new(loc: nil)], loc: nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parse_repetition(atom)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_repetition(atom)
|
|
74
|
+
consume?("{")
|
|
75
|
+
min = read_number
|
|
76
|
+
max = if consume?(",")
|
|
77
|
+
read_number unless current == "}"
|
|
78
|
+
else
|
|
79
|
+
min
|
|
80
|
+
end
|
|
81
|
+
expect("}")
|
|
82
|
+
raise_syntax("invalid repetition") if min.nil? || (!max.nil? && max < min)
|
|
83
|
+
raise_syntax("open repetition is not supported") if max.nil?
|
|
84
|
+
if max > 1000
|
|
85
|
+
raise_diagnostic(
|
|
86
|
+
diagnostic("FLEXR-E007", "repetition limit exceeds 1000",
|
|
87
|
+
help: "split the rule or use a smaller bounded repetition")
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
reject_postfix_quantifier
|
|
91
|
+
|
|
92
|
+
required = Array.new(min) { atom }
|
|
93
|
+
optional = Array.new(max - min) do
|
|
94
|
+
AST::Alt.new(children: [atom, AST::Empty.new(loc: nil)], loc: nil)
|
|
95
|
+
end
|
|
96
|
+
children = required + optional
|
|
97
|
+
return AST::Empty.new(loc: nil) if children.empty?
|
|
98
|
+
return children.first if children.length == 1
|
|
99
|
+
|
|
100
|
+
AST::Seq.new(children: children, loc: nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_atom
|
|
104
|
+
@escaped_value = false
|
|
105
|
+
@last_ranges = nil
|
|
106
|
+
return parse_group if consume?("(")
|
|
107
|
+
return parse_class if consume?("[")
|
|
108
|
+
return parse_anchor if ["^", "$"].include?(current)
|
|
109
|
+
|
|
110
|
+
if consume?(".")
|
|
111
|
+
upper = @options.nobits?(::Regexp::MULTILINE) ? 0x0a - 1 : 0x10ffff
|
|
112
|
+
return AST::CharClass.new(ranges: [[0, upper], [0x0b, 0x10ffff]], negated: false, loc: nil)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if consume?("\\")
|
|
116
|
+
parse_escape
|
|
117
|
+
return AST::CharClass.new(ranges: @last_ranges, negated: false, loc: nil) if @last_ranges
|
|
118
|
+
return codepoint_node(read_codepoint) if @escaped_value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
char = advance
|
|
122
|
+
raise_syntax("unexpected end of expression") unless char
|
|
123
|
+
codepoint_node(char.ord)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_group
|
|
127
|
+
saved_options = @options
|
|
128
|
+
if consume?("?")
|
|
129
|
+
prefix = parse_group_prefix
|
|
130
|
+
return AST::Empty.new(loc: nil) if prefix == :global
|
|
131
|
+
else
|
|
132
|
+
warn_capture
|
|
133
|
+
end
|
|
134
|
+
node = parse_expression
|
|
135
|
+
expect(")")
|
|
136
|
+
@options = saved_options if prefix
|
|
137
|
+
node
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_group_prefix
|
|
141
|
+
return true if consume?(":")
|
|
142
|
+
if peek_prefix?("-mix:")
|
|
143
|
+
@index += 5
|
|
144
|
+
@options &= ~(::Regexp::IGNORECASE | ::Regexp::MULTILINE | ::Regexp::EXTENDED)
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
if ["i", "m", "x", "-"].include?(current)
|
|
148
|
+
add = true
|
|
149
|
+
flags = []
|
|
150
|
+
while ["i", "m", "x", "-"].include?(current)
|
|
151
|
+
if consume?("-")
|
|
152
|
+
add = false
|
|
153
|
+
else
|
|
154
|
+
flags << [advance, add]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
if consume?(":")
|
|
158
|
+
flags.each { |flag, enabled| update_option(flag, enabled) }
|
|
159
|
+
return true
|
|
160
|
+
end
|
|
161
|
+
if consume?(")")
|
|
162
|
+
flags.each { |flag, enabled| update_option(flag, enabled) }
|
|
163
|
+
return :global
|
|
164
|
+
end
|
|
165
|
+
raise_syntax("invalid inline option group")
|
|
166
|
+
end
|
|
167
|
+
raise unsupported("look-around", "use followed_by: or a state instead") if peek_prefix?("=") || peek_prefix?("!") || peek_prefix?("<=") || peek_prefix?("<!")
|
|
168
|
+
raise unsupported("atomic groups", "rewrite the expression as a DFA-compatible expression") if consume?(">")
|
|
169
|
+
raise unsupported("unsupported group syntax", "use a non-capturing group (?:...)")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def update_option(flag, enabled)
|
|
173
|
+
bit = { "i" => ::Regexp::IGNORECASE, "m" => ::Regexp::MULTILINE, "x" => ::Regexp::EXTENDED }.fetch(flag)
|
|
174
|
+
@options = enabled ? (@options | bit) : (@options & ~bit)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def codepoint_node(codepoint)
|
|
178
|
+
return AST::CodepointRange.new(lo: codepoint, hi: codepoint, loc: nil) if @options.nobits?(::Regexp::IGNORECASE)
|
|
179
|
+
|
|
180
|
+
AST::CharClass.new(ranges: fold_ranges([[codepoint, codepoint]]), negated: false, loc: nil)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def fold_ranges(ranges)
|
|
184
|
+
folded = ranges.flat_map do |range|
|
|
185
|
+
next [range] if range.first.is_a?(Module)
|
|
186
|
+
|
|
187
|
+
Unicode::CaseFold.ranges(range.first, range.last)
|
|
188
|
+
end
|
|
189
|
+
merge_ranges(folded)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_class
|
|
193
|
+
negated = consume?("^")
|
|
194
|
+
ranges = []
|
|
195
|
+
@class_depth += 1
|
|
196
|
+
until eof? || current == "]"
|
|
197
|
+
if current == "[" && @source[@index, 2] == "[:"
|
|
198
|
+
ranges.concat(parse_posix_class)
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
first = parse_class_atom
|
|
202
|
+
if consume?("-") && current != "]"
|
|
203
|
+
last = parse_class_atom
|
|
204
|
+
ranges.concat(expand_class_range(first, last))
|
|
205
|
+
else
|
|
206
|
+
ranges.concat(first)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
expect("]")
|
|
210
|
+
@class_depth -= 1
|
|
211
|
+
ranges = merge_ranges(ranges)
|
|
212
|
+
ranges = fold_ranges(ranges) if @options.anybits?(::Regexp::IGNORECASE)
|
|
213
|
+
AST::CharClass.new(ranges: ranges, negated: negated, loc: nil)
|
|
214
|
+
ensure
|
|
215
|
+
@class_depth -= 1 if @class_depth.positive? && current != "]"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parse_posix_class
|
|
219
|
+
expect("[:")
|
|
220
|
+
raw_name = read_until(":]")
|
|
221
|
+
inner_negated = raw_name.start_with?("^")
|
|
222
|
+
name = raw_name.delete_prefix("^")
|
|
223
|
+
return [[AST::Property, inner_negated, "POSIX_#{name}"]] if
|
|
224
|
+
@encoding != Encoding::BINARY && POSIX_CLASSES.include?(name)
|
|
225
|
+
|
|
226
|
+
ranges = case name
|
|
227
|
+
when "alnum" then [[48, 57], [65, 90], [97, 122]]
|
|
228
|
+
when "alpha" then [[65, 90], [97, 122]]
|
|
229
|
+
when "blank" then [[9, 9], [32, 32]]
|
|
230
|
+
when "cntrl" then [[0, 31], [127, 127]]
|
|
231
|
+
when "digit" then [[48, 57]]
|
|
232
|
+
when "graph" then [[33, 126]]
|
|
233
|
+
when "lower" then [[97, 122]]
|
|
234
|
+
when "print" then [[32, 126]]
|
|
235
|
+
when "punct" then [[33, 47], [58, 64], [91, 96], [123, 126]]
|
|
236
|
+
when "space" then [[9, 13], [32, 32]]
|
|
237
|
+
when "upper" then [[65, 90]]
|
|
238
|
+
when "xdigit" then [[48, 57], [65, 70], [97, 102]]
|
|
239
|
+
else
|
|
240
|
+
raise_syntax("unknown POSIX character class: #{name}")
|
|
241
|
+
end
|
|
242
|
+
inner_negated ? complement_ranges(ranges) : ranges
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def parse_class_atom
|
|
246
|
+
if consume?("\\")
|
|
247
|
+
parse_escape
|
|
248
|
+
return [[@last_codepoint, @last_codepoint]] if @escaped_value
|
|
249
|
+
return @last_ranges
|
|
250
|
+
end
|
|
251
|
+
char = advance
|
|
252
|
+
raise_syntax("unterminated character class") unless char
|
|
253
|
+
[[char.ord, char.ord]]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def expand_class_range(first, last)
|
|
257
|
+
raise_syntax("character class range endpoints must be single characters") if first.length != 1 || last.length != 1
|
|
258
|
+
lo = first.first.first
|
|
259
|
+
hi = last.first.first
|
|
260
|
+
raise_syntax("invalid character class range") if lo > hi
|
|
261
|
+
[[lo, hi]]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def parse_escape
|
|
265
|
+
@escaped_value = false
|
|
266
|
+
@last_ranges = nil
|
|
267
|
+
char = advance_raw
|
|
268
|
+
raise_syntax("trailing backslash") unless char
|
|
269
|
+
if ESCAPES.key?(char)
|
|
270
|
+
@escaped_value = true
|
|
271
|
+
@last_codepoint = ESCAPES.fetch(char)
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
case char
|
|
275
|
+
when "d", "D", "w", "W", "s", "S", "h", "H"
|
|
276
|
+
ranges = shorthand_ranges(char)
|
|
277
|
+
@last_ranges = ranges
|
|
278
|
+
nil
|
|
279
|
+
when "p", "P"
|
|
280
|
+
expect("{")
|
|
281
|
+
name = read_until("}")
|
|
282
|
+
ranges = [[AST::Property, char == "P", name]]
|
|
283
|
+
@last_ranges = ranges
|
|
284
|
+
nil
|
|
285
|
+
when "x"
|
|
286
|
+
digits = if consume?("{")
|
|
287
|
+
read_until("}")
|
|
288
|
+
else
|
|
289
|
+
read_exact(2)
|
|
290
|
+
end
|
|
291
|
+
assign_codepoint(digits)
|
|
292
|
+
nil
|
|
293
|
+
when "u"
|
|
294
|
+
digits = if consume?("{")
|
|
295
|
+
value = read_until("}")
|
|
296
|
+
value
|
|
297
|
+
else
|
|
298
|
+
read_exact(4)
|
|
299
|
+
end
|
|
300
|
+
assign_codepoint(digits)
|
|
301
|
+
nil
|
|
302
|
+
when "G", "K", "b", "B", "A", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
|
303
|
+
raise unsupported("\\#{char}", "use a state or followed_by: instead")
|
|
304
|
+
when "k"
|
|
305
|
+
read_until(">") if consume?("<")
|
|
306
|
+
raise unsupported("backreferences", "split the rule into DFA-compatible states")
|
|
307
|
+
else
|
|
308
|
+
@escaped_value = true
|
|
309
|
+
@last_codepoint = char.ord
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def shorthand_ranges(char)
|
|
314
|
+
if @unicode && @encoding != Encoding::BINARY
|
|
315
|
+
property = { "d" => "Nd", "w" => "Word", "s" => "Space" }.fetch(char.downcase, nil)
|
|
316
|
+
return [[AST::Property, char == char.upcase, property]] if property
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
base = {
|
|
320
|
+
"d" => [[48, 57]],
|
|
321
|
+
"w" => [[48, 57], [65, 90], [95, 95], [97, 122]],
|
|
322
|
+
"s" => [[9, 13], [32, 32]],
|
|
323
|
+
"h" => [[9, 9], [32, 32]]
|
|
324
|
+
}.fetch(char.downcase)
|
|
325
|
+
return base unless char == char.upcase
|
|
326
|
+
|
|
327
|
+
complement_ranges(base)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def complement_ranges(ranges)
|
|
331
|
+
out = []
|
|
332
|
+
cursor = 0
|
|
333
|
+
ranges.sort.each do |lo, hi|
|
|
334
|
+
out << [cursor, lo - 1] if cursor < lo
|
|
335
|
+
cursor = hi + 1
|
|
336
|
+
end
|
|
337
|
+
out << [cursor, 0x10ffff] if cursor <= 0x10ffff
|
|
338
|
+
out
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parse_anchor
|
|
342
|
+
char = advance
|
|
343
|
+
return AST::Anchor.new(kind: :bol, loc: nil) if char == "^"
|
|
344
|
+
|
|
345
|
+
AST::Anchor.new(kind: :eol, loc: nil)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def validate_anchor_positions(node)
|
|
349
|
+
anchors = anchor_nodes(node)
|
|
350
|
+
return if anchors.empty?
|
|
351
|
+
|
|
352
|
+
sequence = flatten_sequence(node)
|
|
353
|
+
allowed = [sequence.first, sequence.last].compact
|
|
354
|
+
boundaries_valid = if sequence.length == 1 && sequence.first.is_a?(AST::Anchor)
|
|
355
|
+
%i[bol eol].include?(sequence.first.kind)
|
|
356
|
+
else
|
|
357
|
+
(!sequence.first.is_a?(AST::Anchor) || sequence.first.kind == :bol) &&
|
|
358
|
+
(!sequence.last.is_a?(AST::Anchor) || sequence.last.kind == :eol)
|
|
359
|
+
end
|
|
360
|
+
valid = !anchor_nested_in_alternative?(node) && anchors.all? { |anchor| allowed.include?(anchor) } && boundaries_valid
|
|
361
|
+
return if valid
|
|
362
|
+
|
|
363
|
+
raise_diagnostic(
|
|
364
|
+
diagnostic("FLEXR-E009", "anchors are only valid at the outermost pattern boundaries",
|
|
365
|
+
help: "split alternatives into separate rules or move ^/$ outside the alternation")
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def flatten_sequence(node)
|
|
370
|
+
return [] if node.is_a?(AST::Empty)
|
|
371
|
+
return node.children.flat_map { |child| flatten_sequence(child) } if node.is_a?(AST::Seq)
|
|
372
|
+
|
|
373
|
+
[node]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def anchor_nodes(node)
|
|
377
|
+
return [node] if node.is_a?(AST::Anchor)
|
|
378
|
+
return anchor_nodes(node.child) if node.is_a?(AST::Star)
|
|
379
|
+
return [] unless node.respond_to?(:children)
|
|
380
|
+
|
|
381
|
+
node.children.flat_map { |child| anchor_nodes(child) }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def anchor_nested_in_alternative?(node)
|
|
385
|
+
return node.children.any? { |child| anchor_nodes(child).any? } if node.is_a?(AST::Alt)
|
|
386
|
+
return anchor_nested_in_alternative?(node.child) if node.is_a?(AST::Star)
|
|
387
|
+
return false unless node.respond_to?(:children)
|
|
388
|
+
|
|
389
|
+
node.children.any? { |child| anchor_nested_in_alternative?(child) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def warn_capture
|
|
393
|
+
# The parser intentionally treats captures as non-capturing. Diagnostics
|
|
394
|
+
# are exposed by the source compiler where a source location is known.
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def reject_postfix_quantifier
|
|
398
|
+
return unless ["?", "+"].include?(current)
|
|
399
|
+
|
|
400
|
+
raise unsupported("lazy or possessive quantifier", "use a negated character class")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def merge_ranges(ranges)
|
|
404
|
+
properties, concrete = ranges.partition { |range| range.first.is_a?(Module) }
|
|
405
|
+
merged = concrete.sort_by(&:first).each_with_object([]) do |range, result|
|
|
406
|
+
if result.empty? || range.first > result.last.last + 1
|
|
407
|
+
result << range.dup
|
|
408
|
+
else
|
|
409
|
+
result.last[1] = [result.last.last, range.last].max
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
merged + properties
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def current
|
|
416
|
+
skip_extended_space if @options.anybits?(::Regexp::EXTENDED) && @class_depth.zero?
|
|
417
|
+
@source[@index]
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def advance
|
|
421
|
+
skip_extended_space if @options.anybits?(::Regexp::EXTENDED) && @class_depth.zero?
|
|
422
|
+
advance_raw
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def advance_raw
|
|
426
|
+
char = @source[@index]
|
|
427
|
+
@index += 1 if char
|
|
428
|
+
char
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def consume?(value)
|
|
432
|
+
return false unless @source[@index, value.length] == value
|
|
433
|
+
|
|
434
|
+
@index += value.length
|
|
435
|
+
true
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def expect(value)
|
|
439
|
+
return if consume?(value)
|
|
440
|
+
|
|
441
|
+
raise_syntax("expected `#{value}`")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def peek_prefix?(value)
|
|
445
|
+
@source[@index, value.length] == value
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def read_number
|
|
449
|
+
start = @index
|
|
450
|
+
advance while current&.match?(/[0-9]/)
|
|
451
|
+
return nil if start == @index
|
|
452
|
+
|
|
453
|
+
@source[start...@index].to_i
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def read_exact(count)
|
|
457
|
+
value = @source[@index, count]
|
|
458
|
+
raise_syntax("invalid escape") unless value&.length == count && value.match?(/\A[0-9a-fA-F]+\z/)
|
|
459
|
+
@index += count
|
|
460
|
+
value
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def read_until(terminator)
|
|
464
|
+
start = @index
|
|
465
|
+
finish = @source.index(terminator, @index)
|
|
466
|
+
raise_syntax("unterminated escape") unless finish
|
|
467
|
+
@index = finish + terminator.length
|
|
468
|
+
@source[start...finish]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def eof?
|
|
472
|
+
@index >= @source.length
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def diagnostic(code, message, help: nil)
|
|
476
|
+
Diagnostics.error(code, message, help: help)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def raise_diagnostic(diagnostic)
|
|
480
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def raise_syntax(message)
|
|
484
|
+
raise CompileError.new(message, diagnostic: diagnostic("FLEXR-E001", message))
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def unsupported(feature, help)
|
|
488
|
+
diagnostic = Diagnostics.error("FLEXR-E014", "#{feature} is not supported by flexr", help: help)
|
|
489
|
+
raise UnsupportedRegexpError.new(diagnostic.message, diagnostic: diagnostic)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def read_codepoint
|
|
493
|
+
@last_codepoint
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def assign_codepoint(digits)
|
|
497
|
+
raise_syntax("invalid escape") unless digits.match?(/\A[0-9a-fA-F]+\z/)
|
|
498
|
+
|
|
499
|
+
codepoint = digits.to_i(16)
|
|
500
|
+
raise_syntax("invalid Unicode codepoint") if codepoint > 0x10ffff || codepoint.between?(0xd800, 0xdfff)
|
|
501
|
+
|
|
502
|
+
@escaped_value = true
|
|
503
|
+
@last_codepoint = codepoint
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def skip_extended_space
|
|
507
|
+
loop do
|
|
508
|
+
@index += 1 while @source[@index]&.match?(/\s/)
|
|
509
|
+
break unless @source[@index] == "#"
|
|
510
|
+
|
|
511
|
+
@index += 1
|
|
512
|
+
@index += 1 while @source[@index] && @source[@index] != "\n"
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|