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/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
|