heist 0.1.0 → 0.2.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/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