apricot 0.0.1 → 0.0.2

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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +1 -0
  5. data/Gemfile.lock +229 -11
  6. data/README.md +46 -29
  7. data/Rakefile +1 -1
  8. data/apricot.gemspec +7 -3
  9. data/benchmarks/factorial.rb +51 -0
  10. data/benchmarks/interpolate.rb +20 -0
  11. data/bin/apricot +5 -23
  12. data/examples/bot.apr +1 -4
  13. data/examples/cinch-bot.apr +3 -3
  14. data/examples/sinatra.apr +9 -0
  15. data/kernel/core.apr +124 -75
  16. data/kernel/repl.apr +37 -0
  17. data/lib/apricot.rb +7 -26
  18. data/lib/apricot/boot.rb +24 -0
  19. data/lib/apricot/code_loader.rb +108 -0
  20. data/lib/apricot/compiler.rb +265 -32
  21. data/lib/apricot/generator.rb +10 -3
  22. data/lib/apricot/identifier.rb +25 -10
  23. data/lib/apricot/list.rb +28 -41
  24. data/lib/apricot/macroexpand.rb +14 -8
  25. data/lib/apricot/misc.rb +2 -1
  26. data/lib/apricot/namespace.rb +20 -3
  27. data/lib/apricot/{parser.rb → reader.rb} +221 -194
  28. data/lib/apricot/repl.rb +67 -24
  29. data/lib/apricot/ruby_ext.rb +27 -16
  30. data/lib/apricot/scopes.rb +159 -0
  31. data/lib/apricot/seq.rb +43 -1
  32. data/lib/apricot/special_forms.rb +16 -695
  33. data/lib/apricot/special_forms/def.rb +32 -0
  34. data/lib/apricot/special_forms/do.rb +23 -0
  35. data/lib/apricot/special_forms/dot.rb +112 -0
  36. data/lib/apricot/special_forms/fn.rb +342 -0
  37. data/lib/apricot/special_forms/if.rb +31 -0
  38. data/lib/apricot/special_forms/let.rb +8 -0
  39. data/lib/apricot/special_forms/loop.rb +10 -0
  40. data/lib/apricot/special_forms/quote.rb +9 -0
  41. data/lib/apricot/special_forms/recur.rb +26 -0
  42. data/lib/apricot/special_forms/try.rb +146 -0
  43. data/lib/apricot/variables.rb +65 -0
  44. data/lib/apricot/version.rb +1 -1
  45. data/spec/compiler_spec.rb +53 -450
  46. data/spec/fn_spec.rb +206 -0
  47. data/spec/list_spec.rb +1 -1
  48. data/spec/reader_spec.rb +349 -0
  49. data/spec/spec_helper.rb +40 -4
  50. data/spec/special_forms_spec.rb +203 -0
  51. metadata +99 -133
  52. data/lib/apricot/ast.rb +0 -3
  53. data/lib/apricot/ast/identifier.rb +0 -111
  54. data/lib/apricot/ast/list.rb +0 -99
  55. data/lib/apricot/ast/literals.rb +0 -240
  56. data/lib/apricot/ast/node.rb +0 -45
  57. data/lib/apricot/ast/scopes.rb +0 -147
  58. data/lib/apricot/ast/toplevel.rb +0 -66
  59. data/lib/apricot/ast/variables.rb +0 -64
  60. data/lib/apricot/printers.rb +0 -12
  61. data/lib/apricot/stages.rb +0 -60
  62. data/spec/parser_spec.rb +0 -312
@@ -1,7 +1,14 @@
1
1
  module Apricot
2
- class Generator < Rubinius::Generator
3
- def scopes
4
- @scopes ||= []
2
+ class Generator < Rubinius::ToolSet::Runtime::Generator
3
+ attr_reader :scopes
4
+ attr_accessor :tail_position
5
+
6
+ alias_method :tail_position?, :tail_position
7
+
8
+ def initialize
9
+ super
10
+ @scopes = []
11
+ @tail_position = false
5
12
  end
6
13
 
7
14
  def scope
@@ -1,6 +1,6 @@
1
1
  module Apricot
2
2
  class Identifier
3
- attr_reader :name, :ns, :unqualified_name
3
+ attr_reader :name, :unqualified_name
4
4
 
5
5
  @table = {}
6
6
 
@@ -19,16 +19,15 @@ module Apricot
19
19
  @const_names = @name.to_s.split('::').map(&:to_sym)
20
20
  elsif @name =~ /\A(.+?)\/(.+)\z/
21
21
  @qualified = true
22
- ns_id = Identifier.intern($1)
23
- raise 'namespace in identifier must be a constant' unless ns_id.constant?
22
+ qualifier_id = Identifier.intern($1)
23
+ raise 'Qualifier in qualified identifier must be a constant' unless qualifier_id.constant?
24
24
 
