kapusta 0.9.0 → 0.11.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -2
  3. data/bin/check-all +17 -0
  4. data/bin/compile-examples +70 -0
  5. data/bin/fennel-parity +4 -38
  6. data/examples/account-lockout.kap +11 -0
  7. data/examples/accumulator.kap +2 -0
  8. data/examples/bank-account.kap +2 -0
  9. data/examples/bst-iterator.kap +52 -0
  10. data/examples/circle.kap +18 -0
  11. data/examples/convert-temperature.kap +14 -0
  12. data/examples/count-effects.kap +13 -0
  13. data/examples/counter.kap +2 -0
  14. data/examples/falling-drops.kap +12 -0
  15. data/examples/fennel-parity-examples.txt +40 -0
  16. data/examples/hit-counter.kap +2 -0
  17. data/examples/max-achievable.kap +8 -0
  18. data/examples/module-header.kap +4 -2
  19. data/examples/mruby-runtime-examples.txt +92 -0
  20. data/examples/number-of-1-bits.kap +13 -0
  21. data/examples/number-of-steps.kap +15 -0
  22. data/examples/parking-system.kap +2 -0
  23. data/examples/recent-counter.kap +17 -0
  24. data/examples/scopes.kap +2 -0
  25. data/examples/signal-harvest.kap +16 -0
  26. data/examples/stack.kap +2 -0
  27. data/examples/two-sum-hash.kap +11 -14
  28. data/examples/underscore-patterns.kap +1 -1
  29. data/examples/valid-parentheses-1.kap +2 -0
  30. data/lib/kapusta/cli.rb +11 -6
  31. data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
  32. data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
  33. data/lib/kapusta/compiler/emitter/expressions.rb +1 -0
  34. data/lib/kapusta/compiler/emitter/interop.rb +4 -2
  35. data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
  36. data/lib/kapusta/compiler/emitter/support.rb +63 -24
  37. data/lib/kapusta/compiler/emitter.rb +2 -1
  38. data/lib/kapusta/compiler/normalizer.rb +6 -0
  39. data/lib/kapusta/compiler.rb +15 -6
  40. data/lib/kapusta/errors.rb +6 -0
  41. data/lib/kapusta/formatter.rb +1 -2
  42. data/lib/kapusta/lsp/definition.rb +17 -0
  43. data/lib/kapusta/lsp/rename.rb +3 -1
  44. data/lib/kapusta/lsp/scope_walker.rb +40 -11
  45. data/lib/kapusta/lsp.rb +2 -1
  46. data/lib/kapusta/version.rb +1 -1
  47. data/lib/kapusta.rb +2 -2
  48. data/spec/cli_spec.rb +35 -0
  49. data/spec/examples_errors_spec.rb +20 -0
  50. data/spec/examples_spec.rb +136 -0
  51. data/spec/lsp_spec.rb +71 -3
  52. data/spec/spec_helper.rb +9 -0
  53. metadata +14 -1
data/lib/kapusta/cli.rb CHANGED
@@ -5,7 +5,7 @@ require 'optparse'
5
5
 
6
6
  module Kapusta
7
7
  class CLI
8
- Options = Struct.new(:compile, :help, :version, keyword_init: true)
8
+ Options = Struct.new(:compile, :target, :help, :version, keyword_init: true)
9
9
 
10
10
  def self.start(argv = ARGV)
11
11
  args = argv.dup
@@ -21,8 +21,10 @@ module Kapusta
21
21
  return
22
22
  end
23
23
 
24
+ raise Kapusta::Error, Kapusta::Errors.format(:target_requires_compile) if options.target && !options.compile
25
+
24
26
  if options.compile
25
- compile_file(args)
27
+ compile_file(args, target: options.target)
26
28
  else
27
29
  run_file(args)
28
30
  end
@@ -32,11 +34,14 @@ module Kapusta
32
34
  end
33
35
 
34
36
  def self.parse_options(args)
35
- options = Options.new(compile: false, help: false, version: false)
37
+ options = Options.new(compile: false, target: nil, help: false, version: false)
36
38
 
37
39
  OptionParser.new do |parser|
38
40
  parser.banner = usage
39
41
  parser.on('-c', '--compile', 'Compile .kap to Ruby') { options.compile = true }
