kapusta 0.1.3 → 0.1.5

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.
@@ -36,7 +36,7 @@ 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_callable_call(env.lookup(head.name), args, env, current_scope) if env.defined?(head.name)
39
+ return emit_bound_call(env.lookup(head.name), args, env, current_scope) if env.defined?(head.name)
40
40
 
41
41
  return emit_self_call(head.name, args, env, current_scope)
42
42
  end
@@ -68,9 +68,7 @@ module Kapusta
68
68
  when '.' then emit_lookup(args, env, current_scope)
69
69
  when '?.' then emit_safe_lookup(args, env, current_scope)
70
70
  when ':' then emit_colon(args, env, current_scope)
71
- when '..' then runtime_call(:concat, args.map do |arg|
72
- emit_expr(arg, env, current_scope)
73
- end)
71
+ when '..' then emit_concat(args, env, current_scope)
74
72
  when 'length' then "(#{emit_expr(args[0], env, current_scope)}).length"
75
73
  when 'require' then emit_require(args[0], env, current_scope)
76
74
  when 'module' then emit_module_expr(args, env)
@@ -79,7 +77,7 @@ module Kapusta
79
77
  when 'raise' then emit_raise(args, env, current_scope)
80
78
  when 'ivar' then runtime_call(:get_ivar, 'self', args[0].name.inspect)
81
79
  when 'cvar' then runtime_call(:get_cvar, 'self', args[0].name.inspect)
82
- when 'gvar' then runtime_call(:get_gvar, args[0].name.inspect)
80
+ when 'gvar' then emit_gvar(args[0])
83
81
  when 'ruby' then "Kernel.eval(#{emit_expr(args[0], env, current_scope)})"
84
82
  when 'and' then emit_and(args, env, current_scope)
85
83
  when 'or' then emit_or(args, env, current_scope)
@@ -95,13 +93,31 @@ module Kapusta
95
93
  when '*' then emit_reduce(args, env, current_scope, '1', :*)
96
94
  when '/' then emit_div(args, env, current_scope)
97
95
  when '%' then args.map { |arg| parenthesize(emit_expr(arg, env, current_scope)) }.join(' % ')
98
- when 'print' then runtime_call(:print_values, *args.map do |arg|
99
- emit_expr(arg, env, current_scope)
100
- end)
96
+ when 'print' then emit_print(args, env, current_scope)
101
97
  else
102
98
  raise Error, "unknown special form: #{name}"
103
99
  end
104
100
  end
101
+
102
+ def emit_concat(args, env, current_scope)
103
+ return '""' if args.empty?
104
+
105
+ args.map { |arg| emit_string_part(arg, env, current_scope) }.join(' + ')
106
+ end
107
+
108
+ def emit_print(args, env, current_scope)
109
+ return '$stdout.puts("")' if args.empty?
110
+
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})"
114
+ end
115
+
116
+ def emit_string_part(arg, env, current_scope)
117
+ return arg.inspect if arg.is_a?(String)
118
+
119
+ runtime_call(:stringify, emit_expr(arg, env, current_scope))
120
+ end
105
121
  end
106
122
  end
107
123
  end
@@ -37,26 +37,40 @@ module Kapusta
37
37
  end
38
38
 
39
39
  def emit_module_expr(args, env)
40
- emit_module_wrapper(args[0], emit_sequence(args[1..], env, :module, allow_method_definitions: true).first)
40
+ body = emit_sequence(args[1..], env, :module, allow_method_definitions: true, result: false).first
41
+ emit_module_wrapper(args[0], body)
41
42
  end
42
43
 
43
44
  def emit_class_expr(args, env)
44
45
  name_sym, supers, body_forms = split_class_args(args)
46
+ body = emit_sequence(body_forms, env, :class, allow_method_definitions: true, result: false).first
45
47
  emit_class_wrapper(name_sym, supers, env,
46
- emit_sequence(body_forms, env, :class, allow_method_definitions: true).first)
48
+ body)
47
49
  end
48
50
 
49
51
  def emit_module_wrapper(name_sym, body)
50
52
  mod_var = temp('module')
