sexpr 0.3.0 → 0.4.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.
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