42
+ parser.on('--target=TARGET', 'Compile for mruby') do |target|
43
+ options.target = Kapusta::Compiler.normalize_target(target)
44
+ end
40
45
  parser.on('-h', '--help', 'Show this help') { options.help = true }
41
46
  parser.on('-v', '--version', 'Show version') { options.version = true }
42
47
  end.order!(args)
@@ -44,12 +49,12 @@ module Kapusta
44
49
  options
45
50
  end
46
51
 
47
- def self.compile_file(args)
52
+ def self.compile_file(args, target:)
48
53
  path = args.shift
49
54
  abort usage unless path
50
55
  abort usage unless args.empty?
51
56
 
52
- $stdout.write(Kapusta.compile(File.read(path), path:))
57
+ $stdout.write(Kapusta.compile(File.read(path), path:, target:))
53
58
  end
54
59
 
55
60
  def self.run_file(args)
@@ -64,7 +69,7 @@ module Kapusta
64
69
  end
65
70
 
66
71
  def self.usage
67
- 'usage: kapusta [--compile|-c] <file.kap> | kapusta <file.kap> [args...]'
72
+ 'usage: kapusta [--compile|-c] [--target=mruby] <file.kap> | kapusta <file.kap> [args...]'
68
73
  end
69
74
  end
70
75
  end
@@ -181,6 +181,14 @@ module Kapusta
181
181
 
182
182
  def emit_toplevel_method_bridge(ruby_name)
183
183
  method_name = ruby_name.to_sym.inspect
184
+ if mruby_target?
185
+ return [
186
+ "define_singleton_method(#{method_name}) do |*args|",
187
+ indent("Object.instance_method(#{method_name}).bind(self).call(*args)"),
188
+ 'end'
189
+ ].join("\n")
190
+ end
191
+
184
192
  "define_singleton_method(#{method_name}, Object.instance_method(#{method_name}).bind(self))"
185
193
  end
186
194
 
@@ -229,8 +237,14 @@ module Kapusta
229
237
 
230
238
  binding = env.lookup_if_defined(name)
231
239
  return false if binding.nil?
240
+ return false if method_binding?(binding)
241
+ return false if constant_binding?(binding)
242
+
243
+ true
244
+ end
232
245
 
233
- !method_binding?(binding)
246
+ def constant_binding?(binding)
247
+ binding.is_a?(String) && binding.match?(/\A[A-Z][A-Z0-9_]*\z/)
234
248
  end
235
249
 
236
250
  def emit_let(args, env, current_scope)
@@ -277,7 +291,7 @@ module Kapusta
277
291
  chunks.reject(&:empty?).join("\n")
278
292
  end
279
293
 
280
- def emit_local_form(form, env, current_scope)
294
+ def emit_local_form(form, env, current_scope, allow_constant: false)
281
295
  emit_error!(:local_arity, form: form.head.name) unless form.items.length == 3
282
296
 
283
297
  target = form.items[1]
@@ -285,6 +299,12 @@ module Kapusta
285
299
 
286
300
  if target.is_a?(Sym)
287
301
  validate_binding_symbol!(target)
302
+ if allow_constant && form.head.name == 'local' && (constant_name = constant_name_for(target.name))
303
+ env.define(target.name, constant_name)
304
+ mark_mutability(env, target.name, mutable: false)
305
+ return ["#{constant_name} = #{value_code}\nnil", env]
306
+ end
307
+
288
308
  ruby_name = define_local(env, target.name)
289
309
  mark_mutability(env, target.name, mutable: form.head.name == 'var')
290
310
  ["#{ruby_name} = #{value_code}\nnil", env]
@@ -294,6 +314,11 @@ module Kapusta
294
314
  end
295
315
  end
296
316
 
317
+ def constant_name_for(source_name)
318
+ candidate = source_name.tr('-', '_').upcase
319
+ candidate if candidate.match?(/\A[A-Z][A-Z0-9_]*\z/)
320
+ end
321
+
297
322
  def check_destructure_value!(pattern, value_form)
298
323
  return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
299
324
 
@@ -57,6 +57,25 @@ module Kapusta
57
57
  end
58
58
 
59
59
  def emit_case(args, env, current_scope, mode)