51
- <<~RUBY.chomp
52
- (-> do
53
- #{mod_var} = #{runtime_call(:ensure_module, 'self', name_sym.name.inspect)}
54
- #{mod_var}.module_eval do
55
- #{indent(body)}
56
- end
57
- #{mod_var}
58
- end).call
59
- RUBY
53
+ [
54
+ '(-> do',
55
+ indent("#{mod_var} = #{runtime_call(:ensure_module, 'self', name_sym.name.inspect)}"),
56
+ indent("#{mod_var}.module_eval do"),
57
+ indent(body, 2),
58
+ indent('end'),
59
+ indent(mod_var),
60
+ 'end).call'
61
+ ].join("\n")
62
+ end
63
+
64
+ def emit_direct_module_header(name_sym, body)
65
+ const_name = simple_constant_name(name_sym)
66
+ return unless const_name
67
+
68
+ [
69
+ "module #{const_name}",
70
+ indent(body),
71
+ 'end',
72
+ const_name
73
+ ].join("\n")
60
74
  end
61
75
 
62
76
  def emit_class_wrapper(name_sym, supers, env, body)
@@ -67,15 +81,33 @@ module Kapusta
67
81
  else
68
82
  'Object'
69
83
  end
70
- <<~RUBY.chomp
71
- (-> do
72
- #{klass_var} = #{runtime_call(:ensure_class, 'self', name_sym.name.inspect, super_code)}
73
- #{klass_var}.class_eval do
74
- #{indent(body)}
75
- end
76
- #{klass_var}
77
- end).call
78
- RUBY
84
+ [
85
+ '(-> do',
86
+ indent("#{klass_var} = #{runtime_call(:ensure_class, 'self', name_sym.name.inspect, super_code)}"),
87
+ indent("#{klass_var}.class_eval do"),
88
+ indent(body, 2),
89
+ indent('end'),
90
+ indent(klass_var),
91
+ 'end).call'
92
+ ].join("\n")
93
+ end
94
+
95
+ def emit_direct_class_header(name_sym, supers, body)
96
+ const_name = simple_constant_name(name_sym)
97
+ return unless const_name && supers.nil?
98
+
99
+ [
100
+ "class #{const_name}",
101
+ indent(body),
102
+ 'end',
103
+ const_name
104
+ ].join("\n")
105
+ end
106
+
107
+ def simple_constant_name(name_sym)
108
+ return unless name_sym.is_a?(Sym) && name_sym.name.match?(/\A[A-Z]\w*\z/)
109
+
110
+ name_sym.name
79
111
  end
80
112
 
81
113
  def emit_try(args, env, current_scope)
@@ -103,8 +135,7 @@ module Kapusta
103
135
  lines = ['begin', indent(emit_expr(args[0], env, current_scope))]
104
136
  catches.each do |klass_form, bind_sym, body|
105
137
  rescue_env = env.child
106
- rescue_name = temp(sanitize_local(bind_sym.name))
107
- rescue_env.define(bind_sym.name, rescue_name)
138
+ rescue_name = define_local(rescue_env, bind_sym.name)
108
139
  body_code, = emit_sequence(body, rescue_env, current_scope, allow_method_definitions: false)
109
140
  rescue_line =
110
141
  if klass_form
@@ -176,9 +207,33 @@ module Kapusta
176
207
 
177
208
  def emit_callable_call(callee_code, args, env, current_scope)
178
209
  positional, kwargs, block = split_call_args(args, env, current_scope)
210
+ return emit_direct_callable_call(callee_code, positional) unless kwargs || block
211
+
179
212
  runtime_call(:call, callee_code, "[#{positional.join(', ')}]", kwargs, block)
180
213
  end
181
214
 
215
+ def emit_bound_call(binding, args, env, current_scope)
216
+ return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
217
+
218
+ emit_callable_call(binding, args, env, current_scope)
219
+ end
220
+
221
+ def emit_self_method_binding_call(binding, args, env, current_scope)
222
+ positional = args.map { |arg| emit_expr(arg, env, current_scope) }
223
+ emit_direct_self_method_call(binding.ruby_name, positional)
224
+ end
225
+
226
+ def emit_direct_self_method_call(method_name, positional)
227
+ args = positional.join(', ')
228
+ args.empty? ? "#{method_name}()" : "#{method_name}(#{args})"
229
+ end
230
+
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
+
182
237
  def emit_multisym_call(head, args, env, current_scope)
