sexpr 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +29 -0
- data/README.md +28 -2
- data/examples/bool_expr/bool_expr.citrus +6 -6
- data/examples/bool_expr/bool_expr.rb +68 -1
- data/lib/sexpr.rb +29 -22
- data/lib/sexpr/errors.rb +3 -0
- data/lib/sexpr/grammar.rb +7 -6
- data/lib/sexpr/grammar/options.rb +3 -0
- data/lib/sexpr/grammar/parsing.rb +6 -1
- data/lib/sexpr/grammar/tagging.rb +30 -11
- data/lib/sexpr/node.rb +30 -0
- data/lib/sexpr/parser.rb +2 -2
- data/lib/sexpr/parser/citrus.rb +6 -17
- data/lib/sexpr/parser/ext.rb +9 -0
- data/lib/sexpr/processor.rb +61 -0
- data/lib/sexpr/processor/helper.rb +31 -0
- data/lib/sexpr/processor/null_helper.rb +11 -0
- data/lib/sexpr/processor/sexpr_coercions.rb +44 -0
- data/lib/sexpr/rewriter.rb +20 -0
- data/lib/sexpr/version.rb +1 -1
- data/sexpr.noespec +1 -1
- data/spec/grammar/test_parse.rb +10 -6
- data/spec/grammar/test_sexpr.rb +47 -13
- data/spec/node/test_sexpr_copy.rb +35 -0
- data/spec/node/test_tracking_markers.rb +21 -0
- data/spec/parser/citrus/test_new.rb +0 -4
- data/spec/parser/citrus/test_parse.rb +4 -0
- data/spec/parser/citrus/test_registration.rb +0 -6
- data/spec/parser/citrus/test_to_sexpr.rb +22 -7
- data/spec/processor/helper/test_call.rb +51 -0
- data/spec/processor/test_build_helper_chain.rb +24 -0
- data/spec/processor/test_call.rb +46 -0
- data/spec/processor/test_helper.rb +19 -0
- data/spec/processor/test_main_processor.rb +18 -0
- data/spec/processor/test_sexpr_coercions.rb +46 -0
- data/spec/rewriter/test_copy_and_apply.rb +29 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/test_readme_examples.rb +11 -0
- data/spec/test_rewriter.rb +16 -0
- metadata +106 -80
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
-
|
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)
|
data/lib/sexpr.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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.
|
31
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
data/lib/sexpr/errors.rb
CHANGED
data/lib/sexpr/grammar.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
-
|
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 =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
data/lib/sexpr/node.rb
CHANGED
@@ -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
|