rewrite 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,3 +10,5 @@
10
10
  * added Try to the Prelude
11
11
  * 5 minor enhancement:
12
12
  * called_by_name
13
+ * 6 minor enhancements:
14
+ * ByExample and Unhygienic rewriting
@@ -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