apricot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +26 -0
  7. data/README.md +90 -0
  8. data/Rakefile +9 -0
  9. data/apricot.gemspec +22 -0
  10. data/bin/apricot +58 -0
  11. data/examples/bot.apr +23 -0
  12. data/examples/cinch-bot.apr +12 -0
  13. data/examples/hanoi.apr +10 -0
  14. data/examples/hello.apr +1 -0
  15. data/examples/plot.apr +28 -0
  16. data/examples/quine.apr +1 -0
  17. data/kernel/core.apr +928 -0
  18. data/lib/apricot/ast/identifier.rb +111 -0
  19. data/lib/apricot/ast/list.rb +99 -0
  20. data/lib/apricot/ast/literals.rb +240 -0
  21. data/lib/apricot/ast/node.rb +45 -0
  22. data/lib/apricot/ast/scopes.rb +147 -0
  23. data/lib/apricot/ast/toplevel.rb +66 -0
  24. data/lib/apricot/ast/variables.rb +64 -0
  25. data/lib/apricot/ast.rb +3 -0
  26. data/lib/apricot/compiler.rb +55 -0
  27. data/lib/apricot/cons.rb +27 -0
  28. data/lib/apricot/errors.rb +38 -0
  29. data/lib/apricot/generator.rb +15 -0
  30. data/lib/apricot/identifier.rb +91 -0
  31. data/lib/apricot/list.rb +96 -0
  32. data/lib/apricot/macroexpand.rb +47 -0
  33. data/lib/apricot/misc.rb +11 -0
  34. data/lib/apricot/namespace.rb +59 -0
  35. data/lib/apricot/parser.rb +541 -0
  36. data/lib/apricot/printers.rb +12 -0
  37. data/lib/apricot/repl.rb +254 -0
  38. data/lib/apricot/ruby_ext.rb +254 -0
  39. data/lib/apricot/seq.rb +44 -0
  40. data/lib/apricot/special_forms.rb +735 -0
  41. data/lib/apricot/stages.rb +60 -0
  42. data/lib/apricot/version.rb +3 -0
  43. data/lib/apricot.rb +30 -0
  44. data/spec/compiler_spec.rb +499 -0
  45. data/spec/identifier_spec.rb +58 -0
  46. data/spec/list_spec.rb +96 -0
  47. data/spec/parser_spec.rb +312 -0
  48. data/spec/spec_helper.rb +10 -0
  49. metadata +188 -0