25
- @ns = ns_id.const_names.reduce(Object) do |mod, name|
25
+ @qualifier = qualifier_id.const_names.reduce(Object) do |mod, name|
26
26
  mod.const_get(name)
27
27
  end
28
28
 
29
29
  @unqualified_name = $2.to_sym
30
30
  else
31
- @ns = Apricot.current_namespace
32
31
  @unqualified_name = name
33
32
  end
34
33
  end
@@ -37,14 +36,30 @@ module Apricot
37
36
  @qualified
38
37
  end
39
38
 
40
- def unqualified_name
41
- @unqualified_name
42
- end
43
-
44
39
  def constant?
45
40
  @constant
46
41
  end
47
42
 
43
+ # Does the identifier reference a fn on a namespace?
44
+ def fn?
45
+ qualifier.is_a?(Namespace) && qualifier.fns.include?(@unqualified_name)
46
+ end
47
+
48
+ # Does the identifier reference a method on a module?
49
+ def method?
50
+ !qualifier.is_a?(Namespace) && qualifier.respond_to?(@unqualified_name)
51
+ end
52
+
53
+ # Get the metadata of the object this identifier references, or nil.
54
+ def meta
55
+ qualifier.is_a?(Namespace) && qualifier.vars[@unqualified_name] &&
56
+ qualifier.vars[@unqualified_name].apricot_meta
57
+ end
58
+
59
+ def qualifier
60
+ @qualifier ||= Apricot.current_namespace
61
+ end
62
+
48
63
  def const_names
49
64
  raise "#{@name} is not a constant" unless constant?
50
65
  @const_names
@@ -71,7 +86,7 @@ module Apricot
71
86
  # be parsed as keywords or numbers
72
87
  str = @name.to_s.gsub(/(\\.)|\|/) { $1 || '\|' }
73
88
  "#|#{str}|"
74
- when /\A#{Apricot::Parser::IDENTIFIER}+\z/
89
+ when /\A#{Reader::IDENTIFIER}+\z/
75
90
  @name.to_s
76
91
  else
77
92
  str = @name.to_s.inspect[1..-2]
@@ -4,59 +4,25 @@ module Apricot
4
4
  include Seq
5
5
 
6
6
  def self.[](*args)
7
- list = EmptyList
7
+ list = EMPTY_LIST
8
8
  args.reverse_each do |arg|
9
9
  list = list.cons(arg)
10
10
  end
11
11
  list
12
12
  end
13
13
 
14
- attr_reader :head, :tail
14
+ attr_reader :head, :tail, :count
15
15
 
16
16
  def initialize(head, tail)
17
17
  @head = head
18
- @tail = tail
18
+ @tail = tail || EMPTY_LIST
19
+ @count = tail ? tail.count + 1 : 1
19
20
  end
20
21
 
21
22
  def cons(x)
22
23
  List.new(x, self)
23
24
  end
24
25
 
25
- def each
26
- list = self
27
- until list.empty?
28
- yield list.head
29
- list = list.tail
30
- end
31
- end
32
-
33
- def ==(other)
34
- return true if self.equal? other
35
- return false unless other.is_a? List
36
-
37
- list = self
38
-
39
- until list.empty?
40
- return false if other.empty? || list.head != other.head
41
-
42
- list = list.tail
43
- other = other.tail
44
- end
45
-
46
- other.empty?
47
- end
48
-
49
- alias_method :eql?, :==
50
-
51
- def hash
52
- hashes = map {|x| x.hash }
53
- hashes.reduce(hashes.size) {|acc,hash| acc ^ hash }
54
- end
55
-
56
- def empty?
57
- !@tail
58
- end
59
-
60
26
  def initialize_copy(other)
61
27
  super
62
28
  @tail = other.tail.dup if other.tail && !other.tail.empty?
@@ -69,7 +35,7 @@ module Apricot
69
35
  end
70
36
 
71
37
  def first
72
- empty? ? nil : @head
38
+ @head
73
39
  end
74
40
 
75
41
  def next
@@ -77,7 +43,7 @@ module Apricot
77
43
  end
78
44
 
79
45
  def to_seq
80
- empty? ? nil : self
46
+ self
81
47
  end
82
48
 
83
49
  def inspect
@@ -91,6 +57,27 @@ module Apricot
91
57
 
92
58
  alias_method :to_s, :inspect
93
59
 
94
- EmptyList = new(nil, nil)
60
+ class EmptyList < List
61
+ def initialize
62
+ @count = 0
63
+ end
64
+
65
+ def each
66
+ end
67
+
68
+ def empty?
69
+ true
70
+ end
71
+
72
+ def first
73
+ nil
74
+ end
75
+
76
+ def next
77
+ nil
78
+ end
79
+ end
80
+
81
+ EMPTY_LIST = EmptyList.new
95
82
  end
96
83
  end
@@ -1,30 +1,36 @@
1
1
  module Apricot
