kapusta 0.1.5 → 0.2.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -4
  3. data/examples/bank-account.kap +21 -0
  4. data/examples/baseball-game.kap +11 -0
  5. data/examples/best-time-to-buy-sell-stock.kap +12 -0
  6. data/examples/climbing-stairs.kap +13 -0
  7. data/examples/doto-hygiene.kap +5 -0
  8. data/examples/happy-number.kap +20 -0
  9. data/examples/length-of-last-word.kap +7 -0
  10. data/examples/majority-element.kap +11 -0
  11. data/examples/maximum-subarray.kap +12 -0
  12. data/examples/move-zeroes.kap +13 -0
  13. data/examples/plus-one.kap +14 -0
  14. data/examples/reverse-integer.kap +13 -0
  15. data/examples/roman-to-integer.kap +17 -0
  16. data/examples/stack.kap +27 -10
  17. data/examples/two-sum-hash.kap +17 -0
  18. data/examples/use_bank_account.rb +13 -0
  19. data/examples/valid-parentheses-1.kap +19 -0
  20. data/examples/valid-parentheses-2.kap +8 -0
  21. data/examples/zoo-animal-1.kap +5 -0
  22. data/examples/zoo-animal-inheritance-2.kap +8 -0
  23. data/lib/kapusta/ast.rb +33 -3
  24. data/lib/kapusta/compiler/emitter/bindings.rb +35 -25
  25. data/lib/kapusta/compiler/emitter/control_flow.rb +15 -7
  26. data/lib/kapusta/compiler/emitter/expressions.rb +13 -13
  27. data/lib/kapusta/compiler/emitter/interop.rb +91 -40
  28. data/lib/kapusta/compiler/emitter/patterns.rb +14 -11
  29. data/lib/kapusta/compiler/emitter/support.rb +13 -11
  30. data/lib/kapusta/compiler/emitter.rb +1 -5
  31. data/lib/kapusta/compiler/normalizer.rb +41 -17
  32. data/lib/kapusta/compiler/runtime.rb +0 -152
  33. data/lib/kapusta/env.rb +21 -6
  34. data/lib/kapusta/reader.rb +27 -3
  35. data/lib/kapusta/version.rb +1 -1
  36. data/lib/kapusta.rb +62 -1
  37. data/spec/cli_spec.rb +25 -2
  38. data/spec/examples_spec.rb +257 -81
  39. data/spec/reader_spec.rb +26 -0
  40. metadata +23 -8
  41. data/examples/inheritance.kap +0 -13
@@ -17,7 +17,7 @@ module Kapusta
17
17
  when List then emit_list(form, env, current_scope)
18
18
  when String, Symbol, Numeric, true, false, nil then form.inspect
19
19
  else
20
- raise Error, "cannot emit form: #{form.inspect}"
20
+ emit_error!("cannot emit form: #{form.inspect}")
21
21
  end
22
22
  end
23
23
 
@@ -36,7 +36,9 @@ module Kapusta
36
36
  if head.is_a?(Sym)
37
37
  return emit_special(head.name, args, env, current_scope) if special_form?(head.name)
38
38
  return emit_multisym_call(head, args, env, current_scope) if head.dotted?
39
- return emit_bound_call(env.lookup(head.name), args, env, current_scope) if env.defined?(head.name)
39
+ if (binding = env.lookup_if_defined(head.name))
40
+ return emit_bound_call(binding, args, env, current_scope)
41
+ end
40
42
 
41
43
  return emit_self_call(head.name, args, env, current_scope)
42
44
  end
@@ -69,21 +71,21 @@ module Kapusta
69
71
  when '?.' then emit_safe_lookup(args, env, current_scope)
70
72
  when ':' then emit_colon(args, env, current_scope)
71
73
  when '..' then emit_concat(args, env, current_scope)
72
- when 'length' then "(#{emit_expr(args[0], env, current_scope)}).length"
74
+ when 'length' then "#{parenthesize(emit_expr(args[0], env, current_scope))}.length"
73
75
  when 'require' then emit_require(args[0], env, current_scope)
74
76
  when 'module' then emit_module_expr(args, env)
75
77
  when 'class' then emit_class_expr(args, env)
76
78
  when 'try' then emit_try(args, env, current_scope)