183
238
  base_code, segments = multisym_base(head.segments, env)
184
239
  if segments.empty?
@@ -192,10 +247,21 @@ module Kapusta
192
247
  end
193
248
  method_name = Kapusta.kebab_to_snake(segments.last).to_sym.inspect
194
249
  positional, kwargs, block = split_call_args(args, env, current_scope)
250
+ if segments.length == 1 && !kwargs && !block && direct_method_name?(segments.last)
251
+ return emit_direct_method_call(receiver, Kapusta.kebab_to_snake(segments.last), positional)
252
+ end
253
+
195
254
  runtime_call(:send_call, receiver, method_name, positional, kwargs, block)
196
255
  end
197
256
  end
198
257
 
258
+ def emit_direct_method_call(receiver, method_name, positional)
259
+ args = positional.join(', ')
260
+ rendered_receiver = simple_expression?(receiver) ? receiver : parenthesize(receiver)
261
+ suffix = args.empty? ? method_name : "#{method_name}(#{args})"
262
+ "#{rendered_receiver}.#{suffix}"
263
+ end
264
+
199
265
  def emit_self_call(name, args, env, current_scope)
200
266
  positional, kwargs, block = split_call_args(args, env, current_scope)
201
267
  method_name = Kapusta.kebab_to_snake(name).to_sym.inspect
@@ -235,7 +301,7 @@ module Kapusta
235
301
  name = sym.name
236
302
  return 'self' if name == 'self'
237
303
  return 'Float::INFINITY' if name == 'math.huge'
238
- return env.lookup(name) if env.defined?(name)
304
+ return binding_value_code(env.lookup(name)) if env.defined?(name)
239
305
  return emit_multisym_value(sym, env) if sym.dotted?
240
306
  return 'ARGV' if name == 'ARGV'
241
307
  return name if name.match?(/\A[A-Z]/)
@@ -243,6 +309,13 @@ module Kapusta
243
309
  raise Error, "undefined symbol: #{name}"
244
310
  end
245
311
 
312
+ 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)
317
+ end
318
+
246
319
  def emit_multisym_value(sym, env)
247
320
  base_code, segments = multisym_base(sym.segments, env)
248
321
  return base_code if segments.empty?
@@ -254,7 +327,7 @@ module Kapusta
254
327
  if segments[0] == 'self'
255
328
  ['self', segments[1..]]
256
329
  elsif env.defined?(segments[0])
257
- [env.lookup(segments[0]), segments[1..]]
330
+ [binding_value_code(env.lookup(segments[0])), segments[1..]]
258
331
  else
259
332
  idx = 0
260
333
  const_path = []
@@ -269,8 +342,34 @@ module Kapusta
269
342
  end
270
343
 
271
344
  def parenthesize(code)
345
+ return code if simple_expression?(code)
346
+
272
347
  "(#{code})"
273
348
  end
349
+
350
+ def direct_method_name?(name)
351
+ Kapusta.kebab_to_snake(name).match?(/\A[a-z_]\w*[!?=]?\z/)
352
+ end
353
+
354
+ def direct_global_name?(name)
355
+ name.match?(/\A[a-z_]\w*\z/)
356
+ end
357
+
358
+ def global_name(name)
359
+ Kapusta.kebab_to_snake(name).gsub(/[^a-zA-Z0-9_]/, '_')
360
+ end
361
+
362
+ def simple_expression?(code)
363
+ code.match?(/\A[a-z_]\w*\z/) ||
364
+ code.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/) ||
365
+ code.match?(/\A[a-z_]\w*[!?=]?\([^()\n]*\)\z/) ||
366
+ code.match?(/\A\d+(?:\.\d+)?\z/) ||
367
+ code.match?(/\A[a-z_]\w*(?:\.[a-z_]\w*[!?=]?(?:\([^()\n]*\))?)+\z/) ||
368
+ code.match?(/\A:[a-zA-Z_]\w*[!?=]?\z/) ||
369
+ code.match?(/\A"(?:[^"\\]|\\.)*"\z/) ||
370
+ code.match?(/\A'(?:[^'\\]|\\.)*'\z/) ||
371
+ %w[nil true false self].include?(code)
372
+ end
274
373
  end