2
2
  def self.macroexpand(form)
3
3
  ex = macroexpand_1(form)
4
- ex.equal?(form) ? ex : macroexpand(ex)
4
+
5
+ until ex.equal?(form)
6
+ form = ex
7
+ ex = macroexpand_1(form)
8
+ end
9
+
10
+ ex
5
11
  end
6
12
 
7
13
  def self.macroexpand_1(form)
8
- return form unless form.is_a? List
14
+ return form unless form.is_a? Seq
9
15
 
10
16
  callee = form.first
11
17
  return form unless callee.is_a?(Identifier) && !callee.constant?
12
18
 
13
19
  name = callee.name
14
20
  name_s = name.to_s
15
- args = form.tail
21
+ args = form.rest
16
22
 
17
23
  # Handle the (.method receiver args*) send expression form
18
- if name.length > 1 && name_s != '..' && name_s.start_with?('.')
24
+ if name.length > 1 && name != :'..' && name_s.start_with?('.')
19
25
  raise ArgumentError, "Too few arguments to send expression, expecting (.method receiver ...)" if args.empty?
20
26
 
21
27
  dot = Identifier.intern(:'.')
22
28
  method = Identifier.intern(name_s[1..-1])
23
- return List[dot, args.first, method, *args.tail]
29
+ return List[dot, args.first, method, *args.rest]
24
30
  end
25
31
 
26
32
  # Handle the (Class. args*) shorthand new form
27
- if name.length > 1 && name_s != '..' && name_s.end_with?('.')
33
+ if name.length > 1 && name != :'..' && name_s.end_with?('.')
28
34
  dot = Identifier.intern(:'.')
29
35
  klass = Identifier.intern(name_s[0..-2])
30
36
  new = Identifier.intern(:new)
@@ -32,8 +38,8 @@ module Apricot
32
38
  end
33
39
 
34
40
  # Handle defined macros
35
- if callee.ns.is_a?(Namespace) && callee.ns.vars.include?(callee.unqualified_name)
36
- potential_macro = callee.ns.get_var(callee.unqualified_name)
41
+ if callee.qualifier.is_a?(Namespace) && callee.qualifier.vars.include?(callee.unqualified_name)
42
+ potential_macro = callee.qualifier.get_var(callee.unqualified_name)
37
43
  meta = potential_macro.apricot_meta
38
44
 
39
45
  if meta && meta[:macro]
@@ -6,6 +6,7 @@ module Apricot
6
6
  @gensym = 0
7
7
 
8
8
  def self.gensym(prefix = 'g')
9
- :"#{prefix}__#{@gensym += 1}"
9
+ @gensym += 1
10
+ Identifier.intern("#{prefix}__#{@gensym}")
10
11
  end
11
12
  end
@@ -16,11 +16,12 @@ module Apricot
16
16
  ns
17
17
  end
18
18
 
19
- attr_reader :vars, :fns
19
+ attr_reader :vars, :fns, :aliases
20
20
 
21
21
  def initialize
22
22
  @vars = {}
23
23
  @fns = Set[]
24
+ @aliases = {}
24
25
  end
25
26
 
26
27
  def set_var(name, val)
@@ -42,8 +43,24 @@ module Apricot
42
43
  def get_var(name)
43
44
  # raise may be a function defined on the namespace so we need to
44
45
  # explicitly call the Ruby raise method.
45
- Kernel.raise NameError, "Undefined variable '#{name}' on #{self}" unless @vars.include? name
46
- @vars[name]
46
+ Kernel.raise NameError,
47
+ "Undefined variable '#{name}' on #{self}" unless has_var? name
48
+
49
+ if var = @vars[name]
50
+ var
51
+ elsif ns = @aliases[name]
52
+ ns.get_var(name)
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ def add_alias(name, ns)
59
+ @aliases[name] = ns
60
+ end
61
+
62
+ def has_var?(name)
63
+ @vars.has_key?(name) || @aliases.has_key?(name)
47
64
  end
48
65
  end
49
66
 
@@ -1,63 +1,82 @@
1
1
  require 'stringio'
2
2
 
3
3
  module Apricot