60
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
61
+ return body unless value_var
62
+
63
+ [
64
+ '(-> do',
65
+ indent("#{value_var} = #{value_code}"),
66
+ indent(body),
67
+ 'end).call'
68
+ ].join("\n")
69
+ end
70
+
71
+ def emit_case_statement(args, env, current_scope, mode)
72
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
73
+ return body unless value_var
74
+
75
+ "#{value_var} = #{value_code}\n#{body}"
76
+ end
77
+
78
+ def build_case_parts(args, env, current_scope, mode)
60
79
  emit_error!(:case_no_subject) if args.empty?
61
80
 
62
81
  clauses = args[1..]
@@ -65,20 +84,22 @@ module Kapusta
65
84
 
66
85
  value_code = emit_expr(args[0], env, current_scope)
67
86
  if simple_case_subject?(args[0]) && simple_expression?(value_code)
68
- body = try_emit_native_case(value_code, clauses, env, current_scope, mode)
87
+ body = emit_case_body(value_code, clauses, env, current_scope, mode)
69
88
  emit_error!(:case_unsupported) unless body
70
- return body
89
+ return [value_code, nil, body]
71
90
  end
72
91
 
73
92
  value_var = temp('case_value')
74
- body = try_emit_native_case(value_var, clauses, env, current_scope, mode)
93
+ body = emit_case_body(value_var, clauses, env, current_scope, mode)
75
94
  emit_error!(:case_unsupported) unless body
76
- [
77
- '(-> do',
78
- indent("#{value_var} = #{value_code}"),
79
- indent(body),
80
- 'end).call'
81
- ].join("\n")
95
+ [value_code, value_var, body]
96
+ end
97
+
98
+ def emit_case_body(value_var, clauses, env, current_scope, mode)
99
+ return try_emit_compat_case(value_var, clauses, env, current_scope, mode) if mruby_target?
100
+
101
+ try_emit_native_case(value_var, clauses, env, current_scope, mode) ||
102
+ try_emit_compat_case(value_var, clauses, env, current_scope, mode)
82
103
  end
83
104
 
84
105
  def simple_case_subject?(form)
@@ -113,11 +134,6 @@ module Kapusta
113
134
  ["case #{value_var}", *arms, 'end'].join("\n")
114
135
  end
115
136
 
116
- def wildcard_last?(clauses)
117
- last_pattern = clauses[-2]
118
- last_pattern.is_a?(Sym) && last_pattern.name == '_'
119
- end
120
-
121
137
  def try_native_arm(pattern, body, where_guards, env, current_scope, mode)
122
138
  allow_pins = !where_guards.empty? && mode == :case
123
139
  plan = native_pattern_plan(pattern, env, mode:, allow_pins:)
@@ -132,6 +148,73 @@ module Kapusta
132
148
  ["in #{plan[:pattern]}#{guard_clause}", indent(body_code)].join("\n")
133
149
  end
134
150
 
151
+ def try_emit_compat_case(value_var, clauses, env, current_scope, mode)
152
+ arms = []
153
+ i = 0
154
+ while i < clauses.length
155
+ pattern = clauses[i]
156
+ body = clauses[i + 1]
157
+ inner, where_guards = if where_pattern?(pattern)
158
+ [pattern.items[1], pattern.items[2..]]
159
+ else
160
+ [pattern, []]
161
+ end
162
+ sub_patterns = or_pattern?(inner) ? inner.items[1..] : [inner]
163
+ sub_arms = sub_patterns.map do |sub|
164
+ try_compat_arm(sub, body, where_guards, value_var, env, current_scope, mode)
165
+ end
166
+ return if sub_arms.any?(&:nil?)
167
+
168
+ arms.concat(sub_arms)
169
+ i += 2
170
+ end
171
+ emit_compat_case_lines(arms)
172
+ end
173
+
174
+ def wildcard_last?(clauses)
175
+ last_pattern = clauses[-2]
176
+ last_pattern.is_a?(Sym) && last_pattern.name == '_'
177
+ end
178
+
179
+ def try_compat_arm(pattern, body, where_guards, value_var, env, current_scope, mode)
180
+ allow_pins = !where_guards.empty? && mode == :case
181
+ arm_env = env.child
182
+ plan = compat_pattern_plan(pattern, value_var, env, arm_env, mode:, allow_pins:)
183
+ return unless plan
184
+
185
+ where_guard_codes = where_guards.map { |g| emit_expr(g, arm_env, current_scope) }
186
+ if where_guard_codes.empty?
187
+ guard_codes = plan[:conditions]
188
+ prelude = plan[:prelude]
189
+ else
190
+ prelude_guards = plan[:prelude].map { |line| "begin #{line}; true end" }
191
+ guard_codes = plan[:conditions] + prelude_guards + where_guard_codes
192
+ prelude = []
193
+ end
194
+ body_code = emit_expr(body, arm_env, current_scope)
195
+ [guard_codes, prelude, body_code]
196
+ end
197
+
198
+ def emit_compat_case_lines(arms)
199
+ last_idx = arms.length - 1
200
+ has_unconditional = arms.any? { |conditions, _prelude, _body_code| conditions.empty? }
201
+ lines = ['case']
202
+ arms.each_with_index do |(conditions, prelude, body_code), idx|
203
+ lines << if conditions.empty?
204
+ idx == last_idx ? 'else' : 'when true'
205
+ else
206
+ "when #{conditions.join(' && ')}"
207
+ end
208
+ lines << indent([*prelude, body_code].join("\n"))
209
+ end
210
+ unless has_unconditional
211
+ lines << 'else'
212
+ lines << indent('nil')
213
+ end
214
+ lines << 'end'
215
+ lines.join("\n")
216
+ end
217
+
135
218
  def emit_while(args, env, current_scope)
