sexpr 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/CHANGELOG.md +24 -1
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +2 -0
  4. data/LICENCE.md +1 -1
  5. data/Manifest.txt +1 -0
  6. data/README.md +46 -31
  7. data/examples/bool_expr/bool_expr.citrus +72 -0
  8. data/examples/bool_expr/bool_expr.rb +86 -0
  9. data/examples/bool_expr/bool_expr.sexp.yml +23 -0
  10. data/lib/sexpr.rb +27 -20
  11. data/lib/sexpr/errors.rb +15 -0
  12. data/lib/sexpr/grammar.rb +21 -77
  13. data/lib/sexpr/grammar/matching.rb +56 -0
  14. data/lib/sexpr/grammar/options.rb +53 -0
  15. data/lib/sexpr/grammar/parsing.rb +20 -0
  16. data/lib/sexpr/grammar/tagging.rb +49 -0
  17. data/lib/sexpr/loader.rb +0 -1
  18. data/lib/sexpr/matcher.rb +15 -0
  19. data/lib/sexpr/matcher/alternative.rb +30 -0
  20. data/lib/sexpr/matcher/many.rb +54 -0
  21. data/lib/sexpr/matcher/reference.rb +32 -0
  22. data/lib/sexpr/matcher/rule.rb +31 -0
  23. data/lib/sexpr/matcher/sequence.rb +30 -0
  24. data/lib/sexpr/matcher/terminal.rb +37 -0
  25. data/lib/sexpr/node.rb +16 -0
  26. data/lib/sexpr/parser.rb +48 -0
  27. data/lib/sexpr/parser/citrus.rb +67 -0
  28. data/lib/sexpr/version.rb +2 -2
  29. data/sexpr.gemspec +1 -0
  30. data/sexpr.noespec +2 -1
  31. data/spec/grammar/matching/test_compile_rule.rb +23 -0
  32. data/spec/grammar/matching/test_compile_rule_defn.rb +103 -0
  33. data/spec/grammar/options/test_install_parser.rb +36 -0
  34. data/spec/grammar/options/test_install_path.rb +19 -0
  35. data/spec/grammar/options/test_install_root.rb +27 -0
  36. data/spec/grammar/tagging/test_looks_a_sexpr.rb +20 -0
  37. data/spec/grammar/tagging/test_mod2rulename.rb +19 -0
  38. data/spec/grammar/tagging/test_rule2modname.rb +19 -0
  39. data/spec/grammar/tagging/test_tag_sexpr.rb +28 -0
  40. data/spec/grammar/test_new.rb +15 -0
  41. data/spec/grammar/test_parse.rb +27 -18
  42. data/spec/grammar/test_sexpr.rb +53 -0
  43. data/spec/{alternative → matcher/alternative}/test_eat.rb +1 -1
  44. data/spec/{alternative → matcher/alternative}/test_match_q.rb +1 -1
  45. data/spec/{many → matcher/many}/test_eat.rb +1 -1
  46. data/spec/{many → matcher/many}/test_initialize.rb +1 -1
  47. data/spec/{many → matcher/many}/test_match_q.rb +1 -1
  48. data/spec/{reference → matcher/reference}/test_eat.rb +1 -1
  49. data/spec/{reference → matcher/reference}/test_match_q.rb +1 -1
  50. data/spec/{rule → matcher/rule}/test_eat.rb +1 -1
  51. data/spec/{rule → matcher/rule}/test_match_q.rb +1 -1
  52. data/spec/{sequence → matcher/sequence}/test_eat.rb +1 -1
  53. data/spec/{sequence → matcher/sequence}/test_match_q.rb +1 -1
  54. data/spec/{terminal → matcher/terminal}/test_eat.rb +1 -1
  55. data/spec/{terminal → matcher/terminal}/test_match_q.rb +1 -1
  56. data/spec/{terminal → matcher/terminal}/test_terminal_match.rb +1 -1
  57. data/spec/node/test_sexpr_body.rb +18 -0
  58. data/spec/node/test_sexpr_type.rb +14 -0
  59. data/spec/parser/citrus/test_new.rb +28 -0
  60. data/spec/parser/citrus/test_parse.rb +40 -0
  61. data/spec/parser/citrus/test_recognize.rb +18 -0
  62. data/spec/parser/citrus/test_registration.rb +20 -0
  63. data/spec/parser/citrus/test_to_sexpr.rb +16 -0
  64. data/spec/parser/test_factor.rb +17 -0
  65. data/spec/parser/test_input_text.rb +27 -0
  66. data/spec/spec_helper.rb +18 -1
  67. data/spec/test_load.rb +40 -13
  68. data/spec/test_readme_examples.rb +40 -30
  69. data/spec/test_sexpr.rb +1 -1
  70. metadata +118 -68
  71. data/lib/sexpr/alternative.rb +0 -28
  72. data/lib/sexpr/element.rb +0 -9
  73. data/lib/sexpr/many.rb +0 -52
  74. data/lib/sexpr/reference.rb +0 -30
  75. data/lib/sexpr/rule.rb +0 -29
  76. data/lib/sexpr/sequence.rb +0 -28
  77. data/lib/sexpr/terminal.rb +0 -35
  78. data/spec/bool_expr.yml +0 -19
  79. data/spec/grammar/test_compile_rule.rb +0 -25
  80. data/spec/grammar/test_compile_rule_defn.rb +0 -98
  81. data/spec/grammar/test_fetch.rb +0 -18
  82. data/spec/grammar/test_root.rb +0 -20
  83. data/spec/test_bool_expr.rb +0 -27
