kapusta 0.2.4 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef91159e2623e199fc391fa483440fa9f8ca76f273fbc31caeb0b07e891ee92c
4
- data.tar.gz: 5468818f0ad4b0509a31635d886f3374009e3e8499a197d82f99da22c53e3a12
3
+ metadata.gz: 179b8b97d775c6bff99bd3d5ac3e1df1f6ab8c17637b6e16f9d688ade7f9dd06
4
+ data.tar.gz: 899899c13c9b1d4f85216df5d3989807d874a9ec862be94d48989f851b2d8111
5
5
  SHA512:
6
- metadata.gz: 7bcc51b164c21357c2aa2ef52e4e544cba0116ca8d8b6b1c02ecb55dc62697508a3cbb22d4c56a0a7cf0da77f53d7500827ecd2debe0760fec34ebe4d992be07
7
- data.tar.gz: 40b1b709b5e083b94584424f9fb2ad7c3ec6908786ea80051173df8edaa380b9b5b76d0c485a6377f7e77bbed5799633501204d7a39acf0b919eb74197fabcf2
6
+ metadata.gz: 69247136466e119860e5fee232b0bb21fa40b9d23986f09051a4acccda476a9c42b5e3e63e1382367df6c3008876af2f6a69a9b6fd2bfc0e05e1c9e69ca6a8b0
7
+ data.tar.gz: f65709e841e372a1dcc4c9a453d2251d58fb4fffbecd51fd1f0bcfb7c0423dc51109d6533e1b85d9b39bbc82cc994dcf64bdb5aad586c2b6476858db8e320d25
data/README.md CHANGED
@@ -8,6 +8,12 @@ Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. W
8
8
 
9
9
  For more information about Kapusta, see the official Fennel documentation and tutorials.
10
10
 
11
+ ## Features
12
+
13
+ 1. Compiles to readable Ruby.
14
+ 2. Compiled `.rb` files don't depend on Kapusta. Run with plain `ruby`, or load `.kap` files at runtime via `require 'kapusta'`.
15
+ 3. Two-way Ruby interop.
16
+
11
17
  ## Usage
12
18
 