136
219
  <<~RUBY.chomp
137
220
  (-> do
@@ -83,6 +83,7 @@ module Kapusta
83
83
  when 'require' then emit_require(args[0], env, current_scope)
84
84
  when 'module' then emit_module_expr(args, env)
85
85
  when 'class' then emit_class_expr(args, env)
86
+ when 'end' then emit_error!(:end_outside_header)
86
87
  when 'try' then emit_try(args, env, current_scope)
87
88
  when 'raise' then emit_raise(args, env, current_scope)
88
89
  when 'ivar' then "@#{Kapusta.kebab_to_snake(args[0].name)}"
@@ -102,7 +102,7 @@ module Kapusta
102
102
  segments = constant_segments(name_sym)
103
103
  return unless segments
104
104
 
105
- [build_nested_module(segments, body), segments.join('::')].join("\n")
105
+ build_nested_module(segments, body)
106
106
  end
107
107
 
108
108
  def emit_class_wrapper(name_sym, supers, env, body)
@@ -118,7 +118,7 @@ module Kapusta
118
118
  return unless segments
119
119
 
120
120
  super_code = class_super_code(supers, env)
121
- [build_nested_class(segments, super_code, body), segments.join('::')].join("\n")
121
+ build_nested_class(segments, super_code, body)
122
122
  end
123
123
 
124
124
  def constant_segments(name_sym)
@@ -300,6 +300,8 @@ module Kapusta
300
300
  def emit_bound_call(binding, args, env, current_scope)
301
301
  return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
302
302
 
303
+ emit_error!(:cannot_call_constant, name: binding) if constant_binding?(binding)
304
+
303
305
  emit_callable_call(binding, args, env, current_scope)
304
306
  end
305
307
 
@@ -161,6 +161,131 @@ module Kapusta
161
161
 
162
162
  class PatternNotTranslatable < StandardError; end
163
163
 
164
+ def compat_pattern_plan(pattern, value_code, env, arm_env, mode:, allow_pins:)
165
+ state = { bound_names: {}, conditions: [], prelude: [] }
166
+ compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
167
+ { conditions: state[:conditions], prelude: state[:prelude] }
168
+ rescue PatternNotTranslatable
169
+ nil
170
+ end
171
+
172
+ def compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
173
+ case pattern
174
+ when Sym
175
+ compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
176
+ when Vec
177
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
178
+ when HashLit
179
+ compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
180
+ when List
181
+ if pin_pattern?(pattern)
182
+ compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
183
+ elsif or_pattern?(pattern)
184
+ raise PatternNotTranslatable
185
+ else
186
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
187
+ end
188
+ when nil
189
+ state[:conditions] << "#{value_code}.nil?"
190
+ when Symbol, String, Numeric, true, false
191
+ state[:conditions] << "#{value_code} == #{pattern.inspect}"
192
+ else
193
+ raise PatternNotTranslatable
194
+ end
195
+ end
196
+
197
+ def compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
198
+ name = pattern.name
199
+ return if name == '_'
200
+
201
+ if nil_allowing_pattern_name?(name)
202
+ raise PatternNotTranslatable if state[:bound_names].key?(name)
203
+
204
+ ruby = define_local(arm_env, name)
205
+ state[:bound_names][name] = true
206
+ state[:prelude] << "#{ruby} = #{value_code}"
207
+ return
208
+ end
209
+
210
+ binding = mode == :match ? env.lookup_if_defined(name) : nil
211
+ if state[:bound_names].key?(name)
212
+ raise PatternNotTranslatable
213
+ elsif binding
214
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
215
+ else
216
+ ruby = define_local(arm_env, name)
217
+ state[:bound_names][name] = true
218
+ state[:conditions] << "(#{ruby} = #{value_code}) != nil"
219
+ end
220
+ end
221
+
222
+ def compile_compat_sequence(items, value_code, env, arm_env, mode:, allow_pins:, state:)
223
+ min_length = compat_sequence_min_length(items)
224
+ state[:conditions] << "#{value_code}.is_a?(Array)"
225
+ state[:conditions] << "#{value_code}.length >= #{min_length}"
226
+
227
+ index = 0
228
+ i = 0
229
+ while i < items.length
230
+ if rest_pattern_marker?(items, i)
231
+ sub = items[i + 1]
232
+ raise PatternNotTranslatable unless sub.is_a?(Sym)
233
+
234
+ unless sub.name == '_'
235
+ ruby = define_local(arm_env, sub.name)
236
+ state[:prelude] << "#{ruby} = #{value_code}[#{index}..]"
237
+ end
238
+ i += 2
239
+ else
240
+ compile_compat_pattern(items[i], "#{value_code}[#{index}]", env, arm_env,
241
+ mode:, allow_pins:, state:)
242
+ index += 1
243
+ i += 1
244
+ end
245
+ end
246
+ end
247
+
248
+ def compat_sequence_min_length(items)
249
+ count = 0
250
+ i = 0
251
+ while i < items.length
252
+ if rest_pattern_marker?(items, i)
253
+ i += 2
254
+ else
255
+ count += 1
256
+ i += 1
257
+ end
258
+ end
259
+ count
260
+ end
261
+
262
+ def compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
263
+ state[:conditions] << "#{value_code}.is_a?(Hash)"
264
+ pattern.pairs.each do |key, value|
265
+ lookup = "#{value_code}[#{compile_compat_hash_key(key)}]"
266
+ compile_compat_pattern(value, lookup, env, arm_env, mode:, allow_pins:, state:)
267
+ end
268
+ end
269
+
270
+ def compile_compat_hash_key(key)
271
+ case key
272
+ when Symbol, String, Numeric, true, false, nil then key.inspect
273
+ else raise PatternNotTranslatable
274
+ end
275
+ end
276
+
277
+ def compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
278
+ raise PatternNotTranslatable unless allow_pins && mode == :case
279
+
280
+ name_sym = pattern.items[1]
281
+ raise PatternNotTranslatable unless name_sym.is_a?(Sym)
282
+
283
+ binding = env.lookup_if_defined(name_sym.name)
284
+ raise PatternNotTranslatable unless binding
285
+
286
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
287
+ end
288
+
164
289
  def native_pattern_plan(pattern, env, mode:, allow_pins:)
165
290
  state = { bound_names: {}, binding_names: [], guards: [] }
166
291
  ruby_pattern = compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
@@ -25,27 +25,61 @@ module Kapusta
25
25
  (@form_stack ||= []).last
26
26
  end
27
27
 
28
+ def mruby_target?
29
+ @target == :mruby
30
+ end
31
+
28
32
  def positionable?(form)
29
33
  form.respond_to?(:line) && form.respond_to?(:column)
30
34
  end
31
35
 
32
36
  def emit_forms_with_headers(forms, env, current_scope, result: true)
33
- i = 0
37
+ _ = result
38
+ code, _next = emit_form_run(forms, 0, env, current_scope)
39
+ code
40
+ end
41
+
42
+ def emit_form_run(forms, start, env, current_scope, header_form: nil)
43
+ i = start
34
44
  codes = []
35
45
  while i < forms.length
36
46
  form = forms[i]
47
+ if end_form?(form)
48
+ validate_end_form!(form)
49
+ with_current_form(form) { emit_error!(:end_outside_header) } unless header_form
50
+ return [codes.join("\n"), i + 1]
51
+ end
52
+
37
53
  if bodyless_header?(form)
38
- codes << emit_bodyless_header(form, forms[(i + 1)..], env, current_scope)
39
- break
40
- else
41
- code, env = emit_form_in_sequence(form, env, current_scope,
42
- allow_method_definitions: true,
43
- result_needed: result && i == forms.length - 1)
44
- codes << code
45
- i += 1
54
+ header_code, i = emit_bodyless_header(form, forms, i + 1, env)
55
+ codes << header_code
56
+ next
46
57
  end
58
+
59
+ code, env = emit_form_in_sequence(form, env, current_scope,
60
+ allow_method_definitions: true,
61
+ result_needed: false)
62
+ codes << code
63
+ i += 1
47
64
  end
48
- codes.join("\n")
65
+ with_current_form(header_form) { emit_error!(:unclosed_header) } if header_form
66
+ [codes.join("\n"), i]
67
+ end
68
+
69
+ def end_form?(form)
70
+ form.is_a?(List) && !form.empty? && form.head.is_a?(Sym) && form.head.name == 'end'
71
+ end
72
+
73
+ def validate_end_form!(form)
74
+ with_current_form(form) { emit_error!(:end_with_args) } if form.items.length > 1
75
+ end
76
+
77
+ def validate_header_name!(form, head)
78
+ name_sym = head == 'module' ? form.items[1] : split_class_args(form.items[1..])[0]
79
+ return if constant_segments(name_sym)
80
+
81
+ code = head == 'module' ? :invalid_module_name : :invalid_class_name
82
+ emit_error!(code, name: name_sym.respond_to?(:name) ? name_sym.name : name_sym.inspect)
49
83
  end
50
84
 
51
85
  def bodyless_header?(form)
@@ -65,23 +99,25 @@ module Kapusta
65
99
  end
66
100
  end
67
101
 
68
- def emit_bodyless_header(form, remaining_forms, env, _current_scope)
102
+ def emit_bodyless_header(form, forms, body_start, env)
69
103
  head = form.head.name
70
- name_sym = form.items[1]
71
-
104
+ validate_header_name!(form, head)
72
105
  if head == 'module'
106
+ name_sym = form.items[1]
73
107
  inner = form.items[2..] || []
74
- body =
75
- if inner.length == 1 && bodyless_header?(inner[0])
76
- emit_bodyless_header(inner[0], remaining_forms, env, :module)
77
- else
78
- emit_forms_with_headers(remaining_forms, env, :module, result: false)
79
- end
80
- emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body)
108
+ if inner.length == 1 && bodyless_header?(inner[0])
109
+ inner_code, next_i = emit_bodyless_header(inner[0], forms, body_start, env)
110
+ [emit_direct_module_header(name_sym, inner_code) || emit_module_wrapper(name_sym, inner_code), next_i]
111
+ else
112
+ body, next_i = emit_form_run(forms, body_start, env, :module, header_form: form)
113
+ [emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body), next_i]
114
+ end
81
115
  else
