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.
@@ -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