kapusta 0.3.0 → 0.5.0

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,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ module Compiler
5
+ class MacroExpander
6
+ class Error < Kapusta::Error; end
7
+
8
+ @gensym_counter = 0
9
+
10
+ class << self
11
+ def fresh_gensym(prefix)
12
+ @gensym_counter += 1
13
+ GeneratedSym.new("#{prefix}_g#{@gensym_counter}", @gensym_counter)
14
+ end
15
+
16
+ def fresh_local_gensym(prefix)
17
+ @gensym_counter += 1
18
+ GeneratedSym.new("#{prefix}_local_#{@gensym_counter}", @gensym_counter)
19
+ end
20
+ end
21
+
22
+ def initialize(path: nil)
23
+ @macros = {}
24
+ @path = path
25
+ end
26
+
27
+ def expand_all(forms)
28
+ forms.flat_map { |form| expand_top(form) }
29
+ end
30
+
31
+ private
32
+
33
+ def expand_top(form)
34
+ if form.is_a?(List) && form.head.is_a?(Sym)
35
+ case form.head.name
36
+ when 'macro'
37
+ register_macro_form(form.rest)
38
+ return []
39
+ when 'macros'
40
+ register_macros_form(form.rest)
41
+ return []
42
+ when 'import-macros'
43
+ raise macro_error(:import_macros_unsupported, form)
44
+ end
45
+ end
46
+ [expand(form)]
47
+ end
48
+
49
+ def macro_error(code, form, **args)
50
+ line = form.respond_to?(:line) ? form.line : nil
51
+ column = form.respond_to?(:column) ? form.column : nil
52
+ Error.new(Kapusta::Errors.format(code, **args), path: @path, line:, column:)
53
+ end
54
+
55
+ def expand(form)
56
+ case form
57
+ when List then expand_list(form)
58
+ when Vec then copy_position(Vec.new(form.items.map { |item| expand(item) }), form)
59
+ when HashLit
60
+ copy_position(
61
+ HashLit.new(form.entries.map do |entry|
62
+ entry.is_a?(Array) ? [expand(entry[0]), expand(entry[1])] : entry
63
+ end),
64
+ form
65
+ )
66
+ else
67
+ form
68
+ end
69
+ end
70
+
71
+ def copy_position(target, source)
72
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
73
+
74
+ target.line ||= source.line
75
+ target.column ||= source.column
76
+ target
77
+ end
78
+
79
+ def expand_list(list)
80
+ return list if list.empty?
81
+
82
+ head = list.head
83
+ if head.is_a?(Sym) && !head.is_a?(AutoGensym)
84
+ name = head.name
85
+ case name
86
+ when 'macro'
87
+ register_macro_form(list.rest)
88
+ return List.new([Sym.new('do')])
89
+ when 'macros'
90
+ register_macros_form(list.rest)
91
+ return List.new([Sym.new('do')])
92
+ end
93
+
94
+ key = lookup_key(name)
95
+ if @macros.key?(key)
96
+ args = list.rest
97
+ result = invoke_macro(key, args)
98
+ return copy_position(expand(result), list)
99
+ end
100
+ end
101
+
102
+ copy_position(List.new(list.items.map { |item| expand(item) }), list)
103
+ end
104
+
105
+ def lookup_key(name)
106
+ Kapusta.kebab_to_snake(name).to_sym
107
+ end
108
+
109
+ def register_macro_form(args)
110
+ name_sym, params, *body = args
111
+ raise macro_error(:macro_name_must_be_symbol, name_sym) unless name_sym.is_a?(Sym)
112
+ raise macro_error(:macro_params_must_be_vector, params) unless params.is_a?(Vec)
113
+
114
+ register(name_sym.name, params, body)
115
+ end
116
+
117
+ def register_macros_form(args)
118
+ hash_lit = args[0]
119
+ raise macro_error(:macros_expects_hash, hash_lit) unless hash_lit.is_a?(HashLit)
120
+
121
+ hash_lit.pairs.each do |key, value|
122
+ raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect) unless fn_form?(value)
123
+
124
+ name = key.to_s
125
+ params = value.items[1]
126
+ body = value.items[2..]
127
+ raise macro_error(:macros_entry_params_must_be_vector, params) unless params.is_a?(Vec)
128
+
129
+ register(name, params, body)
130
+ end
131
+ end
132
+
133
+ def fn_form?(value)
134
+ value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
135
+ end
136
+
137
+ def register(source_name, params, body)
138
+ proc_obj = compile_macro(source_name, params, body)
139
+ @macros[lookup_key(source_name)] = proc_obj
140
+ end
141
+
142
+ def compile_macro(name, params, body)
143
+ lowering = Lowering.new
144
+ lowered_body = body.map { |form| lowering.lower(form) }
145
+ gensym_locals = lowering.collected_gensyms
146
+
147
+ wrapped =
148
+ if gensym_locals.empty?
149
+ List.new([Sym.new('fn'), params, *lowered_body])
150
+ else
151
+ let_bindings = gensym_locals.flat_map do |gensym_sym, prefix|
152
+ [gensym_sym, List.new([Sym.new('quasi-gensym'), prefix])]
153
+ end
154
+ inner = lowered_body.length == 1 ? lowered_body.first : List.new([Sym.new('do'), *lowered_body])
155
+ List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
156
+ end
157
+
158
+ macro_path = @path || "(macro #{name})"
159
+ ruby = Compiler.compile_forms([wrapped], path: macro_path)
160
+ TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
161
+ end
162
+
163
+ def invoke_macro(key, args)
164
+ proc_obj = @macros[key]
165
+ proc_obj.call(*args)
166
+ end
167
+
168
+ class Lowering
169
+ def initialize
170
+ @gensyms = {}
171
+ end
172
+
173
+ def collected_gensyms
174
+ @gensyms.map { |prefix, sym| [sym, prefix] }
175
+ end
176
+
177
+ def lower(form)
178
+ case form
179
+ when Quasiquote then copy_position(lower_quasi(form.form), form)
180
+ when Unquote, UnquoteSplice
181
+ raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
182
+ when AutoGensym
183
+ raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
184
+ when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
185
+ when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
186
+ when HashLit
187
+ copy_position(
188
+ HashLit.new(form.entries.map do |entry|
189
+ entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
190
+ end),
191
+ form
192
+ )
193
+ else
194
+ form
195
+ end
196
+ end
197
+
198
+ def copy_position(target, source)
199
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
200
+
201
+ target.line ||= source.line
202
+ target.column ||= source.column
203
+ target
204
+ end
205
+
206
+ def lower_quasi(form)
207
+ case form
208
+ when AutoGensym then gensym_local_for(form.name)
209
+ when Sym then List.new([Sym.new('quasi-sym'), form.name])
210
+ when List then lower_quasi_list(form)
211
+ when Vec then lower_quasi_vec(form)
212
+ when HashLit then lower_quasi_hash(form)
213
+ when Unquote then lower(form.form)
214
+ when UnquoteSplice
215
+ raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
216
+ when Quasiquote
217
+ raise Error, Kapusta::Errors.format(:nested_quasiquote)
218
+ else
219
+ form
220
+ end
221
+ end
222
+
223
+ def lower_quasi_list(list)
224
+ items = list.items
225
+ return List.new([Sym.new('quasi-list')]) if items.empty?
226
+
227
+ if (tail_expr = splice_tail(items))
228
+ head_items = items[0...-1].map { |item| lower_quasi(item) }
229
+ return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
230
+ end
231
+
232
+ lowered_items = items.map { |item| lower_quasi_item(item) }
233
+ List.new([Sym.new('quasi-list'), *lowered_items])
234
+ end
235
+
236
+ def lower_quasi_vec(vec)
237
+ items = vec.items
238
+ if (tail_expr = splice_tail(items))
239
+ head_items = items[0...-1].map { |item| lower_quasi(item) }
240
+ return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
241
+ end
242
+
243
+ lowered_items = items.map { |item| lower_quasi_item(item) }
244
+ List.new([Sym.new('quasi-vec'), *lowered_items])
245
+ end
246
+
247
+ def lower_quasi_hash(hash)
248
+ parts = []
249
+ hash.entries.each do |entry|
250
+ next unless entry.is_a?(Array)
251
+
252
+ key, value = entry
253
+ parts << lower_quasi(key) << lower_quasi(value)
254
+ end
255
+ List.new([Sym.new('quasi-hash'), *parts])
256
+ end
257
+
258
+ def lower_quasi_item(item)
259
+ if item.is_a?(Unquote) && unpack_call?(item.form)
260
+ inner = lower(item.form.items[1])
261
+ List.new([Sym.new('.'), inner, 0])
262
+ else
263
+ lower_quasi(item)
264
+ end
265
+ end
266
+
267
+ def splice_tail(items)
268
+ last = items.last
269
+ return unless last
270
+ return lower(last.form) if last.is_a?(UnquoteSplice)
271
+ return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
272
+
273
+ nil
274
+ end
275
+
276
+ def unpack_call?(form)
277
+ form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
278
+ end
279
+
280
+ def gensym_local_for(prefix)
281
+ @gensyms[prefix] ||= MacroExpander.fresh_local_gensym(prefix)
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -10,9 +10,12 @@ module Kapusta
10
10
  def normalize(form)
