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