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.
- 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
|