rbl 0.0.5 → 0.0.6
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.
- 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