@@ -0,0 +1,254 @@
1
+ require 'readline'
2
+ require 'yaml'
3
+
4
+ module Apricot
5
+ class REPL
6
+ # TODO: make history more configurable
7
+ HISTORY_FILE = "~/.apricot-history"
8
+ MAX_HISTORY_LINES = 1000
9
+
10
+ COMMANDS = {
11
+ "!backtrace" => {
12
+ doc: "Print the backtrace of the most recent exception",
13
+ code: proc do
14
+ puts (@exception ? @exception.awesome_backtrace : "No backtrace")
15
+ end
16
+ },
17
+
18
+ "!bytecode" => {
19
+ doc: "Print the bytecode generated from the previous line",
20
+ code: proc do
21
+ puts (@compiled_code ? @compiled_code.decode : "No previous line")
22
+ end
23
+ },
24
+
25
+ "!exit" => {doc: "Exit the REPL", code: proc { exit }},
26
+
27
+ "!help" => {
28
+ doc: "Print this message",
29
+ code: proc do
30
+ width = 14
31
+
32
+ puts "(doc foo)".ljust(width) +
33
+ "Print the documentation for a function or macro"
34
+ COMMANDS.sort.each {|name, c| puts name.ljust(width) + c[:doc] }
35
+ end
36
+ }
37
+ }
38
+
39
+ COMMAND_COMPLETIONS = COMMANDS.keys.sort
40
+ SPECIAL_COMPLETIONS = SpecialForm::Specials.keys.map(&:to_s)
41
+
42
+ def initialize(prompt = 'apr> ', bytecode = false, history_file = nil)
43
+ @prompt = prompt
44
+ @bytecode = bytecode
45
+ @history_file = File.expand_path(history_file || HISTORY_FILE)
46
+ @line = 1
47
+ end
48
+
49
+ def run
50
+ Readline.completion_append_character = " "
51
+ Readline.basic_word_break_characters = " \t\n\"'`~@;{[("
52
+ Readline.basic_quote_characters = "\""
53
+
54
+ Readline.completion_proc = proc do |s|
55
+ if s.start_with? '!'
56
+ # User is typing a REPL command
57
+ COMMAND_COMPLETIONS.select {|c| c.start_with? s }
58
+ elsif ('A'..'Z').include? s[0]
59
+ # User is typing a constant
60
+ constant_completion(s)
61
+ else
62
+ # User is typing a regular name
63
+ comps = SPECIAL_COMPLETIONS +
64
+ Apricot.current_namespace.vars.keys.map(&:to_s)
65
+ comps.select {|c| c.start_with? s }.sort
66
+ end
67
+ end
68
+
69
+ load_history
70
+ terminal_state = `stty -g`.chomp
71
+
72
+ while code = readline_with_history
73
+ stripped = code.strip
74
+ if stripped.empty?
75
+ next
76
+ elsif stripped.start_with?('!')
77
+ if COMMANDS.include?(stripped) && block = COMMANDS[stripped][:code]
78
+ instance_eval(&block)
79
+ else
80
+ puts "Unknown command: #{stripped}"
81
+ end
82
+ next
83
+ end
84
+
85
+ begin
86
+ @compiled_code =
87
+ Apricot::Compiler.compile_string(code, "(eval)", @line, @bytecode)
88
+ value = Rubinius.run_script @compiled_code
89
+ puts "=> #{value.apricot_inspect}"
90
+ Apricot.current_namespace.set_var(:_, value)
91
+ e = nil
92
+ rescue Apricot::SyntaxError => e
93
+ if e.incomplete?
94
+ begin
95
+ more_code = Readline.readline(' ' * @prompt.length, false)
96
+ if more_code
97
+ code << "\n" << more_code
98
+ Readline::HISTORY << Readline::HISTORY.pop + "\n" +
99
+ ' ' * @prompt.length + more_code
100
+ retry
101
+ else
102
+ print "\r" # print the exception at the start of the line
103
+ end
104
+ rescue Interrupt
105
+ # This is raised by Ctrl-C. Stop trying to read more code and
106
+ # just give up. Remove the current input from history.
107
+ current_code = Readline::HISTORY.pop
108
+ @line -= current_code.count "\n"
109
+ e = nil # ignore the syntax error since the code was Ctrl-C'd
110
+ end
111
+ end
112
+ rescue Interrupt => e
113
+ # Raised by Ctrl-C. Print a newline so the error message is on the
114
+ # next line.
115
+ puts
116
+ rescue SystemExit, SignalException
117
+ raise
118
+ rescue Exception => e
119
+ end
120
+
121
+ if e
122
+ @exception = e
123
+ puts "#{e.class}: #{e.message}"
124
+ end
125
+
126
+ @line += 1 + code.count("\n")
127
+ end
128
+
129
+ puts # Print a newline after Ctrl-D (EOF)
130
+
131
+ ensure
132
+ save_history
133
+ system('stty', terminal_state) if terminal_state # Restore the terminal
134
+ end
135
+
136
+ def load_history
137
+ if File.exist?(@history_file)
138
+ hist = YAML.load_file @history_file
139
+
140
+ if hist.is_a? Array
141
+ hist.each {|x| Readline::HISTORY << x }
142
+ else
143
+ File.open(@history_file) do |f|
144
+ f.each {|line| Readline::HISTORY << line.chomp }
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ def save_history
151
+ return if Readline::HISTORY.empty?
152
+
153
+ File.open(@history_file, "w") do |f|
154
+ hist = Readline::HISTORY.to_a
155
+ hist.shift(hist.size - MAX_HISTORY_LINES) if hist.size > MAX_HISTORY_LINES
156
+ YAML.dump(hist, f, header: true)
157
+ end
158
+ end
159
+
160
+ # Smarter Readline to prevent empty and dups
161
+ # 1. Read a line and append to history
162
+ # 2. Quick Break on nil
163
+ # 3. Remove from history if empty or dup
164
+ def readline_with_history
165
+ line = Readline.readline(@prompt, true)
166
+ return nil if line.nil?
167
+
168
+ if line =~ /\A\s*\z/ || (Readline::HISTORY.size > 1 &&
169
+ Readline::HISTORY[-2] == line)
170
+ Readline::HISTORY.pop
171
+ end
172
+
173
+ line
174
+ rescue Interrupt
175
+ # This is raised by Ctrl-C. Remove the line from history then try to
176
+ # read another line.
177
+ puts "^C"
178
+ Readline::HISTORY.pop
179
+ @line -= 1
180
+ retry
181
+ end
182
+
183
+ # Find constant Foo::Bar::Baz from ["Foo", "Bar", "Baz"] array. Helper for
184
+ # tab-completion of constants.
185
+ def find_constant(const_names)
186
+ const_names.reduce(Object) do |mod, name|
187
+ mod.const_get(name)
188
+ end
189
+ rescue NameError
190
+ # Return nil if the constant doesn't exist.
191
+ nil
192
+ end
193
+
194
+ # Tab-completion for constants and namespaced identifiers
195
+ def constant_completion(s)
196
+ # Split Foo/bar into Foo and bar. If there is no / then id will be nil.
197
+ constant_str, id = s.split('/', 2)
198
+
199
+ # If we have a Foo/bar case, complete the 'bar' part if possible.
200
+ if id
201
+ # Split with -1 returns an extra empty string if constant_str ends in
202
+ # '::'. Then it will fail to find the constant for Foo::/ and we won't
203
+ # try completing Foo::/ to Foo/whatever.
204
+ const_names = constant_str.split('::', -1)
205
+
206
+ const = find_constant(const_names)
207
+
208
+ # If we can't find the constant the user is typing, don't return any
209
+ # completions. If it isn't a Module or Namespace (subclass of Module),
210
+ # we can't complete methods or vars below it. (e.g. in Math::PI/<tab>
211
+ # we can't do any completions)
212
+ return [] unless const && const.is_a?(Module)
213
+
214
+ # Complete the vars of the namespace or the methods of the module.
215
+ potential_completions =
216
+ const.is_a?(Apricot::Namespace) ? const.vars.keys : const.methods
217
+
218
+ # Select the matching vars or methods and format them properly as
219
+ # completions.
220
+ potential_completions.select do |c|
221
+ c.to_s.start_with? id
222
+ end.sort.map do |c|
223
+ "#{constant_str}/#{c}"
224
+ end
225
+
226
+ # Otherwise there is no / and we complete constant names.
227
+ else
228
+ # Split with -1 returns an extra empty string if constant_str ends in
229
+ # '::'. This allows us to differentiate Foo:: and Foo cases.
230
+ const_names = constant_str.split('::', -1)
231
+ curr_name = const_names.pop # The user is currently typing the last name.
232
+
233
+ const = find_constant(const_names)
234
+
235
+ # If we can't find the constant the user is typing, don't return any
236
+ # completions. If it isn't a Module, we can't complete constants below
237
+ # it. (e.g. in Math::PI::<tab> we can't do anything)
238
+ return [] unless const && const.is_a?(Module)
239
+
240
+ # Select the matching constants and format them properly as
241
+ # completions.
242
+ const.constants.select do |c|
243
+ c.to_s.start_with? curr_name
244
+ end.sort.map do |name|
245
+ if const_names.size == 0
246
+ name.to_s
247
+ else
248
+ "#{const_names.join('::')}::#{name}"
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,254 @@
1
+ class Object
2
+ attr_accessor :apricot_meta
3
+
4
+ def apricot_inspect
5
+ inspect
6
+ end
7
+
8
+ def apricot_str
9
+ to_s
10
+ end
11
+
12
+ def apricot_call(*args)
13
+ call(*args)
14
+ end
15
+ end
16
+
17
+ class Array
18
+ # Adapted from Array#inspect. This version prints no commas and calls
19
+ # #apricot_inspect on its elements. e.g. [1 2 3]
20
+ def apricot_inspect
21
+ return '[]' if size == 0
22
+
23
+ str = '['
24
+
25
+ return '[...]' if Thread.detect_recursion self do
26
+ each {|x| str << x.apricot_inspect << ' ' }
27
+ end
28
+
29
+ str.chop!
30
+ str << ']'
31
+ end
32
+
33
+ def apricot_call(idx)
34
+ self[idx]
35
+ end
36
+
37
+ alias_method :apricot_str, :apricot_inspect
38
+
39
+ def to_seq
40
+ if length == 0
41
+ nil
42
+ else
43
+ Seq.new(self, 0)
44
+ end
45
+ end
46
+
47
+ class Seq
48
+ include Apricot::Seq
49
+
50
+ def initialize(array, offset = 0)
51
+ @array = array
52
+ @offset = offset
53
+ end
54
+
55
+ def first
56
+ @array[@offset]
57
+ end
58
+
59
+ def next
60
+ if @offset + 1 < @array.length
61
+ Seq.new(@array, @offset + 1)
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ def each
68
+ @array[@offset..-1].each {|x| yield x }
69
+ end
70
+
71
+ def count
72
+ @array.length - @offset
73
+ end
74
+ end
75
+ end
76
+
77
+ class Hash
78
+ # Adapted from Hash#inspect. Outputs Apricot hash syntax, e.g. {:a 1, :b 2}
79
+ def apricot_inspect
80
+ return '{}' if size == 0
81
+
82
+ str = '{'
83
+
84
+ return '{...}' if Thread.detect_recursion self do
85
+ each_item do |item|
86
+ str << item.key.apricot_inspect
87
+ str << ' '
88
+ str << item.value.apricot_inspect
89
+ str << ', '
90
+ end
91
+ end
92
+
93
+ str.shorten!(2)
94
+ str << '}'
95
+ end
96
+
97
+ def apricot_call(key)
98
+ self[key]
99
+ end
100
+
101
+ alias_method :apricot_str, :apricot_inspect
102
+
103
+ def to_seq
104
+ each_pair.to_a.to_seq
105
+ end
106
+ end
107
+
108
+ class Set
109
+ def apricot_inspect
110
+ return '#{}' if size == 0
111
+
112
+ str = '#{'
113
+
114
+ return '#{...}' if Thread.detect_recursion self do
115
+ each {|x| str << x.apricot_inspect << ' ' }
116
+ end
117
+
118
+ str.chop!
119
+ str << '}'
120
+ end
121
+
122
+ def [](elem)
123
+ elem if self.include? elem
124
+ end
125
+
126
+ alias_method :apricot_call, :[]
127
+ alias_method :apricot_str, :apricot_inspect
128
+
129
+ def to_seq
130
+ to_a.to_seq
131
+ end
132
+ end
133
+
134
+ class Rational
135
+ def apricot_inspect
136
+ if @denominator == 1
137
+ @numerator.to_s
138
+ else
139
+ to_s
140
+ end
141
+ end
142
+
143
+ alias_method :apricot_str, :apricot_inspect
144
+ end
145
+
146
+ class Regexp
147
+ def apricot_inspect
148
+ "#r#{inspect}"
149
+ end
150
+
151
+ alias_method :apricot_str, :apricot_inspect
152
+ end
153
+
154
+ class Symbol
155
+ def apricot_inspect
156
+ str = to_s
157
+
158
+ if str =~ /\A#{Apricot::Parser::IDENTIFIER}+\z/
159
+ ":#{str}"
160
+ else
161
+ ":#{str.inspect}"
162
+ end
163
+ end
164
+
165
+ def apricot_call(o)
166
+ o[self]
167
+ end
168
+ end
169
+
170
+ class Range
171
+ def to_seq
172
+ if first > last || (first == last && exclude_end?)
173
+ nil
174
+ else
175
+ Seq.new(first, last, exclude_end?)
176
+ end
177
+ end
178
+
179
+ class Seq
180
+ include Apricot::Seq
181
+
182
+ def initialize(first, last, exclude)
183
+ @first = first
184
+ @last = last
185
+ @exclude = exclude
186
+ end
187
+
188
+ def first
189
+ @first
190
+ end
191
+
192
+ def next
193
+ next_val = @first.succ
194
+
195
+ if @first == @last || (next_val == @last && @exclude)
196
+ nil
197
+ else
198
+ Seq.new(next_val, @last, @exclude)
199
+ end
200
+ end
201
+
202
+ def each
203
+ prev = nil
204
+ val = @first
205
+
206
+ until prev == @last || (val == @last && @exclude)
207
+ yield val
208
+ prev = val
209
+ val = val.succ
210
+ end
211
+
212
+ self
213
+ end
214
+ end
215
+ end
216
+
217
+ module Enumerable
218
+ def to_list
219
+ list = Apricot::List::EmptyList
220
+ reverse_each {|x| list = list.cons(x) }
221
+ list
222
+ end
223
+ end
224
+
225
+ class NilClass
226
+ include Enumerable
227
+
228
+ def each
229
+ end
230
+
231
+ def empty?
232
+ true
233
+ end
234
+
235
+ # Seq Methods
236
+ # Many functions that return seqs occasionally return nil, so it's
237
+ # convenient if nil can respond to some of the same methods as seqs.
238
+
239
+ def to_seq
240
+ nil
241
+ end
242
+
243
+ def first
244
+ nil
245
+ end
246
+
247
+ def next
248
+ nil
249
+ end
250
+
251
+ def rest
252
+ Apricot::List::EmptyList
253
+ end
254
+ end
@@ -0,0 +1,44 @@
1
+ module Apricot
2
+ # Every seq should include this module and define 'first' and 'next'
3
+ # methods. A seq may redefine 'rest' and 'each' if there is a more efficient
4
+ # way to implement them.
5
+ #
6
+ # 'first' should return the first item in the seq.
7
+ # 'next' should return a seq of the rest of the items in the seq, or nil
8
+ # if there are no more items.
9
+ module Seq
10
+ include Enumerable
11
+
12
+ def rest
13
+ self.next || Apricot::List::EmptyList
14
+ end
15
+
16
+ def each
17
+ s = self
18
+
19
+ while s
20
+ yield s.first
21
+ s = s.next
22
+ end
23
+
24
+ self
25
+ end
26
+
27
+ def to_seq
28
+ self
29
+ end
30
+
31
+ def empty?
32
+ false
33
+ end
34
+
35
+ def to_s
36
+ str = '('
37
+ each {|x| str << x.apricot_inspect << ' ' }
38
+ str.chop!
39
+ str << ')'
40
+ end
41
+
42
+ alias_method :inspect, :to_s
43
+ end
44
+ end