275
374
  end
276
375
  end
@@ -10,8 +10,7 @@ module Kapusta
10
10
  if pattern.is_a?(Sym)
11
11
  return ['nil', env] if pattern.name == '_'
12
12
 
13
- ruby_name = temp(sanitize_local(pattern.name))
14
- env.define(pattern.name, ruby_name)
13
+ ruby_name = define_local(env, pattern.name)
15
14
  ["#{ruby_name} = #{value_code}", env]
16
15
  else
17
16
  bindings_var = temp('bindings')
@@ -20,8 +19,7 @@ module Kapusta
20
19
  "#{bindings_var} = #{runtime_call(:destructure, emit_pattern(pattern), value_code)}"
21
20
  ]
22
21
  pattern_names(pattern).each do |name|
23
- ruby_name = temp(sanitize_local(name))
24
- current_env.define(name, ruby_name)
22
+ ruby_name = define_local(current_env, name)
25
23
  lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
26
24
  end
27
25
  [lines.join("\n"), current_env]
@@ -32,8 +30,7 @@ module Kapusta
32
30
  current_env = env
33
31
  lines = []
34
32
  binding_names.each do |name|
35
- ruby_name = temp(sanitize_local(name))
36
- current_env.define(name, ruby_name)
33
+ ruby_name = define_local(current_env, name)
37
34
  lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
38
35
  end
39
36
  [lines.join("\n"), current_env]
@@ -94,7 +91,7 @@ module Kapusta
94
91
  if state[:bound_names].key?(name)
95
92
  "[:ref, #{name.inspect}]"
96
93
  elsif prefer_pin && mode == :match && env.defined?(name)
97
- "[:pin, #{env.lookup(name)}]"
94
+ "[:pin, #{binding_value_code(env.lookup(name))}]"
98
95
  else
99
96
  state[:bound_names][name] = true
100
97
  state[:binding_names] << name
@@ -124,7 +121,7 @@ module Kapusta
124
121
  raise Error, "bad pin pattern: #{pattern.inspect}" unless name_sym.is_a?(Sym)
125
122
  raise Error, "cannot pin undefined name: #{name_sym.name}" unless env.defined?(name_sym.name)
126
123
 
127
- "[:pin, #{env.lookup(name_sym.name)}]"
124
+ "[:pin, #{binding_value_code(env.lookup(name_sym.name))}]"
128
125
  end
129
126
 
130
127
  def emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
@@ -6,7 +6,7 @@ module Kapusta
6
6
  module Support
7
7
  private
8
8
 
9
- def emit_forms_with_headers(forms, env, current_scope)
9
+ def emit_forms_with_headers(forms, env, current_scope, result: true)
10
10
  i = 0
11
11
  codes = []
12
12
  while i < forms.length
@@ -15,7 +15,9 @@ module Kapusta
15
15
  codes << emit_bodyless_header(form, forms[(i + 1)..], env, current_scope)
16
16
  break
17
17
  else
18
- code, env = emit_form_in_sequence(form, env, current_scope, allow_method_definitions: true)
18
+ code, env = emit_form_in_sequence(form, env, current_scope,
19
+ allow_method_definitions: true,
20
+ result_needed: result && i == forms.length - 1)
19
21
  codes << code
20
22
  i += 1
21
23
  end
@@ -50,25 +52,34 @@ module Kapusta
50
52
  if inner.length == 1 && bodyless_header?(inner[0])
51
53
  emit_bodyless_header(inner[0], remaining_forms, env, :module)
52
54
  else
53
- emit_forms_with_headers(remaining_forms, env, :module)
55
+ emit_forms_with_headers(remaining_forms, env, :module, result: false)
54
56
  end
55
- emit_module_wrapper(name_sym, body)
57
+ emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body)
56
58
  else
57
59
  name_sym, supers, = split_class_args(form.items[1..])
