heist 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/parser/scheme.tt CHANGED
@@ -5,15 +5,25 @@ module Heist
5
5
  end
6
6
 
7
7
  rule cell
8
- ignore quote? ignore (list / atom) ignore <Cell>
8
+ ignore quote cell <QuotedCell> /
9
+ ignore (list / atom) ignore <Cell>
9
10
  end
10
11
 
11
12
  rule quote
12
13
  "'" / "`" / ",@" / ","
13
14
  end
14
15
 
16
+ rule dot
17
+ "."
18
+ end
19
+
15
20
  rule list
16
- ("(" cell* ")" / "[" cell* "]") <List>
21
+ ("(" cells ")" / "[" cells "]") <List>
22
+ end
23
+
24
+ rule cells
25
+ cell+ (dot space cell) /
26
+ cell* ignore
17
27
  end
18
28
 
19
29
  rule atom
@@ -33,7 +43,7 @@ module Heist
33
43
  end
34
44
 
35
45
  rule complex
36
- real "+" real "i" <Complex>
46
+ real:(real / integer) "+" imaginary:(real / integer) "i" <Complex>
37
47
  end
38
48
 
39
49
  rule real
@@ -53,15 +63,23 @@ module Heist
53
63
  end
54
64
 
55
65
  rule identifier
56
- (!delimiter .)+ <Identifier>
66
+ ((!delimiter .) (!delimiter .)+ / (!reserved .)) <Identifier>
57
67
  end
58
68
 
59
69
  rule digit
60
70
  [0-9]
61
71
  end
62
72
 
73
+ rule reserved
74
+ dot / delimiter
75
+ end
76
+
63
77
  rule delimiter
64
- [\(\)\[\]] / space
78
+ quote / paren / space
79
+ end
80
+
81
+ rule paren
82
+ [\(\)\[\]]
65
83
  end
66
84
 
67
85
  rule space
data/lib/repl.rb CHANGED
@@ -10,7 +10,7 @@ module Heist
10
10
  end
11
11
 
12
12
  def run
13
- Heist.info(@runtime)
13
+ puts @runtime.info
14
14
 
15
15
  Readline.completion_append_character = nil
16
16
  Readline.completion_proc = lambda do |prefix|
@@ -27,22 +27,29 @@ module Heist
27
27
  end
28
28
 
29
29
  loop do
30
- input = Readline.readline(prompt)
31
- exit if input.nil?
32
-
33
- push(input)
34
- tree = Heist.parse(@buffer * ' ')
35
- next if tree.nil?
36
-
37
- reset!
38
30
  begin
31
+ input = Readline.readline(prompt)
32
+ exit if input.nil?
33
+
34
+ push(input)
35
+ tree = Heist.parse(@buffer * "\n")
36
+ next if tree.nil?
37
+
38
+ reset!
39
39
  result = @scope.eval(tree)
40
40
  puts "; => #{ result }\n\n" unless result.nil?
41
+
41
42
  rescue Exception => ex
42
43
  return if SystemExit === ex
43
- puts "; [error] #{ ex.message }\n\n"
44
- puts "; backtrace: " + ex.backtrace.join("\n; ") +
45
- "\n\n" unless Heist::RuntimeError === ex
44
+ if Interrupt === ex
45
+ reset! and puts "\n\n"
46
+ else
47
+ puts "; [error] #{ ex.message }\n\n"
48
+ end
49
+
50
+ # debugging
51
+ # puts "; backtrace: " + ex.backtrace.join("\n; ") +
52
+ # "\n\n" unless Heist::RuntimeError === ex
46
53
  end
47
54
  end
48
55
  end
@@ -73,10 +80,6 @@ module Heist
73
80
  while code == code.gsub!(/\([^\(\)\[\]]*\)|\[[^\(\)\[\]]*\]/, ''); end