82
116
  name_sym, supers, = split_class_args(form.items[1..])
83
- body = emit_forms_with_headers(remaining_forms, env, :class, result: false)
84
- emit_direct_class_header(name_sym, supers, body, env) || emit_class_wrapper(name_sym, supers, env, body)
117
+ body, next_i = emit_form_run(forms, body_start, env, :class, header_form: form)
118
+ code = emit_direct_class_header(name_sym, supers, body, env) ||
119
+ emit_class_wrapper(name_sym, supers, env, body)
120
+ [code, next_i]
85
121
  end
86
122
  end
87
123
 
@@ -97,7 +133,8 @@ module Kapusta
97
133
  if named_function_form?(form)
98
134
  emit_named_fn_assignment(form, env, current_scope)
99
135
  elsif local_form?(form)
100
- code, env = emit_local_form(form, env, current_scope)
136
+ code, env = emit_local_form(form, env, current_scope,
137
+ allow_constant: allow_method_definitions)
101
138
  code = code.delete_suffix("\nnil") unless result_needed
102
139
  [code, env]
103
140
  elsif do_form?(form)
@@ -136,7 +173,7 @@ module Kapusta
136
173
  def sequence_statement_form?(form)
137
174
  return false unless form.is_a?(List) && form.head.is_a?(Sym)
