sexpr 0.2.0 → 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.
- data/CHANGELOG.md +24 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/LICENCE.md +1 -1
- data/Manifest.txt +1 -0
- data/README.md +46 -31
- data/examples/bool_expr/bool_expr.citrus +72 -0
- data/examples/bool_expr/bool_expr.rb +86 -0
- data/examples/bool_expr/bool_expr.sexp.yml +23 -0
- data/lib/sexpr.rb +27 -20
- data/lib/sexpr/errors.rb +15 -0
- data/lib/sexpr/grammar.rb +21 -77
- data/lib/sexpr/grammar/matching.rb +56 -0
- data/lib/sexpr/grammar/options.rb +53 -0
- data/lib/sexpr/grammar/parsing.rb +20 -0
- data/lib/sexpr/grammar/tagging.rb +49 -0
- data/lib/sexpr/loader.rb +0 -1
- data/lib/sexpr/matcher.rb +15 -0
- data/lib/sexpr/matcher/alternative.rb +30 -0
- data/lib/sexpr/matcher/many.rb +54 -0
- data/lib/sexpr/matcher/reference.rb +32 -0
- data/lib/sexpr/matcher/rule.rb +31 -0
- data/lib/sexpr/matcher/sequence.rb +30 -0
- data/lib/sexpr/matcher/terminal.rb +37 -0
- data/lib/sexpr/node.rb +16 -0
- data/lib/sexpr/parser.rb +48 -0
- data/lib/sexpr/parser/citrus.rb +67 -0
- data/lib/sexpr/version.rb +2 -2
- data/sexpr.gemspec +1 -0
- data/sexpr.noespec +2 -1
- data/spec/grammar/matching/test_compile_rule.rb +23 -0
- data/spec/grammar/matching/test_compile_rule_defn.rb +103 -0
- data/spec/grammar/options/test_install_parser.rb +36 -0
- data/spec/grammar/options/test_install_path.rb +19 -0
- data/spec/grammar/options/test_install_root.rb +27 -0
- data/spec/grammar/tagging/test_looks_a_sexpr.rb +20 -0
- data/spec/grammar/tagging/test_mod2rulename.rb +19 -0
- data/spec/grammar/tagging/test_rule2modname.rb +19 -0
- data/spec/grammar/tagging/test_tag_sexpr.rb +28 -0
- data/spec/grammar/test_new.rb +15 -0
- data/spec/grammar/test_parse.rb +27 -18
- data/spec/grammar/test_sexpr.rb +53 -0
- data/spec/{alternative → matcher/alternative}/test_eat.rb +1 -1
- data/spec/{alternative → matcher/alternative}/test_match_q.rb +1 -1
- data/spec/{many → matcher/many}/test_eat.rb +1 -1
- data/spec/{many → matcher/many}/test_initialize.rb +1 -1
- data/spec/{many → matcher/many}/test_match_q.rb +1 -1
- data/spec/{reference → matcher/reference}/test_eat.rb +1 -1
- data/spec/{reference → matcher/reference}/test_match_q.rb +1 -1
- data/spec/{rule → matcher/rule}/test_eat.rb +1 -1
- data/spec/{rule → matcher/rule}/test_match_q.rb +1 -1
- data/spec/{sequence → matcher/sequence}/test_eat.rb +1 -1
- data/spec/{sequence → matcher/sequence}/test_match_q.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_eat.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_match_q.rb +1 -1
- data/spec/{terminal → matcher/terminal}/test_terminal_match.rb +1 -1
- data/spec/node/test_sexpr_body.rb +18 -0
- data/spec/node/test_sexpr_type.rb +14 -0
- data/spec/parser/citrus/test_new.rb +28 -0
- data/spec/parser/citrus/test_parse.rb +40 -0
- data/spec/parser/citrus/test_recognize.rb +18 -0
- data/spec/parser/citrus/test_registration.rb +20 -0
- data/spec/parser/citrus/test_to_sexpr.rb +16 -0
- data/spec/parser/test_factor.rb +17 -0
- data/spec/parser/test_input_text.rb +27 -0
- data/spec/spec_helper.rb +18 -1
- data/spec/test_load.rb +40 -13
- data/spec/test_readme_examples.rb +40 -30
- data/spec/test_sexpr.rb +1 -1
- metadata +118 -68
- data/lib/sexpr/alternative.rb +0 -28
- data/lib/sexpr/element.rb +0 -9
- data/lib/sexpr/many.rb +0 -52
- data/lib/sexpr/reference.rb +0 -30
- data/lib/sexpr/rule.rb +0 -29
- data/lib/sexpr/sequence.rb +0 -28
- data/lib/sexpr/terminal.rb +0 -35
- data/spec/bool_expr.yml +0 -19
- data/spec/grammar/test_compile_rule.rb +0 -25
- data/spec/grammar/test_compile_rule_defn.rb +0 -98
- data/spec/grammar/test_fetch.rb +0 -18
- data/spec/grammar/test_root.rb +0 -20
- data/spec/test_bool_expr.rb +0 -27
@@ -0,0 +1,56 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Grammar
|
3
|
+
module Matching
|
4
|
+
|
5
|
+
def root_rule
|
6
|
+
rules[root]
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](rule_name)
|
10
|
+
rules[rule_name]
|
11
|
+
end
|
12
|
+
|
13
|
+
def match?(sexp)
|
14
|
+
root_rule.match?(sexp)
|
15
|
+
end
|
16
|
+
alias :=== :match?
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def compile_rules(rules)
|
21
|
+
Hash[rules.map{|k,v|
|
22
|
+
[k.to_sym, compile_rule(k.to_sym, v)]
|
23
|
+
}]
|
24
|
+
end
|
25
|
+
|
26
|
+
def compile_rule(name, defn)
|
27
|
+
case rule = compile_rule_defn(defn)
|
28
|
+
when Matcher::Terminal, Matcher::Alternative
|
29
|
+
rule
|
30
|
+
else
|
31
|
+
Matcher::Rule.new(name, rule)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def compile_rule_defn(arg, grammar = self)
|
36
|
+
case arg
|
37
|
+
when Matcher
|
38
|
+
arg
|
39
|
+
when Regexp, TrueClass, FalseClass, NilClass
|
40
|
+
Matcher::Terminal.new arg
|
41
|
+
when lambda{|x| x.is_a?(Array) && x.size == 1 && x.first.is_a?(Array)}
|
42
|
+
Matcher::Sequence.new arg.first.map{|s| compile_rule_defn(s) }
|
43
|
+
when Array
|
44
|
+
Matcher::Alternative.new arg.map{|s| compile_rule_defn(s)}
|
45
|
+
when /([\?\+\*])$/
|
46
|
+
Matcher::Many.new compile_rule_defn($`), $1
|
47
|
+
when /^[a-z][a-z_]+$/
|
48
|
+
Matcher::Reference.new arg.to_sym, grammar
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Invalid rule definition: #{arg.inspect}", caller
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end # module Matching
|
55
|
+
end # module Grammar
|
56
|
+
end # module Sexpr
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Grammar
|
3
|
+
module Options
|
4
|
+
|
5
|
+
attr_reader :options
|
6
|
+
attr_reader :path
|
7
|
+
attr_reader :rules
|
8
|
+
attr_reader :root
|
9
|
+
attr_reader :parser
|
10
|
+
|
11
|
+
def install_options(options)
|
12
|
+
@options = options
|
13
|
+
install_path
|
14
|
+
install_rules
|
15
|
+
install_root
|
16
|
+
install_parser
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def option(key)
|
21
|
+
@options[key.to_sym] || @options[key.to_s]
|
22
|
+
end
|
23
|
+
|
24
|
+
def install_path
|
25
|
+
@path = option(:path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def install_rules
|
29
|
+
@rules = option(:rules) || {}
|
30
|
+
@rules = compile_rules(@rules)
|
31
|
+
end
|
32
|
+
|
33
|
+
def install_root
|
34
|
+
@root = option(:root)
|
35
|
+
@root = rules.keys.first unless @root
|
36
|
+
@root = root.to_sym if @root
|
37
|
+
end
|
38
|
+
|
39
|
+
def install_parser
|
40
|
+
@parser = option(:parser)
|
41
|
+
if @parser.is_a?(String) && !File.exists?(@parser)
|
42
|
+
@parser = File.join(File.dirname(path), @parser)
|
43
|
+
end
|
44
|
+
@parser = Parser.factor(@parser) if @parser
|
45
|
+
end
|
46
|
+
|
47
|
+
def compile_rules(rules)
|
48
|
+
@rules = rules
|
49
|
+
end
|
50
|
+
|
51
|
+
end # module Options
|
52
|
+
end # module Grammar
|
53
|
+
end # module Sexpr
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Grammar
|
3
|
+
module Parsing
|
4
|
+
|
5
|
+
def parse(input, options = {})
|
6
|
+
parser!.parse(input, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def parser!
|
12
|
+
unless p = parser
|
13
|
+
raise NoParserError, "No parser set.", caller
|
14
|
+
end
|
15
|
+
p
|
16
|
+
end
|
17
|
+
|
18
|
+
end # module Parsing
|
19
|
+
end # module Grammar
|
20
|
+
end # module Sexpr
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Grammar
|
3
|
+
module Tagging
|
4
|
+
|
5
|
+
def rule2modname(rule)
|
6
|
+
rule.to_s.gsub(/(^|_)([a-z])/){|x| $2.capitalize}.to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def mod2rulename(mod)
|
10
|
+
mod = mod.name.to_s.split('::').last.to_sym if mod.is_a?(Module)
|
11
|
+
mod.to_s.gsub(/[A-Z]/){|x| "_#{x.downcase}"}[1..-1].to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def sexpr(input, options = {})
|
15
|
+
case input
|
16
|
+
when Array
|
17
|
+
tag_sexpr input
|
18
|
+
else
|
19
|
+
tag_sexpr parser!.sexpr(input, options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def tag_sexpr(sexpr, reference = self)
|
26
|
+
if looks_a_sexpr?(sexpr)
|
27
|
+
sexpr = tag_sexpr_with_user_module(sexpr, reference)
|
28
|
+
sexpr[1..-1].each do |child|
|
29
|
+
tag_sexpr(child, reference)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
sexpr
|
33
|
+
end
|
34
|
+
|
35
|
+
def tag_sexpr_with_user_module(sexpr, reference)
|
36
|
+
sexpr.extend(Sexpr)
|
37
|
+
rulename = sexpr.first
|
38
|
+
modname = rule2modname(rulename)
|
39
|
+
mod = reference.const_get(modname) rescue nil
|
40
|
+
mod ? sexpr.extend(mod) : sexpr
|
41
|
+
end
|
42
|
+
|
43
|
+
def looks_a_sexpr?(arg)
|
44
|
+
arg.is_a?(Array) and arg.first.is_a?(Symbol)
|
45
|
+
end
|
46
|
+
|
47
|
+
end # module Tagging
|
48
|
+
end # module Grammar
|
49
|
+
end # module Sexpr
|
data/lib/sexpr/loader.rb
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
|
4
|
+
def ===(sexp)
|
5
|
+
match?(sexp)
|
6
|
+
end
|
7
|
+
|
8
|
+
end # module Matcher
|
9
|
+
end # module Sexpr
|
10
|
+
require_relative "matcher/alternative"
|
11
|
+
require_relative "matcher/many"
|
12
|
+
require_relative "matcher/reference"
|
13
|
+
require_relative "matcher/rule"
|
14
|
+
require_relative "matcher/sequence"
|
15
|
+
require_relative "matcher/terminal"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Alternative
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :terms
|
7
|
+
|
8
|
+
def initialize(terms)
|
9
|
+
@terms = terms
|
10
|
+
end
|
11
|
+
|
12
|
+
def match?(sexp)
|
13
|
+
terms.any?{|t| t.match?(sexp)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def eat(sexp)
|
17
|
+
@terms.each do |alt|
|
18
|
+
res = alt.eat(sexp)
|
19
|
+
return res if res
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"(alt #{terms.inspect})"
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Alternative
|
29
|
+
end # module Matcher
|
30
|
+
end # module Sexpr
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Many
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :term, :min, :max
|
7
|
+
|
8
|
+
def initialize(term, min, max = nil)
|
9
|
+
@term = term
|
10
|
+
@min, @max = minmax(min, max)
|
11
|
+
end
|
12
|
+
|
13
|
+
def match?(sexp)
|
14
|
+
return nil unless sexp.is_a?(Array)
|
15
|
+
eat = eat(sexp)
|
16
|
+
eat && eat.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def eat(sexp)
|
20
|
+
i, last = 0, sexp
|
21
|
+
while sexp && (@max.nil? || i < @max)
|
22
|
+
if res = @term.eat(sexp)
|
23
|
+
last = res
|
24
|
+
i += 1
|
25
|
+
end
|
26
|
+
sexp = res
|
27
|
+
end
|
28
|
+
i >= @min ? last : nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"(many #{term.inspect}, #{min}, #{max})"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def minmax(min, max)
|
38
|
+
case min
|
39
|
+
when Integer
|
40
|
+
[min, max]
|
41
|
+
when '?'
|
42
|
+
[0, 1]
|
43
|
+
when '+'
|
44
|
+
[1, nil]
|
45
|
+
when '*'
|
46
|
+
[0, nil]
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Invalid multiplicity: #{min}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end # class Sequence
|
53
|
+
end # module Matcher
|
54
|
+
end # module Sexpr
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Reference
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :rule_name
|
7
|
+
attr_reader :grammar
|
8
|
+
|
9
|
+
def initialize(rule_name, grammar)
|
10
|
+
@rule_name = rule_name
|
11
|
+
@grammar = grammar
|
12
|
+
end
|
13
|
+
|
14
|
+
def rule
|
15
|
+
@rule ||= @grammar[@rule_name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def match?(sexp)
|
19
|
+
rule && rule.match?(sexp)
|
20
|
+
end
|
21
|
+
|
22
|
+
def eat(sexp)
|
23
|
+
rule && rule.eat(sexp)
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect
|
27
|
+
"(ref #{rule_name}, #{rule.inspect})"
|
28
|
+
end
|
29
|
+
|
30
|
+
end # class Reference
|
31
|
+
end # module Matcher
|
32
|
+
end # module Sexpr
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Rule
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :defn
|
8
|
+
|
9
|
+
def initialize(name, defn)
|
10
|
+
@name = name
|
11
|
+
@defn = defn
|
12
|
+
end
|
13
|
+
|
14
|
+
def match?(sexp)
|
15
|
+
return nil unless sexp.is_a?(Array)
|
16
|
+
return false unless sexp.first && (sexp.first == name)
|
17
|
+
defn.match?(sexp[1..-1])
|
18
|
+
end
|
19
|
+
|
20
|
+
def eat(sexp)
|
21
|
+
return nil unless match?(sexp.first)
|
22
|
+
sexp[1..-1]
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"(rule #{name}, #{defn.inspect})"
|
27
|
+
end
|
28
|
+
|
29
|
+
end # class Rule
|
30
|
+
end # module Matcher
|
31
|
+
end # module Sexpr
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Sequence
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :terms
|
7
|
+
|
8
|
+
def initialize(terms)
|
9
|
+
@terms = terms
|
10
|
+
end
|
11
|
+
|
12
|
+
def match?(sexp)
|
13
|
+
return nil unless sexp.is_a?(Array)
|
14
|
+
eat = eat(sexp)
|
15
|
+
eat && eat.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def eat(sexp)
|
19
|
+
@terms.inject sexp do |rest,rule|
|
20
|
+
rest && rule.eat(rest)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"(seq #{terms.inspect})"
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Sequence
|
29
|
+
end # module Matcher
|
30
|
+
end # module Sexpr
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Matcher
|
3
|
+
class Terminal
|
4
|
+
include Matcher
|
5
|
+
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"(terminal #{value.inspect})"
|
14
|
+
end
|
15
|
+
|
16
|
+
def match?(sexp)
|
17
|
+
terminal_match?(sexp)
|
18
|
+
end
|
19
|
+
|
20
|
+
def eat(sexp)
|
21
|
+
match?(sexp.first) ? sexp[1..-1] : nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def terminal_match?(term)
|
27
|
+
case @value
|
28
|
+
when Regexp
|
29
|
+
@value === term rescue false
|
30
|
+
when TrueClass, FalseClass, NilClass
|
31
|
+
@value == term
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class Terminal
|
36
|
+
end # module Matcher
|
37
|
+
end # module Sexpr
|
data/lib/sexpr/node.rb
ADDED
data/lib/sexpr/parser.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Sexpr
|
2
|
+
module Parser
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def registered_parser_classes
|
7
|
+
@registered_parser_classes ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(parser_class)
|
11
|
+
registered_parser_classes << parser_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_parser_class(external_parser)
|
15
|
+
registered_parser_classes.find{|cl|
|
16
|
+
cl.recognizes?(external_parser)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def factor(external_parser, options = {})
|
21
|
+
return external_parser if Parser===external_parser
|
22
|
+
if cl = find_parser_class(external_parser)
|
23
|
+
cl.new(external_parser, options)
|
24
|
+
else
|
25
|
+
raise UnrecognizedParserError, "Parser not recognized: #{external_parser}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end # class methods
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def input_text(input)
|
34
|
+
case input
|
35
|
+
when lambda{|x| x.respond_to?(:to_path)}
|
36
|
+
input_text File.read(input.to_path)
|
37
|
+
when IO
|
38
|
+
input_text input.read
|
39
|
+
when String
|
40
|
+
input
|
41
|
+
else
|
42
|
+
raise InvalidParseSourceError, "Invalid parse source: #{input}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end # module Parser
|
47
|
+
end # module Sexpr
|
48
|
+
require_relative "parser/citrus"
|