77
79
  when 'raise' then emit_raise(args, env, current_scope)
78
- when 'ivar' then runtime_call(:get_ivar, 'self', args[0].name.inspect)
79
- when 'cvar' then runtime_call(:get_cvar, 'self', args[0].name.inspect)
80
+ when 'ivar' then "@#{Kapusta.kebab_to_snake(args[0].name)}"
81
+ when 'cvar' then "@@#{Kapusta.kebab_to_snake(args[0].name)}"
80
82
  when 'gvar' then emit_gvar(args[0])
81
83
  when 'ruby' then "Kernel.eval(#{emit_expr(args[0], env, current_scope)})"
82
84
  when 'and' then emit_and(args, env, current_scope)
83
85
  when 'or' then emit_or(args, env, current_scope)
84
- when 'not' then "(!#{emit_expr(args[0], env, current_scope)})"
86
+ when 'not' then "!#{parenthesize(emit_expr(args[0], env, current_scope))}"
85
87
  when '=' then emit_compare(args, env, current_scope, '==')
86
- when 'not=' then "(!#{emit_special('=', args, env, current_scope)})"
88
+ when 'not=' then emit_compare_any(args, env, current_scope, '!=')
87
89
  when '<' then emit_compare(args, env, current_scope, '<')
88
90
  when '<=' then emit_compare(args, env, current_scope, '<=')
89
91
  when '>' then emit_compare(args, env, current_scope, '>')
@@ -95,7 +97,7 @@ module Kapusta
95
97
  when '%' then args.map { |arg| parenthesize(emit_expr(arg, env, current_scope)) }.join(' % ')
96
98
  when 'print' then emit_print(args, env, current_scope)
97
99
  else
98
- raise Error, "unknown special form: #{name}"
100
+ emit_error!("unknown special form: #{name}")
99
101
  end
100
102
  end
101
103
 
@@ -106,17 +108,15 @@ module Kapusta
106
108
  end
107
109
 
108
110
  def emit_print(args, env, current_scope)
109
- return '$stdout.puts("")' if args.empty?
111
+ return 'p' if args.empty?
110
112
 
111
- values = args.map { |arg| emit_string_part(arg, env, current_scope) }
112
- output = values.length == 1 ? values.first : "[#{values.join(', ')}].join(\"\\t\")"
113
- "$stdout.puts(#{output})"
113
+ "p(#{args.map { |arg| emit_expr(arg, env, current_scope) }.join(', ')})"
114
114
  end
115
115
 
116
116
  def emit_string_part(arg, env, current_scope)
117
117
  return arg.inspect if arg.is_a?(String)
118
118
 
119
- runtime_call(:stringify, emit_expr(arg, env, current_scope))
119
+ "(#{emit_expr(arg, env, current_scope)}).to_s"
120
120
  end
121
121
  end
122
122
  end
@@ -8,8 +8,11 @@ module Kapusta
8
8
 
9
9
  def emit_lookup(args, env, current_scope)
10
10
  object_code = emit_expr(args[0], env, current_scope)
