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/History.txt +17 -0
- data/Manifest.txt +23 -19
- data/README.txt +84 -52
- data/lib/builtin/library.scm +208 -10
- data/lib/builtin/primitives.rb +154 -92
- data/lib/builtin/syntax.scm +22 -5
- data/lib/heist.rb +49 -17
- data/lib/parser/nodes.rb +47 -24
- data/lib/parser/ruby.rb +29 -0
- data/lib/parser/scheme.rb +455 -143
- data/lib/parser/scheme.tt +23 -5
- data/lib/repl.rb +19 -16
- data/lib/runtime/binding.rb +24 -2
- data/lib/runtime/callable/continuation.rb +23 -2
- data/lib/runtime/callable/function.rb +122 -21
- data/lib/runtime/callable/macro.rb +169 -123
- data/lib/runtime/callable/macro/expansion.rb +137 -2
- data/lib/runtime/callable/macro/matches.rb +125 -41
- data/lib/runtime/callable/macro/tree.rb +141 -0
- data/lib/runtime/callable/syntax.rb +44 -0
- data/lib/runtime/data/cons.rb +234 -0
- data/lib/runtime/data/expression.rb +15 -6
- data/lib/runtime/data/identifier.rb +19 -2
- data/lib/runtime/frame.rb +102 -35
- data/lib/runtime/runtime.rb +44 -19
- data/lib/runtime/scope.rb +145 -30
- data/lib/runtime/stack.rb +103 -1
- data/lib/runtime/stackless.rb +48 -6
- data/test/arithmetic.scm +11 -2
- data/test/continuations.scm +16 -2
- data/test/equivalence.scm +34 -0
- data/test/functional.scm +4 -0
- data/test/lists.scm +78 -0
- data/test/macro-helpers.scm +1 -0
- data/test/macros.scm +111 -24
- data/test/numbers.scm +30 -8
- data/test/test_heist.rb +67 -12
- metadata +25 -21
- data/lib/builtin/syntax.rb +0 -166
- data/lib/runtime/callable/macro/splice.rb +0 -56
- data/lib/runtime/data/list.rb +0 -36
data/lib/parser/scheme.tt
CHANGED
@@ -5,15 +5,25 @@ module Heist
|
|
5
5
|
end
|
6
6
|
|
7
7
|
rule cell
|
8
|
-
ignore quote
|
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
|
-
("("
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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)
|
data/lib/runtime/binding.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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,
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
rule, matches
|
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
|
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
|
-
|
36
|
-
|
37
|
-
|
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.
|
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
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
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 =
|
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
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
133
|
-
|
158
|
+
matches.descend!(Macro.pattern_vars(token, @formals),
|
159
|
+
depth + dx) if followed_by_ellipsis
|
134
160
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
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
|