rbl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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