rbl 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -7
- data/bin/rbl +5 -5
- data/lib/rubylisp/environment.rb +19 -5
- data/lib/rubylisp/evaluator.rb +218 -187
- data/lib/rubylisp/function.rb +154 -0
- data/lib/rubylisp/parser.rb +4 -4
- data/lib/rubylisp/printer.rb +7 -4
- data/lib/rubylisp/rbl_readline.rb +29 -0
- data/lib/rubylisp/reader.rb +26 -28
- data/lib/rubylisp/repl.rb +4 -4
- data/lib/rubylisp/types.rb +51 -82
- data/lib/rubylisp/util.rb +64 -0
- data/lib/rubylisp/version.rb +1 -1
- data/rubylisp/core.rbl +378 -229
- data/rubylisp.gemspec +1 -0
- metadata +19 -2
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'rubylisp/types'
|
2
|
+
|
3
|
+
module RubyLisp
|
4
|
+
class Arity
|
5
|
+
attr_accessor :body, :required_args, :rest_args
|
6
|
+
|
7
|
+
def initialize(ast)
|
8
|
+
unless list? ast
|
9
|
+
raise RuntimeError,
|
10
|
+
"Invalid signature #{arity}; expected a list."
|
11
|
+
end
|
12
|
+
|
13
|
+
bindings, *body = ast
|
14
|
+
|
15
|
+
unless vector? bindings
|
16
|
+
raise RuntimeError,
|
17
|
+
"Bindings must be a vector; got #{body.class}."
|
18
|
+
end
|
19
|
+
|
20
|
+
bindings.each do |binding|
|
21
|
+
unless binding.class == Symbol
|
22
|
+
raise RuntimeError,
|
23
|
+
"Each binding must be a symbol; got #{binding.class}."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# bindings is now an array of strings (symbol names)
|
28
|
+
bindings = bindings.map(&:value)
|
29
|
+
|
30
|
+
ampersand_indices = bindings.to_list.indices {|x| x == '&'}
|
31
|
+
if ampersand_indices.any? {|i| i != bindings.count - 2}
|
32
|
+
raise RuntimeError,
|
33
|
+
"An '&' can only occur right before the last binding."
|
34
|
+
end
|
35
|
+
|
36
|
+
@required_args = bindings.take_while {|binding| binding != '&'}
|
37
|
+
@rest_args = bindings.drop_while {|binding| binding != '&'}.drop(1)
|
38
|
+
@body = body
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class Function < Proc
|
44
|
+
attr_accessor :name, :env, :bindings, :body, :is_macro, :lambda
|
45
|
+
|
46
|
+
def initialize(name, env, asts, &block)
|
47
|
+
super()
|
48
|
+
@name = name
|
49
|
+
@env = env
|
50
|
+
@arities = construct_arities(asts)
|
51
|
+
@is_macro = false
|
52
|
+
@lambda = block
|
53
|
+
end
|
54
|
+
|
55
|
+
def construct_arities(asts)
|
56
|
+
arities_hash = asts.each_with_object({'arities' => [],
|
57
|
+
'required' => 0}) do |ast, result|
|
58
|
+
arity = Arity.new(ast)
|
59
|
+
|
60
|
+
if arity.rest_args.empty?
|
61
|
+
# Prevent conflicts like [x] vs. [y]
|
62
|
+
if result['arities'].any? {|existing|
|
63
|
+
existing.required_args.count == arity.required_args.count &&
|
64
|
+
existing.rest_args.empty?
|
65
|
+
}
|
66
|
+
raise RuntimeError,
|
67
|
+
"Can't have multiple overloads with the same arity."
|
68
|
+
end
|
69
|
+
|
70
|
+
# Prevent conflicts like [& xs] vs. [x]
|
71
|
+
if result['rest_required']
|
72
|
+
unless arity.required_args.count <= result['rest_required']
|
73
|
+
raise RuntimeError,
|
74
|
+
"Can't have a fixed arity function with more params than a " +
|
75
|
+
"variadic function."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
else
|
79
|
+
# Prevent conflicts like [x] vs. [& xs]
|
80
|
+
if arity.required_args.count < result['required']
|
81
|
+
raise RuntimeError,
|
82
|
+
"Can't have a fixed arity function with more params than a " +
|
83
|
+
"variadic function."
|
84
|
+
end
|
85
|
+
|
86
|
+
# Prevent conflicts like [x & xs] vs. [x y & ys]
|
87
|
+
if result['arities'].any? {|existing| !existing.rest_args.empty?}
|
88
|
+
raise RuntimeError,
|
89
|
+
"Can't have more than one variadic overload."
|
90
|
+
end
|
91
|
+
|
92
|
+
result['rest_required'] = arity.required_args.count
|
93
|
+
end
|
94
|
+
|
95
|
+
result['required'] = [result['required'], arity.required_args.count].max
|
96
|
+
result['arities'] << arity
|
97
|
+
end
|
98
|
+
|
99
|
+
arities_hash['arities']
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_arity(args)
|
103
|
+
# Assert that there are enough arguments provided for the arities we have.
|
104
|
+
sexp = list [Symbol.new(@name), *args]
|
105
|
+
variadic = @arities.find {|arity| !arity.rest_args.empty?}
|
106
|
+
fixed_arities = @arities.select {|arity| arity.rest_args.empty?}
|
107
|
+
fixed = fixed_arities.find {|arity| arity.required_args.count == args.count}
|
108
|
+
|
109
|
+
# Return the arity most appropriate for the number of args provided.
|
110
|
+
if fixed
|
111
|
+
fixed
|
112
|
+
elsif variadic
|
113
|
+
assert_at_least_n_args sexp, variadic.required_args.count
|
114
|
+
variadic
|
115
|
+
else
|
116
|
+
raise RuntimeError,
|
117
|
+
"Wrong number of args (#{args.count}) passed to #{@name}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def gen_env(arity, args, env)
|
122
|
+
# set out_env to the current namespace so that `def` occurring within
|
123
|
+
# the fn's environment will define things in the namespace in which the
|
124
|
+
# function is called
|
125
|
+
out_env = env.out_env || env.find_namespace
|
126
|
+
env = Environment.new(outer: @env, out_env: out_env)
|
127
|
+
# so the fn can call itself recursively
|
128
|
+
env.set(@name, self)
|
129
|
+
|
130
|
+
if arity.rest_args.empty?
|
131
|
+
# bind values to the required args
|
132
|
+
arity.required_args.zip(args).each do |k, v|
|
133
|
+
env.set(k, v)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
# bind values to the required args (the rest args are skipped here)
|
137
|
+
arity.required_args.zip(args).each do |k, v|
|
138
|
+
env.set(k, v)
|
139
|
+
end
|
140
|
+
|
141
|
+
# bind the rest argument to the remaining arguments or nil
|
142
|
+
rest_args = if args.count > arity.required_args.count
|
143
|
+
args[arity.required_args.count..-1].to_list
|
144
|
+
else
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
env.set(arity.rest_args.first, rest_args)
|
149
|
+
end
|
150
|
+
|
151
|
+
env
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/rubylisp/parser.rb
CHANGED
@@ -8,18 +8,18 @@ module RubyLisp
|
|
8
8
|
module_function
|
9
9
|
|
10
10
|
def read input
|
11
|
-
|
11
|
+
Reader.read_str input
|
12
12
|
end
|
13
13
|
|
14
14
|
def eval_ast input, env
|
15
|
-
|
15
|
+
Evaluator.eval_ast input, env
|
16
16
|
end
|
17
17
|
|
18
18
|
def print input
|
19
|
-
|
19
|
+
Printer.pr_str input
|
20
20
|
end
|
21
21
|
|
22
|
-
def parse input, env =
|
22
|
+
def parse input, env = Environment.new.stdlib
|
23
23
|
ast = read input
|
24
24
|
result = eval_ast ast, env
|
25
25
|
print result
|
data/lib/rubylisp/printer.rb
CHANGED
@@ -18,11 +18,14 @@ module RubyLisp
|
|
18
18
|
pr_str x.to_hash
|
19
19
|
when Hamster::List
|
20
20
|
"(#{x.map {|item| pr_str(item)}.join(' ')})"
|
21
|
-
when
|
22
|
-
"
|
23
|
-
when
|
24
|
-
RubyLisp::Boolean, RubyLisp::Keyword
|
21
|
+
when Function
|
22
|
+
"#<#{x.is_macro ? 'Macro' : 'Function'}: #{x.name}>"
|
23
|
+
when Symbol
|
25
24
|
x.value
|
25
|
+
when Value
|
26
|
+
x.value.inspect
|
27
|
+
when Object::Symbol
|
28
|
+
":#{x.name}"
|
26
29
|
else
|
27
30
|
x.inspect
|
28
31
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# adapted from: https://github.com/kanaka/mal/blob/master/ruby/mal_readline.rb
|
2
|
+
require 'fileutils'
|
3
|
+
require "readline"
|
4
|
+
|
5
|
+
$history_loaded = false
|
6
|
+
$histfile = "#{ENV['HOME']}/.rbl-history"
|
7
|
+
|
8
|
+
# create history file if it doesn't exist already
|
9
|
+
FileUtils.touch($histfile)
|
10
|
+
|
11
|
+
def _readline(prompt)
|
12
|
+
if !$history_loaded && File.exist?($histfile)
|
13
|
+
$history_loaded = true
|
14
|
+
if File.readable?($histfile)
|
15
|
+
File.readlines($histfile).each {|l| Readline::HISTORY.push(l.chomp)}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if line = Readline.readline(prompt, true)
|
20
|
+
if line.strip.empty? || Readline::HISTORY[-2] == Readline::HISTORY[-1]
|
21
|
+
Readline::HISTORY.pop
|
22
|
+
elsif File.writable?($histfile)
|
23
|
+
File.open($histfile, 'a+') {|f| f.write(line+"\n")}
|
24
|
+
end
|
25
|
+
return line
|
26
|
+
else
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
end
|
data/lib/rubylisp/reader.rb
CHANGED
@@ -39,29 +39,29 @@ module RubyLisp
|
|
39
39
|
token
|
40
40
|
end
|
41
41
|
|
42
|
-
def read_seq(
|
42
|
+
def read_seq(type_name, constructor, end_token, seq=[])
|
43
43
|
case peek
|
44
44
|
when nil
|
45
|
-
raise
|
45
|
+
raise ParseError, "Unexpected EOF while parsing #{type_name}."
|
46
46
|
when end_token
|
47
47
|
next_token
|
48
|
-
|
48
|
+
constructor.call(seq)
|
49
49
|
else
|
50
50
|
seq << read_form
|
51
|
-
read_seq(
|
51
|
+
read_seq(type_name, constructor, end_token, seq)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
55
|
def read_list
|
56
|
-
read_seq RubyLisp
|
56
|
+
read_seq 'list', RubyLisp.method(:list), ')'
|
57
57
|
end
|
58
58
|
|
59
59
|
def read_vector
|
60
|
-
read_seq RubyLisp
|
60
|
+
read_seq 'vector', RubyLisp.method(:vec), ']'
|
61
61
|
end
|
62
62
|
|
63
63
|
def read_hashmap
|
64
|
-
read_seq RubyLisp
|
64
|
+
read_seq 'hash-map', RubyLisp.method(:hash_map), '}'
|
65
65
|
end
|
66
66
|
|
67
67
|
def read_atom
|
@@ -70,38 +70,36 @@ module RubyLisp
|
|
70
70
|
when nil
|
71
71
|
nil
|
72
72
|
when /^\-?\d+$/
|
73
|
-
|
73
|
+
Value.new(token.to_i)
|
74
74
|
when /^\-?\d+\.\d+$/
|
75
|
-
|
75
|
+
Value.new(token.to_f)
|
76
76
|
when /^".*"$/
|
77
77
|
# it's safe to use eval here because the tokenizer ensures that
|
78
78
|
# the token is an escaped string representation
|
79
|
-
|
79
|
+
Value.new(eval(token))
|
80
80
|
# it's a little weird that an unfinished string (e.g. "abc) gets
|
81
81
|
# tokenized as "", but at least the behavior is consistent ¯\_(ツ)_/¯
|
82
82
|
when ""
|
83
|
-
raise
|
84
|
-
"Unexpected EOF while parsing RubyLisp::String."
|
83
|
+
raise ParseError, "Unexpected EOF while parsing string."
|
85
84
|
when /^:/
|
86
|
-
|
85
|
+
Value.new(token[1..-1].to_sym)
|
87
86
|
when 'nil'
|
88
|
-
|
87
|
+
Value.new(nil)
|
89
88
|
when 'true'
|
90
|
-
|
89
|
+
Value.new(true)
|
91
90
|
when 'false'
|
92
|
-
|
91
|
+
Value.new(false)
|
93
92
|
else
|
94
|
-
|
93
|
+
Symbol.new(token)
|
95
94
|
end
|
96
95
|
end
|
97
96
|
|
98
97
|
def read_special_form(special)
|
99
98
|
form = read_form
|
100
99
|
unless form
|
101
|
-
raise
|
102
|
-
"Unexpected EOF while parsing #{special} form."
|
100
|
+
raise ParseError, "Unexpected EOF while parsing #{special} form."
|
103
101
|
end
|
104
|
-
|
102
|
+
list [Symbol.new(special), form]
|
105
103
|
end
|
106
104
|
|
107
105
|
def read_quoted_form
|
@@ -128,23 +126,23 @@ module RubyLisp
|
|
128
126
|
token = peek
|
129
127
|
case token
|
130
128
|
when nil
|
131
|
-
raise
|
129
|
+
raise ParseError, "Unexpected EOF while parsing metadata."
|
132
130
|
when '{'
|
133
131
|
next_token
|
134
132
|
metadata = read_hashmap
|
135
133
|
when /^:/
|
136
134
|
kw = read_form
|
137
|
-
metadata =
|
135
|
+
metadata = hash_map [kw, Value.new(true)]
|
138
136
|
else
|
139
|
-
raise
|
137
|
+
raise ParseError, "Invalid metadata: '#{token}'"
|
140
138
|
end
|
141
139
|
|
142
140
|
form = read_form
|
143
141
|
unless form
|
144
|
-
raise
|
142
|
+
raise ParseError, "Unexpected EOF after metadata."
|
145
143
|
end
|
146
144
|
|
147
|
-
|
145
|
+
list [Symbol.new("with-meta"), form, metadata]
|
148
146
|
end
|
149
147
|
|
150
148
|
def read_form
|
@@ -163,11 +161,11 @@ module RubyLisp
|
|
163
161
|
next_token
|
164
162
|
read_hashmap
|
165
163
|
when ')'
|
166
|
-
raise
|
164
|
+
raise ParseError, "Unexpected ')'."
|
167
165
|
when ']'
|
168
|
-
raise
|
166
|
+
raise ParseError, "Unexpected ']'."
|
169
167
|
when '}'
|
170
|
-
raise
|
168
|
+
raise ParseError, "Unexpected '}'."
|
171
169
|
when "'"
|
172
170
|
next_token
|
173
171
|
read_quoted_form
|
data/lib/rubylisp/repl.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative 'rbl_readline'
|
2
2
|
require 'rubylisp/environment'
|
3
3
|
require 'rubylisp/parser'
|
4
4
|
|
@@ -7,12 +7,12 @@ module RubyLisp
|
|
7
7
|
module_function
|
8
8
|
|
9
9
|
def start
|
10
|
-
env =
|
10
|
+
env = Environment.new(namespace: 'user').stdlib.repl
|
11
11
|
|
12
|
-
while buf =
|
12
|
+
while buf = _readline("#{env.namespace}> ")
|
13
13
|
begin
|
14
14
|
input = buf.nil? ? '' : buf.strip
|
15
|
-
puts input.empty? ? '' :
|
15
|
+
puts input.empty? ? '' : Parser.parse(input, env)
|
16
16
|
rescue => e
|
17
17
|
# If an error happens, print it like Ruby would and continue accepting
|
18
18
|
# REPL input.
|
data/lib/rubylisp/types.rb
CHANGED
@@ -1,18 +1,24 @@
|
|
1
|
+
require 'concurrent/atom'
|
1
2
|
require 'hamster/core_ext'
|
2
3
|
require 'hamster/hash'
|
3
4
|
require 'hamster/list'
|
4
5
|
require 'hamster/vector'
|
6
|
+
require 'rubylisp/function'
|
7
|
+
require 'rubylisp/util'
|
8
|
+
|
9
|
+
include RubyLisp::Util
|
5
10
|
|
6
11
|
# Monkey-patch Ruby symbols to act more like Clojure keywords
|
7
12
|
class Symbol
|
8
13
|
def name
|
9
|
-
|
14
|
+
without_colon = inspect[1..-1]
|
15
|
+
if without_colon[0] == '"' && without_colon[-1] == '"'
|
16
|
+
without_colon[1..-2]
|
17
|
+
else
|
18
|
+
without_colon
|
19
|
+
end
|
10
20
|
end
|
11
21
|
|
12
|
-
def to_s
|
13
|
-
inspect
|
14
|
-
end
|
15
|
-
|
16
22
|
def call(target)
|
17
23
|
if [Hash, Hamster::Hash].member? target.class
|
18
24
|
target[self]
|
@@ -52,88 +58,29 @@ end
|
|
52
58
|
|
53
59
|
module RubyLisp
|
54
60
|
class Value
|
55
|
-
attr_accessor :value
|
61
|
+
attr_accessor :value
|
56
62
|
|
57
63
|
def initialize(value)
|
58
64
|
@value = value
|
59
65
|
end
|
60
66
|
|
61
|
-
def quote
|
62
|
-
@quoted = true
|
63
|
-
self
|
64
|
-
end
|
65
|
-
|
66
|
-
def unquote
|
67
|
-
@quoted = false
|
68
|
-
self
|
69
|
-
end
|
70
|
-
|
71
67
|
def to_s
|
72
68
|
@value.to_s
|
73
69
|
end
|
74
70
|
end
|
75
71
|
|
76
|
-
class Boolean < Value; end
|
77
|
-
class Float < Value; end
|
78
|
-
class Int < Value; end
|
79
|
-
class Keyword < Value; end
|
80
|
-
class String < Value; end
|
81
|
-
|
82
72
|
class ParseError < StandardError; end
|
83
73
|
class RuntimeError < StandardError; end
|
84
74
|
|
85
|
-
class HashMap < Value
|
86
|
-
def initialize(seq)
|
87
|
-
if seq.size.odd?
|
88
|
-
raise RubyLisp::ParseError,
|
89
|
-
"A RubyLisp::HashMap must contain an even number of forms."
|
90
|
-
else
|
91
|
-
@value = Hamster::Hash[seq.each_slice(2).to_a]
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def quote
|
96
|
-
@value.each {|k, v| k.quote; v.quote}
|
97
|
-
@quoted = true
|
98
|
-
self
|
99
|
-
end
|
100
|
-
|
101
|
-
def unquote
|
102
|
-
@value.each {|k, v| k.unquote; v.unquote}
|
103
|
-
@quoted = false
|
104
|
-
self
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
class List < Value
|
109
|
-
def initialize(values)
|
110
|
-
@value = values.to_list
|
111
|
-
end
|
112
|
-
|
113
|
-
def quote
|
114
|
-
@value.each(&:quote)
|
115
|
-
@quoted = true
|
116
|
-
self
|
117
|
-
end
|
118
|
-
|
119
|
-
def unquote
|
120
|
-
@value.each(&:unquote)
|
121
|
-
@quoted = false
|
122
|
-
self
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
class Nil < Value
|
127
|
-
def initialize
|
128
|
-
@value = nil
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
75
|
class Symbol < Value
|
133
76
|
def to_s
|
134
77
|
@value
|
135
78
|
end
|
136
79
|
|
80
|
+
def ==(other)
|
81
|
+
other.is_a?(Symbol) && @value == other.value
|
82
|
+
end
|
83
|
+
|
137
84
|
def resolve(env)
|
138
85
|
# rbl: (.+ 1 2)
|
139
86
|
# ruby: 1.+(2)
|
@@ -151,9 +98,9 @@ module RubyLisp
|
|
151
98
|
first_segment, *segments = @value.split('::')
|
152
99
|
first_segment = Object.const_get first_segment
|
153
100
|
return segments.reduce(first_segment) do |result, segment|
|
154
|
-
if result.
|
101
|
+
if result.is_a? Proc
|
155
102
|
# a method can only be in the final position
|
156
|
-
raise
|
103
|
+
raise RuntimeError, "Invalid value: #{@value}"
|
157
104
|
elsif /^[A-Z]/ =~ segment
|
158
105
|
# get module/class constant
|
159
106
|
result.const_get segment
|
@@ -170,21 +117,43 @@ module RubyLisp
|
|
170
117
|
end
|
171
118
|
end
|
172
119
|
|
173
|
-
|
174
|
-
|
175
|
-
|
120
|
+
module Util
|
121
|
+
module_function
|
122
|
+
|
123
|
+
def hash_map(values)
|
124
|
+
if values.size.odd?
|
125
|
+
raise ParseError, "A hash-map must contain an even number of forms."
|
126
|
+
else
|
127
|
+
Hamster::Hash[values.each_slice(2).to_a]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def map?(x)
|
132
|
+
x.is_a? Hamster::Hash
|
133
|
+
end
|
134
|
+
|
135
|
+
def list(values)
|
136
|
+
values.to_list
|
137
|
+
end
|
138
|
+
|
139
|
+
def list?(x)
|
140
|
+
x.is_a? Hamster::List
|
141
|
+
end
|
142
|
+
|
143
|
+
def vec(values)
|
144
|
+
Hamster::Vector.new(values)
|
145
|
+
end
|
146
|
+
|
147
|
+
def vector(*values)
|
148
|
+
Hamster::Vector.new(values)
|
176
149
|
end
|
177
150
|
|
178
|
-
def
|
179
|
-
|
180
|
-
@quoted = true
|
181
|
-
self
|
151
|
+
def vector?(x)
|
152
|
+
x.is_a? Hamster::Vector
|
182
153
|
end
|
183
154
|
|
184
|
-
def
|
185
|
-
|
186
|
-
@quoted = false
|
187
|
-
self
|
155
|
+
def sequential?(x)
|
156
|
+
list?(x) || vector?(x)
|
188
157
|
end
|
189
158
|
end
|
190
159
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module RubyLisp
|
2
|
+
module Util
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def assert_number_of_args sexp, num_args
|
6
|
+
fn, *args = sexp
|
7
|
+
fn_name = fn.value
|
8
|
+
unless args.count == num_args
|
9
|
+
raise RuntimeError,
|
10
|
+
"Wrong number of arguments passed to `#{fn_name}`; " +
|
11
|
+
"got #{args.count}, expected #{num_args}."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def assert_at_least_n_args sexp, num_args
|
16
|
+
fn, *args = sexp
|
17
|
+
fn_name = fn.value
|
18
|
+
unless args.count >= num_args
|
19
|
+
raise RuntimeError,
|
20
|
+
"Wrong number of arguments passed to `#{fn_name}`; " +
|
21
|
+
"got #{args.count}, expected at least #{num_args}."
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def assert_arg_type sexp, arg_number, arg_type
|
26
|
+
fn = sexp[0]
|
27
|
+
fn_name = fn.value
|
28
|
+
arg = if arg_number == 'last'
|
29
|
+
sexp.last
|
30
|
+
else
|
31
|
+
sexp[arg_number]
|
32
|
+
end
|
33
|
+
|
34
|
+
arg_description = if arg_number == 'last'
|
35
|
+
'The last argument'
|
36
|
+
else
|
37
|
+
"Argument ##{arg_number}"
|
38
|
+
end
|
39
|
+
|
40
|
+
arg_types = if arg_type.class == Array
|
41
|
+
arg_type
|
42
|
+
else
|
43
|
+
[arg_type]
|
44
|
+
end
|
45
|
+
|
46
|
+
expected = case arg_types.count
|
47
|
+
when 1
|
48
|
+
arg_types.first
|
49
|
+
when 2
|
50
|
+
arg_types.join(' or ')
|
51
|
+
else
|
52
|
+
last_arg_type = arg_types.pop
|
53
|
+
arg_types.join(', ') + " or #{last_arg_type}"
|
54
|
+
end
|
55
|
+
|
56
|
+
if arg_types.none? {|type| arg.is_a? type}
|
57
|
+
raise RuntimeError,
|
58
|
+
"#{arg_description} to `#{fn_name}` must be a " +
|
59
|
+
"#{expected}; got a #{arg.class}."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/rubylisp/version.rb
CHANGED