74
81
  calls = code.scan(/[\(\[](?:[^\(\)\[\]\s]*\s*)/).flatten
75
82
 
76
- calls.pop while calls.size > 1 and
77
- SPECIAL.include?(calls.last[1..-1].strip) and
78
- SPECIAL.include?(calls[-2][1..-1].strip)
79
-
80
83
  offsets = calls.inject([]) do |list, call|
81
84
  index = list.empty? ? 0 : list.last.last - @indent
82
85
  index = @indent + (line.index(call, index) || 0)
@@ -1,26 +1,48 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # A +Binding+ is a simple data object that couples an +Expression+ to a
5
+ # +Scope+ so the expression can be evaluated at some later time. Evaluation
6
+ # is typically memoized, though this is not always the case. <tt>Binding</tt>s
7
+ # are analogous to the notion of 'promises' in Scheme, though in Heist
8
+ # promises are implemented (as suggested in R5RS) using a <tt>(delay)</tt>
9
+ # macro that produces memoized closures. <tt>Binding</tt>s are used to
10
+ # implement lazy evaluation, and to maintain lexical scope in hygienic
11
+ # macro expansions by tying variables in the expansion to the lexical
12
+ # scope of the macro.
13
+ #
14
+ # The +Binding+ class mixes in the +Expression+ module purely as a flag
15
+ # to indicate to the evaluator that it can be evaluated.
16
+ #
4
17
  class Binding
5
18
  include Expression
6
19
 
7
20
  attr_reader :expression, :scope
8
21
 
22
+ # To initialize a +Binding+ supply an +Expression+ and a +Scope+ in
23
+ # which to evaluate it. An optional third argument states whether the
24
+ # evaluation should be memoized.
9
25
  def initialize(expression, scope, memoized = true)
10
26
  @expression = expression
11
27
  @scope = scope
12
28
  @memoized = !!memoized
13
29
  end
14
30
 
15
- def extract
31
+ # Evaluates the +Binding+ and returns the result. The +Expression+
32
+ # is only ever evaluated once if the +Binding+ has been memoized.
33
+ def force!
16
34
  return @value if defined?(@value) and @memoized
17
35
  @value = Heist.evaluate(@expression, @scope)
18
36
  end
19
37
 
38
+ # This method is provided as a convenience so that a +Binding+ may
39
+ # be treated like any other expression during evaluation. All it
40
+ # does is return the result of calling <tt>force!</tt>.
20
41
  def eval(scope)
21
- extract
42
+ force!
22
43
  end
23
44
 
45
+ # Returns a string representation of the binding's +Expression+.
24
46
  def to_s
25
47
  @expression.to_s
26
48
  end
@@ -1,19 +1,40 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # The +Continuation+ class encapsulates the first-class continuations as
5
+ # generated by the Scheme <tt>(call/cc)</tt> procedure. It inherits from
6
+ # +Function+ so as to be recognised as a callable object by the +Stack+
7
+ # execution model. Each +Continuation+ contains a snapshot of the state
8
+ # of the +Stack+ at the moment it was created, and this state is restored,
9
+ # overriding whatever state is then in effect, when the +Continuation+ is
10
+ # called.
4
11
  class Continuation < Function
12
+
13
+ # A +Continuation+ is initialized with a +Stack+ object, which it saves a
14
+ # snapshot of. The topmost frame on the stack will typically be a call
15
+ # to <tt>(call/cc)</tt> or some other +Continuation+-generating function,
16
+ # and is thus discarded as this call site marks the hole that should be
17
+ # filled when the +Continuation+ is called and the saved stack is resumed.
5
18
  def initialize(stack)
6
19
  @stack = stack.copy(false)
7
20
  @target = stack.last.target
8
21
  end
9
22
 
23
+ # Calls the +Continuation+ with the current +Scope+ and a +Cons+ list of
24
+ # parameters to the continuation. Returns a copy of the saved +Stack+
25
+ # with the innermost hole filled with the value of the parameter to the
26
+ # continuation call. The currently running +Stack+ will see this return
27
+ # value as an instruction to throw out the current state of the stack and
28
+ # replace it with the stack returned by this method before continuing
29
+ # execution.
10
30
  def call(scope, cells)
11
- filler = Heist.evaluate(cells.first, scope)
12
31
  stack = @stack.copy
13
- stack.fill!(@target, filler)
32
+ stack.fill!(@target, cells.car)
14
33
  stack
15
34
  end
16
35
 
36
+ # Returns a string representation of the +Continuation+ for console
37
+ # output.
17
38
  def to_s
18
39
  "#<continuation>"
19
40
  end
@@ -1,53 +1,154 @@
1
1
  module Heist
2
2
  class Runtime
3
3
 
4
+ # There are several types of callable object in Heist: +Function+, +Syntax+,
5
+ # +Macro+ and +Continuation+. For convenience of type detection, +Function+
6
+ # is treated as a the base class for all these, though they all typically
7
+ # override <tt>Function</tt>'s core methods. The intention is to treat all
8
+ # these types as uniformly as possible, though this does introduce some
9
+ # API problems which still need resolving. In any case, the current state
10
+ # of things:
11
+ #
12
+ # ============
13
+ # | Function |
14
+ # ============
15
+ # |
16
+ # ---------------------------
17
+ # | |
18
+ # ========== ================
19
+ # | Syntax | | Continuation |
20
+ # ========== ================
21
+ # |
22
+ # =========
23
+ # | Macro |
24
+ # =========
25
+ #
26
+ # Heist is designed to be reasonably modular in that none of Scheme's
27
+ # special forms or built-in functions are mentioned in the parser; instead
28
+ # they are all implemented as first-class functions and stored in the
29
+ # global scope when you initialize a Heist +Runtime+. The Heist runtime
30
+ # does implement a fair amount of Scheme-specific functionality but you're
31
+ # not forced to use any of it: at its core it is designed as a general
32
+ # processor for s-expressions and could be used as a backend for any
33
+ # Lisp-style language. The only hard-and-fast rule is that, to evaluate
34
+ # an expression, you evaluate its first element, and call the resulting
35
+ # function with the remaining elements.
36
+ #
37
+ # The Heist callable types -- +Function+, +Syntax+, +Macro+ and
38
+ # +Continuation+ -- differ primarily in terms of what they do with their
39
+ # arguments when called. All are passed the current +Scope+ and a +Cons+
40
+ # list containing the rest of the current expression when they are
41
+ # called; it is up to the type of function being called what to do with
42
+ # the expression.
43
+ #
44
+ # The +Function+ class is used for representing the basic idea of a Lisp
45
+ # procedure; a reusable block of code that can accept zero, one or more
46
+ # arguments, return output values and cause side effects on the state of
47
+ # the running program. Heist functions can be implemented using Ruby
48
+ # blocks or Scheme lists.
49
+ #
50
+ # For information on other callable types, see +Syntax+, +Macro+ and
51
+ # +Continuation+.
52
+ #
4
53
  class Function
5
54
  attr_reader :body, :name
6
55
 
7
- def initialize(scope, formals = [], body = nil, &block)
8
- @scope = scope
9
- @body = body ? List.from(body) : block
10
- @formals = formals.map { |id| id.to_s }
11
- @lazy = scope.runtime.lazy?
12
- @eager = !scope.runtime.stackless?
56
+ # A +Function+ can be initialized in one of two ways. Both types
57
+ # require a +Scope+ as the first parameter: this is the lexical
58
+ # scope in which the +Function+ is defined. The other parameters
59
+ # should consist either of a Ruby block that accepts the procedure's
60
+ # arguments and returns its result:
61
+ #
62
+ # env = Scope.new
63
+ # env['plus'] = Function.new(env) { |a,b| a + b }
64
+ #
65
+ # Or, the parameters should be a list of formal argument names for
66
+ # the procedure, and a list containing zero or more Scheme expressions
67
+ # that make up the procedure's body. If the list of formals is an
68
+ # improper list, the tail name will be assigned a list of any
69
+ # parameters remaining after all the preceeding formals have been
70
+ # assigned values. If the list of formals is not a list but a single
71
+ # identifier, that identifier will be assigned a list of all the
72
+ # parameters when the procedure is called.
73
+ #
74
+ def initialize(scope, formals = nil, body = nil, &block)
75
+ @formals, @rest = [], formals
76
+ if Cons === formals
77
+ tail = formals.tail.cdr
78
+ @formals = formals.map { |id| id.to_s }
79
+ @rest = (Identifier === tail) ? tail : nil
80
+ end
81
+ @scope = scope
82
+ @body = body || block
83
+ @lazy = scope.runtime.lazy?
84
+ @eager = !scope.runtime.stackless?
13
85
  end
14
86
 
87
+ # Assigns a name to the +Function+, used primarily for console output.
88
+ # A +Function+ keeps the first name it is bound to. This is really only
89
+ # of use to REPL users; functions are first-class data and do not
90
+ # need names in order to operate correctly.
15
91
  def name=(name)
16
92
  @name ||= name.to_s
17
93
  end
18
94
 
95
+ # Calls the +Function+ with a calling +Scope+ (in which to evaluate the
96
+ # parameters), and a +Cons+ list containing the parameter expressions.
97
+ # In lazy mode, the parameter expressions are wrapped in +Binding+
98
+ # objects for later execution. If using the continuation-supporting
99
+ # +Stack+ interpreter (as indicated by the <tt>@eager</tt> flag), the
100
+ # parameters will already have been evaluated and should not be
101
+ # re-evaluated lest we try to interpret data lists as code. Returns the
102
+ # result of calling +apply+ with the evaluated parameters.
19
103
  def call(scope, cells)
20
- params, closure = [], Scope.new(@scope)
21
- cells.each_with_index do |arg, i|
22
- params[i] = closure[@formals[i]] = lazy? ?
23
- Binding.new(arg, scope) :
24
- (@eager ? arg : Heist.evaluate(arg, scope))
104
+ params = cells.map do |arg|
105
+ lazy? ? Binding.new(arg, scope) :
106
+ (@eager ? arg : Heist.evaluate(arg, scope))
25
107
  end
108
+ apply(params)
109
+ end
110
+
111
+ # Returns the result of applying the +Function+ to the given collection
112
+ # of +params+. If the procedure is primitive, this will return a value
113
+ # immediately. If it's a Scheme function, this simply binds the +params+
114
+ # to the procedure's formal arguments and returns a +Body+ with the
115
+ # procedure's body expressions and the newly allocated +Scope+. We use
116
+ # a trampoline to call <tt>Function</tt>s iteratively to allow tail
117
+ # call optimisation. (See +Stackless+, +Stack+, +Frame+ and +Body+ for
118
+ # more information.)
119
+ def apply(params)
26
120
  return @body.call(*params) if primitive?
121
+ closure = Scope.new(@scope)
122
+ idx = 0
123
+ @formals.each do |name|
124
+ closure[name] = params[idx]
125
+ idx += 1
126
+ end
127
+ closure[@rest] = Cons.construct(params[idx..-1]) if @rest
27
128
  Body.new(@body, closure)
28
129
  end
29
130
 
131
+ # Returns +true+ iff the +Function+ is a primitive, that is to say it
132
+ # is implemented in Ruby rather than Scheme.
30
133
  def primitive?
31
134
  Proc === @body
32
135
  end
33
136
 
137
+ # Returns +true+ iff the +Function+ is lazy, i.e. it does not
138
+ # evaluate its arguments unless it needs their values. Only non-
139
+ # primitive functions can be lazy since we have no way of telling
140
+ # when Ruby needs to access a value. Besides, primitive functions
141
+ # will typically use all their parameters.
34
142
  def lazy?
35
143
  @lazy and not primitive?
36
144
  end
37
145
 
146
+ # Returns a string placeholder for the +Function+, containing its
147
+ # name if it has one.
38
148
  def to_s
39
149
  "#<procedure:#{ @name }>"
40
150
  end
41
- end
42
-
43
- class Syntax < Function
44
- def call(scope, cells)
45
- @body.call(scope, *cells)
46
- end
47
-
48
- def to_s
49
- "#<syntax:#{ @name }>"
50
- end
151
+ alias :inspect :to_s
51
152
  end
52
153
 
53
154
  end
@@ -1,168 +1,214 @@
1
- # Quotations taken from the R5RS spec
2
- # http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html
3
1
  module Heist
4
2
  class Runtime
5
3
 
4
+ # The +Macro+ class is a type of +Function+ (it inherits from +Syntax+ to
5
+ # let the evaluator know it consumes syntax rather than values) that is
6
+ # used to represent non-primitive syntax as defined by the Scheme
7
+ # <tt>(syntax-rules)</tt> macro system. Calling a +Macro+ involves passing
8
+ # it an input +Expression+, which it will either convert into another
9
+ # +Expression+ according to user-defined rules, or it will raise an
10
+ # exception if the input is syntactically invalid. On success, a +Macro+
11
+ # will return an +Expansion+, which contains an +Expression+ that can be
12
+ # directly evaluated or expanded further.
13
+ #
14
+ # Heist does not have distinct macro expansion and runtime phases; its
15
+ # macros are first-class runtime objects and all expansion takes place
16
+ # as macros are encountered while the program is running. Once a macro
17
+ # call is processed, the expansion is inlined into the source tree,
18
+ # replacing the macro call to avoid further unnecessary expansions.
19
+ #
20
+ # +Macro+ uses several auxiliary classes to do its work. See +Matches+,
21
+ # +Tree+ and +Expansion+ for more information.
22
+ #
6
23
  class Macro < Syntax
7
- ELLIPSIS = '...'
8
24
 
9
- %w[expansion splice matches].each do |klass|
25
+ # The ellipsis identifier, used to indicate repeated patterns
26
+ ELLIPSIS = Identifier.new('...')
27
+
28
+ # Array of reserved symbols that cannot be used as pattern vars
29
+ RESERVED = %w[_ ...]
30
+
31
+ %w[expansion matches tree].each do |klass|
10
32
  require RUNTIME_PATH + 'callable/macro/' + klass
11
33
  end
12
34
 
13
- def initialize(scope, *args)
14
- super
15
- @hygienic = scope.runtime.hygienic?
35
+ # Takes an s-expression and returns an array of the pattern variables
36
+ # it contains. The optional second argument should be an array, and
37
+ # specifies a list of names to exclude, for example to exclude the formal
38
+ # keywords of a macro transformer. The final argument is for internal use
39
+ # only; we call this method recursively but pass down a single array to
40
+ # push results onto to avoid lots of array joins.
41
+ def self.pattern_vars(pattern, excluded = [], results = [])
42
+ return results if Cons::NULL == pattern
43
+ case pattern
44
+ when Identifier then
45
+ name = pattern.to_s
46
+ return if excluded.include?(name) or RESERVED.include?(name)
47
+ results << name unless results.include?(name)
48
+ when Cons then
49
+ tail = pattern.each { |cell| pattern_vars(cell, excluded, results) }
50
+ pattern_vars(tail.cdr, excluded, results)
51
+ end
52
+ results
16
53
  end
17
54
 
55
+ # Calls the +Macro+ with the current +Scope+ and the +Cons+ list of the
56
+ # rest of the expression, i.e. the syntax tree for the macro to operate
57
+ # on. Returns an +Expansion+ if the given syntax is found to be valid,
58
+ # otherwise raises an exception.
18
59
  def call(scope, cells)
19
- @calling_scope = scope
20
- rule, matches = *rule_for(cells)
21
-
22
- return Expansion.new(expand_template(rule.last, matches)) if rule
23
-
24
- # TODO include macro name in error message,
25
- # and be more specific about which pattern failed
26
- input = cells.map { |c| c.to_s } * ' '
60
+ rule, matches = *rule_for(cells, scope)
61
+ return Expansion.new(@scope, scope, rule.cdr.car, matches) if rule
27
62
  raise SyntaxError.new(
28
- "Bad syntax: no macro expansion found for (#{@name} #{input})")
63
+ "Bad syntax: no macro expansion found for #{Cons.new(@name, cells)}")
29
64
  end
30
65
 
66
+ # Returns a string placeholder for the +Macro+, containing its
67
+ # name if it has one.
31
68
  def to_s
32
69
  "#<macro:#{ @name }>"
33
70
  end
34
71
 
35
- private
36
-
37
- def rule_for(cells)
72
+ # Takes a +Cons+ expression and a +Scope+ (required for determining
73
+ # the binding of macro keywords), and returns a tuple containing an
74
+ # expansion template and a set of pattern match data for the first
75
+ # rule in the macro that matches the input. If no such rule is found,
76
+ # returns +nil+.
77
+ def rule_for(cells, scope)
38
78
  @body.each do |rule|
39
- matches = rule_matches(rule.first.rest, cells)
79
+ matches = rule_matches(scope, rule.car.cdr, cells)
40
80
  return [rule, matches] if matches
41
81
  end
42
- nil
82
+ return nil
43
83
  end
44
84
 
85
+ # Takes a +Scope+ (the scope from which the macro is being called,
86
+ # required for determining keyword bindings), an +Expression+
87
+ # representing a macro pattern, and an input +Expression+ from
88
+ # the expression the macro was called with. If the pattern matches
89
+ # the input, a +Matches+ object is returned, otherwise we return
90
+ # +nil+. The +matches+ and +depth+ arguments are for internal use
91
+ # only and are passed down as the match algorithm recurses over
92
+ # the pattern and input expressions.
93
+ #
94
+ # +matches+ is a +Matches+ instance that stores data about which
95
+ # input expressions correspond to which pattern variables, and how
96
+ # often they repeat. +depth+ indicates the repetition depth, that
97
+ # is how many ellipses appear following the current pattern.
98
+ #
99
+ # From the R5RS spec
100
+ # http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html
101
+ #
45
102
  # More formally, an input form F matches a pattern P if and only if:
46
- #
47
- # * P is a non-literal identifier; or
48
- # * P is a literal identifier and F is an identifier with the
49
- # same binding; or
50
- # * P is a list (P1 ... Pn) and F is a list of n forms that match
51
- # P1 through Pn, respectively; or
52
- # * P is an improper list (P1 P2 ... Pn . Pn+1) and F is a list
53
- # or improper list of n or more forms that match P1 through Pn,
54
- # respectively, and whose nth 'cdr' matches Pn+1; or
55
- # * P is of the form (P1 ... Pn Pn+1 <ellipsis>) where <ellipsis>
56
- # is the identifier '...' and F is a proper list of at least n forms,
57
- # the first n of which match P1 through Pn, respectively, and
58
- # each remaining element of F matches Pn+1; or
59
- # * P is a vector of the form #(P1 ... Pn) and F is a vector of n
60
- # forms that match P1 through Pn; or
61
- # * P is of the form #(P1 ... Pn Pn+1 <ellipsis>) where <ellipsis>
62
- # is the identifier '...' and F is a vector of n or more forms the
63
- # first n of which match P1 through Pn, respectively, and each
64
- # remaining element of F matches Pn+1; or
65
- # * P is a datum and F is equal to P in the sense of the 'equal?'
66
- # procedure.
67
- #
103
+ #
104
+ # * P is a non-literal identifier; or
105
+ # * P is a literal identifier and F is an identifier with the
106
+ # same binding; or
107
+ # * P is a list (P1 ... Pn) and F is a list of n forms that match
108
+ # P1 through Pn, respectively; or
109
+ # * P is an improper list (P1 P2 ... Pn . Pn+1) and F is a list
110
+ # or improper list of n or more forms that match P1 through Pn,
111
+ # respectively, and whose nth 'cdr' matches Pn+1; or
112
+ # * P is of the form (P1 ... Pn Pn+1 <ellipsis>) where <ellipsis>
113
+ # is the identifier '...' and F is a proper list of at least n forms,
114
+ # the first n of which match P1 through Pn, respectively, and
115
+ # each remaining element of F matches Pn+1; or
116
+ # * P is a vector of the form #(P1 ... Pn) and F is a vector of n
117
+ # forms that match P1 through Pn; or
118
+ # * P is of the form #(P1 ... Pn Pn+1 <ellipsis>) where <ellipsis>
119
+ # is the identifier '...' and F is a vector of n or more forms the
120
+ # first n of which match P1 through Pn, respectively, and each
121
+ # remaining element of F matches Pn+1; or
122
+ # * P is a datum and F is equal to P in the sense of the 'equal?'
123
+ # procedure.
124
+ #
68
125
  # It is an error to use a macro keyword, within the scope of its
69
126
  # binding, in an expression that does not match any of the patterns.
70
- #
71
- def rule_matches(pattern, input, matches = Matches.new, depth = 0)
127
+ #
128
+ def rule_matches(scope, pattern, input, matches = nil, depth = 0)
129
+ matches ||= Matches.new(pattern, @formals)
72
130
  case pattern
73
131
 
74
- when List then
75
- return nil unless List === input
76
- idx = 0
77
- pattern.each_with_index do |token, i|
78
- followed_by_ellipsis = (pattern[i+1].to_s == ELLIPSIS)
79
- dx = followed_by_ellipsis ? 1 : 0
80
-
81
- matches.depth = depth + dx
82
- next if token.to_s == ELLIPSIS
132
+ when Cons then
133
+ # If pattern is NULL, the input must also be NULL
134
+ return (Cons::NULL == input ? matches : nil) if pattern.null?
135
+
136
+ # Fail if the pattern is a list and the input is not
137
+ return nil unless Cons === input
138
+
139
+ # Iterate over the pattern, consuming input as we go
140
+ pattern_pair, input_pair = pattern, input
141
+ skip = lambda { pattern_pair = pattern_pair.cdr }
142
+
143
+ while Cons === pattern_pair and not pattern_pair.null?
144
+ token = pattern_pair.car
83
145
 
84
- consume = lambda { rule_matches(token, input[idx], matches, depth + dx) }
85
- return nil unless value = consume[] or followed_by_ellipsis
86
- next unless value
87
- idx += 1
146
+ # Skip the current pattern token if it's an ellipsis
147
+ skip[] and next if token == ELLIPSIS
88
148
 
89
- idx += 1 while idx < input.size and
90
- followed_by_ellipsis and
91
- consume[]
92
- end
93
- return nil unless idx == input.size
94
-
95
- when Identifier then
96
- return (pattern.to_s == input.to_s) if @formals.include?(pattern.to_s)
97
- matches.put(pattern, input)
98
- return nil if input.nil?
99
-
100
- else
101
- return pattern == input ? true : nil
102
- end
103
- matches
104
- end
105
-
106
- # When a macro use is transcribed according to the template of the
107
- # matching <syntax rule>, pattern variables that occur in the template
108
- # are replaced by the subforms they match in the input. Pattern variables
109
- # that occur in subpatterns followed by one or more instances of the
110
- # identifier '...' are allowed only in subtemplates that are followed
111
- # by as many instances of '...'. They are replaced in the output by all
112
- # of the subforms they match in the input, distributed as indicated. It
113
- # is an error if the output cannot be built up as specified.
114
- #
115
- # Identifiers that appear in the template but are not pattern variables
116
- # or the identifier '...' are inserted into the output as literal
117
- # identifiers. If a literal identifier is inserted as a free identifier
118
- # then it refers to the binding of that identifier within whose scope
119
- # the instance of 'syntax-rules' appears. If a literal identifier is
120
- # inserted as a bound identifier then it is in effect renamed to prevent
121
- # inadvertent captures of free identifiers.
122
- #
123
- def expand_template(template, matches, depth = 0, inspection = false)
124
- case template
125
-
126
- when List then
127
- result = List.new
128
- template.each_with_index do |cell, i|
129
- followed_by_ellipsis = (template[i+1].to_s == ELLIPSIS)
149
+ # Increment the repetition depth if the next pattern
150
+ # token is an ellipsis, and inform the +Matches+ object
151
+ # that the pattern vars in the current pattern have
152
+ # hit a repetition boundary. Note we do not increment
153
+ # +depth+ itself since this would persist for the remaining
154
+ # tokens in the pattern after we get past the ellipsis.
155
+ followed_by_ellipsis = (pattern_pair.cdr.car == ELLIPSIS rescue false)
130
156
  dx = followed_by_ellipsis ? 1 : 0
131
157
 
132
- matches.inspecting(depth + 1) if followed_by_ellipsis and
133
- not inspection
158
+ matches.descend!(Macro.pattern_vars(token, @formals),
159
+ depth + dx) if followed_by_ellipsis
134
160
 
135
- if cell.to_s == ELLIPSIS and not inspection
136
- repeater = template[i-1]
137
- matches.expand! { result << expand_template(repeater, matches, depth + 1) }
138
- matches.depth = depth
139
- else
140
- inspect = inspection || (followed_by_ellipsis && depth + 1)
141
- value = expand_template(cell, matches, depth + dx, inspect)
142
- result << value unless inspect
161
+ # Set up a closure to consume input using the current
162
+ # pattern expression. Calls +rule_matches+ with the
163
+ # current scope, pattern, input, and +Matches+ object.
164
+ consume = lambda do
165
+ Cons === input_pair and not input_pair.null? and
166
+ rule_matches(scope, token, input_pair.car, matches, depth + dx)
143
167
  end
168
+
169
+ # If the next pattern token is not an ellipsis, fail
170
+ # unless the pattern token matches the input token.
171
+ #
172
+ # If the next token is an ellipsis, consume input
173
+ # using the current pattern until the pattern no
174
+ # longer matches the current input
175
+ #
176
+ return nil unless consume[] or followed_by_ellipsis
177
+ input_pair = input_pair.cdr
178
+ input_pair = input_pair.cdr while followed_by_ellipsis and consume[]
179
+
180
+ skip[]
144
181
  end
145
- result
182
+
183
+ # We're done iterating over the pattern, so the current
184
+ # pattern token will be NULL or some non-Cons object (if
185
+ # the pattern is an improper list). Fail unless the remaining
186
+ # input matches this object.
187
+ return nil unless rule_matches(scope, pattern_pair, input_pair, matches, depth)
146
188
 
189
+ # If the pattern is a formal keyword for the macro (a
190
+ # 'literal identifier' in the terms of the spec), return
191
+ # a boolean indicating whether the input is an identifier
192
+ # with the same binding, that is to say the two identifiers
193
+ # refer to the same location in memory (or both refer to
194
+ # no location). If it's a normal pattern variable, store
195
+ # the current input, whatever it is, in the +matches+.
147
196
  when Identifier then
148
- return matches.get(template) if matches.defined?(template)
149
- return Identifier.new(template) unless @hygienic
150
-
151
- @scope.defined?(template) ?
152
- Binding.new(template, @scope, false) :
153
- rename(template)
197
+ if @formals.include?(pattern.to_s)
198
+ return pattern == input && @scope.innermost_binding(pattern) ==
199
+ scope.innermost_binding(input)
200
+ else
201
+ matches.put(pattern, input)
202
+ end
154
203
 
204
+ # If all above type checks on the pattern fail, assume the
205
+ # pattern is literal data and make sure the input matches.
155
206
  else
156
- template
207
+ return pattern == input ? matches : nil
157
208
  end
209
+ matches
158
210
  end
159
211
 
160
- def rename(id)
161
- return id unless @calling_scope.defined?(id)
162
- i = 1
163
- i += 1 while @calling_scope.defined?("#{id}#{i}")
164
- Identifier.new("#{id}#{i}")
165
- end
166
212
  end
167
213
 
168
214
  end