11
- keys = args[1..].map { |arg| emit_expr(arg, env, current_scope) }.join(', ')
12
- runtime_call(:get_path, object_code, "[#{keys}]")
11
+ keys = args[1..].map { |arg| emit_expr(arg, env, current_scope) }
12
+ return object_code if keys.empty?
13
+
14
+ receiver = simple_expression?(object_code) ? object_code : parenthesize(object_code)
15
+ "#{receiver}#{keys.map { |k| "[#{k}]" }.join}"
13
16
  end
14
17
 
15
18
  def emit_safe_lookup(args, env, current_scope)
@@ -20,9 +23,16 @@ module Kapusta
20
23
 
21
24
  def emit_colon(args, env, current_scope)
22
25
  receiver = emit_expr(args[0], env, current_scope)
23
- method_name = emit_method_name(args[1], env, current_scope)
26
+ method_form = args[1]
24
27
  positional, kwargs, block = split_call_args(args[2..], env, current_scope)
25
- runtime_call(:send_call, receiver, method_name, positional, kwargs, block)
28
+ literal_name = method_form if method_form.is_a?(Symbol) || method_form.is_a?(String)
29
+ if literal_name && !kwargs && !block && direct_method_name?(literal_name.to_s)
30
+ return emit_direct_method_call(receiver, Kapusta.kebab_to_snake(literal_name.to_s), positional)
31
+ end
32
+
33
+ method_name = emit_method_name(method_form, env, current_scope)
34
+ parts = build_call_args([method_name, *positional], kwargs, block)
35
+ "#{parenthesize(receiver)}.public_send(#{parts})"
26
36
  end
27
37
 
28
38
  def emit_require(arg, env, current_scope)
@@ -33,9 +43,30 @@ module Kapusta
33
43
  when String then arg.inspect
34
44
  else "(#{emit_expr(arg, env, current_scope)}).to_s"
35
45
  end
46
+ if kapusta_require?(arg)
47
+ return [
48
+ 'unless defined?(Kapusta)',
49
+ indent('require "kapusta"'),
50
+ 'end',
51
+ "Kapusta.require(#{path_code}, relative_to: #{@path.inspect})"
52
+ ].join("\n")
53
+ end
54
+
36
55
  "require #{path_code}"
37
56
  end
38
57
 
58
+ def kapusta_require?(arg)
59
+ path =
60
+ case arg
61
+ when Sym then arg.name
62
+ when Symbol then arg.to_s
63
+ when String then arg
64
+ end
65
+ return false unless path
66
+
67
+ path.end_with?('.kap') || path.start_with?('./', '../') || File.absolute_path?(path)
68
+ end
69
+
39
70
  def emit_module_expr(args, env)
40
71
  body = emit_sequence(args[1..], env, :module, allow_method_definitions: true, result: false).first
41
72
  emit_module_wrapper(args[0], body)
@@ -184,6 +215,15 @@ module Kapusta
184
215
  end.join(' && ')
185
216
  end
186
217
 
218
+ def emit_compare_any(args, env, current_scope, operator)
219
+ values = args.map { |arg| emit_expr(arg, env, current_scope) }
220
+ return 'false' if values.length <= 1
221
+
222
+ (0...(values.length - 1)).map do |i|
223
+ "#{parenthesize(values[i])} #{operator} #{parenthesize(values[i + 1])}"
224
+ end.join(' || ')
225
+ end
226
+
187
227
  def emit_reduce(args, env, current_scope, empty_value, operator)
188
228
  return empty_value if args.empty?
189
229
 
@@ -207,9 +247,16 @@ module Kapusta
207
247
 
208
248
  def emit_callable_call(callee_code, args, env, current_scope)
209
249
  positional, kwargs, block = split_call_args(args, env, current_scope)
210
- return emit_direct_callable_call(callee_code, positional) unless kwargs || block
250
+ rendered = build_call_args(positional, kwargs, block)
251
+ suffix = rendered.empty? ? '.call' : ".call(#{rendered})"
252
+ "#{parenthesize(callee_code)}#{suffix}"
253
+ end
211
254
 
212
- runtime_call(:call, callee_code, "[#{positional.join(', ')}]", kwargs, block)
255
+ def build_call_args(positional, kwargs, block)
256
+ parts = positional.dup
257
+ parts << "**#{kwargs}" if kwargs
258
+ parts << "&#{block}" if block
259
+ parts.join(', ')
213
260
  end
214
261
 
215
262
  def emit_bound_call(binding, args, env, current_scope)
@@ -228,30 +275,31 @@ module Kapusta
228
275
  args.empty? ? "#{method_name}()" : "#{method_name}(#{args})"
229
276
  end
230
277
 
231
- def emit_direct_callable_call(callee_code, positional)
232
- rendered_args = positional.join(', ')
233
- suffix = rendered_args.empty? ? '.call' : ".call(#{rendered_args})"
234
- "#{parenthesize(callee_code)}#{suffix}"
235
- end
236
-
237
278
  def emit_multisym_call(head, args, env, current_scope)
238
279
  base_code, segments = multisym_base(head.segments, env)
239
280
  if segments.empty?
240
281
  emit_callable_call(base_code, args, env, current_scope)
241
282
  else
242
- receiver =
243
- if segments.length == 1
244
- base_code
245
- else
246
- runtime_call(:method_path_value, base_code, segments[0...-1].inspect)
247
- end
248
- method_name = Kapusta.kebab_to_snake(segments.last).to_sym.inspect
283
+ receiver = emit_method_path(base_code, segments[0...-1])
249
284
  positional, kwargs, block = split_call_args(args, env, current_scope)
250
- if segments.length == 1 && !kwargs && !block && direct_method_name?(segments.last)
285
+ if !kwargs && !block && direct_method_name?(segments.last)
251
286
  return emit_direct_method_call(receiver, Kapusta.kebab_to_snake(segments.last), positional)
252
287
  end
253
288
 
254
- runtime_call(:send_call, receiver, method_name, positional, kwargs, block)
289
+ method_name = Kapusta.kebab_to_snake(segments.last).to_sym.inspect
290
+ parts = build_call_args([method_name, *positional], kwargs, block)
291
+ "#{receiver}.public_send(#{parts})"
292
+ end
293
+ end
294
+
295
+ def emit_method_path(base_code, segments)
296
+ segments.reduce(base_code) do |acc, segment|
297
+ snake = Kapusta.kebab_to_snake(segment)
298
+ if direct_method_name?(segment)
299
+ "#{acc}.#{snake}"
300
+ else
301
+ "#{acc}.public_send(#{snake.to_sym.inspect})"
302
+ end
255
303
  end
256
304
  end
257
305
 
@@ -265,7 +313,8 @@ module Kapusta
265
313
  def emit_self_call(name, args, env, current_scope)
266
314
  positional, kwargs, block = split_call_args(args, env, current_scope)
267
315
  method_name = Kapusta.kebab_to_snake(name).to_sym.inspect
268
- runtime_call(:invoke_self, 'self', method_name, positional, kwargs, block)
316
+ parts = build_call_args([method_name, *positional], kwargs, block)
317
+ "send(#{parts})"
269
318
  end
270
319
 
271
320
  def split_call_args(args, env, current_scope)
@@ -301,33 +350,31 @@ module Kapusta
301
350
  name = sym.name
302
351
  return 'self' if name == 'self'
303
352
  return 'Float::INFINITY' if name == 'math.huge'
304
- return binding_value_code(env.lookup(name)) if env.defined?(name)
353
+
354
+ if (binding = env.lookup_if_defined(sym))
355
+ return binding_value_code(binding)
356
+ end
305
357
  return emit_multisym_value(sym, env) if sym.dotted?
306
358
  return 'ARGV' if name == 'ARGV'
307
359
  return name if name.match?(/\A[A-Z]/)
308
360
 
309
- raise Error, "undefined symbol: #{name}"
361
+ emit_error!("undefined symbol: #{name}")
310
362
  end
311
363
 
312
364
  def emit_gvar(sym)
313
- ruby_name = global_name(sym.name)
314
- return "$#{ruby_name}" if direct_global_name?(ruby_name)
315
-
316
- runtime_call(:get_gvar, sym.name.inspect)
365
+ "$#{global_name(sym.name)}"
317
366
  end
318
367
 
319
368
  def emit_multisym_value(sym, env)
320
369
  base_code, segments = multisym_base(sym.segments, env)
321
- return base_code if segments.empty?
322
-
323
- runtime_call(:method_path_value, base_code, segments.inspect)
370
+ emit_method_path(base_code, segments)
324
371
  end
325
372
 
326
373
  def multisym_base(segments, env)
327
374
  if segments[0] == 'self'
328
375
  ['self', segments[1..]]
329
- elsif env.defined?(segments[0])
330
- [binding_value_code(env.lookup(segments[0])), segments[1..]]
376
+ elsif (binding = env.lookup_if_defined(segments[0]))
377
+ [binding_value_code(binding), segments[1..]]
331
378
  else
332
379
  idx = 0
333
380
  const_path = []
@@ -335,7 +382,7 @@ module Kapusta
335
382
  const_path << segments[idx]
336
383
  idx += 1
337
384
  end
338
- raise Error, "bad multisym: #{segments.join('.')}" if const_path.empty?
385
+ emit_error!("bad multisym: #{segments.join('.')}") if const_path.empty?
339
386
 
340
387
  [const_path.join('::'), segments[idx..]]
341
388
  end
@@ -351,10 +398,6 @@ module Kapusta
351
398
  Kapusta.kebab_to_snake(name).match?(/\A[a-z_]\w*[!?=]?\z/)
352
399
  end
353
400
 
354
- def direct_global_name?(name)
355
- name.match?(/\A[a-z_]\w*\z/)
356
- end
357
-
358
401
  def global_name(name)
359
402
  Kapusta.kebab_to_snake(name).gsub(/[^a-zA-Z0-9_]/, '_')
360
403
  end
@@ -364,11 +407,19 @@ module Kapusta
364
407
  code.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/) ||