@@ -1,3 +1,26 @@
1
+ # 0.3.0 / 2012-02-21
2
+
3
+ * Breaking changes
4
+
5
+ * `Sexpr.load` only takes one argument, merging both the rules and the options inside a
6
+ unique Hash object (or something coercible to a Hash, such as YAML).
7
+ * The YAML grammar format now requires rules to be specified under a "rules" entry. See
8
+ the README for an example.
9
+
10
+ * Major enhancements
11
+
12
+ * A lexical parser can now be specified under a "parser" entry. Only Citrus is supported
13
+ for now.
14
+ * A loaded grammar (i.e. returned by `Sexpr.load`) is now a module. Therefore assigning
15
+ the result to a constant makes perfect sense and benefits from the ruby's magic naming
16
+ feature.
17
+ * A loaded grammar now respond to a :sexpr method that parses (if needed) and returns a
18
+ s-expression. The latter, and all its sub-expressions are automatically tagged with the
19
+ Sexpr module, as well as with user-defined modules. The latter are automatically discovered
20
+ with a convention over configuration heuristics that associates rule names to module names.
21
+ That convention may however be overridden with specific grammar methods (see the BoolExpr
22
+ example).
23
+
1
24
  # 0.2.0 / 2012-02-21
2
25
 
3
26
  * Enhancements
@@ -10,4 +33,4 @@
10
33
 
11
34
  * Enhancements
12
35
 
13
- * Birthday!
36
+ * Birthday!
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source 'http://rubygems.org'
2
2
 
3
3
  group :development do
4
4
  gem "epath", "~> 0.0.1"
5
+ gem "citrus", "~> 2.4"
5
6
  gem "rake", "~> 0.9.2"
6
7
  gem "rspec", "~> 2.8.0"
7
8
  gem "wlang", "~> 0.10.2"
@@ -1,6 +1,7 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ citrus (2.4.1)
4
5
  diff-lcs (1.1.3)
5
6
  epath (0.0.1)
6
7
  rake (0.9.2.2)
@@ -19,6 +20,7 @@ PLATFORMS
19
20
  ruby
20
21
 
21
22
  DEPENDENCIES
23
+ citrus (~> 2.4)
22
24
  epath (~> 0.0.1)
23
25
  rake (~> 0.9.2)
24
26
  rspec (~> 2.8.0)
data/LICENCE.md CHANGED
@@ -19,4 +19,4 @@
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -5,6 +5,7 @@ CHANGELOG.md
5
5
  Gemfile
6
6
  Gemfile.lock
7
7
  bin/**/*
8
+ examples/**/*
8
9
  lib/**/*
9
10
  LICENCE.md
10
11
  Manifest.txt
data/README.md CHANGED
@@ -4,43 +4,58 @@ A ruby compilation framework around s-expressions.
4
4
 
5
5
  ## Example
6
6
 
7
+ # Let load a grammar defined in YAML
7
8
  grammar = SexpGrammar.load(<<-YAML)
