sexpr 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG.md +29 -0
  2. data/README.md +28 -2
  3. data/examples/bool_expr/bool_expr.citrus +6 -6
  4. data/examples/bool_expr/bool_expr.rb +68 -1
  5. data/lib/sexpr.rb +29 -22
  6. data/lib/sexpr/errors.rb +3 -0
  7. data/lib/sexpr/grammar.rb +7 -6
  8. data/lib/sexpr/grammar/options.rb +3 -0
  9. data/lib/sexpr/grammar/parsing.rb +6 -1
  10. data/lib/sexpr/grammar/tagging.rb +30 -11
  11. data/lib/sexpr/node.rb +30 -0
  12. data/lib/sexpr/parser.rb +2 -2
  13. data/lib/sexpr/parser/citrus.rb +6 -17
  14. data/lib/sexpr/parser/ext.rb +9 -0
  15. data/lib/sexpr/processor.rb +61 -0
  16. data/lib/sexpr/processor/helper.rb +31 -0
  17. data/lib/sexpr/processor/null_helper.rb +11 -0
  18. data/lib/sexpr/processor/sexpr_coercions.rb +44 -0
  19. data/lib/sexpr/rewriter.rb +20 -0
  20. data/lib/sexpr/version.rb +1 -1
  21. data/sexpr.noespec +1 -1
  22. data/spec/grammar/test_parse.rb +10 -6
  23. data/spec/grammar/test_sexpr.rb +47 -13
  24. data/spec/node/test_sexpr_copy.rb +35 -0
  25. data/spec/node/test_tracking_markers.rb +21 -0
  26. data/spec/parser/citrus/test_new.rb +0 -4
  27. data/spec/parser/citrus/test_parse.rb +4 -0
  28. data/spec/parser/citrus/test_registration.rb +0 -6
  29. data/spec/parser/citrus/test_to_sexpr.rb +22 -7
  30. data/spec/processor/helper/test_call.rb +51 -0
  31. data/spec/processor/test_build_helper_chain.rb +24 -0
  32. data/spec/processor/test_call.rb +46 -0
  33. data/spec/processor/test_helper.rb +19 -0
  34. data/spec/processor/test_main_processor.rb +18 -0
  35. data/spec/processor/test_sexpr_coercions.rb +46 -0
  36. data/spec/rewriter/test_copy_and_apply.rb +29 -0
  37. data/spec/spec_helper.rb +22 -0
  38. data/spec/test_readme_examples.rb +11 -0
  39. data/spec/test_rewriter.rb +16 -0
  40. metadata +106 -80
@@ -1,3 +1,32 @@
1
+ # 0.4.0 / 2012-02-23
2
+
3
+ * Major enhancements
4
+
5
+ * A processing/rewriting framework has been added to Sexpr. See the `Processor` and `Rewriter`
6
+ classes, as well as the boolean expression example.
7
+ * Tracking markers can now decorate s-expressions, provided they include the `Sexpr` module.
8
+ Tracking markers are a simple Hash of meta-information (i.e. not taken into account for
9
+ equality for s-expressions). Such markers can be set with `Grammar#sexpr(sexpr, markers)`.
10
+ Default markers are typically provided by parsers for traceability of the s-expression
11
+ with the source text it comes from.
12
+
13
+ * Minor enhancements
14
+
15
+ * `Citrus::Parser#parse` is now idempotent and so is `Grammar#parse` therefore.
16
+ * The module to use for finding tag modules through `const_get` can now be overridden in
17
+ `Grammar#tagging_reference`.
18
+ * Default parsing options can now be specified in `Grammar#default_parse_options`. These
19
+ options are used by `Grammar#sexpr` when parsing is needed.
20
+
21
+ * Breaking changes
22
+
23
+ * `Parser.factor` does no longer accept options. This is to avoid the 'yet another options'
24
+ symptom and favor convention over configuration.
25
+ * Accordingly, `Sexpr::Citrus::Parser` no longer takes options at construction either.
26
+ * `Grammar#sexpr` does no longer allow parsing options as second argument, but takes tracking
27
+ markers (see enhancements). To palliate to this, default parsing options can now be
28
+ specified through `Grammar#default_parse_options` (see enhancements).
29
+
1
30
  # 0.3.0 / 2012-02-21