365
408
  code.match?(/\A[a-z_]\w*[!?=]?\([^()\n]*\)\z/) ||
366
409
  code.match?(/\A\d+(?:\.\d+)?\z/) ||
367
- code.match?(/\A[a-z_]\w*(?:\.[a-z_]\w*[!?=]?(?:\([^()\n]*\))?)+\z/) ||
410
+ code.match?(/\A[a-z_]\w*(?:\.[a-z_]\w*[!?=]?(?:\([^()\n]*\))?|\[[^\[\]]*\])+\z/) ||
368
411
  code.match?(/\A:[a-zA-Z_]\w*[!?=]?\z/) ||
369
412
  code.match?(/\A"(?:[^"\\]|\\.)*"\z/) ||
370
413
  code.match?(/\A'(?:[^'\\]|\\.)*'\z/) ||
371
- %w[nil true false self].include?(code)
414
+ %w[nil true false self].include?(code) ||
415
+ negation_simple?(code)
416
+ end
417
+
418
+ def negation_simple?(code)
419
+ return false unless code.start_with?('!') && code.length > 1
420
+
421
+ rest = code[1..]
422
+ simple_expression?(rest) || (rest.start_with?('(') && rest.end_with?(')'))
372
423
  end
373
424
  end
374
425
  end
@@ -10,7 +10,7 @@ module Kapusta
10
10
  if pattern.is_a?(Sym)