8
-
9
- # alternative rule
10
- bool_expr:
11
- - bool_and
12
- - bool_or
13
- - bool_not
14
- - var_ref
15
- - literal
16
-
17
- # non-terminal
18
- bool_and:
19
- - [ bool_expr, bool_expr ]
20
- bool_or:
21
- - [ bool_expr, bool_expr ]
22
- bool_not:
23
- - [ bool_expr ]
24
- literal:
25
- - [ truth_value ]
26
- var_ref:
27
- - [ var_name ]
28
-
29
- # terminals
30
- var_name:
31
- !ruby/regexp /^[a-z]+$/
32
- truth_value:
33
- - true
34
- - false
35
-
9
+ rules:
10
+ # alternative rule
11
+ bool_expr:
12
+ - bool_and
13
+ - bool_or
14
+ - bool_not
15
+ - var_ref
16
+ - bool_lit
17
+
18
+ # non-terminal
19
+ bool_and:
20
+ - [ bool_expr, bool_expr ]
21
+ bool_or:
22
+ - [ bool_expr, bool_expr ]
23
+ bool_not:
24
+ - [ bool_expr ]
25
+ bool_lit:
26
+ - [ truth_value ]
27
+ var_ref:
28
+ - [ var_name ]
29
+
30
+ # terminals
31
+ var_name:
32
+ !ruby/regexp /^[a-z]+$/
33
+ truth_value:
34
+ - true
35
+ - false
36
36
  YAML
37
37
 
38
- grammar === [:bool_and, [:bool_not, [:var_ref, "x"]], [:literal, true]]
38
+ ### Checking the structure of s-expressions
39
+
40
+ # the grammar can be used to verify the structure of s-expressions
41
+ grammar === [:bool_and, [:bool_not, [:var_ref, "x"]], [:bool_lit, true]]
39
42
  # => true
40
43
 
41
- grammar === [:bool_and, [:literal, "true"]]
44
+ grammar === [:bool_and, [:bool_lit, "true"]]
42
45
  # => false (second term is missing)
43
46
 
47
+ ### Including s-expression tools
48
+
49
+ # the grammar can also be used to automatically have support on top of
50
+ # such s-expressions
51
+ expr = grammar.sexpr([:bool_lit, true])
52
+
53
+ expr.sexpr_type
54
+ # => :bool_lit
55
+
56
+ expr.sexpr_body
57
+ # => [true]
58
+
44
59
  ## Links
45
60
 
