rbl 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -8,18 +8,18 @@ module RubyLisp
8
8
  module_function
9
9
 
10
10
  def read input
11
- RubyLisp::Reader.read_str input
11
+ Reader.read_str input
12
12
  end
13
13
 
14
14
  def eval_ast input, env
15
- RubyLisp::Evaluator.eval_ast input, env
15
+ Evaluator.eval_ast input, env
16
16
  end
17
17
 
18
18
  def print input
19
- RubyLisp::Printer.pr_str input
19
+ Printer.pr_str input
20
20
  end
21
21
 
22
- def parse input, env = RubyLisp::Environment.new.stdlib
22
+ def parse input, env = Environment.new.stdlib
23
23
  ast = read input
24
24
  result = eval_ast ast, env
25
25
  print result
@@ -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 RubyLisp::List
22
- "(#{x.value.map {|item| pr_str(item)}.join(' ')})"
23
- when RubyLisp::Symbol, RubyLisp::String, RubyLisp::Int, RubyLisp::Float,
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
@@ -39,29 +39,29 @@ module RubyLisp
39
39
  token
40
40
  end
41
41
 
42
- def read_seq(type, end_token, seq=[])
42
+ def read_seq(type_name, constructor, end_token, seq=[])
43
43
  case peek
44
44
  when nil
45
- raise RubyLisp::ParseError, "Unexpected EOF while parsing #{type}."
45
+ raise ParseError, "Unexpected EOF while parsing #{type_name}."
46
46
  when end_token
47
47
  next_token
48
- type.new(seq)
48
+ constructor.call(seq)
49
49
  else
50
50
  seq << read_form
51
- read_seq(type, end_token, 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::List, ')'
56
+ read_seq 'list', RubyLisp.method(:list), ')'
57
57
  end
58
58
 
59
59
  def read_vector
60
- read_seq RubyLisp::Vector, ']'
60
+ read_seq 'vector', RubyLisp.method(:vec), ']'
61
61
  end
62
62
 
63
63
  def read_hashmap
64
- read_seq RubyLisp::HashMap, '}'
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
- RubyLisp::Int.new(token.to_i)
73
+ Value.new(token.to_i)
74
74
  when /^\-?\d+\.\d+$/
75
- RubyLisp::Float.new(token.to_f)
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
- RubyLisp::String.new(eval(token))
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 RubyLisp::ParseError,
84
- "Unexpected EOF while parsing RubyLisp::String."
83
+ raise ParseError, "Unexpected EOF while parsing string."
85
84
  when /^:/
86
- RubyLisp::Keyword.new(token[1..-1].to_sym)
85
+ Value.new(token[1..-1].to_sym)
87
86
  when 'nil'
88
- RubyLisp::Nil.new
87
+ Value.new(nil)
89
88
  when 'true'
90
- RubyLisp::Boolean.new(true)
89
+ Value.new(true)
91
90
  when 'false'
92
- RubyLisp::Boolean.new(false)
91
+ Value.new(false)
93
92
  else
94
- RubyLisp::Symbol.new(token)
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 RubyLisp::ParseError,
102
- "Unexpected EOF while parsing #{special} form."
100
+ raise ParseError, "Unexpected EOF while parsing #{special} form."
103
101
  end
104
- RubyLisp::List.new([RubyLisp::Symbol.new(special), form])
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 RubyLisp::ParseError, "Unexpected EOF while parsing metadata."
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 = RubyLisp::HashMap.new([kw, RubyLisp::Boolean.new(true)])
135
+ metadata = hash_map [kw, Value.new(true)]
138
136
  else
139
- raise RubyLisp::ParseError, "Invalid metadata: '#{token}'"
137
+ raise ParseError, "Invalid metadata: '#{token}'"
140
138
  end
141
139
 
142
140
  form = read_form
143
141
  unless form
144
- raise RubyLisp::ParseError, "Unexpected EOF after metadata."
142
+ raise ParseError, "Unexpected EOF after metadata."
145
143
  end
146
144
 
147
- RubyLisp::List.new([RubyLisp::Symbol.new("with-meta"), form, metadata])
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 RubyLisp::ParseError, "Unexpected ')'."
164
+ raise ParseError, "Unexpected ')'."
167
165
  when ']'
168
- raise RubyLisp::ParseError, "Unexpected ']'."
166
+ raise ParseError, "Unexpected ']'."
169
167
  when '}'
170
- raise RubyLisp::ParseError, "Unexpected '}'."
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
- require 'readline'
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 = RubyLisp::Environment.new(namespace: 'user').stdlib.repl
10
+ env = Environment.new(namespace: 'user').stdlib.repl
11
11
 
12
- while buf = Readline.readline("#{env.namespace}> ", true)
12
+ while buf = _readline("#{env.namespace}> ")
13
13
  begin
14
14
  input = buf.nil? ? '' : buf.strip
15
- puts input.empty? ? '' : RubyLisp::Parser.parse(input, env)
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.
@@ -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
- to_s[1..-1]
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, :quoted
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.class == Proc
101
+ if result.is_a? Proc
155
102
  # a method can only be in the final position
156
- raise RubyLisp::RuntimeError, "Invalid value: #{@value}"
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
- class Vector < Value
174
- def initialize(values)
175
- @value = Hamster::Vector.new(values)
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 quote
179
- @value.each(&:quote)
180
- @quoted = true
181
- self
151
+ def vector?(x)
152
+ x.is_a? Hamster::Vector
182
153
  end
183
154
 
184
- def unquote
185
- @value.each(&:unquote)
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
@@ -1,3 +1,3 @@
1
1
  module RubyLisp
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end