138
175
 
139
- %w[let while for each].include?(form.head.name)
176
+ %w[let while for each case match].include?(form.head.name)
140
177
  end
141
178
 
142
179
  def emit_sequence_statement_form(form, env, current_scope, result_needed:)
@@ -152,6 +189,8 @@ module Kapusta
152
189
  return ["#{code}\nnil", env] if result_needed && current_scope != :toplevel
153
190
 
154
191
  return [code, env]
192
+ when 'case', 'match'
193
+ return [emit_case_statement(form.rest, env, current_scope, form.head.name.to_sym), env] unless result_needed
155
194
  end
156
195
 
157
196
  [emit_expr(form, env, current_scope), env]
@@ -24,8 +24,9 @@ module Kapusta
24
24
  include EmitterModules::Interop
25
25
  include EmitterModules::Patterns
26
26
 
27
- def initialize(path:)
27
+ def initialize(path:, target: nil)
28
28
  @path = path
29
+ @target = target
29
30
  @temp_index = 0
30
31
  end
31
32
 
@@ -40,6 +40,12 @@ module Kapusta
40
40
  return inherit_position(List.new(items), list) unless head.is_a?(Sym)
41
41
 
42
42
  case head.name
43
+ when 'defn'
44
+ name_sym = items[1]
45
+ raise compiler_error(:fn_no_params, list) unless name_sym.is_a?(Sym)
46
+
47
+ self_name = inherit_position(Sym.new("self.#{name_sym.name}"), name_sym)
48
+ inherit_position(List.new([inherit_position(Sym.new('fn'), head), self_name, *items[2..]]), list)
43
49
  when 'when'
