sexpr 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|