46
- https://github.com/blambeau/sexpr
61
+ https://github.com/blambeau/sexpr
@@ -0,0 +1,72 @@
1
+ #
2
+ # This is a Citrus grammar for boolean expressions.
3
+ #
4
+ # The parser is automatically loaded under the BoolExpr::Parser constant by Citrus
5
+ # itself. It is also automatically registered under BoolExpr::Grammar.parser by
6
+ # `Sexpr.load` when invoked on the bool_expr.sexp.yml file.
7
+ #
8
+ # The coupon codes returns s-expressions that correctly refer to the abstract grammar
9
+ # definition in that file (AST).
10
+ #
11
+ grammar BoolExpr::Parser
12
+
13
+ rule bool_expr
14
+ bool_or
15
+ end
16
+
17
+ rule bool_or
18
+ (l:bool_and spaces 'or' spaces r:bool_or){
19
+ [:bool_or, l.value, r.value]
20
+ }
21
+ | bool_and
22
+ end
23
+
24
+ rule bool_and
25
+ (l:bool_not spaces 'and' spaces r:bool_and){
26
+ [:bool_and, l.value, r.value]
27
+ }
28
+ | bool_not
29
+ end
30
+
31
+ rule bool_not
32
+ ('not' spacing e:bool_not){
33
+ [:bool_not, e.value]
34
+ }
35
+ | bool_term
36
+ end
37
+
38
+ rule bool_term
39
+ bool_paren | bool_lit | var_ref
40
+ end
41
+
42
+ rule bool_paren
43
+ ('(' spacing e:bool_or spacing ')'){
44
+ e.value
45
+ }
46
+ end
47
+
48
+ rule bool_lit
49
+ ("true" | "false"){
50
+ [:bool_lit, ::Kernel.eval(strip) ]
51
+ }
52
+ end
53
+
54
+ rule var_ref
55
+ (!(keyword spacing) [a-z]+){
56
+ [:var_ref, strip]
57
+ }
58
+ end
59
+
60
+ rule spacing
61
+ [ \t]+ | &'(' | !.
62
+ end
63
+
64
+ rule spaces
65
+ [ \t]+
66
+ end
67
+
68
+ rule keyword
69
+ "true" | "false" | "or" | "and" | "not"
70
+ end
71
+
72
+ end
@@ -0,0 +1,86 @@
1
+ require 'sexpr'
2
+
3
+ # Let load the grammar from the .yml definition file.
4
+ BoolExpr = Sexpr.load File.expand_path('../bool_expr.sexp.yml', __FILE__)
5
+
6
+ # A Sexpr grammar is simply a module. Ruby allows us to re-open it later.
7
+ module BoolExpr
8
+
9
+ # These are the modules automatically installed on the s-expressions
10
+ # that the grammar produces
11
+ module And; end
12
+ module Or; end
13
+ module Not; end
14
+ module Lit; end
15
+ module VarRef; end
16
+
17
+ # The two following methods allows converting rule names (e.g. bool_and)
18
+ # to module names (And). A default implementation is provided by Sexpr
19
+ # that enforces convention over configuration (BoolAnd <-> bool_and). We
20
+ # override the methods here for the sake of the example/documentation.
21
+
22
+ def rule2modname(rule)
23
+ (rule.to_s =~ /^bool_(.*)$/) ? $1.capitalize.to_sym : super
24
+ end
25
+
26
+ def mod2rulename(mod)
27
+ rule = super
28
+ (rule.to_s =~ /^bool_(.*)$/) ? const_get($1.to_sym) : rule
29
+ end
30
+
31
+ end
32
+
33
+ describe BoolExpr do
34
+ subject{ BoolExpr }
35
+
36
+ describe "the parsing feature" do
37
+
38
+ it 'parses boolean expressions without error' do
39
+ subject.parse("x and y").should be_a(Citrus::Match)
40
+ end
41
+
42
+ it 'provides a shortcut to get s-expressions directly' do
43
+ subject.sexpr("x and y").should eq([:bool_and, [:var_ref, "x"], [:var_ref, "y"]])
44
+ end
45
+
46
+ end # parsing
47
+
48
+ describe "the tagging feature" do
49
+
50
+ it 'tags parsing results with the Sexpr module' do
51
+ sexpr = subject.sexpr("x and y")
52
+ sexpr.should be_a(Sexpr)
53
+ sexpr.sexpr_type.should eq(:bool_and)
54
+ sexpr.sexpr_body.should eq([[:var_ref, "x"], [:var_ref, "y"]])
55
+ end
56
+
57
+ it 'tags parsing results with user modules' do
58
+ subject.sexpr("x and y").should be_a(BoolExpr::And)
59
+ end
60
+
61
+ it 'allows tagging manually' do
62
+ subject.sexpr([:bool_lit, true]).should be_a(BoolExpr::Lit)
63
+ end
64
+
65
+ it 'applies tagging recursively' do
66
+ sexpr = subject.sexpr([:bool_not, [:bool_lit, true]])
67
+ sexpr.last.should be_a(BoolExpr::Lit)
68
+ end
69
+
70
+ end # taggging
71
+
72
+ describe 'the validation feature' do
73
+
74
+ it 'validates s-expressions' do
75
+ subject.match?([:bool_lit, true]).should be_true
76
+ subject.match?([:bool_lit, "x"]).should be_false
77
+ end
78
+
79
+ it 'validates s-expressions against specific rules' do
80
+ subject[:bool_lit].match?([:bool_lit, true]).should be_true
81
+ subject[:bool_and].match?([:bool_lit, true]).should be_false
82
+ end
83
+
84
+ end # validating
85
+
86
+ end if defined?(RSpec)
@@ -0,0 +1,23 @@
1
+ parser:
2
+ bool_expr.citrus
3
+ rules:
4
+ bool_expr:
5
+ - bool_and
6
+ - bool_or
7
+ - bool_not
8
+ - var_ref
9
+ - bool_lit
10
+ bool_and:
11
+ - [ bool_expr+ ]
12
+ bool_or:
13
+ - [ bool_expr+ ]
14
+ bool_not:
15
+ - [ bool_expr ]
16
+ bool_lit:
17
+ - [ literal ]
18
+ var_ref:
19
+ - [ var_name ]
20
+ var_name:
21
+ !ruby/regexp /^[a-z]+$/
22
+ literal:
23
+ [true, false]
@@ -1,31 +1,38 @@
1
1
  require_relative "sexpr/version"
2
2
  require_relative "sexpr/loader"
3
+ require_relative "sexpr/errors"
3
4
  #
4
5
  # A helper to manipulate sexp grammars
5
6
  #
6
7
  module Sexpr
7
8
 