13
19
  ```
@@ -0,0 +1,9 @@
1
+ (fn manhattan [edge]
2
+ (let [{:from [x1 y1] :to [x2 y2]} edge]
3
+ (+ (: (- x1 x2) :abs) (: (- y1 y2) :abs))))
4
+
5
+ (fn total-distance [edges]
6
+ (accumulate [total 0 _ edge (ipairs edges)]
7
+ (+ total (manhattan edge))))
8
+
9
+ (print (total-distance [{:from [0 0] :to [3 4]} {:from [1 1] :to [4 5]}]))
@@ -0,0 +1,14 @@
1
+ (fn subtract-product-sum [n]
2
+ (var x n)
3
+ (var product 1)
4
+ (var sum 0)
5
+ (while (> x 0)
6
+ (let [d (% x 10)]
7
+ (set product (* product d))
8
+ (set sum (+ sum d))
9
+ (set x (: (/ x 10) :floor))))
10
+ (- product sum))
11
+
12
+ (print (subtract-product-sum 234))
13
+ (print (subtract-product-sum 4421))
14
+ (print (subtract-product-sum 1))
@@ -281,12 +281,23 @@ module Kapusta
281
281
  else
282
282
  define_local(env, target.name)
283
283
  end
284
- ["#{ruby_name} = #{value_code}", env]
284
+ [emit_assignment(ruby_name, value_code), env]
285
285
  else
286
286
  [emit_set_target(target, value_code, env, current_scope), env]
287
287
  end
288
288
  end
289
289
 
290
+ def emit_assignment(lhs, value_code)
291
+ prefix = "#{lhs} "
292
+ if value_code.start_with?(prefix) &&
293
+ (m = value_code[prefix.length..].match(/\A(\S+) (.*)\z/m)) &&
294
+ !m[1].include?('=')
295
+ "#{lhs} #{m[1]}= #{m[2]}"
296
+ else
297
+ "#{lhs} = #{value_code}"
298
+ end
299
+ end
300
+
290
301
  def emit_set_expr(args, env, current_scope)
291
302
  target = args[0]
292
303
  value_code = emit_expr(args[1], env, current_scope)
@@ -302,7 +313,7 @@ module Kapusta
302
313
  last = segments.last
303
314
  snake = Kapusta.kebab_to_snake(last)
304
315
  if direct_method_name?(last)
305
- "#{receiver}.#{snake} = #{value_code}"
316
+ emit_assignment("#{receiver}.#{snake}", value_code)
306
317
  else
307
318
  "#{receiver}.public_send(:\"#{snake}=\", #{value_code})"
308
319
  end
@@ -310,7 +321,7 @@ module Kapusta
310
321
  binding = env.lookup(target.name)
311
322
  emit_error!("cannot set method binding: #{target.name}") if method_binding?(binding)
312
323
 
313
- "#{binding} = #{value_code}"
324
+ emit_assignment(binding, value_code)
314
325
  end
315
326
  when List
316
327
  head = target.head
@@ -319,13 +330,13 @@ module Kapusta
319
330
  keys = target.items[2..].map { |item| emit_expr(item, env, current_scope) }
320
331
  receiver = simple_expression?(object_code) ? object_code : parenthesize(object_code)
321
332
  prefix = keys[0...-1].map { |k| "[#{k}]" }.join
322
- "#{receiver}#{prefix}[#{keys.last}] = #{value_code}"
333
+ emit_assignment("#{receiver}#{prefix}[#{keys.last}]", value_code)
323
334
  elsif head.is_a?(Sym) && head.name == 'ivar'
324
- "@#{Kapusta.kebab_to_snake(target.items[1].name)} = #{value_code}"
335
+ emit_assignment("@#{Kapusta.kebab_to_snake(target.items[1].name)}", value_code)
325
336
  elsif head.is_a?(Sym) && head.name == 'cvar'
326
- "@@#{Kapusta.kebab_to_snake(target.items[1].name)} = #{value_code}"
337
+ emit_assignment("@@#{Kapusta.kebab_to_snake(target.items[1].name)}", value_code)
327
338
  elsif head.is_a?(Sym) && head.name == 'gvar'
328
- "$#{global_name(target.items[1].name)} = #{value_code}"
339
+ emit_assignment("$#{global_name(target.items[1].name)}", value_code)
329
340
  else
330
341
  emit_error!("bad set target: #{target.inspect}")
331
342
  end
@@ -45,13 +45,13 @@ module Kapusta
45
45
  emit_sequence_value_assignment(acc_var, body)
46
46
  end
47
47
  end
48
- <<~RUBY.chomp
49
- (-> do
50
- #{acc_var} = #{emit_expr(bindings[1], env, current_scope)}
51
- #{iter_code}
52
- #{acc_var}
53
- end).call
54
- RUBY
48
+ [
49
+ '(-> do',
50
+ indent("#{acc_var} = #{emit_expr(bindings[1], env, current_scope)}"),
51
+ indent(iter_code),
52
+ indent(acc_var),
53
+ 'end).call'
54
+ ].join("\n")
55
55
  end
56
56
 
57
57
  def emit_faccumulate(args, env, current_scope)
@@ -73,29 +73,43 @@ module Kapusta
73
73
  current_scope:,
74
74
  body_code: accumulating_body
75
75
  )
76
- <<~RUBY.chomp
77
- (-> do
78
- #{acc_var} = #{emit_expr(bindings[1], env, current_scope)}
79
- #{indent(loop_code)}
80
- #{acc_var}
81
- end).call
82
- RUBY
76
+ [
77
+ '(-> do',
78
+ indent("#{acc_var} = #{emit_expr(bindings[1], env, current_scope)}"),
79
+ indent(loop_code),
80
+ indent(acc_var),
81
+ 'end).call'
82
+ ].join("\n")
83
83
  end
84
84
 
85
85
  def emit_hashfn(args, env, current_scope)
86
- args_var = temp('args')
87
- hash_env = env.child
88
- hash_env.define('$', "#{args_var}[0]")
89
- (1..9).each do |index|
90
- hash_env.define("$#{index}", "#{args_var}[#{index - 1}]")
86
+ if needs_explicit_args?(args[0])
87
+ args_var = temp('args')
88
+ hash_env = env.child
89
+ hash_env.define('$', "#{args_var}[0]")
90
+ (1..9).each { |i| hash_env.define("$#{i}", "#{args_var}[#{i - 1}]") }
91
+ hash_env.define('$...', args_var)
92
+ body_code = emit_expr(args[0], hash_env, current_scope)
93
+ ["->(*#{args_var}) do", indent(body_code), 'end'].join("\n")
94
+ else
95
+ hash_env = env.child
96
+ hash_env.define('$', '_1')
97
+ (1..9).each { |i| hash_env.define("$#{i}", "_#{i}") }
98
+ body_code = emit_expr(args[0], hash_env, current_scope)
99
+ ['proc do', indent(body_code), 'end'].join("\n")
91
100
  end
92
- hash_env.define('$...', args_var)
93
- body_code = emit_expr(args[0], hash_env, current_scope)
94
- <<~RUBY.chomp
95
- ->(*#{args_var}) do
96
- #{body_code}
101
+ end
102
+
103
+ def needs_explicit_args?(form)
104
+ case form
105
+ when Sym then form.name == '$...'
106
+ when List, Vec then form.items.any? { |item| needs_explicit_args?(item) }
107
+ when HashLit
108
+ form.entries.any? do |entry|
109
+ entry.is_a?(Array) ? entry.any? { |item| needs_explicit_args?(item) } : needs_explicit_args?(entry)
97
110
  end
98
- RUBY
111
+ else false
112
+ end
99
113
  end
100
114
 
101
115
  def emit_iteration(bindings_vec, env, current_scope)
@@ -108,11 +122,16 @@ module Kapusta
108
122
  when 'ipairs'
109
123
  body_env = env.child
110
124
  value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
125
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
126
+ if ignored_pattern?(binding_pats[0])
127
+ bind_code = value_bind || ''
128
+ body_code = yield(body_env)
129
+ return iteration_block("#{coll_code}.each do |#{value_var}|", bind_code, body_code)
130
+ end
111
131
  index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
112
132
  bind_code = [index_bind, value_bind].compact.join("\n")
113
133
  body_code = yield(body_env)
114
- header = "#{emit_expr(iter_expr.items[1], env, current_scope)}" \
115
- ".each_with_index do |#{value_var}, #{index_var}|"
134
+ header = "#{coll_code}.each_with_index do |#{value_var}, #{index_var}|"
116
135
  return iteration_block(header, bind_code, body_code)
117
136
  when 'pairs'
118
137
  body_env = env.child
@@ -141,6 +160,10 @@ module Kapusta
141
160
  end
142
161
  end
143
162
 
163
+ def ignored_pattern?(pattern)
164
+ pattern.is_a?(Sym) && !pattern.dotted? && pattern.name == '_'
165
+ end
166
+
144
167
  def bind_iteration_param(pattern, fallback_name, env)
145
168
  if pattern.is_a?(Sym) && !pattern.dotted?
146
169
  ruby_name = pattern.name == '_' ? '_' : define_local(env, pattern.name)
@@ -168,35 +191,36 @@ module Kapusta
168
191
  end
169
192
 
170
193
  def emit_collection_result(result_var, initial_code, iter_code)
171
- <<~RUBY.chomp
172
- (-> do
173
- #{result_var} = #{initial_code}
174
- #{indent(iter_code)}
175
- #{result_var}
176
- end).call
177
- RUBY
194
+ [
195
+ '(-> do',
196
+ indent("#{result_var} = #{initial_code}"),
197
+ indent(iter_code),
198
+ indent(result_var),
199
+ 'end).call'
200
+ ].join("\n")
178
201
  end
179
202
 
180
203
  def emit_array_collection_step(result_var, body_code)
181
204
  value_var = temp('value')
182
- <<~RUBY.chomp
183
- #{emit_sequence_value_assignment(value_var, body_code)}
184
- #{result_var} << #{value_var} unless #{value_var}.nil?
185
- RUBY
205
+ [
206
+ emit_sequence_value_assignment(value_var, body_code),
207
+ "#{result_var} << #{value_var} unless #{value_var}.nil?"
208
+ ].join("\n")
186
209
  end
187
210
 
188
211
  def emit_hash_collection_step(result_var, body_code)
189
212
  pair_var = temp('pair')
190
- <<~RUBY.chomp
191
- #{emit_sequence_value_assignment(pair_var, body_code)}
192
- if #{pair_var}.is_a?(Array) && #{pair_var}.length == 2 && !#{pair_var}[0].nil? && !#{pair_var}[1].nil?
193
- #{result_var}[#{pair_var}[0]] = #{pair_var}[1]
194
- end
195
- RUBY
213
+ [
214
+ emit_sequence_value_assignment(pair_var, body_code),
215
+ "if #{pair_var}.is_a?(Array) && #{pair_var}.length == 2 && " \
216
+ "!#{pair_var}[0].nil? && !#{pair_var}[1].nil?",
217
+ indent("#{result_var}[#{pair_var}[0]] = #{pair_var}[1]"),
218
+ 'end'
219
+ ].join("\n")
196
220
  end
197
221
 
198
222
  def emit_sequence_value_assignment(target_var, body_code)
199
- return "#{target_var} = #{body_code}" unless body_code.include?("\n")
223
+ return emit_assignment(target_var, body_code) unless body_code.include?("\n")
200
224
 
201
225
  ["#{target_var} = begin", indent(body_code), 'end'].join("\n")
202
226
  end
@@ -16,6 +16,8 @@ module Kapusta
16
16
 
17
17
  cond = emit_expr(args[0], env, current_scope)
18
18
  truthy = emit_if_branch(args[1], env, current_scope)
19
+ return "#{truthy} if #{cond}" if args.length == 2 && !truthy.include?("\n") && !cond.include?("\n")
20
+
19
21
  lines = ["if #{cond}", indent(truthy)]
20
22
  append_else_lines(lines, args[2..], env, current_scope)
21
23
  lines << 'end'
@@ -57,81 +59,52 @@ module Kapusta
57
59
 
58
60
  def emit_case(args, env, current_scope, mode)
59
61
  value_var = temp('case_value')
60
- body = build_case_clauses(value_var, args[1..], env, current_scope, mode)
61
- <<~RUBY.chomp
62
- (-> do
63
- #{value_var} = #{emit_expr(args[0], env, current_scope)}
64
- #{body}
65
- end).call
66
- RUBY
62
+ body = try_emit_native_case(value_var, args[1..], env, current_scope, mode)
63
+ emit_error!('case/match clauses use patterns this compiler cannot translate') unless body
64
+ [
65
+ '(-> do',
66
+ indent("#{value_var} = #{emit_expr(args[0], env, current_scope)}"),
67
+ indent(body),
68
+ 'end).call'
69
+ ].join("\n")
67
70
  end
68
71
 
69
- def build_case_clauses(value_var, clauses, env, current_scope, mode)
70
- return 'nil' if clauses.empty?
71
-
72
- pattern = clauses[0]
73
- body = clauses[1]
74
- else_code = build_case_clauses(value_var, clauses[2..], env, current_scope, mode)
75
- emit_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
76
- end
72
+ def try_emit_native_case(value_var, clauses, env, current_scope, mode)
73
+ arms = []
74
+ i = 0
75
+ while i < clauses.length
76
+ pattern = clauses[i]
77
+ body = clauses[i + 1]
78
+ inner, where_guards = if where_pattern?(pattern)
79
+ [pattern.items[1], pattern.items[2..]]
80
+ else
81
+ [pattern, []]
82
+ end
83
+ sub_patterns = or_pattern?(inner) ? inner.items[1..] : [inner]
84
+ sub_arms = sub_patterns.map do |sub|
85
+ try_native_arm(sub, body, where_guards, env, current_scope, mode)
86
+ end
87
+ return if sub_arms.any?(&:nil?)
77
88
 
78
- def emit_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
79
- if where_pattern?(pattern)
80
- emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
81
- else
82
- emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
89
+ arms.concat(sub_arms)
90
+ i += 2
83
91
  end
92
+ arms << ['else', indent('nil')].join("\n")
93
+ ["case #{value_var}", *arms, 'end'].join("\n")
84
94
  end
85
95
 
86
- def emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
87
- match_var = temp('match')
88
- bindings_var = temp('bindings')
89
- plan = pattern_match_plan(pattern, env, mode:, allow_pins: false)
90
- arm_env = env.child
91
- assign_code, arm_env = emit_bindings_from_match(plan[:bindings], bindings_var, arm_env)
92
- body_code = emit_expr(body, arm_env, current_scope)
93
- arm_body = [assign_code, body_code].reject(&:empty?).join("\n")
94
- <<~RUBY.chomp
95
- #{match_var} = #{runtime_call(:match_pattern, plan[:pattern], value_var)}
96
- if #{match_var}[0]
97
- #{bindings_var} = #{match_var}[1]
98
- #{arm_body}
99
- else
100
- #{indent(else_code)}
101
- end
102
- RUBY
103
- end
96
+ def try_native_arm(pattern, body, where_guards, env, current_scope, mode)
97
+ allow_pins = !where_guards.empty? && mode == :case
98
+ plan = native_pattern_plan(pattern, env, mode:, allow_pins:)
99
+ return unless plan
104
100
 
105
- def emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
106
- inner = pattern.items[1]
107
- guards = pattern.items[2..]
108
- match_var = temp('match')
109
- bindings_var = temp('bindings')
110
- plan = pattern_match_plan(inner, env, mode:, allow_pins: mode == :case)
111
101
  arm_env = env.child
112
- assign_code, arm_env = emit_bindings_from_match(plan[:bindings], bindings_var, arm_env)
113
- guard_code = emit_case_guards(guards, arm_env, current_scope)
102
+ plan[:bindings].each { |name| arm_env.define(name, sanitize_local(name)) }
103
+ guard_codes = plan[:guards] +
104
+ where_guards.map { |g| emit_expr(g, arm_env, current_scope) }
105
+ guard_clause = guard_codes.empty? ? '' : " if #{guard_codes.join(' && ')}"
114
106
  body_code = emit_expr(body, arm_env, current_scope)
115
- bindings_line = assign_code.empty? ? '' : "\n #{assign_code}"
116
- <<~RUBY.chomp
117
- #{match_var} = #{runtime_call(:match_pattern, plan[:pattern], value_var)}
118
- if #{match_var}[0]
119
- #{bindings_var} = #{match_var}[1]#{bindings_line}
120
- if #{guard_code}
121
- #{body_code}
122
- else
123
- #{indent(else_code, 2)}
124
- end
125
- else
126
- #{indent(else_code)}
127
- end
128
- RUBY
129
- end
130
-
131
- def emit_case_guards(guards, env, current_scope)
132
- return 'true' if guards.empty?
133
-
134
- guards.map { |guard| parenthesize(emit_expr(guard, env, current_scope)) }.join(' && ')
107
+ ["in #{plan[:pattern]}#{guard_clause}", indent(body_code)].join("\n")
135
108
  end
136
109
 
137
110
  def emit_while(args, env, current_scope)
@@ -17,8 +17,9 @@ module Kapusta
17
17
 
18
18
  def emit_safe_lookup(args, env, current_scope)
19
19
  object_code = emit_expr(args[0], env, current_scope)
20
- keys = args[1..].map { |arg| emit_expr(arg, env, current_scope) }.join(', ')
21
- runtime_call(:qget_path, object_code, "[#{keys}]")
20
+ keys = args[1..].map { |arg| emit_expr(arg, env, current_scope) }
21
+ receiver = simple_expression?(object_code) ? object_code : parenthesize(object_code)
22
+ keys.reduce(receiver) { |acc, key| "#{acc}&.[](#{key})" }
22
23
  end
23
24
 
24
25
  BINARY_OPERATOR_METHODS = %w[<=> ** << >> & | ^ === =~].freeze
@@ -53,35 +54,27 @@ module Kapusta
53
54
  end
54
55
 
55
56
  def emit_require(arg, env, current_scope)
56
- path_code =
57
- case arg
58
- when Sym then arg.name.inspect
59
- when Symbol then arg.to_s.inspect
60
- when String then arg.inspect
61
- else "(#{emit_expr(arg, env, current_scope)}).to_s"
62
- end
63
- if kapusta_require?(arg)
64
- return [
65
- 'unless defined?(Kapusta)',
66
- indent('require "kapusta"'),
67
- 'end',
68
- "Kapusta.require(#{path_code}, relative_to: #{@path.inspect})"
69
- ].join("\n")
57
+ literal = require_path_literal(arg)
58
+ if literal&.match?(%r{\A\.\.?/})
59
+ cleaned = literal.delete_suffix('.kap').sub(%r{\A\./}, '')
60
+ return "require_relative #{cleaned.inspect}"
70
61
  end
71
62
 
63
+ path_code =
64
+ if literal
65
+ literal.inspect
66
+ else
67
+ "(#{emit_expr(arg, env, current_scope)}).to_s"
68
+ end
72
69
  "require #{path_code}"
73
70
  end
74
71
 
75
- def kapusta_require?(arg)
76
- path =
77
- case arg
78
- when Sym then arg.name
79
- when Symbol then arg.to_s
80
- when String then arg
81
- end
82
- return false unless path
83
-
84
- path.end_with?('.kap') || path.start_with?('./', '../') || File.absolute_path?(path)
72
+ def require_path_literal(arg)
73
+ case arg
74
+ when Sym then arg.name
75
+ when Symbol then arg.to_s
76
+ when String then arg
77
+ end
85
78
  end
86
79
 
87
80
  def emit_module_expr(args, env)
@@ -97,65 +90,65 @@ module Kapusta
97
90
  end
98
91
 
99
92
  def emit_module_wrapper(name_sym, body)
100
- mod_var = temp('module')
101
- [
102
- '(-> do',
103
- indent("#{mod_var} = #{runtime_call(:ensure_module, 'self', name_sym.name.inspect)}"),
104
- indent("#{mod_var}.module_eval do"),
105
- indent(body, 2),
106
- indent('end'),
107
- indent(mod_var),
108
- 'end).call'
109
- ].join("\n")
93
+ segments = constant_segments(name_sym)
94
+ emit_error!("invalid module name: #{name_sym.name}") unless segments
95
+ inner = build_nested_module(segments, body)
96
+ ['(-> do', indent(inner), indent(segments.join('::')), 'end).call'].join("\n")
110
97
  end
111
98
 
112
99
  def emit_direct_module_header(name_sym, body)
113
- const_name = simple_constant_name(name_sym)
114
- return unless const_name
100
+ segments = constant_segments(name_sym)
101
+ return unless segments
115
102
 
116
- [
117
- "module #{const_name}",
118
- indent(body),
119
- 'end',
120
- const_name
121
- ].join("\n")
103
+ [build_nested_module(segments, body), segments.join('::')].join("\n")
122
104
  end
123
105
 
124
106
  def emit_class_wrapper(name_sym, supers, env, body)
125
- klass_var = temp('class')
126
- super_code =
127
- if supers.is_a?(Vec) && !supers.items.empty?
128
- emit_expr(supers.items.first, env, :toplevel)
129
- else
130
- 'Object'
131
- end
132
- [
133
- '(-> do',
134
- indent("#{klass_var} = #{runtime_call(:ensure_class, 'self', name_sym.name.inspect, super_code)}"),
135
- indent("#{klass_var}.class_eval do"),
136
- indent(body, 2),
137
- indent('end'),
138
- indent(klass_var),
139
- 'end).call'
140
- ].join("\n")
107
+ segments = constant_segments(name_sym)
108
+ emit_error!("invalid class name: #{name_sym.name}") unless segments
109
+ super_code = class_super_code(supers, env)
110
+ inner = build_nested_class(segments, super_code, body)
111
+ ['(-> do', indent(inner), indent(segments.join('::')), 'end).call'].join("\n")
141
112
  end
142
113
 
143
- def emit_direct_class_header(name_sym, supers, body)
144
- const_name = simple_constant_name(name_sym)
145
- return unless const_name && supers.nil?
114
+ def emit_direct_class_header(name_sym, supers, body, env)
115
+ segments = constant_segments(name_sym)
116
+ return unless segments
146
117
 
147
- [
148
- "class #{const_name}",
149
- indent(body),
150
- 'end',
151
- const_name
152
- ].join("\n")
118
+ super_code = class_super_code(supers, env)
119
+ [build_nested_class(segments, super_code, body), segments.join('::')].join("\n")
153
120
  end
154
121
 
155
- def simple_constant_name(name_sym)
156
- return unless name_sym.is_a?(Sym) && name_sym.name.match?(/\A[A-Z]\w*\z/)
122
+ def constant_segments(name_sym)
123
+ return unless name_sym.is_a?(Sym)
157
124
 
158
- name_sym.name
125
+ segments = name_sym.dotted? ? name_sym.segments : [name_sym.name]
126
+ return unless segments.all? { |s| s.match?(/\A[A-Z]\w*\z/) }
127
+
128
+ segments
129
+ end
130
+
131
+ def class_super_code(supers, env)
132
+ return unless supers.is_a?(Vec) && !supers.items.empty?
133
+
134
+ emit_expr(supers.items.first, env, :toplevel)
135
+ end
136
+
137
+ def build_nested_module(segments, body)
138
+ inner = ["module #{segments.last}", indent(body), 'end'].join("\n")
139
+ wrap_in_modules(segments[0...-1], inner)
140
+ end
141
+
142
+ def build_nested_class(segments, super_code, body)
143
+ header = super_code ? "class #{segments.last} < #{super_code}" : "class #{segments.last}"
144
+ inner = [header, indent(body), 'end'].join("\n")
145
+ wrap_in_modules(segments[0...-1], inner)
146
+ end
147
+
148
+ def wrap_in_modules(parents, inner)
149
+ parents.reverse.reduce(inner) do |acc, mod_name|
150
+ ["module #{mod_name}", indent(acc), 'end'].join("\n")
151
+ end
159
152
  end
160
153
 
161
154
  def emit_try(args, env, current_scope)