58
- body = emit_forms_with_headers(remaining_forms, env, :class)
59
- emit_class_wrapper(name_sym, supers, env, body)
60
+ body = emit_forms_with_headers(remaining_forms, env, :class, result: false)
61
+ emit_direct_class_header(name_sym, supers, body) || emit_class_wrapper(name_sym, supers, env, body)
60
62
  end
61
63
  end
62
64
 
63
- def emit_form_in_sequence(form, env, current_scope, allow_method_definitions:)
64
- if allow_method_definitions && method_definition_form?(form) && %i[module class].include?(current_scope)
65
- [emit_method_definition(form, env), env]
66
- elsif named_function_form?(form)
65
+ def emit_form_in_sequence(form, env, current_scope, allow_method_definitions:, result_needed: true)
66
+ if allow_method_definitions &&
67
+ method_definition_form?(form) &&
68
+ %i[toplevel module class].include?(current_scope)
69
+ code, env = emit_definition_form(form, env, current_scope)
70
+ return [code, env] if code
71
+ end
72
+
73
+ if named_function_form?(form)
67
74
  emit_named_fn_assignment(form, env, current_scope)
68
75
  elsif local_form?(form)
69
- emit_local_form(form, env, current_scope)
76
+ code, env = emit_local_form(form, env, current_scope)
77
+ code = code.delete_suffix("\nnil") unless result_needed
78
+ [code, env]
70
79
  elsif do_form?(form)
71
- emit_do_form(form.rest, env, current_scope)
80
+ emit_do_form(form.rest, env, current_scope, result_needed:)
81
+ elsif sequence_statement_form?(form)
82
+ emit_sequence_statement_form(form, env, current_scope, result_needed:)
72
83
  elsif set_new_local_form?(form, env)
73
84
  emit_set_form(form, env, current_scope)
74
85
  else
@@ -76,22 +87,51 @@ module Kapusta
76
87
  end
77
88
  end
78
89
 
79
- def emit_do_form(forms, env, current_scope)
80
- body, new_env = emit_sequence(forms, env, current_scope, allow_method_definitions: false)
90
+ def emit_do_form(forms, env, current_scope, result_needed: true)
91
+ body, new_env = emit_sequence(forms, env, current_scope,
92
+ allow_method_definitions: false,
93
+ result: result_needed)
94
+ return [body, new_env] unless result_needed
95
+
81
96
  ["begin\n#{indent(body)}\nend", new_env]
82
97
  end
83
98
 
84
- def emit_sequence(forms, env, current_scope, allow_method_definitions:)
99
+ def emit_sequence(forms, env, current_scope, allow_method_definitions:, result: true)
85
100
  current_env = env
86
101
  codes = []
87
- forms.each do |form|
102
+ forms.each_with_index do |form, index|
88
103
  code, current_env = emit_form_in_sequence(form, current_env, current_scope,
89
- allow_method_definitions:)
104
+ allow_method_definitions:,
105
+ result_needed: result && index == forms.length - 1)
90
106
  codes << code
91
107
  end
92
108
  [codes.join("\n"), current_env]
93
109
  end
94
110
 
111
+ def sequence_statement_form?(form)
112
+ return false unless form.is_a?(List) && form.head.is_a?(Sym)
113
+
114
+ %w[let while for each].include?(form.head.name)
115
+ end
116
+
117
+ def emit_sequence_statement_form(form, env, current_scope, result_needed:)
118
+ case form.head.name
119
+ when 'let'
120
+ return [emit_let_statement(form.rest, env, current_scope), env] unless result_needed
121
+ when 'while'
122
+ return [emit_while_statement(form.rest, env, current_scope), env]
123
+ when 'for'
124
+ return [emit_for_statement(form.rest, env, current_scope), env]
125
+ when 'each'
126
+ code = emit_each_statement(form.rest, env, current_scope)
127
+ return ["#{code}\nnil", env] if result_needed
128
+
129
+ return [code, env]
130
+ end
131
+
132
+ [emit_expr(form, env, current_scope), env]
133
+ end
134
+
95
135
  def special_form?(name)
96
136
  Compiler::SPECIAL_FORMS.include?(name)
97
137
  end
@@ -140,7 +180,35 @@ module Kapusta
140
180
 
141
181
  def temp(prefix)
142
182
  @temp_index += 1