11
11
  case form
12
12
  when List then normalize_list(form)
13
- when Vec then Vec.new(form.items.map { |item| normalize(item) })
13
+ when Vec then inherit_position(Vec.new(form.items.map { |item| normalize(item) }), form)
14
14
  when HashLit
15
- HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] })
15
+ inherit_position(
16
+ HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] }),
17
+ form
18
+ )
16
19
  else
17
20
  form
18
21
  end
@@ -32,19 +35,28 @@ module Kapusta
32
35
 
33
36
  head = list.head
34
37
  items = list.items.map { |item| normalize(item) }
35
- return List.new(items) unless head.is_a?(Sym)
38
+ return inherit_position(List.new(items), list) unless head.is_a?(Sym)
36
39
 
37
40
  case head.name
38
41
  when 'when'
42
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
43
+
39
44
  cond = items[1]
40
45
  body = wrap_do(items[2..])
41
- List.new([Sym.new('if'), cond, body])
46
+ inherit_position(List.new([Sym.new('if'), cond, body]), list)
42
47
  when 'unless'
48
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
49
+
43
50
  cond = items[1]
44
51
  body = wrap_do(items[2..])
45
- List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body])
52
+ inherit_position(List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body]), list)
46
53
  when 'tset'
47
- List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]])
54
+ raise compiler_error(:tset_no_value, list) if items.length < 4
55
+
56
+ inherit_position(
57
+ List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
58
+ list
59
+ )
48
60
  when 'pcall'
