risp 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +65 -0
- data/Rakefile +65 -0
- data/bin/risp +79 -0
- data/lib/risp.rb +522 -0
- data/test/test_risp_reader.rb +25 -0
- metadata +82 -0
data/README
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
= Risp
|
2
|
+
|
3
|
+
Risp (Ruby lisp) is a quite tiny, toy interpreter of lisp.
|
4
|
+
|
5
|
+
For almost people, it seems not to be appropriate Lisp --
|
6
|
+
it lacks even a macro!!
|
7
|
+
|
8
|
+
Features: variables are bound to Lisp-1 namespace, there's no
|
9
|
+
special-form which is appropriate `defun'. It means a scheme's
|
10
|
+
way is invalid. Please use lambda.
|
11
|
+
|
12
|
+
(define (hoge x) ;; raise type error for list
|
13
|
+
(+ x 1))
|
14
|
+
|
15
|
+
There's no package system, macro and beautiful API.
|
16
|
+
|
17
|
+
|
18
|
+
+ Types
|
19
|
+
* integer
|
20
|
+
* string
|
21
|
+
* t
|
22
|
+
* nil
|
23
|
+
* symbol
|
24
|
+
* list
|
25
|
+
* function
|
26
|
+
|
27
|
+
+ Builtin functions and special-forms
|
28
|
+
* car
|
29
|
+
* cdr
|
30
|
+
* cons
|
31
|
+
* eq
|
32
|
+
* atom
|
33
|
+
* quote
|
34
|
+
* cond
|
35
|
+
* define
|
36
|
+
* lambda
|
37
|
+
* +
|
38
|
+
|
39
|
+
|
40
|
+
= Install
|
41
|
+
|
42
|
+
$ gem install risp
|
43
|
+
|
44
|
+
|
45
|
+
= Usage
|
46
|
+
|
47
|
+
$ risp somefile.l
|
48
|
+
|
49
|
+
$ risp -i
|
50
|
+
|
51
|
+
|
52
|
+
= Dependence
|
53
|
+
|
54
|
+
Risp is developed on:
|
55
|
+
ruby 1.9.1p429 (2010-07-02 revision 28523) [i386-mswin32]
|
56
|
+
|
57
|
+
|
58
|
+
= Lisence
|
59
|
+
|
60
|
+
Under the modified BSD lisence.
|
61
|
+
|
62
|
+
|
63
|
+
---
|
64
|
+
|
65
|
+
arikui <arikui.ruby@gmail.com>
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require_relative 'lib/risp'
|
6
|
+
|
7
|
+
LIBS = FileList["lib/*.rb"]
|
8
|
+
TESTS = FileList["test/test_*.rb"]
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
gemspec = Gem::Specification.new do |s|
|
13
|
+
s.name = %q{risp}
|
14
|
+
s.version = Risp.version
|
15
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
16
|
+
s.authors = ["arikui"]
|
17
|
+
s.date = %q{2010-11-19}
|
18
|
+
s.description = %q{A toy lisp interpreter.}
|
19
|
+
s.email = %q{arikui.ruby@gmail.com}
|
20
|
+
s.executable = 'risp'
|
21
|
+
s.extra_rdoc_files = ["README"]
|
22
|
+
s.files = ["README",
|
23
|
+
"Rakefile",
|
24
|
+
"bin/risp",
|
25
|
+
*TESTS, *LIBS]
|
26
|
+
s.homepage = %q{http://wiki.github.com/arikui1911/risp}
|
27
|
+
s.rdoc_options = ["--title", "risp documentation",
|
28
|
+
"--charset", "utf-8",
|
29
|
+
"--opname", "index.html",
|
30
|
+
"--line-numbers",
|
31
|
+
"--main", "README",
|
32
|
+
"--inline-source",
|
33
|
+
"--exclude", "^(examples|extras)/"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubyforge_project = %q{risp}
|
36
|
+
s.rubygems_version = %q{1.3.7}
|
37
|
+
s.summary = %q{A toy lisp interpreter; provides `pure-lisp' functions and Lisp-1 namespaces. Currently it lacks package system and macro(!!).}
|
38
|
+
s.test_files = TESTS.to_a
|
39
|
+
if s.respond_to? :specification_version then
|
40
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
41
|
+
s.specification_version = 3
|
42
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
43
|
+
else
|
44
|
+
end
|
45
|
+
else
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
gem_task = Rake::GemPackageTask.new(gemspec)
|
50
|
+
gem_task.define
|
51
|
+
|
52
|
+
desc "Upload the gem file #{gem_task.gem_file}"
|
53
|
+
task 'cutter' => 'gem' do |t|
|
54
|
+
gem "push #{gem_task.package_dir}/#{gem_task.gem_file}"
|
55
|
+
end
|
56
|
+
|
57
|
+
Rake::TestTask.new do |t|
|
58
|
+
t.warning = true
|
59
|
+
end
|
60
|
+
|
61
|
+
Rake::RDocTask.new do |t|
|
62
|
+
t.rdoc_dir = 'doc'
|
63
|
+
t.rdoc_files = LIBS.include("README")
|
64
|
+
t.options.push '-S', '-N'
|
65
|
+
end
|
data/bin/risp
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) 2010, arikui
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
# Redistribution and use in source and binary forms, with or
|
7
|
+
# without modification, are permitted provided that the
|
8
|
+
# following conditions are met:
|
9
|
+
|
10
|
+
# * Redistributions of source code must retain the above
|
11
|
+
# copyright notice, this list of conditions and the
|
12
|
+
# following disclaimer.
|
13
|
+
# * Redistributions in binary form must reproduce the above
|
14
|
+
# copyright notice, this list of conditions and the following
|
15
|
+
# disclaimer in the documentation and/or other materials
|
16
|
+
# provided with the distribution.
|
17
|
+
# * Neither the name of the arikui-risp nor the names of its
|
18
|
+
# contributors may be used to endorse or promote products
|
19
|
+
# derived from this software without specific prior written
|
20
|
+
# permission.
|
21
|
+
|
22
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
23
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
24
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
25
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
26
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
27
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
28
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
29
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
30
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
31
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
32
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
33
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
34
|
+
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
35
|
+
|
36
|
+
require 'risp'
|
37
|
+
require 'optparse'
|
38
|
+
|
39
|
+
Version = Risp.version
|
40
|
+
interactive_p = false
|
41
|
+
|
42
|
+
ARGV.options do |o|
|
43
|
+
o.banner = "Usage: #{o.program_name} [-options] [source-file]"
|
44
|
+
o.on '-i', '--interactive', 'turn on interactive mode' do
|
45
|
+
interactive_p = true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_input(file, &block)
|
50
|
+
file ? File.open(file, &block) : yield($stdin)
|
51
|
+
end
|
52
|
+
|
53
|
+
ARGV.parse!
|
54
|
+
|
55
|
+
source = interactive_p ? nil : ARGV.shift
|
56
|
+
interpreter = Risp::Interpreter.new
|
57
|
+
|
58
|
+
with_input(source) do |f|
|
59
|
+
reader = Risp::Reader.new(f)
|
60
|
+
loop do
|
61
|
+
begin
|
62
|
+
if interactive_p
|
63
|
+
print "Risp> "
|
64
|
+
sexp = reader.read
|
65
|
+
break if sexp.serialize == 'QUIT' || reader.eof?
|
66
|
+
r = sexp.evaluate(interpreter)
|
67
|
+
r.print($stdout)
|
68
|
+
puts
|
69
|
+
else
|
70
|
+
break if reader.eof?
|
71
|
+
sexp = reader.read
|
72
|
+
sexp.evaluate(interpreter)
|
73
|
+
end
|
74
|
+
rescue Risp::RuntimeError, Risp::ReadError => ex
|
75
|
+
$stderr.puts ex.message
|
76
|
+
exit 1 unless interactive_p
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/risp.rb
ADDED
@@ -0,0 +1,522 @@
|
|
1
|
+
# Copyright (c) 2010, arikui
|
2
|
+
# All rights reserved.
|
3
|
+
|
4
|
+
# Redistribution and use in source and binary forms, with or
|
5
|
+
# without modification, are permitted provided that the
|
6
|
+
# following conditions are met:
|
7
|
+
|
8
|
+
# * Redistributions of source code must retain the above
|
9
|
+
# copyright notice, this list of conditions and the
|
10
|
+
# following disclaimer.
|
11
|
+
# * Redistributions in binary form must reproduce the above
|
12
|
+
# copyright notice, this list of conditions and the following
|
13
|
+
# disclaimer in the documentation and/or other materials
|
14
|
+
# provided with the distribution.
|
15
|
+
# * Neither the name of the arikui-risp nor the names of its
|
16
|
+
# contributors may be used to endorse or promote products
|
17
|
+
# derived from this software without specific prior written
|
18
|
+
# permission.
|
19
|
+
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
21
|
+
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
22
|
+
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
23
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
24
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
25
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
26
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
27
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
28
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
29
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
30
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
31
|
+
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
32
|
+
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
33
|
+
|
34
|
+
require 'singleton'
|
35
|
+
|
36
|
+
module Risp
|
37
|
+
VERSION = '0.0.1'
|
38
|
+
|
39
|
+
def self.version
|
40
|
+
VERSION
|
41
|
+
end
|
42
|
+
|
43
|
+
class ReadError < RuntimeError ; end
|
44
|
+
|
45
|
+
class Reader
|
46
|
+
def initialize(f, filename = '-', lineno = 1)
|
47
|
+
@f = f
|
48
|
+
@filename = filename
|
49
|
+
@lineno = lineno
|
50
|
+
@read_table = {}
|
51
|
+
DEFAULT_READ_TABLE.each do |char, m|
|
52
|
+
@read_table[char] = method(m)
|
53
|
+
end
|
54
|
+
@read_table.default = method(DEFAULT_READ_TABLE.default)
|
55
|
+
end
|
56
|
+
|
57
|
+
def read
|
58
|
+
get_sexp()
|
59
|
+
end
|
60
|
+
|
61
|
+
def eof?
|
62
|
+
@f.eof?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def getc
|
68
|
+
c = @f.getc
|
69
|
+
@lineno += 1 if c == "\n"
|
70
|
+
c
|
71
|
+
end
|
72
|
+
|
73
|
+
def error(msg)
|
74
|
+
raise ReadError, "#{@filename}:#{@lineno}: #{msg}"
|
75
|
+
end
|
76
|
+
|
77
|
+
DEFAULT_READ_TABLE = {
|
78
|
+
'(' => :get_list,
|
79
|
+
"'" => :get_quote,
|
80
|
+
'"' => :get_string,
|
81
|
+
}
|
82
|
+
DEFAULT_READ_TABLE.default = :get_number_or_symbol
|
83
|
+
|
84
|
+
def get_sexp
|
85
|
+
while ch = getc()
|
86
|
+
func = @read_table.fetch(ch, nil) and return func.call(ch)
|
87
|
+
/\A[\s\n]/ =~ ch or return @read_table.default.call(ch)
|
88
|
+
end
|
89
|
+
Nil.instance
|
90
|
+
end
|
91
|
+
|
92
|
+
def skip_spaces
|
93
|
+
while ch = getc()
|
94
|
+
break unless /\A[\s\n]/ =~ ch
|
95
|
+
end
|
96
|
+
ch
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_list(lparen)
|
100
|
+
ch = skip_spaces()
|
101
|
+
error "Un-closed list, missing right paren before EOF" unless ch
|
102
|
+
return Nil.instance if ')' == ch
|
103
|
+
@f.ungetc ch
|
104
|
+
top = list = List.new
|
105
|
+
loop do
|
106
|
+
list.car = get_sexp()
|
107
|
+
ch = skip_spaces()
|
108
|
+
error"Un-closed list, missing right paren before EOF" unless ch
|
109
|
+
case ch
|
110
|
+
when ')' then break
|
111
|
+
when '.'
|
112
|
+
list.cdr = get_sexp()
|
113
|
+
ch = skip_spaces()
|
114
|
+
error "Un-closed list, missing right paren before EOF" unless ch
|
115
|
+
error "Not-right paren token follows after dot-pair." unless ch == ')'
|
116
|
+
return top
|
117
|
+
else
|
118
|
+
@f.ungetc ch
|
119
|
+
list.cdr = List.new
|
120
|
+
list = list.cdr
|
121
|
+
end
|
122
|
+
end
|
123
|
+
top
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_quote(q)
|
127
|
+
list = List.new
|
128
|
+
list.car = Symbol.new('quote')
|
129
|
+
list.cdr = List.new
|
130
|
+
list.cdr.car = get_sexp()
|
131
|
+
list
|
132
|
+
end
|
133
|
+
|
134
|
+
ESC_SEQ_TABLE = {
|
135
|
+
'"' => '"',
|
136
|
+
'n' => "\n",
|
137
|
+
't' => "\t",
|
138
|
+
}
|
139
|
+
|
140
|
+
def get_string(dq)
|
141
|
+
buf = ''
|
142
|
+
quoted = false
|
143
|
+
while ch = getc()
|
144
|
+
if quoted
|
145
|
+
seq = ESC_SEQ_TABLE[ch] and buf << seq
|
146
|
+
quoted = false
|
147
|
+
else
|
148
|
+
case ch
|
149
|
+
when '"' then break
|
150
|
+
when '\\' then quoted = true
|
151
|
+
else buf << ch
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
error "Un-closed string, missing later double quote." unless ch
|
156
|
+
String.new(buf)
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_number_or_symbol(char)
|
160
|
+
token = char.dup
|
161
|
+
while ch = getc()
|
162
|
+
case ch
|
163
|
+
when /\A[\s\n]/ then break
|
164
|
+
when '(', ')'
|
165
|
+
@f.ungetc ch
|
166
|
+
break
|
167
|
+
end
|
168
|
+
token << ch
|
169
|
+
end
|
170
|
+
case token
|
171
|
+
when /\A\d+\Z/ then Integer.new(Kernel.Integer(token))
|
172
|
+
when 't' then T.instance
|
173
|
+
when 'nil' then Nil.instance
|
174
|
+
else Symbol.new(token)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
module Sexp
|
180
|
+
def function?
|
181
|
+
false
|
182
|
+
end
|
183
|
+
|
184
|
+
def symbol?
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
def atom?
|
189
|
+
false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class Cell
|
194
|
+
include Sexp
|
195
|
+
|
196
|
+
def initialize
|
197
|
+
self.car = Nil.instance
|
198
|
+
self.cdr = Nil.instance
|
199
|
+
end
|
200
|
+
|
201
|
+
attr_accessor :car
|
202
|
+
attr_accessor :cdr
|
203
|
+
end
|
204
|
+
|
205
|
+
class List < Cell
|
206
|
+
def print(f)
|
207
|
+
f.print '('
|
208
|
+
print_inner(f, [self])
|
209
|
+
f.print ')'
|
210
|
+
end
|
211
|
+
|
212
|
+
def print_inner(f, appreaed_list)
|
213
|
+
car.print(f)
|
214
|
+
case cdr
|
215
|
+
when Nil
|
216
|
+
;
|
217
|
+
when List
|
218
|
+
f.print ' '
|
219
|
+
if appreaed_list.any?{|x| x.equal?(cdr) }
|
220
|
+
f.print '...'
|
221
|
+
else
|
222
|
+
appreaed_list << cdr
|
223
|
+
cdr.print_inner(f, appreaed_list)
|
224
|
+
end
|
225
|
+
else
|
226
|
+
f.print ' . '
|
227
|
+
cdr.print(f)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def serialize
|
232
|
+
inspect
|
233
|
+
end
|
234
|
+
|
235
|
+
def evaluate(intp)
|
236
|
+
operator = car.evaluate(intp)
|
237
|
+
intp.apply(operator, cdr)
|
238
|
+
end
|
239
|
+
|
240
|
+
def to_a
|
241
|
+
ret = []
|
242
|
+
to_a_main ret, self
|
243
|
+
ret
|
244
|
+
end
|
245
|
+
|
246
|
+
def to_a_main(buf, lst)
|
247
|
+
unless lst == Nil.instance
|
248
|
+
buf << lst.car
|
249
|
+
to_a_main(buf, lst.cdr)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def length
|
254
|
+
length_main self
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def length_main(lst)
|
260
|
+
lst == Nil.instance ? 0 : 1 + length_main(lst.cdr)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class Nil
|
265
|
+
include Sexp
|
266
|
+
include Singleton
|
267
|
+
|
268
|
+
def print(f)
|
269
|
+
f.print "NIL"
|
270
|
+
end
|
271
|
+
|
272
|
+
def serialize
|
273
|
+
"NIL"
|
274
|
+
end
|
275
|
+
|
276
|
+
def evaluate(intp)
|
277
|
+
self
|
278
|
+
end
|
279
|
+
|
280
|
+
def to_a
|
281
|
+
[]
|
282
|
+
end
|
283
|
+
|
284
|
+
def atom?
|
285
|
+
true
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class Atom
|
290
|
+
include Sexp
|
291
|
+
|
292
|
+
def atom?
|
293
|
+
true
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
class T < Atom
|
298
|
+
include Singleton
|
299
|
+
|
300
|
+
def print(f)
|
301
|
+
f.print "T"
|
302
|
+
end
|
303
|
+
|
304
|
+
def serialize
|
305
|
+
"T"
|
306
|
+
end
|
307
|
+
|
308
|
+
def evaluate(intp)
|
309
|
+
self
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Symbol < Atom
|
314
|
+
CACHE = {}
|
315
|
+
|
316
|
+
def self.new(name)
|
317
|
+
CACHE[name] ||= super(name)
|
318
|
+
end
|
319
|
+
|
320
|
+
def initialize(name)
|
321
|
+
@name = name
|
322
|
+
end
|
323
|
+
|
324
|
+
attr_reader :name
|
325
|
+
|
326
|
+
def print(f)
|
327
|
+
f.print serialize()
|
328
|
+
end
|
329
|
+
|
330
|
+
def serialize
|
331
|
+
name.upcase
|
332
|
+
end
|
333
|
+
|
334
|
+
def evaluate(intp)
|
335
|
+
intp.symbol_value(self)
|
336
|
+
end
|
337
|
+
|
338
|
+
def symbol?
|
339
|
+
true
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Number < Atom
|
344
|
+
end
|
345
|
+
|
346
|
+
class Integer < Number
|
347
|
+
def initialize(n)
|
348
|
+
@value = n
|
349
|
+
end
|
350
|
+
|
351
|
+
attr_reader :value
|
352
|
+
|
353
|
+
def print(f)
|
354
|
+
f.print serialize()
|
355
|
+
end
|
356
|
+
|
357
|
+
def serialize
|
358
|
+
value.to_s
|
359
|
+
end
|
360
|
+
|
361
|
+
def evaluate(intp)
|
362
|
+
self
|
363
|
+
end
|
364
|
+
|
365
|
+
def +(other)
|
366
|
+
Integer.new(self.value + other.value)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class String < Atom
|
371
|
+
def initialize(value)
|
372
|
+
@value = value
|
373
|
+
end
|
374
|
+
|
375
|
+
attr_reader :value
|
376
|
+
|
377
|
+
def print(f)
|
378
|
+
f.print serialize()
|
379
|
+
end
|
380
|
+
|
381
|
+
def serialize
|
382
|
+
value
|
383
|
+
end
|
384
|
+
|
385
|
+
def evaluate(intp)
|
386
|
+
self
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
class NativeFunction < Atom
|
391
|
+
def function?
|
392
|
+
true
|
393
|
+
end
|
394
|
+
|
395
|
+
def initialize(arity = nil, &body)
|
396
|
+
@arity = arity
|
397
|
+
@body = body
|
398
|
+
end
|
399
|
+
|
400
|
+
def print(f)
|
401
|
+
f.print serialize
|
402
|
+
end
|
403
|
+
|
404
|
+
def serialize
|
405
|
+
"#{inspect.split(/\s+/)[0]}>"
|
406
|
+
end
|
407
|
+
|
408
|
+
def call(intp, args)
|
409
|
+
args = args.to_a
|
410
|
+
if @arity && @arity != args.size
|
411
|
+
raise RuntimeError, "wrong number of arguments (#{args.size} for #{@arity})"
|
412
|
+
end
|
413
|
+
@body.call(*args)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class Function < Atom
|
418
|
+
def function?
|
419
|
+
true
|
420
|
+
end
|
421
|
+
|
422
|
+
def initialize(params, body)
|
423
|
+
@params = params.to_a
|
424
|
+
@body = body
|
425
|
+
end
|
426
|
+
|
427
|
+
def print(f)
|
428
|
+
f.print serialize
|
429
|
+
end
|
430
|
+
|
431
|
+
def serialize
|
432
|
+
"#{inspect.split(/\s+/)[0]}>"
|
433
|
+
end
|
434
|
+
|
435
|
+
def call(intp, args)
|
436
|
+
args = args.to_a
|
437
|
+
unless @params.size == args.size
|
438
|
+
raise RuntimeError, "wrong number of arguments (#{args.size} for #{@params.size})"
|
439
|
+
end
|
440
|
+
ret = nil
|
441
|
+
intp.local_environment(@params.zip(args)) do
|
442
|
+
@body.each{|s| ret = s.evaluate(intp) }
|
443
|
+
end
|
444
|
+
ret
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
class RuntimeError < ::RuntimeError ; end
|
449
|
+
|
450
|
+
class Interpreter
|
451
|
+
def initialize
|
452
|
+
@specials = {}
|
453
|
+
@frames = []
|
454
|
+
@specials[Symbol.new('car')] = NativeFunction.new(1){|list|
|
455
|
+
list.evaluate(self).car
|
456
|
+
}
|
457
|
+
@specials[Symbol.new('cdr')] = NativeFunction.new(1){|list|
|
458
|
+
list.evaluate(self).cdr
|
459
|
+
}
|
460
|
+
@specials[Symbol.new('cons')] = NativeFunction.new(2){|car, cdr|
|
461
|
+
list = List.new
|
462
|
+
list.car = car.evaluate(self)
|
463
|
+
list.cdr = cdr.evaluate(self)
|
464
|
+
list
|
465
|
+
}
|
466
|
+
@specials[Symbol.new('eq')] = NativeFunction.new(2){|x, y|
|
467
|
+
x.evaluate(self).equal?(y.evaluate(self)) ? T.instance : Nil.instance
|
468
|
+
}
|
469
|
+
@specials[Symbol.new('atom')] = NativeFunction.new(1){|sexp|
|
470
|
+
sexp.evaluate(self).atom? ? T.instance : Nil.instance
|
471
|
+
}
|
472
|
+
@specials[Symbol.new('quote')] = NativeFunction.new(1){|sexp|
|
473
|
+
sexp
|
474
|
+
}
|
475
|
+
@specials[Symbol.new('cond')] = NativeFunction.new{|*forms|
|
476
|
+
result = forms.each{|form|
|
477
|
+
test, *body = form.to_a
|
478
|
+
if test.evaluate(self) == T.instance
|
479
|
+
ret = nil
|
480
|
+
body.each{|s| ret = s.evaluate(self) }
|
481
|
+
break ret
|
482
|
+
end
|
483
|
+
}
|
484
|
+
result.equal?(forms) ? Nil.instance : result
|
485
|
+
}
|
486
|
+
@specials[Symbol.new('define')] = NativeFunction.new(2){|bindee, value|
|
487
|
+
case
|
488
|
+
when bindee.symbol?
|
489
|
+
(@frames.last || @specials)[bindee] = value.evaluate(self)
|
490
|
+
else
|
491
|
+
raise RuntimeError, "#{bindee.serialize} is not a symbol."
|
492
|
+
end
|
493
|
+
}
|
494
|
+
@specials[Symbol.new('lambda')] = NativeFunction.new{|params, *body|
|
495
|
+
Function.new(params, body)
|
496
|
+
}
|
497
|
+
@specials[Symbol.new('+')] = NativeFunction.new{|*args|
|
498
|
+
args.map{|arg| arg.evaluate(self) }.inject(Integer.new(0), :+)
|
499
|
+
}
|
500
|
+
end
|
501
|
+
|
502
|
+
def local_environment(alist)
|
503
|
+
f = alist.each_with_object({}){|(sym, val), h| h[sym] = val }
|
504
|
+
@frames.push f
|
505
|
+
ret = yield
|
506
|
+
@frames.pop
|
507
|
+
ret
|
508
|
+
end
|
509
|
+
|
510
|
+
def symbol_value(sym)
|
511
|
+
@frames.reverse_each do |frame|
|
512
|
+
v = frame[sym] and return v
|
513
|
+
end
|
514
|
+
@specials[sym] or raise RuntimeError, "unbound variable - `#{sym.name}'"
|
515
|
+
end
|
516
|
+
|
517
|
+
def apply(function, args)
|
518
|
+
raise RuntimeError, "not a function - #{function}" unless function.function?
|
519
|
+
function.call(self, args)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'expectations'
|
2
|
+
require 'stringio'
|
3
|
+
require 'risp'
|
4
|
+
|
5
|
+
Expectations do
|
6
|
+
expect Risp::Integer do
|
7
|
+
Risp::Reader.new(StringIO.new("123")).read
|
8
|
+
end
|
9
|
+
|
10
|
+
expect 123 do
|
11
|
+
Risp::Reader.new(StringIO.new("123")).read.value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
__END__
|
17
|
+
require 'test/unit'
|
18
|
+
require 'stringio'
|
19
|
+
|
20
|
+
class TestRispReader < Test::Unit::TestCase
|
21
|
+
def test_integer
|
22
|
+
sexp = Risp::Reader.new(StringIO.new("123")).read
|
23
|
+
assert_kind_of Risp::Integer, sexp
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: risp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- arikui
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-19 00:00:00 +09:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: A toy lisp interpreter.
|
23
|
+
email: arikui.ruby@gmail.com
|
24
|
+
executables:
|
25
|
+
- risp
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README
|
30
|
+
files:
|
31
|
+
- README
|
32
|
+
- Rakefile
|
33
|
+
- bin/risp
|
34
|
+
- test/test_risp_reader.rb
|
35
|
+
- lib/risp.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://wiki.github.com/arikui1911/risp
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --title
|
43
|
+
- risp documentation
|
44
|
+
- --charset
|
45
|
+
- utf-8
|
46
|
+
- --opname
|
47
|
+
- index.html
|
48
|
+
- --line-numbers
|
49
|
+
- --main
|
50
|
+
- README
|
51
|
+
- --inline-source
|
52
|
+
- --exclude
|
53
|
+
- ^(examples|extras)/
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: risp
|
77
|
+
rubygems_version: 1.3.7
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: A toy lisp interpreter; provides `pure-lisp' functions and Lisp-1 namespaces. Currently it lacks package system and macro(!!).
|
81
|
+
test_files:
|
82
|
+
- test/test_risp_reader.rb
|