rewrite 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +2 -0
- data/Manifest.txt +19 -0
- data/lib/rewrite/by_example.rb +39 -0
- data/lib/rewrite/by_example/any_entity.rb +32 -0
- data/lib/rewrite/by_example/bind.rb +41 -0
- data/lib/rewrite/by_example/bind_sequence.rb +46 -0
- data/lib/rewrite/by_example/composition.rb +104 -0
- data/lib/rewrite/by_example/entity_matcher.rb +48 -0
- data/lib/rewrite/by_example/length_one.rb +50 -0
- data/lib/rewrite/by_example/literal_entity.rb +25 -0
- data/lib/rewrite/by_example/nil_entity.rb +26 -0
- data/lib/rewrite/by_example/object_to_matcher.rb +221 -0
- data/lib/rewrite/by_example/returning.rb +29 -0
- data/lib/rewrite/by_example/sequence.rb +57 -0
- data/lib/rewrite/by_example/sexp_entity.rb +51 -0
- data/lib/rewrite/by_example/symbol_entity.rb +37 -0
- data/lib/rewrite/by_example/unhygienic.rb +57 -0
- data/lib/rewrite/by_example/union_of_entities_sequence.rb +39 -0
- data/lib/rewrite/version.rb +1 -1
- data/test/test_by_example.rb +61 -37
- data/test/test_object_to_sequence_and_matcher.rb +128 -0
- data/website/index.html +4 -2
- data/website/index.txt +2 -0
- metadata +22 -2
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
@@ -7,6 +7,22 @@ Rakefile
|
|
7
7
|
config/hoe.rb
|
8
8
|
config/requirements.rb
|
9
9
|
lib/rewrite.rb
|
10
|
+
lib/rewrite/by_example.rb
|
11
|
+
lib/rewrite/by_example/any_entity.rb
|
12
|
+
lib/rewrite/by_example/bind.rb
|
13
|
+
lib/rewrite/by_example/bind_sequence.rb
|
14
|
+
lib/rewrite/by_example/composition.rb
|
15
|
+
lib/rewrite/by_example/entity_matcher.rb
|
16
|
+
lib/rewrite/by_example/length_one.rb
|
17
|
+
lib/rewrite/by_example/literal_entity.rb
|
18
|
+
lib/rewrite/by_example/nil_entity.rb
|
19
|
+
lib/rewrite/by_example/object_to_matcher.rb
|
20
|
+
lib/rewrite/by_example/returning.rb
|
21
|
+
lib/rewrite/by_example/sequence.rb
|
22
|
+
lib/rewrite/by_example/sexp_entity.rb
|
23
|
+
lib/rewrite/by_example/symbol_entity.rb
|
24
|
+
lib/rewrite/by_example/unhygienic.rb
|
25
|
+
lib/rewrite/by_example/union_of_entities_sequence.rb
|
10
26
|
lib/rewrite/def_var.rb
|
11
27
|
lib/rewrite/evaluation_strategies.rb
|
12
28
|
lib/rewrite/prelude.rb
|
@@ -26,9 +42,12 @@ tasks/deployment.rake
|
|
26
42
|
tasks/environment.rake
|
27
43
|
tasks/website.rake
|
28
44
|
test/test_andand.rb
|
45
|
+
test/test_by_example.rb
|
29
46
|
test/test_call_by_name.rb
|
30
47
|
test/test_call_by_thunk.rb
|
48
|
+
test/test_call_splatted_by_name.rb
|
31
49
|
test/test_helper.rb
|
50
|
+
test/test_object_to_sequence_and_matcher.rb
|
32
51
|
test/test_rewriter_helpers.rb
|
33
52
|
test/test_syntax_let.rb
|
34
53
|
test/test_with.rb
|
@@ -0,0 +1,39 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
Dir["#{File.dirname(__FILE__)}/by_example/*.rb"].each do |element|
|
5
|
+
require File.expand_path(element)
|
6
|
+
end
|
7
|
+
|
8
|
+
module Rewrite
|
9
|
+
|
10
|
+
module ByExample
|
11
|
+
module ClassMethods
|
12
|
+
# Sigh, I know that 'clever' variable names are not helpful, but I *need* to remind
|
13
|
+
# myself why I am doing this: Using a ByExample to extract what we want from a sexp
|
14
|
+
# is eating my own dog food.
|
15
|
+
DOGFOODER = ObjectToMatcher.from_object(
|
16
|
+
s(:proc,
|
17
|
+
nil,
|
18
|
+
Bind.new(:body, AnyEntity.new)
|
19
|
+
)
|
20
|
+
)
|
21
|
+
|
22
|
+
def from(&proc)
|
23
|
+
body = DOGFOODER.unfold(proc.to_sexp) or raise "Don't know how to handle #{proc.to_ruby}"
|
24
|
+
SexpEntity.from(body)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.included(receiver)
|
34
|
+
receiver.extend ClassMethods
|
35
|
+
receiver.send :include, InstanceMethods
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) +'/entity_matcher.rb')
|
4
|
+
|
5
|
+
module Rewrite
|
6
|
+
|
7
|
+
module ByExample
|
8
|
+
|
9
|
+
# Matches any entity and optionally binds that entity's value to a name in the
|
10
|
+
# unfolded result. AnyEntity.new('name') binds whatever it matches to
|
11
|
+
# 'name'.
|
12
|
+
#
|
13
|
+
# Not isomorphic to BindSequence: if you want to match a sequence
|
14
|
+
# consisting of exactly one entity and bind that to 'name', you need
|
15
|
+
# to combine AnyEntity with LengthOne:
|
16
|
+
#
|
17
|
+
# LengthOne.new(AnyEntity.new('name))
|
18
|
+
class AnyEntity < EntityMatcher
|
19
|
+
|
20
|
+
def unfold (sexp)
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
'__'
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) +'/entity_matcher.rb')
|
4
|
+
|
5
|
+
module Rewrite
|
6
|
+
|
7
|
+
module ByExample
|
8
|
+
|
9
|
+
# Abstract class that matches any entity and optionally binds that entity's
|
10
|
+
# value to a name in the unfolded result. Useful for creating classes that bind
|
11
|
+
# and match simultaneously
|
12
|
+
class Bind < EntityMatcher
|
13
|
+
|
14
|
+
attr_accessor :name, :entity_matcher
|
15
|
+
|
16
|
+
def initialize(name, entity_matcher)
|
17
|
+
self.name = name.to_sym
|
18
|
+
self.entity_matcher = entity_matcher
|
19
|
+
end
|
20
|
+
|
21
|
+
def unfold (sexp)
|
22
|
+
{ self.name => sexp } if entity_matcher.unfold(sexp)
|
23
|
+
end
|
24
|
+
|
25
|
+
def fold (enum_of_bindings)
|
26
|
+
enum_of_bindings[self.name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
if name
|
31
|
+
"#{entity_matcher.to_s} => #{name.inspect}"
|
32
|
+
else
|
33
|
+
'*'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) +'/sequence.rb')
|
4
|
+
|
5
|
+
module Rewrite
|
6
|
+
|
7
|
+
module ByExample
|
8
|
+
|
9
|
+
# Matches any sequence of zero or more entities and binds the list
|
10
|
+
# to a name in the unfolded result. BindSequence.new('name') binds
|
11
|
+
# whatever sequence of entities it matches to 'name'.
|
12
|
+
#
|
13
|
+
# Not isomorphic to AnyEntity: AnyEntity matches one entity,
|
14
|
+
# BindSequence matches a sequence of entities. If you want to match
|
15
|
+
# a single entity that happens to consist
|
16
|
+
#
|
17
|
+
# LengthOne.new(Bind.new('name), AnyEntity.new)
|
18
|
+
class BindSequence < Sequence
|
19
|
+
attr_reader :unfolder_lambda, :name
|
20
|
+
|
21
|
+
def initialize(name)
|
22
|
+
@name = name.to_sym
|
23
|
+
@unfolder_lambda = lambda { |arr| { @name => s(*arr) } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def unfolders_by_length(length)
|
27
|
+
[ unfolder_lambda ]
|
28
|
+
end
|
29
|
+
|
30
|
+
def length_range
|
31
|
+
0..10000
|
32
|
+
end
|
33
|
+
|
34
|
+
def fold(enum_of_bindings)
|
35
|
+
enum_of_bindings[name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"__* => #{name.inspect}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Rewrite
|
4
|
+
|
5
|
+
module ByExample
|
6
|
+
|
7
|
+
# A Composition is an aggregation of Sequence. So where a LengthOne and
|
8
|
+
# a BindSequence are both leaves of a sequence tree, a Composition is a
|
9
|
+
# node.
|
10
|
+
class Composition < Sequence
|
11
|
+
attr_reader :sub_sequences, :length_range
|
12
|
+
|
13
|
+
def initialize(*sub_sequences)
|
14
|
+
@sub_sequences = sub_sequences#.map { |sub| Sequence.from_object(sub) }
|
15
|
+
@length_range = range_sum(@sub_sequences)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unfolders_by_length(length)
|
19
|
+
unfolders_by_length_helper(sub_sequences(), length)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
sub_sequences.map { |ss| ss.to_s }.join(', ')
|
24
|
+
end
|
25
|
+
|
26
|
+
def fold(enum_of_bindings)
|
27
|
+
sub_sequences.inject(s()) { |refolded, sub_sequence|
|
28
|
+
folded = sub_sequence.fold(enum_of_bindings)
|
29
|
+
if folded.nil?
|
30
|
+
s(*(refolded + [nil]))
|
31
|
+
elsif sub_sequence.is_a? LengthOne
|
32
|
+
s(*(refolded + [folded]))
|
33
|
+
else
|
34
|
+
s(*(refolded + folded))
|
35
|
+
end
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def merge_results(result1, result2)
|
42
|
+
if result1 && result2
|
43
|
+
result = result1.dup
|
44
|
+
result2.each do |key, value|
|
45
|
+
if result.include?(key)
|
46
|
+
return nil unless result[key].eql?(value)
|
47
|
+
else
|
48
|
+
result[key] = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def unfolders_by_length_helper(subs, length)
|
56
|
+
range_of_lengths = range_sum(subs)
|
57
|
+
if !(range_of_lengths === length)
|
58
|
+
[]
|
59
|
+
elsif subs.empty? # implied by the test above # TODO: short blog post about this
|
60
|
+
[]
|
61
|
+
elsif subs.length == 1
|
62
|
+
subs.first.unfolders_by_length(length)
|
63
|
+
else
|
64
|
+
head_subsequence = subs.first
|
65
|
+
tail_subsequences = subs[1..-1]
|
66
|
+
range_of_tail_lengths = range_sum(tail_subsequences)
|
67
|
+
required_head_lengths = (length - range_of_tail_lengths.end)..(length - range_of_tail_lengths.begin)
|
68
|
+
head_lengths_to_try = range_intersection(head_subsequence.length_range, required_head_lengths)
|
69
|
+
return [] if head_lengths_to_try.begin > head_lengths_to_try.end
|
70
|
+
head_lengths_to_try.inject([]) { |accumulated_unfolders_for_entire_composition, length_of_head_subsequence_to_try|
|
71
|
+
head_unfolders = head_subsequence.unfolders_by_length(length_of_head_subsequence_to_try)
|
72
|
+
tail_unfolders = unfolders_by_length_helper(tail_subsequences, length - length_of_head_subsequence_to_try)
|
73
|
+
accumulated_unfolders_for_entire_composition + head_unfolders.inject([]) { |accumulated_unfolders_for_entire_composition2, head_unfolder|
|
74
|
+
accumulated_unfolders_for_entire_composition2 + tail_unfolders.map { |tail_unfolder|
|
75
|
+
lambda { |arr|
|
76
|
+
head_portion, tail_portion = arr[0,length_of_head_subsequence_to_try], arr[length_of_head_subsequence_to_try..-1]
|
77
|
+
merge_results(
|
78
|
+
head_unfolder.call(head_portion),
|
79
|
+
tail_unfolder.call(tail_portion)
|
80
|
+
)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def range_intersection(range1, range2)
|
89
|
+
([range1.begin, range2.begin].max)..([range1.end, range2.end].min)
|
90
|
+
end
|
91
|
+
|
92
|
+
def range_sum(enumerable_of_sequences)
|
93
|
+
enumerable_of_sequences.inject(0..0) { |total_range, each_seq|
|
94
|
+
each_range = each_seq.length_range
|
95
|
+
(total_range.begin + each_range.begin)..(total_range.end + each_range.end)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Rewrite
|
4
|
+
|
5
|
+
module ByExample
|
6
|
+
|
7
|
+
# Matches a single something against a sexp
|
8
|
+
# such as an array or a literal symbol
|
9
|
+
class EntityMatcher
|
10
|
+
|
11
|
+
attr_accessor :predicate
|
12
|
+
|
13
|
+
def such_that!(&predicate_clause)
|
14
|
+
self.predicate = predicate_clause
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# takes a sexp and returns nil or an enumeration of
|
19
|
+
# bindings, typically as a hash. Note that the empty hash
|
20
|
+
# is truthy, and this is what we want: it means a match
|
21
|
+
# that doesn't perform any bindings
|
22
|
+
#
|
23
|
+
# Note the identity: if x.fold(enum_of_bindings) is not nil,
|
24
|
+
# then enum_of_bindings == x.unfold(x.fold(enum_of_bindings))
|
25
|
+
def unfold(sexp)
|
26
|
+
raise 'implemented by includer'
|
27
|
+
end
|
28
|
+
remove_method :unfold
|
29
|
+
|
30
|
+
# takes an enumeration of bindings and returns nil or a sexp.
|
31
|
+
#
|
32
|
+
# Note the identity: if x.unfold(sexp) is not nil, then
|
33
|
+
# sexp == x.fold(x.unfold(sexp))
|
34
|
+
def fold(enum_of_bindings)
|
35
|
+
raise 'implemented by includer'
|
36
|
+
end
|
37
|
+
remove_method :fold
|
38
|
+
|
39
|
+
# advice for unfold
|
40
|
+
def after_unfold
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Rewrite
|
4
|
+
|
5
|
+
module ByExample
|
6
|
+
|
7
|
+
# Natches a sequence containing one item. Wraps an entity matcher,
|
8
|
+
# with the wrapped matcher matching the one item.
|
9
|
+
#
|
10
|
+
# Confusing? Consider SymbolEntity.new(:foo). It matches a single
|
11
|
+
# entity, :foo. Whereas LengthOne.new(SymbolEntity.new(:foo)) matches
|
12
|
+
# a sequence of length one where that one thing happens to match :foo.
|
13
|
+
#
|
14
|
+
# This distinction is important because sequences can only consist of
|
15
|
+
# collections of other sequences. So the LengthOnes are leaves of
|
16
|
+
# the sequence trees.
|
17
|
+
class LengthOne < Sequence
|
18
|
+
|
19
|
+
attr_reader :matcher
|
20
|
+
|
21
|
+
def initialize(entity_matcher)
|
22
|
+
@matcher = entity_matcher
|
23
|
+
@lambda = lambda { |arr| entity_matcher.unfold(arr.first) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def unfolders_by_length(length)
|
27
|
+
if length == 1
|
28
|
+
[ @lambda ]
|
29
|
+
else
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def length_range
|
35
|
+
1..1
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"<#{matcher.to_s}>"
|
40
|
+
end
|
41
|
+
|
42
|
+
def fold(enum_of_bindings)
|
43
|
+
matcher.fold(enum_of_bindings)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) +'/any_entity.rb')
|
4
|
+
require File.expand_path(File.dirname(__FILE__) +'/bind.rb')
|
5
|
+
require File.expand_path(File.dirname(__FILE__) +'/sexp_entity.rb')
|
6
|
+
|
7
|
+
module Rewrite
|
8
|
+
|
9
|
+
module ByExample
|
10
|
+
|
11
|
+
class LiteralEntity < SexpEntity
|
12
|
+
|
13
|
+
def initialize(name = Rewrite.gensym)
|
14
|
+
super()
|
15
|
+
self.sequence = Composition.new(
|
16
|
+
LengthOne.new(ObjectToMatcher.from_object(:lit)),
|
17
|
+
LengthOne.new(Bind.new(name, AnyEntity.new))
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Rewrite
|
4
|
+
|
5
|
+
module ByExample
|
6
|
+
|
7
|
+
# Matches nil.
|
8
|
+
class NilEntity < EntityMatcher
|
9
|
+
|
10
|
+
def unfold (sexp)
|
11
|
+
{} if sexp.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def fold (enum_of_bindings)
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"nil"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|