49
61
  fn = items[1]
50
62
  args = items[2..]
@@ -65,14 +77,28 @@ module Kapusta
65
77
  List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
66
78
  ])
67
79
  when '->', '->>', '-?>', '-?>>'
68
- normalize(thread(items[1..], head.name))
80
+ inherit_position(normalize(thread(items[1..], head.name)), list)
69
81
  when 'doto'
70
- normalize(doto(items[1..]))
82
+ inherit_position(normalize(doto(items[1..])), list)
71
83
  else
72
- List.new(items)
84
+ inherit_position(List.new(items), list)
73
85
  end
74
86
  end
75
87
 
88
+ def inherit_position(target, source)
89
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
90
+
91
+ target.line ||= source.line
92
+ target.column ||= source.column
93
+ target
94
+ end
95
+
96
+ def compiler_error(code, form, **args)
97
+ line = form.respond_to?(:line) ? form.line : nil
98
+ column = form.respond_to?(:column) ? form.column : nil
99
+ Compiler::Error.new(Kapusta::Errors.format(code, **args), line:, column:)
100
+ end
101
+
76
102
  def wrap_do(forms)
77
103
  return if forms.empty?
78
104
  return forms.first if forms.length == 1
@@ -3,12 +3,13 @@
3
3
  require_relative 'error'
4
4
  require_relative 'compiler/normalizer'
5
5
  require_relative 'compiler/emitter'
6
+ require_relative 'compiler/macro_expander'
6
7
 
7
8
  module Kapusta
8
9
  module Compiler
9
10
  class Error < Kapusta::Error; end
10
11
  SPECIAL_FORMS = %w[
11
- fn lambda λ let local var set if when unless case match
12
+ fn lambda λ let local var global set if when unless case match
12
13
  while for each do values
13
14
  -> ->> -?> -?>> doto
14
15
  icollect collect fcollect accumulate faccumulate
@@ -27,12 +28,23 @@ module Kapusta
27
28
  = not= < <= > >=
28
29
  + - * / %
29
30
  print
31
+ macro macros import-macros
32
+ quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
30
33
  ].freeze
31
34
 
32
35
  def self.compile(source, path: '(kapusta)')
33
36
  forms = Reader.read_all(source)
37
+ expanded = MacroExpander.new(path:).expand_all(forms)
38
+ compile_forms(expanded, path:)
39
+ rescue Kapusta::Error => e
40
+ raise e.with_defaults(path:)
41
+ end
42
+
43
+ def self.compile_forms(forms, path: '(kapusta)')
34
44
  normalized = Normalizer.new.normalize_all(forms)
35
45
  Emitter.new(path:).emit_file(normalized)
46
+ rescue Kapusta::Error => e
47
+ raise e.with_defaults(path:)
36
48
  end
37
49
 
38
50
  def self.run(source, path: '(kapusta)')