11
11
  return ['nil', env] if pattern.name == '_'
12
12
 
13
- ruby_name = define_local(env, pattern.name)
13
+ ruby_name = define_local(env, pattern)
14
14
  ["#{ruby_name} = #{value_code}", env]
15
15
  else
16
16
  bindings_var = temp('bindings')
@@ -61,7 +61,7 @@ module Kapusta
61
61
  elsif or_pattern?(pattern)
62
62
  emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
63
63
  elsif where_pattern?(pattern)
64
- raise Error, '`where` is only valid as a case/match clause head'
64
+ emit_error!('`where` is only valid as a case/match clause head')
65
65
  else
66
66
  emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
67
67
  end
@@ -70,7 +70,7 @@ module Kapusta
70
70
  when Symbol, String, Numeric, true, false
71
71
  "[:lit, #{pattern.inspect}]"
72
72
  else
73
- raise Error, "bad pattern: #{pattern.inspect}"
73
+ emit_error!("bad pattern: #{pattern.inspect}")
74
74
  end
75
75
  end
76
76
 
@@ -88,10 +88,11 @@ module Kapusta
88
88
  end
89
89
 
90
90
  def emit_named_match_pattern(name, env, mode:, state:, allow_nil:, prefer_pin:)
91
+ binding = prefer_pin && mode == :match ? env.lookup_if_defined(name) : nil
91
92
  if state[:bound_names].key?(name)
92
93
  "[:ref, #{name.inspect}]"
93
- elsif prefer_pin && mode == :match && env.defined?(name)
94
- "[:pin, #{binding_value_code(env.lookup(name))}]"
94
+ elsif binding
95
+ "[:pin, #{binding_value_code(binding)}]"
95
96
  else
96
97
  state[:bound_names][name] = true
97
98
  state[:binding_names] << name
@@ -115,13 +116,15 @@ module Kapusta
115
116
  end
116
117
 
117
118
  def emit_pin_match_pattern(pattern, env, mode:, allow_pins:)
118
- raise Error, 'pin patterns are only supported inside `case` guards' unless allow_pins && mode == :case
119
+ emit_error!('pin patterns are only supported inside `case` guards') unless allow_pins && mode == :case
119
120
 
120
121
  name_sym = pattern.items[1]