143
- "__kap_#{prefix}_#{@temp_index}"
183
+ "kap_#{prefix}_#{@temp_index}"
184
+ end
185
+
186
+ def define_local(env, source_name, shadow: false)
187
+ ruby_name = local_name(source_name, env, shadow:)
188
+ env.define(source_name, ruby_name)
189
+ ruby_name
190
+ end
191
+
192
+ def local_name(source_name, env, shadow:)
193
+ base = sanitize_local(source_name)
194
+ base = "user_#{base}" if reserved_generated_name?(base)
195
+ return base unless ruby_name_defined?(env, base, shadow:)
196
+
197
+ index = 2
198
+ loop do
199
+ candidate = "#{base}_#{index}"
200
+ return candidate unless ruby_name_defined?(env, candidate, shadow:)
201
+
202
+ index += 1
203
+ end
204
+ end
205
+
206
+ def ruby_name_defined?(env, name, shadow:)
207
+ shadow ? env.local_ruby_name_defined?(name) : env.ruby_name_defined?(name)
208
+ end
209
+
210
+ def reserved_generated_name?(name)
211
+ name.start_with?('kap_', '__kap_')
144
212
  end
145
213
 
146
214
  def runtime_helper(name)
@@ -156,11 +224,18 @@ module Kapusta
156
224
  "#{runtime_helper(name)}(#{rendered_args.join(', ')})"
157
225
  end
158
226
 
227
+ def method_binding?(binding)
228
+ binding.is_a?(Env::MethodBinding)
229
+ end
230
+
231
+ def binding_value_code(binding)
232
+ method_binding?(binding) ? "method(#{binding.ruby_name.to_sym.inspect})" : binding
233
+ end
234
+
159
235
  def parse_counted_for_bindings(bindings, env, current_scope)
160
236
  name_sym = bindings[0]
161
- ruby_name = temp(sanitize_local(name_sym.name))
162
237
  loop_env = env.child
163
- loop_env.define(name_sym.name, ruby_name)
238
+ ruby_name = define_local(loop_env, name_sym.name)
164
239
  start_code = emit_expr(bindings[1], env, current_scope)
165
240
  finish_code = emit_expr(bindings[2], env, current_scope)
166
241
  step_code = '1'
@@ -184,24 +259,25 @@ module Kapusta
184
259
  step_var = temp('step')
185
260
  cmp_var = temp('cmp')
186
261
  until_code = until_form ? "break if #{emit_expr(until_form, loop_env, current_scope)}" : nil
187
- <<~RUBY.chomp
188
- #{ruby_name} = #{start_code}
189
- #{finish_var} = #{finish_code}
190
- #{step_var} = #{step_code}
191
- #{cmp_var} = #{step_var} >= 0 ? :<= : :>=
192
- while #{ruby_name}.public_send(#{cmp_var}, #{finish_var})
193
- #{until_code}
194
- #{indent(body_code)}
195
- #{ruby_name} += #{step_var}
196
- end
197
- RUBY
262
+ body = [until_code, body_code, "#{ruby_name} += #{step_var}"].compact.reject(&:empty?).join("\n")
263
+ [
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})",
269
+ indent(body),
270
+ 'end'
271
+ ].join("\n")
198
272
  end
199
273
 
200
274
  def sanitize_local(name)
201
275
  base = Kapusta.kebab_to_snake(name)
202
276
  base = base.gsub('?', '_q').gsub('!', '_bang')
203
277
  base = base.gsub(/[^a-zA-Z0-9_]/, '_')
204
- base = "_#{base}" if base.empty? || base.match?(/\A\d/) || self.class::RUBY_KEYWORDS.include?(base)
278
+ if base.empty? || base.match?(/\A\d/) || base.match?(/\A[A-Z]/) || self.class::RUBY_KEYWORDS.include?(base)
279
+ base = "_#{base}"
280
+ end
205
281
  base
206
282
  end
207
283
  end
@@ -74,7 +74,7 @@ module Kapusta
74
74
  end
75
75
 
76
76
  def wrap_do(forms)
77
- return nil if forms.empty?
77
+ return if forms.empty?
78
78
  return forms.first if forms.length == 1
79
79
 
80
80
  List.new([Sym.new('do'), *forms])