data/lib/kapusta/error.rb CHANGED
@@ -1,5 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- class Error < StandardError; end
4
+ class Error < StandardError
5
+ attr_reader :path, :line, :column, :reason
6
+
7
+ def initialize(reason, path: nil, line: nil, column: nil)
8
+ @reason = reason
9
+ @path = path
10
+ @line = line
11
+ @column = column
12
+ super(formatted)
13
+ end
14
+
15
+ def formatted
16
+ prefix = [path, line, column].compact.join(':')
17
+ prefix.empty? ? reason : "#{prefix}: #{reason}"
18
+ end
19
+
20
+ def with_defaults(path: nil, line: nil, column: nil)
21
+ copy = self.class.new(@reason,
22
+ path: @path || path,
23
+ line: @line || line,
24
+ column: @column || column)
25
+ copy.set_backtrace(backtrace) if backtrace
26
+ copy
27
+ end
28
+ end
5
29
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ # rubocop:disable Style/FormatStringToken
5
+ module Errors
6
+ MESSAGES = {
7
+ accumulate_no_iterator: 'expected initial value and iterator binding table',
8
+ auto_gensym_outside_quasiquote: 'auto-gensym %{name}# outside quasiquote',
9
+ bad_multisym: 'bad multisym: %{path}',
10
+ bad_set_target: 'bad set target: %{target}',
11
+ bad_shorthand: 'bad shorthand',
12
+ bind_table_dots: 'unable to bind table ...',
13
+ cannot_call_literal: 'cannot call literal value %{value}',
14
+ cannot_emit_form: 'cannot emit form: %{form}',
15
+ cannot_set_method_binding: 'cannot set method binding: %{name}',
16
+ case_no_patterns: 'expected at least one pattern/body pair',
17
+ case_odd_patterns: 'expected even number of pattern/body pairs',
18
+ case_unsupported: 'case/match clauses use patterns this compiler cannot translate',
19
+ could_not_destructure_literal: 'could not destructure literal',
20
+ could_not_read_number: 'could not read number "%{token}"',
21
+ counted_no_range: 'expected range to include start and stop',
22
+ destructure_unsupported: 'destructure pattern this compiler cannot translate: %{pattern}',
23
+ dot_no_args: 'expected table argument',
24
+ each_no_binding: 'expected binding table',
25
+ empty_call: 'expected a function, macro, or special to call',
26
+ empty_token: 'empty token',
27
+ expected_var: 'expected var %{name}',
28
+ fn_no_params: 'expected parameters table',
29
+ global_arity: 'expected name and value',
30
+ global_non_symbol_name: 'unable to bind %{type} %{value}',
31
+ icollect_no_iterator: 'expected iterator binding table',
32
+ if_no_body: 'expected condition and body',
33
+ import_macros_unsupported: 'import-macros is not yet supported',
34
+ invalid_class_name: 'invalid class name: %{name}',
35
+ invalid_module_name: 'invalid module name: %{name}',
36
+ let_no_body: 'expected body expression',
37
+ let_odd_bindings: 'expected even number of name/value bindings',
38
+ local_arity: '%{form}: expected name and value',
39
+ macro_name_must_be_symbol: 'macro name must be a symbol',
40
+ macro_params_must_be_vector: 'macro params must be a vector',
41
+ macro_unsafe_bind: 'macro tried to bind %{name} without gensym',
42
+ macros_entry_must_be_fn: 'macros entry value must be a fn form, got %{form}',
43
+ macros_entry_params_must_be_vector: 'macros entry params must be a vector',
44
+ macros_expects_hash: 'macros expects a hash literal',
45
+ nested_quasiquote: 'nested quasiquote is not supported',
46
+ odd_forms_in_hash: 'odd number of forms in hash',
47
+ rest_not_last: 'expected rest argument before last parameter',
48
+ shadowed_special: 'local %{name} was overshadowed by a special form or macro',
49
+ special_must_be_toplevel: '%{name} must appear at the top level and is consumed by the macro expander',
50
+ tset_no_value: 'tset: expected table, key, and value arguments',
51
+ unclosed_delimiter: "unclosed opening delimiter '%{char}'",
52
+ undefined_symbol: 'undefined symbol: %{name}',
53
+ unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
54
+ unexpected_eof: 'unexpected eof',
55
+ unknown_special_form: 'unknown special form: %{name}',
56
+ unquote_outside_quasiquote: 'unquote outside quasiquote',
57
+ unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
58
+ unterminated_string: 'unterminated string',
59
+ vararg_with_operator: 'tried to use vararg with operator',
60
+ when_no_body: '%{form}: expected body'
61
+ }.freeze
62
+
63
+ def self.format(code, **args)
64
+ template = MESSAGES.fetch(code) { raise ArgumentError, "unknown error code: #{code.inspect}" }
65
+ args.empty? ? template.dup : (template % args)
66
+ end
67
+ end
68
+ # rubocop:enable Style/FormatStringToken
69
+ end