rewrite 0.2.0 → 0.3.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/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
|