8
- def self.load(input, options = {})
9
- case input
10
- when lambda{|x| x.respond_to?(:to_path)}
11
- require 'yaml'
12
- load(YAML.load_file(input.to_path), options)
13
- when String
14
- require 'yaml'
15
- load(YAML.load(input), options)
16
- when Hash
17
- Grammar.new(input, options)
18
- else
19
- raise ArgumentError, "Invalid argument for Sexpr::Grammar: #{input}"
20
- end
9
+ PathLike = lambda{|x|
10
+ x.respond_to?(:to_path) or (x.is_a?(String) and File.exists?(x))
11
+ }
12
+
13
+ def self.load(input)
14
+ defn = case input
15
+ when PathLike
16
+ require 'yaml'
17
+ path = input.respond_to?(:to_path) ? input.to_path : input.to_s
18
+ YAML.load_file(path).merge(:path => input)
19
+ when String
20
+ require 'yaml'
21
+ YAML.load(input)
22
+ when Hash
23
+ input
24
+ else
25
+ raise ArgumentError, "Invalid argument for Sexpr::Grammar: #{input}"
26
+ end
27
+ Grammar.new defn
28
+ end
29
+
30
+ def self.sexpr(arg)
31
+ Object.new.extend(Sexpr::Grammar::Tagging).sexpr(arg)
21
32
  end
22
33
 
23
34
  end # module Sexpr
35
+ require_relative "sexpr/node"
24
36
  require_relative "sexpr/grammar"
25
- require_relative "sexpr/element"
26
- require_relative "sexpr/alternative"
27
- require_relative "sexpr/many"
28
- require_relative "sexpr/reference"
29
- require_relative "sexpr/rule"
30
- require_relative "sexpr/sequence"
31
- require_relative "sexpr/terminal"
37
+ require_relative "sexpr/matcher"
38
+ require_relative "sexpr/parser"
@@ -0,0 +1,15 @@
1
+ module Sexpr
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class UnrecognizedParserError < Error
7
+ end
8
+
9
+ class InvalidParseSourceError < Error
10
+ end
11
+
12
+ class NoParserError < Error
13
+ end
14
+
15
+ end
@@ -1,82 +1,26 @@
1
+ require_relative 'grammar/options'
2
+ require_relative 'grammar/matching'
3
+ require_relative 'grammar/parsing'
4
+ require_relative 'grammar/tagging'
1
5
  module Sexpr
2
- class Grammar
3
-
4
- attr_reader :rules, :options
5
-
6
- def initialize(rules = {}, options = {})
7
- @rules = compile_rules(rules)
8
- @options = options
9
- end
10
-
11
- def [](rule_name)
12
- @rules[rule_name]
13
- end
14
-
15
- def root
16
- @root ||= begin
17
- root = options[:root] || rules.keys.first
18
- root = self[root] if root.is_a?(Symbol)
19
- end
20
- end
21
-
22
- def parser
23
- options[:parser]
24
- end
25
-
26
- def parse(input)
27
- case input
28
- when lambda{|x| x.respond_to?(:to_path)}
29
- parse(File.read(input.to_path))
30
- when IO
31
- parse(input.read)
32
- when String
33
- unless p = parser
34
- raise NoParserError, "No parser set.", caller
35
- end
36
- p.parse(input).value
37
- end
38
- end
39
-
40
- def match?(sexp)
41
- root.match?(sexp)
42
- end
43
- alias :=== :match?
44
-
45
- private
46
-
47
- def compile_rules(rules)
48
- Hash[rules.map{|k,v|
49
- [k.to_sym, compile_rule(k.to_sym, v)]
50
- }]
51
- end
52
-
53
- def compile_rule(name, defn)
54
- case rule = compile_rule_defn(defn)
55
- when Terminal, Alternative
56
- rule
57
- else
58
- Rule.new(name, rule)
59
- end
60
- end
61
-
62
- def compile_rule_defn(arg)
63
- case arg
64
- when Element
65
- arg
66
- when Regexp, TrueClass, FalseClass, NilClass
67
- Terminal.new arg
68
- when lambda{|x| x.is_a?(Array) && x.size == 1 && x.first.is_a?(Array)}
69
- Sequence.new arg.first.map{|s| compile_rule_defn(s) }
70
- when Array
71
- Alternative.new arg.map{|s| compile_rule_defn(s)}
72
- when /([\?\+\*])$/
73
- Many.new compile_rule_defn($`), $1
74
- when /^[a-z][a-z_]+$/
75
- Reference.new arg.to_sym, self
76
- else
77
- raise ArgumentError, "Invalid rule definition: #{arg.inspect}", caller
6
+ module Grammar
7
+ include Options
8
+ include Matching
9
+ include Parsing
10
+ include Tagging
11
+
12
+ def self.new(options = {})
13
+ unless options.is_a?(Hash)
14
+ raise ArgumentError, "Invalid grammar definition: #{options.inspect}"
78
15
  end
16
+ Module.new.tap{|g|
17
+ g.instance_eval{
18
+ include(Grammar)
19
+ extend(self)
20
+ install_options(options)
21
+ }
22
+ }
79
23
  end
80
24
 
81
- end # class Grammar
25
+ end # module Grammar
82
26
  end # module Sexpr