2
31
 
3
32
  * Breaking changes
data/README.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  A ruby compilation framework around s-expressions.
4
4
 
5
+ ## Links
6
+
7
+ https://github.com/blambeau/sexpr
8
+
9
+ ## Features/Problems
10
+
11
+ * Provides a YAML format for describing grammars (abstract syntax trees, more precisely).
12
+ * Provides a simple way to check the validity of a s-expression against a given grammar.
13
+ * Provides a framework for processing and rewriting abstract syntax trees.
14
+ * Focusses on the semantic pass, not the syntactic one.
15
+ * Smoothly, yet not tightly, integrates with the Citrus PEG parser (for the syntactic pass).
16
+
5
17
  ## Example
6
18
 
7
19
  # Let load a grammar defined in YAML
@@ -50,12 +62,26 @@ A ruby compilation framework around s-expressions.
50
62
  # such s-expressions
51
63
  expr = grammar.sexpr([:bool_lit, true])
52
64
 
65
+ Sexpr === expr
66
+ # => true
67
+
53
68
  expr.sexpr_type
54
69
  # => :bool_lit
55
70
 
56
71
  expr.sexpr_body
57
72
  # => [true]
58
73
 
59
- ## Links
74
+ # Rewriting s-expressions through copying is easy...
75
+ copy = expr.sexpr_copy do |base,child|
76
+ # copy a s-expression ala Enumerable#inject (base is [:bool_lit] initially)
77
+ base << [:bool_lit, !child]
78
+ end
79
+ # => [:bool_lit, [:bool_lit, false]]
80
+
81
+ # ... and is tag preserving (including User-included modules)
82
+ Sexpr === copy
83
+ # true
84
+
85
+ ### Where to read next?
60
86
 
61
- https://github.com/blambeau/sexpr
87
+ Have a look at the examples directory.
@@ -16,21 +16,21 @@ grammar BoolExpr::Parser
16
16
 
17
17
  rule bool_or
18
18
  (l:bool_and spaces 'or' spaces r:bool_or){
19
- [:bool_or, l.value, r.value]
19
+ [:bool_or, l.sexpr, r.sexpr]
20
20
  }
21
21
  | bool_and
22
22
  end
23
23
 
24
24
  rule bool_and
25
25
  (l:bool_not spaces 'and' spaces r:bool_and){
26
- [:bool_and, l.value, r.value]
26
+ [:bool_and, l.sexpr, r.sexpr]
27
27
  }
28
28
  | bool_not
29
29
  end
30
30
 
31
31
  rule bool_not
32
32
  ('not' spacing e:bool_not){
33
- [:bool_not, e.value]
33
+ [:bool_not, e.sexpr]
34
34
  }
35
35
  | bool_term
36
36
  end
@@ -41,7 +41,7 @@ grammar BoolExpr::Parser
41
41
 
42
42
  rule bool_paren
43
43
  ('(' spacing e:bool_or spacing ')'){
44
- e.value
44
+ e.sexpr
45
45
  }
46
46
  end
47
47
 
@@ -52,13 +52,13 @@ grammar BoolExpr::Parser
52
52
  end
53
53
 
54
54
  rule var_ref
