birman 0.2.1 → 0.3.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 +4 -4
- data/CHANGELOG.md +214 -0
- data/README.md +405 -15
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +105 -13
- data/lib/birman/compiler/actions.rb +309 -0
- data/lib/birman/compiler/emitter.rb +940 -0
- data/lib/birman/compiler/literals.rb +29 -0
- data/lib/birman/compiler/nodes.rb +158 -0
- data/lib/birman/compiler/regexps.rb +182 -0
- data/lib/birman/compiler/runtime.rb +758 -0
- data/lib/birman/compiler/types.rb +128 -0
- data/lib/birman/compiler.rb +155 -0
- data/lib/birman/context.rb +151 -29
- data/lib/birman/error_formatter.rb +6 -0
- data/lib/birman/grammar.rb +139 -17
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +310 -53
- data/lib/birman/nodes.rb +17 -0
- data/lib/birman/result.rb +41 -2
- data/lib/birman/rule_set.rb +38 -6
- data/lib/birman/rules.rb +27 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +30 -0
- metadata +11 -1
data/lib/birman/rule_set.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "The rule and lexeme macros"
|
|
5
|
+
# responsibility = "Turning rule blocks into methods, and guarding rule names"
|
|
6
|
+
# pattern = "Class-level DSL (extend)"
|
|
3
7
|
module Birman
|
|
4
8
|
# The `rule` and `lexeme` macros. Extend any module or class with it.
|
|
5
9
|
#
|
|
@@ -7,7 +11,18 @@ module Birman
|
|
|
7
11
|
# private `__rule_name` builds the matcher. Because rules are just
|
|
8
12
|
# methods, they travel through `include` and can be overridden like
|
|
9
13
|
# any other Ruby method.
|
|
14
|
+
# [module.RuleSet]
|
|
15
|
+
# purpose = "Defines rules; extend a module with it to write a reusable rule module"
|
|
16
|
+
# collaborators = ["Grammar (extends its classes with this)", "Ref"]
|
|
10
17
|
module RuleSet
|
|
18
|
+
# [method.rule]
|
|
19
|
+
# params = ["name: Symbol", "block: returns the rule's matcher (String, Regexp or Matcher)"]
|
|
20
|
+
# returns = "nil"
|
|
21
|
+
# side_effects = [
|
|
22
|
+
# "Defines name (returns a Ref) and a private __rule_name",
|
|
23
|
+
# "Resets a grammar class's cached instance"
|
|
24
|
+
# ]
|
|
25
|
+
# raises = ["ArgumentError if name would replace a method the grammar relies on (send, match, token...)"]
|
|
11
26
|
def rule(name, &)
|
|
12
27
|
check_rule_name!(name)
|
|
13
28
|
define_method(:"__rule_#{name}", &)
|
|
@@ -18,22 +33,39 @@ module Birman
|
|
|
18
33
|
|
|
19
34
|
# A rule for a token (a number, a string, an identifier): it skips
|
|
20
35
|
# trailing spacing when the grammar declares `ignore`.
|
|
36
|
+
# [method.lexeme]
|
|
37
|
+
# params = ["name: Symbol", "body: block returning the token's matcher"]
|
|
38
|
+
# returns = "nil"
|
|
39
|
+
# side_effects = ["Defines a rule that skips trailing spacing via the grammar's ignore rule"]
|
|
21
40
|
def lexeme(name, &body)
|
|
22
|
-
rule(name) { Lexeme.new(Birman.coerce(instance_exec(&body))
|
|
41
|
+
rule(name) { Lexeme.new(Birman.coerce(instance_exec(&body))) }
|
|
23
42
|
end
|
|
24
43
|
|
|
25
44
|
private
|
|
26
45
|
|
|
27
46
|
# A rule named after a method the grammar relies on (send, class,
|
|
28
|
-
# match, token
|
|
29
|
-
# fine, and so are private
|
|
47
|
+
# match, token, or a private helper in any of Birman's modules) would
|
|
48
|
+
# silently replace it. Redefining a rule is fine, and so are private
|
|
49
|
+
# Kernel names like print or select, and the grammar's own private
|
|
50
|
+
# helpers: those are the grammar's to replace.
|
|
30
51
|
def check_rule_name!(name)
|
|
31
52
|
return if private_method_defined?(:"__rule_#{name}")
|
|
32
53
|
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
birman = [Object, Combinators, Grammar] | ancestors.select { _1.name&.start_with?("Birman::") }
|
|
55
|
+
owner = birman.find { _1.public_method_defined?(name) } ||
|
|
56
|
+
(birman - [Object]).find { _1.private_method_defined?(name, false) } ||
|
|
35
57
|
(instance_method(name).owner if method_defined?(name))
|
|
36
|
-
|
|
58
|
+
return unless owner
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "rule :#{name} would replace #{owner}##{name}, #{why_taken(owner, birman)}, " \
|
|
61
|
+
"since rules are methods too; give the rule another name"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def why_taken(owner, birman)
|
|
65
|
+
if owner == Object then "a method every Ruby object has"
|
|
66
|
+
elsif birman.include?(owner) then "which Birman relies on"
|
|
67
|
+
else "a method of this grammar"
|
|
68
|
+
end
|
|
37
69
|
end
|
|
38
70
|
end
|
|
39
71
|
end
|
data/lib/birman/rules.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Ready-made rule modules"
|
|
5
|
+
# responsibility = "Whitespace, comments, numbers, strings and identifiers"
|
|
6
|
+
# usage = "include the modules a grammar needs; later includes override earlier ones"
|
|
3
7
|
module Birman
|
|
4
8
|
# A standard library of rule modules. Include the ones you need;
|
|
5
9
|
# later includes override earlier ones (Comments replaces
|
|
@@ -10,6 +14,9 @@ module Birman
|
|
|
10
14
|
module Rules
|
|
11
15
|
# Whitespace as the `spacing` rule, plus a `token` helper for grammars
|
|
12
16
|
# that don't declare `ignore`.
|
|
17
|
+
# [module.Rules.Whitespace]
|
|
18
|
+
# rules = ["spacing: zero or more whitespace characters (quiet)"]
|
|
19
|
+
# helpers = ["token(m): m then spacing, for grammars without ignore"]
|
|
13
20
|
module Whitespace
|
|
14
21
|
extend RuleSet
|
|
15
22
|
|
|
@@ -22,6 +29,9 @@ module Birman
|
|
|
22
29
|
|
|
23
30
|
# Whitespace including comments. Works on its own or after
|
|
24
31
|
# Whitespace. Override comment_pattern for other comment syntaxes.
|
|
32
|
+
# [module.Rules.Comments]
|
|
33
|
+
# rules = ["spacing: whitespace and comments (quiet)", "comment: one comment"]
|
|
34
|
+
# helpers = ["comment_pattern: override for other syntaxes (default /#[^\\n]*/)", "token(m)"]
|
|
25
35
|
module Comments
|
|
26
36
|
extend RuleSet
|
|
27
37
|
|
|
@@ -33,35 +43,47 @@ module Birman
|
|
|
33
43
|
end
|
|
34
44
|
|
|
35
45
|
# Integers and floats, as Ruby numbers.
|
|
46
|
+
# [module.Rules.Numbers]
|
|
47
|
+
# rules = ["integer: Integer", "float: Float", "number: float or integer"]
|
|
36
48
|
module Numbers
|
|
37
49
|
extend RuleSet
|
|
38
50
|
|
|
39
|
-
lexeme(:integer) { match(/\d+/).map(
|
|
40
|
-
lexeme(:float) { match(/\d+\.\d+/).map(
|
|
51
|
+
lexeme(:integer) { match(/\d+/).map(:to_i).label("integer") }
|
|
52
|
+
lexeme(:float) { match(/\d+\.\d+/).map(:to_f).label("float") }
|
|
41
53
|
lexeme(:number) { (float | integer).label("number") }
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
# Double-quoted strings with backslash escapes. The opening quote
|
|
45
57
|
# commits, so an unterminated string is reported where it breaks.
|
|
58
|
+
# [module.Rules.Strings]
|
|
59
|
+
# rules = ["string: double-quoted, escapes \\n \\t \\r \\0, value is the String"]
|
|
60
|
+
# helpers = ["decode_escape(text): the character a backslash escape stands for"]
|
|
61
|
+
# notes = "The opening quote commits, so an unterminated string is reported where it breaks"
|
|
46
62
|
module Strings
|
|
47
63
|
extend RuleSet
|
|
48
64
|
|
|
49
65
|
ESCAPES = { "n" => "\n", "t" => "\t", "r" => "\r", "0" => "\0" }.freeze
|
|
50
66
|
|
|
51
67
|
lexeme(:string) do
|
|
52
|
-
(str('"')
|
|
53
|
-
.map
|
|
68
|
+
seq(str('"'), commit, only(string_part.rep), str('"').label("closing quote"))
|
|
69
|
+
.map(:join)
|
|
54
70
|
.label("string")
|
|
55
71
|
end
|
|
56
72
|
|
|
57
73
|
rule(:string_part) do
|
|
58
|
-
(match(/\\./).
|
|
74
|
+
(match(/\\./).action(:decode_escape) |
|
|
59
75
|
match(/[^"\\\n]+/)).label("string character")
|
|
60
76
|
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def decode_escape(text) = ESCAPES.fetch(text[1], text[1])
|
|
61
81
|
end
|
|
62
82
|
|
|
63
83
|
# Identifiers that can't be reserved words. Declare those in the
|
|
64
84
|
# grammar with `keywords "if", "then", ...`.
|
|
85
|
+
# [module.Rules.Identifiers]
|
|
86
|
+
# rules = ["identifier: [A-Za-z_]\\w*, not one of the grammar's keywords"]
|
|
65
87
|
module Identifiers
|
|
66
88
|
extend RuleSet
|
|
67
89
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Running a matcher without writing a grammar class"
|
|
5
|
+
# responsibility = "Birman.parse and Birman.parse!, via a cached anonymous grammar per matcher"
|
|
6
|
+
# dependencies = ["Birman::Grammar", "Birman::AST"]
|
|
7
|
+
module Birman
|
|
8
|
+
# Parses input with matcher as the whole grammar: Success or Failure,
|
|
9
|
+
# exactly as Grammar.parse, and bad input never raises.
|
|
10
|
+
#
|
|
11
|
+
# Birman.parse(Birman.seq("let", Birman.match(/\w+/)), "let x", ignore: /\s*/)
|
|
12
|
+
# # => #<data Birman::Success value=["let", "x"]>
|
|
13
|
+
#
|
|
14
|
+
# ignore is a matcher (a String, Regexp or Matcher), not a rule name:
|
|
15
|
+
# there is no grammar here for a name to refer to. Literals skip it
|
|
16
|
+
# after themselves, and it's skipped at the start of input.
|
|
17
|
+
# [method."Birman.parse"]
|
|
18
|
+
# params = ["matcher: String | Regexp | Matcher", "input: String", "ignore: String | Regexp | Matcher | nil"]
|
|
19
|
+
# returns = "Birman::Success(value) when all of input parses, else Birman::Failure"
|
|
20
|
+
# raises = ["ArgumentError if matcher or ignore refers to a rule, or an .action/.check method Birman lacks"]
|
|
21
|
+
def self.parse(matcher, input, ignore: nil) = Standalone.grammar(matcher, ignore).parse(input)
|
|
22
|
+
|
|
23
|
+
# [method."Birman.parse!"]
|
|
24
|
+
# params = ["matcher, input, ignore: as Birman.parse", "formatter: #call(failure) = ErrorFormatter.new"]
|
|
25
|
+
# returns = "The parsed value"
|
|
26
|
+
# raises = ["Birman::ParseError", "ArgumentError, as Birman.parse"]
|
|
27
|
+
def self.parse!(matcher, input, ignore: nil, **) = Standalone.grammar(matcher, ignore).parse!(input, **)
|
|
28
|
+
|
|
29
|
+
# The anonymous grammar classes behind Birman.parse: the matcher is
|
|
30
|
+
# the root rule and ignore the ignore rule. Matchers are immutable
|
|
31
|
+
# values, so equal ones share a class, and with it the per-grammar
|
|
32
|
+
# caches (literal matchers, rule matchers). The cache is bounded, so
|
|
33
|
+
# matchers built afresh for each parse (with a new .map block, say)
|
|
34
|
+
# don't pile up.
|
|
35
|
+
# [module.Standalone]
|
|
36
|
+
# purpose = "Builds and caches the anonymous grammar Birman.parse runs"
|
|
37
|
+
module Standalone
|
|
38
|
+
ROOT = :__standalone_root
|
|
39
|
+
IGNORE = :__standalone_ignore
|
|
40
|
+
LIMIT = 256
|
|
41
|
+
|
|
42
|
+
@grammars = {}
|
|
43
|
+
@lock = Mutex.new
|
|
44
|
+
|
|
45
|
+
# [method."Standalone.grammar"]
|
|
46
|
+
# params = ["matcher: String | Regexp | Matcher", "ignore: String | Regexp | Matcher | nil"]
|
|
47
|
+
# returns = "A grammar class whose root rule is matcher (to_ebnf, to_ruby and parse work on it)"
|
|
48
|
+
# raises = ["ArgumentError if matcher or ignore refers to a rule, or an .action/.check method Birman lacks"]
|
|
49
|
+
def self.grammar(matcher, ignore)
|
|
50
|
+
key = [matcher, ignore].freeze
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
@grammars[key] ||= begin
|
|
53
|
+
@grammars.shift if @grammars.size >= LIMIT
|
|
54
|
+
build(matcher, ignore)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.build(matcher, ignore)
|
|
60
|
+
grammar = Class.new do
|
|
61
|
+
include Grammar
|
|
62
|
+
|
|
63
|
+
root ROOT
|
|
64
|
+
rule(ROOT) { matcher }
|
|
65
|
+
next unless ignore
|
|
66
|
+
|
|
67
|
+
rule(IGNORE) { ignore }
|
|
68
|
+
self.ignore IGNORE
|
|
69
|
+
end
|
|
70
|
+
check!(grammar, [matcher, ignore].compact.map { Birman.coerce(_1) })
|
|
71
|
+
grammar
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A rule reference, or an action or check calling a method only a
|
|
75
|
+
# grammar of your own would have, raises only when the parse reaches
|
|
76
|
+
# it (and the method as a NoMethodError on an anonymous class), so a
|
|
77
|
+
# mistake could hide in an untried alternative. Checked up front.
|
|
78
|
+
def self.check!(grammar, matchers)
|
|
79
|
+
matchers.each { each_matcher(_1) { |m| check_matcher!(grammar, m) } }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.check_matcher!(grammar, matcher)
|
|
83
|
+
case matcher
|
|
84
|
+
when Ref
|
|
85
|
+
raise ArgumentError, "undefined rule #{matcher.name.inspect}: " \
|
|
86
|
+
"a matcher run by Birman.parse can't refer to rules; define them in a grammar class"
|
|
87
|
+
when Action, Check
|
|
88
|
+
name = matcher.method_name
|
|
89
|
+
return if grammar.method_defined?(name) || grammar.private_method_defined?(name)
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, "undefined method #{name.inspect} for .#{matcher.is_a?(Action) ? "action" : "check"}: " \
|
|
92
|
+
"a matcher run by Birman.parse can only use Birman's own methods there; " \
|
|
93
|
+
"define yours in a grammar class"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Every matcher inside root, root included, once each.
|
|
98
|
+
def self.each_matcher(root)
|
|
99
|
+
queue = [root]
|
|
100
|
+
seen = {}.compare_by_identity
|
|
101
|
+
while (m = queue.shift)
|
|
102
|
+
next if seen[m]
|
|
103
|
+
|
|
104
|
+
seen[m] = true
|
|
105
|
+
yield m
|
|
106
|
+
queue.concat(m.to_h.values.flat_map { Array(_1) }.grep(Matcher)) if m.is_a?(Data)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
private_class_method :build, :check!, :check_matcher!, :each_matcher
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/birman/version.rb
CHANGED
data/lib/birman.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# [file]
|
|
4
|
+
# purpose = "Entry point: require \"birman\" loads the whole library"
|
|
5
|
+
# dependencies = ["strscan (standard library)"]
|
|
6
|
+
# requires_ruby = ">= 3.2"
|
|
7
|
+
|
|
3
8
|
# A small PEG parser library for Ruby 3.2+.
|
|
4
9
|
#
|
|
5
10
|
# Greeting = Birman.node(:name)
|
|
@@ -22,9 +27,31 @@ module Birman
|
|
|
22
27
|
# The character offset of a byte offset into input. Free for ASCII
|
|
23
28
|
# input (Ruby caches ascii_only?), linear otherwise; used once per
|
|
24
29
|
# parse for a Failure, and only on demand for a Span.
|
|
30
|
+
# [method."Birman.char_offset"]
|
|
31
|
+
# params = ["input: String", "byte_pos: Integer"]
|
|
32
|
+
# returns = "Integer: the character offset of byte_pos"
|
|
25
33
|
def self.char_offset(input, byte_pos)
|
|
26
34
|
input.ascii_only? ? byte_pos : input.byteslice(0, byte_pos).length
|
|
27
35
|
end
|
|
36
|
+
|
|
37
|
+
# The byte offset of the first byte in input that doesn't start a
|
|
38
|
+
# valid character, or nil if input is valid. Grammar#parse fails
|
|
39
|
+
# there before a regexp can raise on it. Ruby splits invalid bytes
|
|
40
|
+
# into characters of their own, so everything before it is valid,
|
|
41
|
+
# and a Failure's line and column count as usual.
|
|
42
|
+
# [method."Birman.invalid_byte"]
|
|
43
|
+
# params = ["input: String"]
|
|
44
|
+
# returns = "Integer | nil: the byte offset of the first invalid byte"
|
|
45
|
+
def self.invalid_byte(input)
|
|
46
|
+
return if input.valid_encoding?
|
|
47
|
+
|
|
48
|
+
pos = 0
|
|
49
|
+
input.each_char do |char|
|
|
50
|
+
return pos unless char.valid_encoding?
|
|
51
|
+
|
|
52
|
+
pos += char.bytesize
|
|
53
|
+
end
|
|
54
|
+
end
|
|
28
55
|
end
|
|
29
56
|
|
|
30
57
|
require "strscan"
|
|
@@ -37,6 +64,9 @@ require_relative "birman/matchers"
|
|
|
37
64
|
require_relative "birman/rule_set"
|
|
38
65
|
require_relative "birman/combinators"
|
|
39
66
|
require_relative "birman/grammar"
|
|
67
|
+
require_relative "birman/standalone"
|
|
40
68
|
require_relative "birman/error_formatter"
|
|
41
69
|
require_relative "birman/ast"
|
|
70
|
+
require_relative "birman/left_recursion"
|
|
42
71
|
require_relative "birman/rules"
|
|
72
|
+
require_relative "birman/compiler"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: birman
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tad Thorley
|
|
@@ -23,14 +23,24 @@ files:
|
|
|
23
23
|
- lib/birman.rb
|
|
24
24
|
- lib/birman/ast.rb
|
|
25
25
|
- lib/birman/combinators.rb
|
|
26
|
+
- lib/birman/compiler.rb
|
|
27
|
+
- lib/birman/compiler/actions.rb
|
|
28
|
+
- lib/birman/compiler/emitter.rb
|
|
29
|
+
- lib/birman/compiler/literals.rb
|
|
30
|
+
- lib/birman/compiler/nodes.rb
|
|
31
|
+
- lib/birman/compiler/regexps.rb
|
|
32
|
+
- lib/birman/compiler/runtime.rb
|
|
33
|
+
- lib/birman/compiler/types.rb
|
|
26
34
|
- lib/birman/context.rb
|
|
27
35
|
- lib/birman/error_formatter.rb
|
|
28
36
|
- lib/birman/grammar.rb
|
|
37
|
+
- lib/birman/left_recursion.rb
|
|
29
38
|
- lib/birman/matchers.rb
|
|
30
39
|
- lib/birman/nodes.rb
|
|
31
40
|
- lib/birman/result.rb
|
|
32
41
|
- lib/birman/rule_set.rb
|
|
33
42
|
- lib/birman/rules.rb
|
|
43
|
+
- lib/birman/standalone.rb
|
|
34
44
|
- lib/birman/version.rb
|
|
35
45
|
homepage: https://github.com/phaedryx/birman
|
|
36
46
|
licenses:
|