121
- raise Error, "bad pin pattern: #{pattern.inspect}" unless name_sym.is_a?(Sym)
122
- raise Error, "cannot pin undefined name: #{name_sym.name}" unless env.defined?(name_sym.name)
122
+ emit_error!("bad pin pattern: #{pattern.inspect}") unless name_sym.is_a?(Sym)
123
123
 
124
- "[:pin, #{binding_value_code(env.lookup(name_sym.name))}]"
124
+ binding = env.lookup_if_defined(name_sym.name)
125
+ emit_error!("cannot pin undefined name: #{name_sym.name}") unless binding
126
+
127
+ "[:pin, #{binding_value_code(binding)}]"
125
128
  end
126
129
 
127
130
  def emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
@@ -136,7 +139,7 @@ module Kapusta
136
139
  compiled = emit_match_pattern(subpattern, env, mode:, allow_pins:, state: alt_state)
137
140
  alt_names = alt_state[:binding_names][initial_names..]
138
141
  canonical_names ||= alt_names
139
- raise Error, 'all `or` patterns must bind the same names' if canonical_names.sort != alt_names.sort
142
+ emit_error!('all `or` patterns must bind the same names') if canonical_names.sort != alt_names.sort
140
143
 
141
144
  compiled
142
145
  end
@@ -174,7 +177,7 @@ module Kapusta
174
177
  when Symbol, String, Numeric, true, false
175
178
  "[:lit, #{pattern.inspect}]"
176
179
  else
177
- raise Error, "bad pattern: #{pattern.inspect}"
180
+ emit_error!("bad pattern: #{pattern.inspect}")
178
181
  end
179
182
  end
180
183
 
@@ -6,6 +6,10 @@ module Kapusta
6
6
  module Support
7
7
  private
8
8
 
9
+ def emit_error!(message)
10
+ raise Error, "#{@path}: #{message}"
11
+ end
12
+
9
13
  def emit_forms_with_headers(forms, env, current_scope, result: true)
10
14
  i = 0
11
15
  codes = []
@@ -191,7 +195,7 @@ module Kapusta
191
195
 
192
196
  def local_name(source_name, env, shadow:)
193
197
  base = sanitize_local(source_name)
194
- base = "user_#{base}" if reserved_generated_name?(base)
198
+ base = "user_#{base}" if !generated_symbol?(source_name) && reserved_generated_name?(base)
195
199
  return base unless ruby_name_defined?(env, base, shadow:)
196
200
 
197
201
  index = 2
@@ -211,6 +215,10 @@ module Kapusta
211
215
  name.start_with?('kap_', '__kap_')
212
216
  end
213
217
 
218
+ def generated_symbol?(source_name)
219
+ source_name.is_a?(GeneratedSym)
220
+ end
221
+
214
222
  def runtime_helper(name)
215
223
  helper = name.to_sym
216
224
  @runtime_helpers << helper unless @runtime_helpers.include?(helper)
@@ -255,24 +263,18 @@ module Kapusta
255
263
 
256
264
  def emit_counted_loop(ruby_name:, start_code:, finish_code:, step_code:,
257
265
  until_form:, loop_env:, current_scope:, body_code:)
258
- finish_var = temp('finish')
259
- step_var = temp('step')
260
- cmp_var = temp('cmp')
261
266
  until_code = until_form ? "break if #{emit_expr(until_form, loop_env, current_scope)}" : nil
262
- body = [until_code, body_code, "#{ruby_name} += #{step_var}"].compact.reject(&:empty?).join("\n")
267
+ body = [until_code, body_code].compact.reject(&:empty?).join("\n")
268
+ step_part = step_code == '1' ? '' : ", #{step_code}"
263
269
  [
264
- "#{ruby_name} = #{start_code}",
265
- "#{finish_var} = #{finish_code}",
266
- "#{step_var} = #{step_code}",
267
- "#{cmp_var} = #{step_var} >= 0 ? :<= : :>=",
268
- "while #{ruby_name}.public_send(#{cmp_var}, #{finish_var})",
270
+ "#{parenthesize(start_code)}.step(#{finish_code}#{step_part}) do |#{ruby_name}|",
269
271
  indent(body),
270
272
  'end'
271
273
  ].join("\n")
