rbl 0.0.1

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,209 @@
1
+ require 'rubylisp/types'
2
+
3
+ module RubyLisp
4
+ class Reader
5
+ # from kanaka/mal
6
+ TOKEN_REGEX = %r{
7
+ # ignore whitespace and commas
8
+ [\s,]*
9
+
10
+ # match any of...
11
+ (
12
+ # the splice-unquote reader macro
13
+ ~@|
14
+
15
+ # special characters
16
+ [\[\]{}()'`~^@]|
17
+
18
+ # strings
19
+ "(?:\\.|[^\\"])*"|
20
+
21
+ # comments
22
+ ;.*|
23
+
24
+ # any sequence of non-special characters
25
+ # e.g. symbols, numbers, keywords, booleans, etc.
26
+ [^\s\[\]{}('"`,;)]*
27
+ )
28
+ }x
29
+
30
+ attr_accessor :tokens, :position
31
+
32
+ def peek
33
+ @tokens[@position]
34
+ end
35
+
36
+ def next_token
37
+ token = peek
38
+ @position += 1
39
+ token
40
+ end
41
+
42
+ def read_seq(type, end_token, seq=[])
43
+ case peek
44
+ when nil
45
+ raise RubyLisp::ParseError, "Unexpected EOF while parsing #{type}."
46
+ when end_token
47
+ next_token
48
+ type.new(seq)
49
+ else
50
+ seq << read_form
51
+ read_seq(type, end_token, seq)
52
+ end
53
+ end
54
+
55
+ def read_list
56
+ read_seq RubyLisp::List, ')'
57
+ end
58
+
59
+ def read_vector
60
+ read_seq RubyLisp::Vector, ']'
61
+ end
62
+
63
+ def read_hashmap
64
+ read_seq RubyLisp::HashMap, '}'
65
+ end
66
+
67
+ def read_atom
68
+ token = next_token
69
+ case token
70
+ when nil
71
+ nil
72
+ when /^\-?\d+$/
73
+ RubyLisp::Int.new(token.to_i)
74
+ when /^\-?\d+\.\d+$/
75
+ RubyLisp::Float.new(token.to_f)
76
+ when /^".*"$/
77
+ # it's safe to use eval here because the tokenizer ensures that
78
+ # the token is an escaped string representation
79
+ RubyLisp::String.new(eval(token))
80
+ # it's a little weird that an unfinished string (e.g. "abc) gets
81
+ # tokenized as "", but at least the behavior is consistent ¯\_(ツ)_/¯
82
+ when ""
83
+ raise RubyLisp::ParseError,
84
+ "Unexpected EOF while parsing RubyLisp::String."
85
+ when /^:/
86
+ RubyLisp::Keyword.new(token[1..-1].to_sym)
87
+ when 'nil'
88
+ RubyLisp::Nil.new
89
+ when 'true'
90
+ RubyLisp::Boolean.new(true)
91
+ when 'false'
92
+ RubyLisp::Boolean.new(false)
93
+ else
94
+ RubyLisp::Symbol.new(token)
95
+ end
96
+ end
97
+
98
+ def read_special_form(special)
99
+ form = read_form
100
+ unless form
101
+ raise RubyLisp::ParseError,
102
+ "Unexpected EOF while parsing #{special} form."
103
+ end
104
+ RubyLisp::List.new([RubyLisp::Symbol.new(special), form])
105
+ end
106
+
107
+ def read_quoted_form
108
+ read_special_form 'quote'
109
+ end
110
+
111
+ def read_quasiquoted_form
112
+ read_special_form 'quasiquote'
113
+ end
114
+
115
+ def read_unquoted_form
116
+ read_special_form 'unquote'
117
+ end
118
+
119
+ def read_splice_unquoted_form
120
+ read_special_form 'splice-unquote'
121
+ end
122
+
123
+ def read_deref_form
124
+ read_special_form 'deref'
125
+ end
126
+
127
+ def read_form_with_metadata
128
+ token = peek
129
+ case token
130
+ when nil
131
+ raise RubyLisp::ParseError, "Unexpected EOF while parsing metadata."
132
+ when '{'
133
+ next_token
134
+ metadata = read_hashmap
135
+ when /^:/
136
+ kw = read_form
137
+ metadata = RubyLisp::HashMap.new([kw, RubyLisp::Boolean.new(true)])
138
+ else
139
+ raise RubyLisp::ParseError, "Invalid metadata: '#{token}'"
140
+ end
141
+
142
+ form = read_form
143
+ unless form
144
+ raise RubyLisp::ParseError, "Unexpected EOF after metadata."
145
+ end
146
+
147
+ RubyLisp::List.new([RubyLisp::Symbol.new("with-meta"), form, metadata])
148
+ end
149
+
150
+ def read_form
151
+ case peek
152
+ when /^;/
153
+ # ignore comments
154
+ next_token
155
+ read_form
156
+ when '('
157
+ next_token
158
+ read_list
159
+ when '['
160
+ next_token
161
+ read_vector
162
+ when '{'
163
+ next_token
164
+ read_hashmap
165
+ when ')'
166
+ raise RubyLisp::ParseError, "Unexpected ')'."
167
+ when ']'
168
+ raise RubyLisp::ParseError, "Unexpected ']'."
169
+ when '}'
170
+ raise RubyLisp::ParseError, "Unexpected '}'."
171
+ when "'"
172
+ next_token
173
+ read_quoted_form
174
+ when '`'
175
+ next_token
176
+ read_quasiquoted_form
177
+ when '~'
178
+ next_token
179
+ read_unquoted_form
180
+ when '~@'
181
+ next_token
182
+ read_splice_unquoted_form
183
+ when '@'
184
+ next_token
185
+ read_deref_form
186
+ when '^'
187
+ next_token
188
+ read_form_with_metadata
189
+ else
190
+ read_atom
191
+ end
192
+ end
193
+
194
+ def tokenize str
195
+ @tokens = str.strip.scan(TOKEN_REGEX).flatten[0...-1]
196
+ @position = 0
197
+ end
198
+
199
+ def Reader.read_str str
200
+ reader = Reader.new
201
+ reader.tokenize(str)
202
+ forms = []
203
+ while reader.position < reader.tokens.count
204
+ forms << reader.read_form
205
+ end
206
+ forms
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,26 @@
1
+ require 'readline'
2
+ require 'rubylisp/environment'
3
+ require 'rubylisp/parser'
4
+
5
+ module RubyLisp
6
+ module REPL
7
+ module_function
8
+
9
+ def start
10
+ env = RubyLisp::Environment.new(namespace: 'user').stdlib.repl
11
+
12
+ while buf = Readline.readline("#{env.namespace}> ", true)
13
+ begin
14
+ input = buf.nil? ? '' : buf.strip
15
+ puts input.empty? ? '' : RubyLisp::Parser.parse(input, env)
16
+ rescue => e
17
+ # If an error happens, print it like Ruby would and continue accepting
18
+ # REPL input.
19
+ puts e.backtrace
20
+ .join("\n\t")
21
+ .sub("\n\t", ": #{e}#{e.class ? " (#{e.class})" : ''}\n\t")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,169 @@
1
+ require 'hamster/core_ext'
2
+ require 'hamster/hash'
3
+ require 'hamster/list'
4
+ require 'hamster/vector'
5
+
6
+ # Monkey-patch Ruby symbols to act more like Clojure keywords
7
+ class Symbol
8
+ def to_s
9
+ inspect
10
+ end
11
+
12
+ def call(target)
13
+ if [Hash, Hamster::Hash].member? target.class
14
+ target[self]
15
+ else
16
+ target.instance_variable_get "@#{self.to_s}".to_sym
17
+ end
18
+ end
19
+ end
20
+
21
+ # Monkey-patch Hamster types to have nicer (and more Clojure-like) string
22
+ # representations
23
+ module Hamster
24
+ class Cons
25
+ def to_s
26
+ "(#{join " "})"
27
+ end
28
+ end
29
+
30
+ module EmptyList
31
+ def EmptyList.to_s
32
+ "()"
33
+ end
34
+ end
35
+
36
+ class Vector
37
+ def to_s
38
+ "[#{join " "}]"
39
+ end
40
+ end
41
+ end
42
+
43
+ module RubyLisp
44
+ class Value
45
+ attr_accessor :value, :quoted
46
+
47
+ def initialize(value)
48
+ @value = value
49
+ end
50
+
51
+ def quote
52
+ @quoted = true
53
+ self
54
+ end
55
+
56
+ def unquote
57
+ @quoted = false
58
+ self
59
+ end
60
+
61
+ def to_s
62
+ @value.to_s
63
+ end
64
+ end
65
+
66
+ class Boolean < Value; end
67
+ class Float < Value; end
68
+ class Int < Value; end
69
+ class Keyword < Value; end
70
+ class String < Value; end
71
+
72
+ class ParseError < StandardError; end
73
+ class RuntimeError < StandardError; end
74
+
75
+ class HashMap < Value
76
+ def initialize(seq)
77
+ if seq.size.odd?
78
+ raise RubyLisp::ParseError,
79
+ "A RubyLisp::HashMap must contain an even number of forms."
80
+ else
81
+ @value = Hamster::Hash[seq.each_slice(2).to_a]
82
+ end
83
+ end
84
+ end
85
+
86
+ class List < Value
87
+ def initialize(values)
88
+ @value = values.to_list
89
+ end
90
+
91
+ def quote
92
+ @value.each(&:quote)
93
+ @quoted = true
94
+ self
95
+ end
96
+
97
+ def unquote
98
+ @value.each(&:unquote)
99
+ @quoted = false
100
+ self
101
+ end
102
+ end
103
+
104
+ class Nil < Value
105
+ def initialize
106
+ @value = nil
107
+ end
108
+ end
109
+
110
+ class Symbol < Value
111
+ def to_s
112
+ @value
113
+ end
114
+
115
+ def resolve(env)
116
+ # rbl: (.+ 1 2)
117
+ # ruby: 1.+(2)
118
+ instance_method = /^\.(.*)/.match(@value).to_a[1]
119
+ if instance_method
120
+ return lambda {|obj, *args| obj.send instance_method, *args}
121
+ end
122
+
123
+ # rbl: (File::open "/tmp/foo")
124
+ # ruby: File::open("/tmp/foo") OR File.open("/tmp/foo")
125
+ #
126
+ # rbl: Foo::Bar::BAZ
127
+ # ruby: Foo::Bar::BAZ
128
+ if /^\w+(::\w+)+$/ =~ @value
129
+ first_segment, *segments = @value.split('::')
130
+ first_segment = Object.const_get first_segment
131
+ return segments.reduce(first_segment) do |result, segment|
132
+ if result.class == Proc
133
+ # a method can only be in the final position
134
+ raise RubyLisp::RuntimeError, "Invalid value: #{@value}"
135
+ elsif /^[A-Z]/ =~ segment
136
+ # get module/class constant
137
+ result.const_get segment
138
+ else
139
+ # if module/class method doesn't exist, trigger a NoMethodError
140
+ result.send segment unless result.respond_to? segment
141
+ # call module/class method
142
+ lambda {|*args| result.send segment, *args }
143
+ end
144
+ end
145
+ end
146
+
147
+ env.get @value
148
+ end
149
+ end
150
+
151
+ class Vector < Value
152
+ def initialize(values)
153
+ @value = Hamster::Vector.new(values)
154
+ end
155
+
156
+ def quote
157
+ @value.each(&:quote)
158
+ @quoted = true
159
+ self
160
+ end
161
+
162
+ def unquote
163
+ @value.each(&:unquote)
164
+ @quoted = false
165
+ self
166
+ end
167
+ end
168
+ end
169
+
@@ -0,0 +1,3 @@
1
+ module RubyLisp
2
+ VERSION = "0.0.1"
3
+ end
data/lib/rubylisp.rb ADDED
@@ -0,0 +1 @@
1
+ # ʕ •ᴥ• ʔ nothing to see here
data/rubylisp/core.rbl ADDED
@@ -0,0 +1,245 @@
1
+ ; TODO:
2
+ ; and, or
3
+ ; implement macros
4
+ ; defn
5
+ ; str
6
+ ; comment
7
+ ; ->, ->>
8
+ ; private vars/fns
9
+ ; if-not, when, when-not, if-let, when-let
10
+ ; cond
11
+ ; case
12
+
13
+ (ns rubylisp.core)
14
+
15
+ ;; MATH & LOGIC
16
+
17
+ (def +
18
+ (fn + [x y & more]
19
+ (let [xy (.+ x y)]
20
+ (if more
21
+ (apply + xy more)
22
+ xy))))
23
+
24
+ (def -
25
+ (fn - [x y & more]
26
+ (let [xy (.- x y)]
27
+ (if more
28
+ (apply - xy more)
29
+ xy))))
30
+
31
+ (def inc
32
+ (fn inc [x]
33
+ (+ x 1)))
34
+
35
+ (def dec
36
+ (fn dec [x]
37
+ (- x 1)))
38
+
39
+ (def *
40
+ (fn * [x y & more]
41
+ (let [xy (.* x y)]
42
+ (if more
43
+ (apply * xy more)
44
+ xy))))
45
+
46
+ (def /
47
+ (fn / [x y & more]
48
+ (let [xy (./ x y)]
49
+ (if more
50
+ (apply / xy more)
51
+ xy))))
52
+
53
+ (def <
54
+ (fn < [x y & more]
55
+ (let [xy (.< x y)]
56
+ (if more
57
+ (apply < xy more)
58
+ xy))))
59
+
60
+ (def <=
61
+ (fn <= [x y & more]
62
+ (let [xy (.<= x y)]
63
+ (if more
64
+ (apply <= xy more)
65
+ xy))))
66
+
67
+ (def =
68
+ (fn = [x y & more]
69
+ (let [xy (.== x y)]
70
+ (if more
71
+ (apply = xy more)
72
+ xy))))
73
+
74
+ (def not=
75
+ (fn not= [x y & more]
76
+ (let [xy (.!= x y)]
77
+ (if more
78
+ (apply not= xy more)
79
+ xy))))
80
+
81
+ (def boolean
82
+ (fn boolean [x]
83
+ (if x true false)))
84
+
85
+ (def not
86
+ (fn not [x]
87
+ (if x false true)))
88
+
89
+ (def >
90
+ (fn > [x y & more]
91
+ (let [xy (.> x y)]
92
+ (if more
93
+ (apply > xy more)
94
+ xy))))
95
+
96
+ (def >=
97
+ (fn >= [x y & more]
98
+ (let [xy (.>= x y)]
99
+ (if more
100
+ (apply >= xy more)
101
+ xy))))
102
+
103
+ (def pos?
104
+ (fn pos? [x]
105
+ (> (count x) 0)))
106
+
107
+ (def neg?
108
+ (fn neg? [x]
109
+ (< (count x) 0)))
110
+
111
+ (def zero?
112
+ (fn zero? [x]
113
+ (= 0 x)))
114
+
115
+ ;; LISTS & COLLECTIONS
116
+
117
+ (def nil?
118
+ (fn nil? [x]
119
+ (= nil x)))
120
+
121
+ (def count
122
+ (fn count [x]
123
+ (if (nil? x)
124
+ 0
125
+ (.count x))))
126
+
127
+ (def empty?
128
+ (fn empty? [coll]
129
+ (if (nil? coll)
130
+ true
131
+ (.empty? coll))))
132
+
133
+ (def list
134
+ (fn list [& args]
135
+ (.to_list (if (nil? args)
136
+ []
137
+ args))))
138
+
139
+ (def list?
140
+ (fn list? [x]
141
+ (.is_a? x Hamster::List)))
142
+
143
+ (def string?
144
+ (fn string? [x]
145
+ (.is_a? x Kernel::String)))
146
+
147
+ (def seq
148
+ (fn seq [x]
149
+ (if (not (empty? x))
150
+ (if (string? x)
151
+ (.to_list (.chars x))
152
+ (.to_list x)))))
153
+
154
+ (def cons
155
+ (fn cons [x y]
156
+ (let [lst (if (nil? y) (Hamster::List::empty) (.to_list y))]
157
+ (.cons lst x))))
158
+
159
+ (def first
160
+ (fn first [coll]
161
+ (.first coll)))
162
+
163
+ (def last
164
+ (fn last [coll]
165
+ (.last coll)))
166
+
167
+ (def rest
168
+ (fn rest [coll]
169
+ (let [lst (if (nil? coll) (Hamster::List::empty) (.to_list coll))]
170
+ (.tail lst))))
171
+
172
+ (def take
173
+ (fn take [n coll]
174
+ (.to_list (.take coll n))))
175
+
176
+ ;; TODO: support mapping over multiple collections
177
+ (def map
178
+ (fn map [f coll]
179
+ (if (empty? coll)
180
+ ()
181
+ (cons (f (first coll)) (map f (rest coll))))))
182
+
183
+ (def reverse
184
+ (fn reverse [coll]
185
+ (if (empty? coll)
186
+ coll
187
+ (+ [(last coll)]
188
+ (reverse (.slice coll 0 (dec (count coll))))))))
189
+
190
+ ;; TODO: implement as multiple arity fn
191
+ ;; TODO: zero-arity that produces a lazy list, i.e. Hamster::iterate(0,
192
+ ;; &:next).take(5)
193
+ ;; (requires some kind of block syntax)
194
+ (def range
195
+ (fn range [& args]
196
+ (if (= 1 (count args))
197
+ (apply range 0 args)
198
+ (apply Hamster::interval args))))
199
+
200
+ ;; TODO: implement as multiple arity fn
201
+ (def repeat
202
+ (fn repeat [& args]
203
+ (if (= 1 (count args))
204
+ (apply Hamster::repeat args)
205
+ (apply Hamster::replicate args))))
206
+
207
+ ;; STRINGS
208
+
209
+ (def str
210
+ (fn str [& args]
211
+ (.join (map .to_s args))))
212
+
213
+ ;; I/O
214
+
215
+ (def pr-str
216
+ (fn pr-str [& args]
217
+ (if (zero? (count args))
218
+ ""
219
+ (apply RubyLisp::Printer::pr_str args))))
220
+
221
+ (def prn
222
+ (fn prn [& args]
223
+ (Kernel::puts (apply pr-str args))
224
+ nil))
225
+
226
+ (def print
227
+ (fn print [& args]
228
+ (Kernel::print (.join (map str args) " "))
229
+ nil))
230
+
231
+ (def println
232
+ (fn print [& args]
233
+ (Kernel::puts (.join (map str args) " "))
234
+ nil))
235
+
236
+ ;; RUBY INTEROP
237
+
238
+ (def =@
239
+ (fn =@ [obj kw value]
240
+ (.instance_variable_set obj (.to_sym (+ "@" (.to_s kw))) value)))
241
+
242
+ (def class
243
+ (fn class [x]
244
+ (.class x)))
245
+
data/rubylisp/repl.rbl ADDED
@@ -0,0 +1,2 @@
1
+ ; TODO
2
+ (Kernel::puts "Loaded rubylisp.repl")
data/rubylisp.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rubylisp/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rbl"
8
+ spec.version = RubyLisp::VERSION
9
+ spec.authors = ["Dave Yarwood"]
10
+ spec.email = ["dave.yarwood@gmail.com"]
11
+
12
+ spec.summary = "A Lisp dialect of Ruby"
13
+ spec.description = "A Lisp dialect of Ruby"
14
+ spec.homepage = "https://github.com/daveyarwood/rubylisp"
15
+ spec.license = "MIT"
16
+
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against " \
21
+ "public gem pushes."
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{^(test|spec|features)/})
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_runtime_dependency 'hamster', '3.0.0'
34
+ end