4
- class Parser
4
+ class Reader
5
5
  IDENTIFIER = /[^'`~()\[\]{}";,\s]/
6
6
  OCTAL = /[0-7]/
7
7
  HEX = /[0-9a-fA-F]/
8
8
  DIGITS = ('0'..'9').to_a + ('a'..'z').to_a
9
- CHAR_ESCAPES = {"a" => "\a", "b" => "\b", "t" => "\t", "n" => "\n",
10
- "v" => "\v", "f" => "\f", "r" => "\r", "e" => "\e"}
11
- REGEXP_OPTIONS = {'i' => Regexp::IGNORECASE, 'x' => Regexp::EXTENDED,
12
- 'm' => Regexp::MULTILINE}
9
+
10
+ CHAR_ESCAPES = {
11
+ "a" => "\a", "b" => "\b", "t" => "\t", "n" => "\n",
12
+ "v" => "\v", "f" => "\f", "r" => "\r", "e" => "\e"
13
+ }
14
+
15
+ REGEXP_OPTIONS = {
16
+ 'i' => Regexp::IGNORECASE,
17
+ 'x' => Regexp::EXTENDED,
18
+ 'm' => Regexp::MULTILINE
19
+ }
20
+
21
+ QUOTE = Identifier.intern(:quote)
22
+ UNQUOTE = Identifier.intern(:unquote)
23
+ UNQUOTE_SPLICING = Identifier.intern(:'unquote-splicing')
24
+ CONCAT = Identifier.intern(:concat)
25
+ APPLY = Identifier.intern(:apply)
26
+ LIST = Identifier.intern(:list)
27
+ FN = Identifier.intern(:fn)
13
28
 
14
29
  FnState = Struct.new(:args, :rest)
15
30
 
16
31
  # @param [IO] io an input stream object to read forms from
17
- def initialize(io, filename = "(none)", line = 1)
32
+ def initialize(io, filename = '(none)', line = 1)
18
33
  @filename = filename
19
34
  @io = io
20
35
  @location = 0
21
36
  @line = line
22
37
 
23
38
  @fn_state = []
24
- @syntax_quote_gensyms = []
39
+
40
+ # Read the first character
41
+ next_char
25
42
  end
26
43
 
27
- def self.parse_file(filename)
28
- File.open(filename) {|f| new(f, filename).parse(true) }
44
+ def self.read_file(filename)
45
+ File.open(filename) {|f| new(f, filename).read }
29
46
  end
30
47
 
31
- def self.parse_string(source, filename = "(none)", line = 1)
32
- new(StringIO.new(source, "r"), filename, line).parse
48
+ def self.read_string(source, filename = '(none)', line = 1)
49
+ new(StringIO.new(source), filename, line).read
33
50
  end
34
51
 
35
- # @return [Array<AST::Node>] a list of the forms in the program
36
- def parse(evaluate = false)
37
- program = []
38
- next_char
52
+ # Return a list of the forms that were read.
53
+ def read
54
+ forms = []
39
55
 
40
56
  skip_whitespace
41
57
  while @char
42
- program << parse_form
58
+ forms << read_form
43
59
  skip_whitespace
44
60
  end
45
61
 
46
- Apricot::AST::TopLevel.new(program, @filename, 1, evaluate)
62
+ forms
47
63
  end
48
64
 
49
- # @return AST::Node an AST node representing the form read
50
- def parse_one
51
- next_char
65
+ def read_one
52
66
  skip_whitespace
53
- parse_form
67
+ form = read_form
68
+
69
+ # Unget the last character because the reader always reads one character
70
+ # ahead.
71
+ @io.ungetc(@char) if @char
72
+
73
+ form
54
74
  end
55
75
 
56
76
  private
57
- # Parse Lisp forms until the given character is encountered
58
- # @param [String] terminator the character to stop parsing at
59
- # @return [Array<AST::Node>] a list of the Lisp forms parsed
60
- def parse_forms_until(terminator)
77
+
78
+ # Read forms until the given character is encountered
79
+ def read_forms_until(terminator)
61
80
  skip_whitespace
62
81
  forms = []
63
82
 
@@ -67,7 +86,7 @@ module Apricot
67
86
  return forms
68
87
  end
69
88
 
70
- forms << parse_form
89
+ forms << read_form
71
90
  skip_whitespace
72
91
  end
73
92
 
@@ -75,39 +94,38 @@ module Apricot
75
94
  incomplete_error "Unexpected end of program, expected #{terminator}"
76
95
  end
77
96
 
78
- # Parse a single Lisp form
79
- # @return [AST::Node] an AST node representing the form
80
- def parse_form
97
+ # Read a single Lisp form
98
+ def read_form
81
99
  case @char
82
- when '#' then parse_dispatch
83
- when "'" then parse_quote
84
- when "`" then parse_syntax_quote
85
- when "~" then parse_unquote
86
- when '(' then parse_list
87
- when '[' then parse_array
88
- when '{' then parse_hash
89
- when '"' then parse_string
90
- when ':' then parse_symbol
91
- when /\d/ then parse_number
100
+ when '#' then read_dispatch
101
+ when "'" then read_quote
102
+ when "`" then read_syntax_quote
103
+ when "~" then read_unquote
104
+ when '(' then read_list
105
+ when '[' then read_array
106
+ when '{' then read_hash
107
+ when '"' then read_string
108
+ when ':' then read_symbol
109
+ when /\d/ then read_number
92
110
  when IDENTIFIER
93
111
  if @char =~ /[+-]/ && peek_char =~ /\d/
94
- parse_number
112
+ read_number
95
113
  else
96
- parse_identifier
114
+ read_identifier
97
115
  end
98
116
  else syntax_error "Unexpected character: #{@char}"
99
117
  end
100
118
  end
101
119
 
102
- def parse_dispatch
120
+ def read_dispatch
103
121
  next_char # skip #
104
122
  case @char
105
- when '|' then parse_pipe_identifier
106
- when '{' then parse_set
107
- when '(' then parse_fn
108
- when 'r' then parse_regex
109
- when 'q' then parse_quotation(false)
110
- when 'Q' then parse_quotation(true)
123
+ when '|' then read_pipe_identifier
124
+ when '{' then read_set
125
+ when '(' then read_fn
126
+ when 'r' then read_regex
127
+ when 'q' then read_quotation(false)
128
+ when 'Q' then read_quotation(true)
111
129
  else syntax_error "Unknown reader macro: ##{@char}"
112
130
  end
113
131
  end
@@ -126,163 +144,144 @@ module Apricot
126
144
  next_char; next_char # skip #_
127
145
  skip_whitespace
128
146
  incomplete_error "Unexpected end of program after #_, expected a form" unless @char
129
- parse_form # discard next form
147
+ read_form # discard next form
130
148
  else
131
149
  next_char
132
150
  end
133
151
  end
134
152
  end
135
153
 
136
- def parse_quote
154
+ def read_quote
155
+ line = @line
137
156
  next_char # skip the '
138
157
  skip_whitespace
139
158
  incomplete_error "Unexpected end of program after quote ('), expected a form" unless @char
140
159
 
141
- form = parse_form
142
- quote = AST::Identifier.new(@line, :quote)
143
- AST::List.new(@line, [quote, form])
160
+ with_location List[QUOTE, read_form], line
144
161
  end
145
162
 
146
- def parse_syntax_quote
163
+ def read_syntax_quote
164
+ line = @line
147
165
  next_char # skip the `
148
166
  skip_whitespace
149
167
  incomplete_error "Unexpected end of program after syntax quote (`), expected a form" unless @char
150
168
 
151
- @syntax_quote_gensyms << {}
152
- form = syntax_quote(parse_form)
153
- @syntax_quote_gensyms.pop
154
-
155
- form
169
+ with_location syntax_quote(read_form, {}), line
156
170
  end
157
171
 
158
- def syntax_quote(form)
159
- quote = AST::Identifier.new(@line, :quote)
160
-
172
+ def syntax_quote(form, gensyms)
161
173
  case form
162
- when AST::List
163
- if is_unquote?(form)
164
- form[1]
165
- elsif is_unquote_splicing?(form)
174
+ when Seq
175
+ if is_unquote? form
176
+ form.rest.first
177
+ elsif is_unquote_splicing? form
166
178
  syntax_error "splicing unquote (~@) not in list"
167
179
  else
168
- concat = AST::Identifier.new(@line, :concat)
169
- AST::List.new(@line, [concat] + syntax_quote_list(form.elements))
180
+ syntax_quote_list(form, gensyms)
170
181
  end
171
- when AST::ArrayLiteral
172
- syntax_quote_coll(:array, form.elements)
173
- when AST::SetLiteral
174
- syntax_quote_coll(:set, form.elements)
175
- when AST::HashLiteral
176
- syntax_quote_coll(:hash, form.elements)
177
- when AST::Identifier
182
+ when Array
183
+ syntax_quote_coll(:array, form, gensyms)
184
+ when Set
185
+ syntax_quote_coll(:'hash-set', form, gensyms)
186
+ when Hash
187
+ syntax_quote_coll(:hash, form.to_a.flatten(1), gensyms)
188
+ when Identifier
178
189
  name = form.name
190
+
179
191
  if name.to_s.end_with?('#')
180
- @syntax_quote_gensyms.last[name] ||= Apricot.gensym(name)
181
- id = AST::Identifier.new(@line, @syntax_quote_gensyms.last[name])
182
- AST::List.new(@line, [quote, id])
192
+ gensyms[name] ||= Apricot.gensym(name)
193
+ List[QUOTE, gensyms[name]]
183
194
  else
184
- AST::List.new(@line, [quote, form])
195
+ List[QUOTE, form]
185
196
  end
186
- when AST::BasicLiteral
187
- form
188
197
  else
189
- AST::List.new(@line, [quote, form])
198
+ form
190
199
  end
191
200
  end
192
201
 
193
- def syntax_quote_coll(creator_name, elements)
194
- apply = AST::Identifier.new(@line, :apply)
195
- concat = AST::Identifier.new(@line, :concat)
196
- creator = AST::Identifier.new(@line, creator_name)
197
- list = AST::List.new(@line, [concat] + syntax_quote_list(elements))
198
- AST::List.new(@line, [apply, creator, list])
202
+ def syntax_quote_coll(creator_name, elements, gensyms)
203
+ creator = Identifier.intern(creator_name)
204
+ list = syntax_quote_list(elements, gensyms)
205
+ List[APPLY, creator, list]
199
206
  end
200
207
 
201
- def syntax_quote_list(elements)
202
- list = AST::Identifier.new(@line, :list)
203
-
204
- elements.map do |form|
205
- if is_unquote?(form)
206
- AST::List.new(@line, [list, form[1]])
207
- elsif is_unquote_splicing?(form)
208
- form[1]
208
+ def syntax_quote_list(elements, gensyms)
209
+ parts = elements.map do |form|
210
+ if is_unquote? form
211
+ List[LIST, form.rest.first]
212
+ elsif is_unquote_splicing? form
213
+ form.rest.first
209
214
  else
210
- AST::List.new(@line, [list, syntax_quote(form)])
215
+ List[LIST, syntax_quote(form, gensyms)]
211
216
  end
212
217
  end
213
- end
214
218
 
215
- def is_unquote?(form)
216
- form.is_a?(AST::List) &&
217
- form[0].is_a?(AST::Identifier) &&
218
- form[0].name == :unquote
219
+ Cons.new(CONCAT, parts)
219
220
  end
220
221
 
221
- def is_unquote_splicing?(form)
222
- form.is_a?(AST::List) &&
223
- form[0].is_a?(AST::Identifier) &&
224
- form[0].name == :'unquote-splicing'
225
- end
226
-
227
- def parse_unquote
228
- unquote = :unquote
222
+ def read_unquote
223
+ line = @line
224
+ unquote_type = UNQUOTE
229
225
  next_char # skip the ~
230
226
 
231
227
  if @char == '@'
232
228
  next_char # skip the ~@
233
- unquote = :'unquote-splicing'
229
+ unquote_type = UNQUOTE_SPLICING
234
230
  end
235
231
 
236
232
  skip_whitespace
237
233
 
238
234
  unless @char
239
- syntax = unquote == :unquote ? '~' : '~@'
235
+ syntax = (unquote_type == UNQUOTE ? '~' : '~@')
240
236
  incomplete_error "Unexpected end of program after #{syntax}, expected a form"
241
237
  end
242
238
 
243
- form = parse_form
244
- unquote = AST::Identifier.new(@line, unquote)
245
- AST::List.new(@line, [unquote, form])
239
+ with_location List[unquote_type, read_form], line
246
240
  end
247
241
 
248
- def parse_fn
242
+ def read_fn
243
+ line = @line
244
+
249
245
  @fn_state << FnState.new([], nil)
250
- body = parse_list
246
+ body = read_list
251
247
  state = @fn_state.pop
252
248
 
253
- state.args << :'&' << state.rest if state.rest
249
+ state.args << Identifier.intern(:'&') << state.rest if state.rest
250
+
254
251
  args = state.args.map.with_index do |x, i|
255
- AST::Identifier.new(body.line, x || Apricot.gensym("p#{i + 1}"))
252
+ x || Apricot.gensym("p#{i + 1}")
256
253
  end
257
254
 
258
- AST::List.new(body.line, [AST::Identifier.new(body.line, :fn),
259
- AST::ArrayLiteral.new(body.line, args),
260
- body])
255
+ with_location List[FN, args, body], line
261
256
  end
262
257
 
263
- def parse_list
258
+ def read_list
259
+ line = @line
264
260
  next_char # skip the (
265
- AST::List.new(@line, parse_forms_until(')'))
261
+ with_location read_forms_until(')').to_list, line
266
262
  end
267
263
 
268
- def parse_array
264
+ def read_array
265
+ line = @line
269
266
  next_char # skip the [
270
- AST::ArrayLiteral.new(@line, parse_forms_until(']'))
267
+ with_location read_forms_until(']'), line
271
268
  end
272
269
 
273
- def parse_hash
270
+ def read_hash
271
+ line = @line
274
272
  next_char # skip the {
275
- forms = parse_forms_until('}')
273
+ forms = read_forms_until('}')
276
274
  syntax_error "Odd number of forms in key-value hash" if forms.count.odd?
277
- AST::HashLiteral.new(@line, forms)
275
+ with_location hashify(forms), line
278
276
  end
279
277
 
280
- def parse_set
278
+ def read_set
279
+ line = @line
281
280
  next_char # skip the {
282
- AST::SetLiteral.new(@line, parse_forms_until('}'))
281
+ with_location read_forms_until('}').to_set, line
283
282
  end
284
283
 
285
- def parse_string
284
+ def read_string
286
285
  line = @line
287
286
  next_char # skip the opening "
288
287
  string = ""
@@ -290,38 +289,41 @@ module Apricot
290
289
  while @char
291
290
  if @char == '"'
292
291
  next_char # consume the "
293
- return AST::StringLiteral.new(line, string)
292
+ return with_location string, line
294
293
  end
295
294
 
296
- string << parse_string_char
295
+ string << read_string_char
297
296
  end
298
297
 
299
298
  # Can only reach here if we run out of chars without getting a "
300
299
  incomplete_error "Unexpected end of program while parsing string"
301
300
  end
302
301
 
303
- def parse_string_char
304
- char = if @char == "\\"
305
- next_char
306
- if CHAR_ESCAPES.has_key?(@char)
307
- CHAR_ESCAPES[consume_char]
308
- elsif @char =~ OCTAL
309
- char_escape_helper(8, OCTAL, 3)
310
- elsif @char == 'x'
311
- next_char
312
- syntax_error "Invalid hex character escape" unless @char =~ HEX
313
- char_escape_helper(16, HEX, 2)
314
- else
315
- consume_char
316
- end
317
- else
318
- consume_char
319
- end
302
+ def read_string_char
303
+ char =
304
+ if @char == "\\"
305
+ next_char
306
+ if CHAR_ESCAPES.has_key?(@char)
307
+ CHAR_ESCAPES[consume_char]
308
+ elsif @char =~ OCTAL
309
+ char_escape_helper(8, OCTAL, 3)
310
+ elsif @char == 'x'
311
+ next_char
312
+ syntax_error "Invalid hex character escape" unless @char =~ HEX
313
+ char_escape_helper(16, HEX, 2)
314
+ else
315
+ consume_char
316
+ end
317
+ else
318
+ consume_char
319
+ end
320
+
320
321
  incomplete_error "Unexpected end of file while parsing character escape" unless char
322
+
321
323
  char
322
324
  end
323
325
 
324
- # Parse digits in a certain base for string character escapes
326
+ # Read digits in a certain base for string character escapes
325
327
  def char_escape_helper(base, regex, n)
326
328
  number = ""
327
329
 
@@ -334,20 +336,10 @@ module Apricot
334
336
  number.to_i(base).chr
335
337
  end
336
338
 
337
- def delimiter_helper(c)
338
- case c
339
- when '(' then ')'
340
- when '[' then ']'
341
- when '{' then '}'
342
- when '<' then '>'
343
- else c
344
- end
345
- end
346
-
347
- def parse_regex
339
+ def read_regex
348
340
  line = @line
349
341
  next_char # skip the r
350
- delimiter = delimiter_helper(@char)
342
+ delimiter = opposite_delimiter(@char)
351
343
  next_char # skip delimiter
352
344
  regex = ""
353
345
 
@@ -355,7 +347,7 @@ module Apricot
355
347
  if @char == delimiter
356
348
  next_char # consume delimiter
357
349
  options = regex_options_helper
358
- return AST::RegexLiteral.new(line, regex, options)
350
+ return with_location Regexp.new(regex, options), line
359
351
  elsif @char == "\\" && peek_char == delimiter
360
352
  next_char
361
353
  elsif @char == "\\" && peek_char == "\\"
@@ -383,21 +375,21 @@ module Apricot
383
375
  options
384
376
  end
385
377
 
386
- def parse_quotation(double_quote)
378
+ def read_quotation(double_quote)
387
379
  line = @line
388
380
  next_char # skip the prefix
389
- delimiter = delimiter_helper(@char)
381
+ delimiter = opposite_delimiter(@char)
390
382
  next_char # skip delimiter
391
383
  string = ""
392
384
 
393
385
  while @char
394
386
  if @char == delimiter
395
387
  next_char # consume delimiter
396
- return AST::StringLiteral.new(line, string)
388
+ return with_location string, line
397
389
  end
398
390
 
399
391
  if double_quote
400
- string << parse_string_char
392
+ string << read_string_char
401
393
  elsif @char == "\\" && (peek_char == delimiter || peek_char == "\\")
402
394
  next_char
403
395
  string << consume_char
@@ -409,7 +401,7 @@ module Apricot
409
401
  incomplete_error "Unexpected end of program while parsing quotation"
410
402
  end
411
403
 
412
- def parse_symbol
404
+ def read_symbol
413
405
  line = @line
414
406
  next_char # skip the :
415
407
  symbol = ""
@@ -418,7 +410,7 @@ module Apricot
418
410
  next_char # skip opening "
419
411
  while @char
420
412
  break if @char == '"'
421
- symbol << parse_string_char
413
+ symbol << read_string_char
422
414
  end
423
415
  incomplete_error "Unexpected end of program while parsing symbol" unless @char == '"'
424
416
  next_char # skip closing "
@@ -431,10 +423,10 @@ module Apricot
431
423
  syntax_error "Empty symbol name" if symbol.empty?
432
424
  end
433
425
 
434
- AST::SymbolLiteral.new(line, symbol.to_sym)
426
+ symbol.to_sym
435
427
  end
436
428
 
437
- def parse_number
429
+ def read_number
438
430
  number = ""
439
431
 
440
432
  while @char =~ IDENTIFIER
@@ -444,22 +436,23 @@ module Apricot
444
436
 
445
437
  case number
446
438
  when /^[+-]?\d+$/
447
- AST.new_integer(@line, number.to_i)
439
+ number.to_i
448
440
  when /^([+-]?)(\d+)r([a-zA-Z0-9]+)$/
449
441
  sign, radix, digits = $1, $2.to_i, $3
450
442
  syntax_error "Radix out of range: #{radix}" unless 2 <= radix && radix <= 36
451
443
  syntax_error "Invalid digits for radix in number: #{number}" unless digits.downcase.chars.all? {|d| DIGITS[0..radix-1].include?(d) }
452
- AST.new_integer(@line, (sign + digits).to_i(radix))
444
+ (sign + digits).to_i(radix)
453
445
  when /^[+-]?\d+\.?\d*(?:e[+-]?\d+)?$/
454
- AST::FloatLiteral.new(@line, number.to_f)
446
+ number.to_f
455
447
  when /^([+-]?\d+)\/(\d+)$/
456
- AST::RationalLiteral.new(@line, $1.to_i, $2.to_i)
448
+ Rational($1.to_i, $2.to_i)
457
449
  else
458
450
  syntax_error "Invalid number: #{number}"
459
451
  end
460
452
  end
461
453
 
462
- def parse_identifier
454
+ def read_identifier
455
+ line = @line
463
456
  identifier = ""
464
457
 
465
458
  while @char =~ IDENTIFIER
@@ -467,32 +460,33 @@ module Apricot
467
460
  next_char
468
461
  end
469
462
 
463
+ case identifier
464
+ when 'true' then return true
465
+ when 'false' then return false
466
+ when 'nil' then return nil
467
+ end
468
+
469
+ state = @fn_state.last
470
+
470
471
  # Handle % identifiers in #() syntax
471
- if (state = @fn_state.last) && identifier[0] == '%'
472
- identifier = case identifier[1..-1]
472
+ if state && identifier[0] == '%'
473
+ case identifier[1..-1]
473
474
  when '' # % is equivalent to %1
474
- state.args[0] ||= Apricot.gensym('p1')
475
+ state.args[0] ||= with_location Apricot.gensym('p1'), line
475
476
  when '&'
476
- state.rest ||= Apricot.gensym('rest')
477
+ state.rest ||= with_location Apricot.gensym('rest'), line
477
478
  when /^[1-9]\d*$/
478
479
  n = identifier[1..-1].to_i
479
- state.args[n - 1] ||= Apricot.gensym("p#{n}")
480
+ state.args[n - 1] ||= with_location Apricot.gensym("p#{n}"), line
480
481
  else
481
482
  syntax_error "arg literal must be %, %& or %integer"
482
483
  end
483
484
  else
484
- identifier = identifier.to_sym
485
- end
486
-
487
- case identifier
488
- when :true, :false, :nil
489
- AST::Literal.new(@line, identifier)
490
- else
491
- AST::Identifier.new(@line, identifier)
485
+ with_location Identifier.intern(identifier), line
492
486
  end
493
487
  end
494
488
 
495
- def parse_pipe_identifier
489
+ def read_pipe_identifier
496
490
  line = @line
497
491
  next_char # skip the |
498
492
  identifier = ""
@@ -500,10 +494,10 @@ module Apricot
500
494
  while @char
501
495
  if @char == '|'
502
496
  next_char # consume the |
503
- return AST::Identifier.new(line, identifier.to_sym)
497
+ return with_location Identifier.intern(identifier), line
504
498
  end
505
499
 
506
- identifier << parse_string_char
500
+ identifier << read_string_char
507
501
  end
508
502
 
509
503
  incomplete_error "Unexpected end of program while parsing pipe identifier"
@@ -537,5 +531,38 @@ module Apricot
537
531
  def incomplete_error(message)
538
532
  raise SyntaxError.new(@filename, @line, message, true)
539
533
  end
534
+
535
+ def with_location(obj, line)
536
+ obj.apricot_meta = {line: line}
537
+ obj
538
+ end
539
+
540
+ def cons(head, tail)
541
+ tail.cons(head)
542
+ end
543
+
544
+ def is_unquote?(form)
545
+ form.is_a?(Seq) && form.first == UNQUOTE
546
+ end
547
+
548
+ def is_unquote_splicing?(form)
549
+ form.is_a?(Seq) && form.first == UNQUOTE_SPLICING
550
+ end
551
+
552
+ def hashify(array)
553
+ array.each_slice(2).with_object({}) do |(key, value), hash|
554
+ hash[key] = value
555
+ end
556
+ end
557
+
558
+ def opposite_delimiter(c)
559
+ case c
560
+ when '(' then ')'
561
+ when '[' then ']'
562
+ when '{' then '}'
563
+ when '<' then '>'
564
+ else c
565
+ end
566
+ end
540
567
  end
541
568
  end