272
274
  end
273
275
 
274
276
  def sanitize_local(name)
275
- base = Kapusta.kebab_to_snake(name)
277
+ base = Kapusta.kebab_to_snake(name.respond_to?(:name) ? name.name : name)
276
278
  base = base.gsub('?', '_q').gsub('!', '_bang')
277
279
  base = base.gsub(/[^a-zA-Z0-9_]/, '_')
278
280
  if base.empty? || base.match?(/\A\d/) || base.match?(/\A[A-Z]/) || self.class::RUBY_KEYWORDS.include?(base)
@@ -34,11 +34,7 @@ module Kapusta
34
34
  env = Env.new
35
35
  body = emit_forms_with_headers(forms, env, :toplevel)
36
36
  helpers = Runtime.helper_source(@runtime_helpers)
37
- [
38
- '# frozen_string_literal: true',
39
- helpers,
40
- body
41
- ].reject(&:empty?).join("\n\n") << "\n"
37
+ [helpers, body].reject(&:empty?).join("\n\n") << "\n"
42
38
  end
43
39
  end
44
40
  end
@@ -38,11 +38,11 @@ module Kapusta
38
38
  when 'when'
39
39
  cond = items[1]
40
40
  body = wrap_do(items[2..])
41
- List.new([Sym.new('if'), cond, body, nil])
41
+ List.new([Sym.new('if'), cond, body])
42
42
  when 'unless'
43
43
  cond = items[1]
44
44
  body = wrap_do(items[2..])
45
- List.new([Sym.new('if'), cond, nil, body])
45
+ List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body])
46
46
  when 'tset'
47
47
  List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]])
48
48
  when 'pcall'
@@ -85,29 +85,48 @@ module Kapusta
85
85
  short = %w[-?> -?>>].include?(kind)
86
86
  position = %w[-> -?>].include?(kind) ? :first : :last
87
87
 
88
+ return thread_short(forms, position) if short
89
+
88
90
  forms[1..].reduce(value) do |memo, form|
89
- threaded =
90
- if form.is_a?(List)
91
- if position == :first
92
- List.new([form.items[0], memo, *form.items[1..]])
93
- else
94
- List.new([*form.items, memo])
95
- end
96
- else
97
- List.new([form, memo])
98
- end
99
-
100
- if short
101
- List.new([Sym.new('if'), List.new([Sym.new('='), memo, nil]), nil, threaded])
91
+ thread_step(memo, form, position)
92
+ end
93
+ end
94
+
95
+ def thread_short(forms, position)
96
+ forms[1..].reduce(forms.first) do |memo, form|
97
+ temp = thread_temp
98
+ List.new([
99
+ Sym.new('let'),
100
+ Vec.new([temp, memo]),
101
+ List.new([
102
+ Sym.new('if'),
103
+ List.new([Sym.new('='), temp, nil]),
104
+ nil,
105
+ thread_step(temp, form, position)
106
+ ])
107
+ ])
108
+ end
109
+ end
110
+
111
+ def thread_step(memo, form, position)
112
+ if form.is_a?(List)
113
+ if position == :first
114
+ List.new([form.items[0], memo, *form.items[1..]])
102
115
  else
103
- threaded
116
+ List.new([*form.items, memo])
104
117
  end
118
+ else
119
+ List.new([form, memo])
105
120
  end
106
121
  end
107
122
 
123
+ def thread_temp
124
+ gensym('kap_thread')
125
+ end
126
+
108
127
  def doto(forms)
109
128
  value = forms.first
110
- temp = Sym.new('__doto__')
129
+ temp = gensym('kap_doto')
111
130
  body = forms[1..].map do |form|
112
131
  if form.is_a?(List)
113
132
  List.new([form.items[0], temp, *form.items[1..]])
@@ -117,6 +136,11 @@ module Kapusta
117
136
  end
118
137
  List.new([Sym.new('let'), Vec.new([temp, value]), *body, temp])
119
138
  end
139
+
140
+ def gensym(prefix)
141
+ @gensym_index = (@gensym_index || 0) + 1
142
+ GeneratedSym.new("#{prefix}_#{@gensym_index}", @gensym_index)
143
+ end
120
144
  end
121
145
  end
122
146
  end