44
50
  raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
45
51
 
@@ -10,7 +10,7 @@ module Kapusta
10
10
  module Compiler
11
11
  class Error < Kapusta::Error; end
12
12
  CORE_SPECIAL_FORMS = %w[
13
- fn lambda λ let local var global set if when unless case match
13
+ fn defn lambda λ let local var global set if when unless case match
14
14
  while for each do values
15
15
  -> ->> -?> -?>> doto
16
16
  icollect collect fcollect accumulate faccumulate
@@ -19,7 +19,7 @@ module Kapusta
19
19
  ..
20
20
  length
21
21
  require
22
- module class
22
+ module class end
23
23
  try catch finally
24
24
  raise
25
25
  ivar cvar gvar
@@ -34,17 +34,17 @@ module Kapusta
34
34
  ].freeze
35
35
  SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
36
36
 
37
- def self.compile(source, path: '(kapusta)')
37
+ def self.compile(source, path: '(kapusta)', target: nil)
38
38
  forms = Reader.read_all(source)
39
39
  expanded = MacroExpander.new(path:).expand_all(forms)
40
- compile_forms(expanded, path:)
40
+ compile_forms(expanded, path:, target:)
41
41
  rescue Kapusta::Error => e
42
42
  raise e.with_defaults(path:)
43
43
  end
44
44
 
45
- def self.compile_forms(forms, path: '(kapusta)')
45
+ def self.compile_forms(forms, path: '(kapusta)', target: nil)
46
46
  normalized = Normalizer.new.normalize_all(forms)
47
- Emitter.new(path:).emit_file(normalized)
47
+ Emitter.new(path:, target: normalize_target(target)).emit_file(normalized)
48
48
  rescue Kapusta::Error => e
49
49
  raise e.with_defaults(path:)
50
50
  end
@@ -57,5 +57,14 @@ module Kapusta
57
57
  def self.run_file(path)
58
58
  run(File.read(path), path:)
59
59
  end
60
+
61
+ def self.normalize_target(target)
62
+ case target
63
+ when nil then nil
64
+ when :mruby, 'mruby' then :mruby
65
+ else
66
+ raise Error, Kapusta::Errors.format(:unknown_target, target: target.inspect)
67
+ end
68
+ end
60
69
  end
61
70
  end