55
- (!(keyword spacing) [a-z]+){
55
+ (!(keyword (spaces | !.)) [a-z]+){
56
56
  [:var_ref, strip]
57
57
  }
58
58
  end
59
59
 
60
60
  rule spacing
61
- [ \t]+ | &'(' | !.
61
+ [ \t]*
62
62
  end
63
63
 
64
64
  rule spaces
@@ -1,4 +1,5 @@
1
1
  require 'sexpr'
2
+ require 'citrus'
2
3
 
3
4
  # Let load the grammar from the .yml definition file.
4
5
  BoolExpr = Sexpr.load File.expand_path('../bool_expr.sexp.yml', __FILE__)
@@ -28,7 +29,34 @@ module BoolExpr
28
29
  (rule.to_s =~ /^bool_(.*)$/) ? const_get($1.to_sym) : rule
29
30
  end
30
31
 
31
- end
32
+ # This class pushes `[:not, ...]` as far as possible in boolean expressions.
33
+ # It provides an example of s-expression rewriter
34
+ class NotPushProcessor < Sexpr::Rewriter
35
+
36
+ # Let the default implementation know that we are working on the BoolExpr
37
+ # grammar. This way, all rewriting results will automatically be tagged
38
+ # with the correct modules above (And, Not, ...)
39
+ grammar BoolExpr
40
+
41
+ # The main rewriting rule, that pushes a NOT according to the different
42
+ # cases
43
+ def on_bool_not(sexpr)
44
+ case expr = sexpr.last
45
+ when And then call [:bool_or, [:bool_not, expr[1]], [:bool_not, expr[2]] ]
46
+ when Or then call [:bool_and, [:bool_not, expr[1]], [:bool_not, expr[2]] ]
47
+ when Not then call expr.last
48
+ when Lit then [:bool_lit, !expr.last]
49
+ else
50
+ sexpr
51
+ end
52
+ end
53
+
54
+ # By default, we simply copy the node and apply rewriting rules on children
55
+ alias :on_missing :copy_and_apply
56
+
57
+ end # class NotPushProcessor
58
+
59
+ end # module BoolExpr
32
60
 
33
61
  describe BoolExpr do
34
62
  subject{ BoolExpr }
@@ -37,6 +65,8 @@ describe BoolExpr do
37
65
 
38
66
  it 'parses boolean expressions without error' do
39
67
  subject.parse("x and y").should be_a(Citrus::Match)
68
+ subject.parse("not(y)").should be_a(Citrus::Match)
69
+ subject.parse("not(true)").should be_a(Citrus::Match)
40
70
  end
41
71
 
42
72
  it 'provides a shortcut to get s-expressions directly' do
@@ -83,4 +113,41 @@ describe BoolExpr do
83
113
 
84
114
  end # validating
85
115
 
116
+ describe BoolExpr::NotPushProcessor do
117
+
118
+ def _(expr)
119
+ BoolExpr.sexpr(expr)
120
+ end
121
+
122
+ def rw(expr)
123
+ BoolExpr::NotPushProcessor.new.call(expr)
124
+ end
125
+
126
+ it 'does nothing on variable references' do
127
+ rw("not x").should eq([:bool_not, [:var_ref, "x"]])
128
+ end
129
+
130
+ it 'rewrites literals through negating them' do
131
+ rw("not true").should eq(_ "false")
132
+ rw("not false").should eq(_ "true")
133
+ end
134
+
135
+ it 'rewrites not through removing them' do
136
+ rw("not not true").should eq(_ "true")
137
+ end
138
+
139
+ it 'rewrites or through and of negated terms' do
140
+ rw("not(x or y)").should eq(_ "not(x) and not(y)")
141
+ end
142
+
143
+ it 'rewrites and through or of negated terms' do
144
+ rw("not(x and y)").should eq(_ "not(x) or not(y)")
145
+ end
146
+
147
+ it 'rewrites recursively' do
148
+ rw("not(x and not(y))").should eq(_ "not(x) or y")
149
+ end
150
+
151
+ end # rewriting
152
+
86
153
  end if defined?(RSpec)
@@ -1,38 +1,45 @@
1
+ require 'yaml'
1
2
  require_relative "sexpr/version"
2
3
  require_relative "sexpr/loader"
3
4
  require_relative "sexpr/errors"
5
+ require_relative "sexpr/node"
6
+ require_relative "sexpr/grammar"
7
+ require_relative "sexpr/matcher"
8
+ require_relative "sexpr/parser"
9
+ require_relative "sexpr/processor"
10
+ require_relative "sexpr/rewriter"
4
11
  #
5
12
  # A helper to manipulate sexp grammars
6
13
  #
7
14
  module Sexpr
15
+ extend Grammar::Tagging
8
16
 
9
17
  PathLike = lambda{|x|
10
18
  x.respond_to?(:to_path) or (x.is_a?(String) and File.exists?(x))
11
19
  }
12
20
 
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
21
+ def self.load(input, options = {})
22
+ case input
23
+ when PathLike then load_file input, options
24
+ when String then load_string input, options
25
+ when Hash then load_hash input, options
26
+ else
27
+ raise ArgumentError, "Invalid argument for Sexpr::Grammar: #{input}"
28
+ end
28
29
  end
29
30
 
30
- def self.sexpr(arg)
31
- Object.new.extend(Sexpr::Grammar::Tagging).sexpr(arg)
31
+ def self.load_file(input, options = {})
32
+ path = input.to_path rescue input.to_s
33
+ load_hash YAML.load_file(path), options.merge(:path => input)
32
34
  end
33
35
 
34
- end # module Sexpr
35
- require_relative "sexpr/node"
36
- require_relative "sexpr/grammar"
37
- require_relative "sexpr/matcher"
38
- require_relative "sexpr/parser"
36
+ def self.load_string(input, options = {})
37
+ load_hash YAML.load(input), options
38
+ end
39
+
40
+ def self.load_hash(input, options = {})
41
+ raise ArgumentError, "Invalid grammar definition: #{input}" unless Hash===input
42
+ Grammar.new input, options
43
+ end
44
+
45
+ end # module Sexpr
@@ -12,4 +12,7 @@ module Sexpr
12
12
  class NoParserError < Error
13
13
  end
14
14
 
15
+ class UnexpectedSexprError < Error
16
+ end
17
+
15
18
  end
@@ -6,21 +6,22 @@ module Sexpr
6
6
  module Grammar
7
7
  include Options
8
8
  include Matching
9
- include Parsing
10
9
  include Tagging
10
+ include Parsing
11
11
 
12
- def self.new(options = {})
13
- unless options.is_a?(Hash)
14
- raise ArgumentError, "Invalid grammar definition: #{options.inspect}"
15
- end
12
+ def self.new(input = {}, options = {})
16
13
  Module.new.tap{|g|
17
14
  g.instance_eval{
18
15
  include(Grammar)
19
16
  extend(self)
20
- install_options(options)
17
+ install_options(input.merge(options))
21
18
  }
22
19
  }
23
20
  end
24
21
 
22
+ def tagging_reference
23
+ self
24
+ end
25
+
25
26
  end # module Grammar
26
27
  end # module Sexpr
@@ -39,6 +39,9 @@ module Sexpr
39
39
  def install_parser
40
40
  @parser = option(:parser)
41
41
  if @parser.is_a?(String) && !File.exists?(@parser)
42
+ unless path
43
+ raise Errno::ENOENT, "#{@parser} (no main path)"
44
+ end
42
45
  @parser = File.join(File.dirname(path), @parser)
43
46
  end
44
47
  @parser = Parser.factor(@parser) if @parser
@@ -2,7 +2,12 @@ module Sexpr
2
2
  module Grammar
3
3
  module Parsing
4
4
 
5
- def parse(input, options = {})
5
+ def default_parse_options
6
+ {}
7
+ end
8
+
9
+ def parse(input, options = nil)
10
+ options = default_parse_options.merge(options || {})
6
11
  parser!.parse(input, options)
7
12
  end
8
13
 
@@ -11,39 +11,58 @@ module Sexpr
11
11
  mod.to_s.gsub(/[A-Z]/){|x| "_#{x.downcase}"}[1..-1].to_sym
12
12
  end
13
13
 
14
- def sexpr(input, options = {})
14
+ def tagging_reference
15
+ nil
16
+ end
17
+
18
+ def sexpr(input, markers = nil)
15
19
  case input
16
20
  when Array
17
- tag_sexpr input
21
+ tag_sexpr input, tagging_reference, markers
18
22
  else
19
- tag_sexpr parser!.sexpr(input, options)
23
+ sexpr = parser!.to_sexpr(parse(input))
24
+ tag_sexpr sexpr, tagging_reference, markers, true
20
25
  end
21
26
  end
22
27
 
23
28
  private
24
29
 
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
30
+ def tag_sexpr(sexpr, reference, markers = nil, force = false)
31
+ return sexpr unless looks_a_sexpr?(sexpr)
32
+ return sexpr if Sexpr===sexpr and not(force) and markers.nil?
33
+
34
+ # set the Sexpr modules
35
+ sexpr.extend(Sexpr) unless Sexpr===sexpr
36
+ tag_sexpr_with_user_module(sexpr, reference) if reference
37
+
38
+ # set the markers if any
39
+ if markers
40
+ markers = sexpr.tracking_markers.merge(markers) if Sexpr===sexpr
41
+ sexpr.tracking_markers = markers
42
+ end
43
+
44
+ # recurse
45
+ sexpr[1..-1].each do |child|
46
+ tag_sexpr(child, reference, nil, force)
31
47
  end
32
48
  sexpr
33
49
  end
34
50
 
35
51
  def tag_sexpr_with_user_module(sexpr, reference)
36
- sexpr.extend(Sexpr)
37
52
  rulename = sexpr.first
38
53
  modname = rule2modname(rulename)
39
54
  mod = reference.const_get(modname) rescue nil
40
- mod ? sexpr.extend(mod) : sexpr
55
+ sexpr.extend(mod) if mod
41
56
  end
42
57
 
43
58
  def looks_a_sexpr?(arg)
44
59
  arg.is_a?(Array) and arg.first.is_a?(Symbol)
45
60
  end
46
61
 
62
+ def parser!
63
+ raise NoParserError, "No parser set.", caller
64
+ end
65
+
47
66
  end # module Tagging
48
67
  end # module Grammar
49
68
  end # module Sexpr
@@ -1,6 +1,16 @@
1
1
  module Sexpr
2
2
  module Node
3
3
 
4
+ EMPTY_TRACKING_MARKERS = {}
5
+
6
+ def tracking_markers
7
+ @tracking_markers ||= EMPTY_TRACKING_MARKERS
8
+ end
9
+
10
+ def tracking_markers=(markers)
11
+ @tracking_markers = markers
12
+ end
13
+
4
14
  def sexpr_type
5
15
  first
6
16
  end
@@ -11,6 +21,26 @@ module Sexpr
11
21
  end
12
22
  alias :sexp_body :sexpr_body
13
23
 
24
+ def sexpr_copy(&block)
25
+ if block
26
+ copy = sexpr_copy_tagging([ sexpr_type ])
27
+ sexpr_body.inject(copy, &block)
28
+ else
29
+ sexpr_copy_tagging(self[0..-1])
30
+ end
31
+ end
32
+ alias :dup :sexpr_copy
33
+
34
+ private
35
+
36
+ def sexpr_copy_tagging(copy)
37
+ (class << self; self; end).included_modules.each do |mod|
38
+ copy.extend(mod) unless mod === copy
39
+ end
40
+ copy.tracking_markers = tracking_markers
41
+ copy
42
+ end
43
+
14
44
  end # module Node
15
45
  include Node
16
46
  end # module Sexpr