kapusta 0.11.2 → 0.13.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: 0ba2ee0fbbd336f3604e9c51014a458c0a54eefcb30939fc6f2a72ba25941fb6
4
- data.tar.gz: 687f46ff98b74d65d523404ba9350fd62921eada8e105f4fe0d9215eac036118
3
+ metadata.gz: b547075d7dcb73121e61dc17577cb8262265eaffb6ee0d74b88a74ce83c8e09a
4
+ data.tar.gz: 877f490eca3f0d4419e38b4619f77470aff18ddd8fede555f2963beb8db5994f
5
5
  SHA512:
6
- metadata.gz: 070c9c5f4435f2e825f0bdb70ceb25f3cc5eba62745cae80f9d348aa4fc886d74a5f8ed06a93a62d572adbfadfbce5e93b3e37993a5d637e68d615af6e1ecfa7
7
- data.tar.gz: fc1697cfb31821c867c468666eee7bb3cd6af985c446cba8ea35e3c50c5928750a0989ce31c250d2c1d47f79b4f338e319a17cb00abb314c7213a1e3f2eb89e1
6
+ metadata.gz: cddc220382f4c7f59788f4624f9e86f55bc58fe2b3fa6978f4e6d8bebaa870ff73be57786ecf2db94f14d6db460b3f485b6f82195e7fd21e255c7a7612ab50c2
7
+ data.tar.gz: 6c653e095b1c3d406f95e7f1659b615a2a59eaa21ef77e709cfc2dfc398d3526fb493f5054cb541acd36b203e690dbc28626e951e3e8f9d5e27366c0d5885834
data/README.md CHANGED
@@ -87,6 +87,7 @@ def ack(m, n)
87
87
  ack(m - 1, ack(m, n - 1))
88
88
  end
89
89
  end
90
+
90
91
  p ack(2, 3)
91
92
  p ack(3, 3)
92
93
  ```
@@ -103,6 +104,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
103
104
  | `string.format`, `table.insert`, etc. | use Ruby methods and stdlib instead |
104
105
  | `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
105
106
  | `(print x)` is Lua's `print` (bare) | `(print x)` is Ruby's `p` (inspect-style) |
107
+ | `(.. "x: " nil)` errors at runtime | `(.. "x: " nil)` produces `"x: "` (Ruby `nil.to_s`) |
106
108
  | `with-open`, `tail!` | not provided |
107
109
 
108
110
  Kapusta-specific additions:
@@ -0,0 +1,32 @@
1
+ (fn arrange-coins [n]
2
+ (let [rows (faccumulate [acc {:sum 0 :rows 0} i 1 n]
3
+ (let [new-sum (+ (. acc :sum) i)]
4
+ (if (<= new-sum n)
5
+ {:sum new-sum :rows i}
6
+ acc)))
7
+ used (faccumulate [acc {:sum 0 :rows 0} i 1 n]
8
+ (let [new-sum (+ (. acc :sum) i)]
9
+ (if (<= new-sum n)
10
+ {:sum new-sum :rows i}
11
+ acc)))]
12
+ [(. rows :rows) (. used :sum)]))
13
+
14
+ (let [[r u] (arrange-coins 0)]
15
+ (print r)
16
+ (print u))
17
+
18
+ (let [[r u] (arrange-coins 1)]
19
+ (print r)
20
+ (print u))
21
+
22
+ (let [[r u] (arrange-coins 5)]
23
+ (print r)
24
+ (print u))
25
+
26
+ (let [[r u] (arrange-coins 8)]
27
+ (print r)
28
+ (print u))
29
+
30
+ (let [[r u] (arrange-coins 10)]
31
+ (print r)
32
+ (print u))
@@ -0,0 +1,24 @@
1
+ (fn sign [n]
2
+ (if (> n 0) 1
3
+ (< n 0) -1
4
+ 0))
5
+
6
+ (fn array-sign [nums]
7
+ (accumulate [acc 1 _ n (ipairs nums)]
8
+ (* acc (sign n))))
9
+
10
+ (fn join [tbl sep]
11
+ (var s "")
12
+ (each [_ x (ipairs tbl)]
13
+ (if (= s "")
14
+ (set s (.. x))
15
+ (set s (.. s sep x))))
16
+ s)
17
+
18
+ (fn debug-sign [label nums]
19
+ (local pretty #(.. "[" (join $ ", ") "]"))
20
+ (.. "case[" label "] in " (pretty nums) " out " (array-sign nums)))
21
+
22
+ (print (debug-sign "mixed" [-1 -2 -3 -4 3 2 1]))
23
+ (print (debug-sign "withzero" [1 5 0 2 -3]))
24
+ (print (debug-sign "allneg" [-1 1 -1 1 -1]))
@@ -15,7 +15,7 @@
15
15
 
16
16
  (fn initialize [root]
17
17
  (set @stack [])
18
- (self.push-left root))
18
+ (push-left root))
19
19
 
20
20
  (fn push-left [node]
21
21
  (var n node)
@@ -27,7 +27,7 @@
27
27
  (fn next []
28
28
  (let [stack @stack
29
29
  node (stack.pop)]
30
- (self.push-left (node.right))
30
+ (push-left (node.right))
31
31
  (node.val)))
32
32
 
33
33
  (fn has-next? []
@@ -0,0 +1,23 @@
1
+ (fn debug-data [packet seq]
2
+ (local no-nil #(or $ "nil"))
3
+ (let [[_ packet-seq] packet]
4
+ (.. "packet[:ping, " (no-nil packet-seq) "] seq " (no-nil seq))))
5
+
6
+ (fn show-case [packet seq]
7
+ (case packet
8
+ [:ping seq] (debug-data packet seq)
9
+ _ "other"))
10
+
11
+ (fn show-match [packet seq]
12
+ (match packet
13
+ [:ping seq] (debug-data packet seq)
14
+ _ "other"))
15
+
16
+ (print (.. "case: " (show-case [:ping 42] 7)))
17
+ (print (.. "case: " (show-case [:ping 42] nil)))
18
+ (print (.. "case: " (show-case [:ping nil] nil)))
19
+ (print (.. "case: " (show-case [:ping 42] 42)))
20
+ (print (.. "match: " (show-match [:ping 42] 7)))
21
+ (print (.. "match: " (show-match [:ping 42] nil)))
22
+ (print (.. "match: " (show-match [:ping nil] nil)))
23
+ (print (.. "match: " (show-match [:ping 42] 42)))
@@ -0,0 +1,16 @@
1
+ (fn divisibility-stats [n]
2
+ (let [threes (faccumulate [acc 0 i 1 n]
3
+ (let [step (if (= 0 (% i 3)) 1 0)]
4
+ (+ acc step)))
5
+ fives (faccumulate [acc 0 i 1 n]
6
+ (let [step (if (= 0 (% i 5)) 1 0)]
7
+ (+ acc step)))]
8
+ [threes fives]))
9
+
10
+ (let [[t f] (divisibility-stats 30)]
11
+ (print t)
12
+ (print f))
13
+
14
+ (let [[t f] (divisibility-stats 100)]
15
+ (print t)
16
+ (print f))
@@ -0,0 +1,7 @@
1
+ (fn equal-sums? [a b]
2
+ (= (accumulate [s 0 _ x (ipairs a)] (+ s x))
3
+ (accumulate [s 0 _ x (ipairs b)] (+ s x))))
4
+
5
+ (print (equal-sums? [1 2 3] [3 2 1]))
6
+ (print (equal-sums? [1 2 3] [4 5 6]))
7
+ (print (equal-sums? [0] [0]))
@@ -1,11 +1,16 @@
1
1
  ackermann
2
2
  anonymous-greeter
3
+ array-sign
4
+ arrange-coins
5
+ case-vs-match
3
6
  classify-wallet
4
7
  climbing-stairs
5
8
  convert-temperature
6
9
  count-effects
7
10
  describe
8
11
  destructure
12
+ divisibility-stats
13
+ equal-sums
9
14
  even-squares
10
15
  factorial
11
16
  falling-drops
@@ -15,9 +20,9 @@ gcd
15
20
  hashfn
16
21
  leap-year
17
22
  macros-dbg
23
+ macros-import
18
24
  macros-import-helpers
19
25
  macros-import-whole
20
- macros-import
21
26
  macros-multi
22
27
  macros-swap
23
28
  macros-thrice-if
@@ -26,10 +31,12 @@ macros-when-let
26
31
  match
27
32
  max-achievable
28
33
  min-max
34
+ nested-nil-pattern
35
+ non-constant-local
29
36
  or-patterns
30
- power-of-three
31
37
  packet-router
32
38
  points
39
+ power-of-three
33
40
  primes
34
41
  safe-lookup
35
42
  shapes
@@ -3,6 +3,7 @@ accumulator
3
3
  ackermann
4
4
  anagram
5
5
  anonymous-greeter
6
+ arrange-coins
6
7
  bank-account
7
8
  baseball-game
8
9
  best-time-to-buy-sell-stock
@@ -11,6 +12,7 @@ binary-to-decimal
11
12
  block-sort
12
13
  bst-iterator
13
14
  calc
15
+ case-vs-match
14
16
  circle
15
17
  classify-wallet
16
18
  climbing-stairs
@@ -20,8 +22,9 @@ count-effects
20
22
  counter
21
23
  describe
22
24
  destructure
23
- doto-hygiene
25
+ divisibility-stats
24
26
  doto
27
+ doto-hygiene
25
28
  egg-count
26
29
  even-squares
27
30
  exceptions
@@ -38,9 +41,9 @@ kwargs
38
41
  leap-year
39
42
  length-of-last-word
40
43
  macros-dbg
44
+ macros-import
41
45
  macros-import-helpers
42
46
  macros-import-whole
43
- macros-import
44
47
  macros-multi
45
48
  macros-swap
46
49
  macros-thrice-if
@@ -53,6 +56,7 @@ max-achievable
53
56
  maximum-subarray
54
57
  min-max
55
58
  module-header
59
+ nested-nil-pattern
56
60
  move-zeroes
57
61
  number-of-1-bits
58
62
  number-of-steps
@@ -84,8 +88,8 @@ thread-styles
84
88
  threading
85
89
  tic-tac-toe
86
90
  tset
87
- two-sum-hash
88
91
  two-sum
92
+ two-sum-hash
89
93
  ugly-number
90
94
  underscore-patterns
91
95
  valid-parentheses-1
@@ -0,0 +1,7 @@
1
+ (fn check [packet]
2
+ (case packet
3
+ [:ping seq] (.. "got " seq)
4
+ _ "other"))
5
+
6
+ (print (check [:ping 42]))
7
+ (print (check [:ping nil]))
@@ -0,0 +1,11 @@
1
+ (local hash-fn #(or $ "nil"))
2
+ (local regular-fn (fn [x] (+ x 1)))
3
+ (local lambda-fn (λ [x] (* x 2)))
4
+ (local vec-binding [10 20 30])
5
+ (local hash-binding {:a 1})
6
+
7
+ (print (hash-fn nil))
8
+ (print (regular-fn 5))
9
+ (print (lambda-fn 5))
10
+ (print (length vec-binding))
11
+ (print (. hash-binding :a))
@@ -2,4 +2,4 @@
2
2
  (fn initialize [name] (set (ivar name) name))
3
3
  (fn name [] (ivar name))
4
4
  (fn kingdom [] "animalia")
5
- (fn label [] (.. (self.name) " the animal")))
5
+ (fn label [] (.. name " the animal")))
@@ -1,7 +1,7 @@
1
1
  (require "./zoo-animal-1")
2
2
 
3
3
  (class Zoo.Dog [Zoo.Animal]
4
- (fn label [] (.. (self.name) " the dog"))
4
+ (fn label [] (.. name " the dog"))
5
5
  (fn bark [] "woof"))
6
6
 
7
7
  (let [dog (Zoo.Dog.new "Poppy")]
@@ -57,7 +57,7 @@ module Kapusta
57
57
 
58
58
  def build_simple_block_parts(pattern, body, env, current_scope)
59
59
  body_env = env.child
60
- params = pattern.items.map { |sym| define_local(body_env, sym.name, shadow: true) }
60
+ params = pattern.items.map { |sym| define_local(body_env, sym, shadow: true) }
61
61
  body_code, = emit_sequence(body, body_env, current_scope,
62
62
  allow_method_definitions: false, result: false)
63
63
  [params, body_code]
@@ -81,9 +81,21 @@ module Kapusta
81
81
  end
82
82
 
83
83
  def emit_definition_form(form, env, current_scope)
84
- return [emit_method_definition(form, env), env] unless current_scope == :toplevel
84
+ return emit_toplevel_method_definition(form, env) if current_scope == :toplevel
85
85
 
86
- emit_toplevel_method_definition(form, env)
86
+ code = emit_method_definition(form, env)
87
+ register_self_method_binding(form, env)
88
+ [code, env]
89
+ end
90
+
91
+ def register_self_method_binding(form, env)
92
+ name_sym = form.items[1]
93
+ return unless name_sym.is_a?(Sym) && !name_sym.dotted?
94
+
95
+ ruby_name = Kapusta.kebab_to_snake(name_sym.name)
96
+ return unless direct_method_name?(ruby_name)
97
+
98
+ env.define(name_sym.name, Env::SelfMethodBinding.new(ruby_name))
87
99
  end
88
100
 
89
101
  def emit_toplevel_method_definition(form, env)
@@ -234,7 +246,7 @@ module Kapusta
234
246
 
235
247
  binding = env.lookup_if_defined(name)
236
248
  return false if binding.nil?
237
- return false if method_binding?(binding)
249
+ return false if callable_method_binding?(binding)
238
250
  return false if constant_binding?(binding)
239
251
 
240
252
  true
@@ -296,7 +308,9 @@ module Kapusta
296
308
 
297
309
  if target.is_a?(Sym)
298
310
  validate_binding_symbol!(target)
299
- if allow_constant && form.head.name == 'local' && (constant_name = constant_name_for(target.name))
311
+ if allow_constant && form.head.name == 'local' &&
312
+ constant_value?(form.items[2]) &&
313
+ (constant_name = constant_name_for(target.name))
300
314
  env.define(target.name, constant_name)
301
315
  mark_mutability(env, target.name, mutable: false)
302
316
  return ["#{constant_name} = #{value_code}\nnil", env]
@@ -316,6 +330,13 @@ module Kapusta
316
330
  candidate if candidate.match?(/\A[A-Z][A-Z0-9_]*\z/)
317
331
  end
318
332
 
333
+ def constant_value?(value_form)
334
+ case value_form
335
+ when Numeric, String, ::Symbol, true, false, nil then true
336
+ else false
337
+ end
338
+ end
339
+
319
340
  def check_destructure_value!(pattern, value_form)
320
341
  return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
321
342
 
@@ -376,7 +397,7 @@ module Kapusta
376
397
  binding = env.lookup_if_defined(target.name)
377
398
  ruby_name =
378
399
  if binding
379
- emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
400
+ emit_error!(:cannot_set_method_binding, name: target.name) if callable_method_binding?(binding)
380
401
 
381
402
  binding
382
403
  else
@@ -420,7 +441,7 @@ module Kapusta
420
441
  end
421
442
  else
422
443
  binding = env.lookup(target.name)
423
- emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
444
+ emit_error!(:cannot_set_method_binding, name: target.name) if callable_method_binding?(binding)
424
445
  emit_error!(:expected_var, name: target.name) unless mutable_binding?(env, target.name)
425
446
 
426
447
  emit_assignment(binding, value_code)
@@ -111,30 +111,23 @@ module Kapusta
111
111
  bindings = args[0].items
112
112
  emit_error!(:accumulate_no_iterator) if bindings.length < 5
113
113
 
114
- acc_name = bindings[0]
115
- loop_name = bindings[2]
116
- loop_env = env.child
117
- acc_var = define_local(loop_env, acc_name.name)
118
- loop_var = define_local(loop_env, loop_name.name)
119
- body_code, = emit_sequence(args[1..], loop_env, current_scope, allow_method_definitions: false)
120
- accumulating_body = emit_sequence_value_assignment(acc_var, body_code)
121
- loop_code = emit_counted_loop(
122
- ruby_name: loop_var,
123
- start_code: emit_expr(bindings[3], env, current_scope),
124
- finish_code: emit_expr(bindings[4], env, current_scope),
125
- step_code: bindings[5] ? emit_expr(bindings[5], env, current_scope) : '1',
126
- until_form: nil,
127
- loop_env:,
128
- current_scope:,
129
- body_code: accumulating_body
130
- )
131
- [
132
- '(-> do',
133
- indent("#{acc_var} = #{emit_expr(bindings[1], env, current_scope)}"),
134
- indent(loop_code),
135
- indent(acc_var),
136
- 'end).call'
137
- ].join("\n")
114
+ body_env = env.child
115
+ acc_var = define_local(body_env, bindings[0].name)
116
+ loop_var = define_local(body_env, bindings[2].name)
117
+
118
+ init_code = emit_expr(bindings[1], env, current_scope)
119
+ start_code = emit_expr(bindings[3], env, current_scope)
120
+ finish_code = emit_expr(bindings[4], env, current_scope)
121
+ step_code = bindings[5] ? emit_expr(bindings[5], env, current_scope) : nil
122
+ body_code, = emit_sequence(args[1..], body_env, current_scope, allow_method_definitions: false)
123
+
124
+ receiver =
125
+ if step_code
126
+ "#{parenthesize(start_code)}.step(#{finish_code}, #{step_code})"
127
+ else
128
+ "(#{start_code}..#{finish_code})"
129
+ end
130
+ inject_block(receiver, "#{acc_var}, #{loop_var}", init_code, '', body_code)
138
131
  end
139
132
 
140
133
  def emit_hashfn(args, env, current_scope)
@@ -80,13 +80,17 @@ module Kapusta
80
80
  end
81
81
 
82
82
  def emit_module_expr(args, env)
83
- body = emit_sequence(args[1..], env, :module, allow_method_definitions: true, result: false).first
83
+ body = with_class_body do
84
+ emit_sequence(args[1..], env.child, :module, allow_method_definitions: true, result: false).first
85
+ end
84
86
  emit_module_wrapper(args[0], body)
85
87
  end
86
88
 
87
89
  def emit_class_expr(args, env)
88
90
  name_sym, supers, body_forms = split_class_args(args)
89
- body = emit_sequence(body_forms, env, :class, allow_method_definitions: true, result: false).first
91
+ body = with_class_body do
92
+ emit_sequence(body_forms, env.child, :class, allow_method_definitions: true, result: false).first
93
+ end
90
94
  emit_class_wrapper(name_sym, supers, env,
91
95
  body)
92
96
  end
@@ -298,7 +302,7 @@ module Kapusta
298
302
  end
299
303
 
300
304
  def emit_bound_call(binding, args, env, current_scope)
301
- return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
305
+ return emit_self_method_binding_call(binding, args, env, current_scope) if callable_method_binding?(binding)
302
306
 
303
307
  emit_error!(:cannot_call_constant, name: binding) if constant_binding?(binding)
304
308
 
@@ -438,10 +442,15 @@ module Kapusta
438
442
  return emit_multisym_value(sym, env) if sym.dotted?
439
443
  return 'ARGV' if name == 'ARGV'
440
444
  return name if name.match?(/\A[A-Z]/)
445
+ return Kapusta.kebab_to_snake(name) if implicit_self_method?(name)
441
446
 
442
447
  emit_error!(:undefined_symbol, name:)
443
448
  end
444
449
 
450
+ def implicit_self_method?(name)
451
+ in_class_body? && direct_method_name?(Kapusta.kebab_to_snake(name))
452
+ end
453
+
445
454
  def emit_gvar(sym)
446
455
  "$#{global_name(sym.name)}"
447
456
  end
@@ -41,29 +41,29 @@ module Kapusta
41
41
 
42
42
  def emit_form_run(forms, start, env, current_scope, header_form: nil)
43
43
  i = start
44
- codes = []
44
+ entries = []
45
45
  while i < forms.length
46
46
  form = forms[i]
47
47
  if end_form?(form)
48
48
  validate_end_form!(form)
49
49
  with_current_form(form) { emit_error!(:end_outside_header) } unless header_form
50
- return [codes.join("\n"), i + 1]
50
+ return [join_definition_aware(entries, current_scope), i + 1]
51
51
  end
52
52
 
53
53
  if bodyless_header?(form)
54
54
  header_code, i = emit_bodyless_header(form, forms, i + 1, env)
55
- codes << header_code
55
+ entries << [form, header_code]
56
56
  next
57
57
  end
58
58
 
59
59
  code, env = emit_form_in_sequence(form, env, current_scope,
60
60
  allow_method_definitions: true,
61
61
  result_needed: false)
62
- codes << code
62
+ entries << [form, code]
63
63
  i += 1
64
64
  end
65
65
  with_current_form(header_form) { emit_error!(:unclosed_header) } if header_form
66
- [codes.join("\n"), i]
66
+ [join_definition_aware(entries, current_scope), i]
67
67
  end
68
68
 
69
69
  def end_form?(form)
@@ -109,12 +109,16 @@ module Kapusta
109
109
  inner_code, next_i = emit_bodyless_header(inner[0], forms, body_start, env)
110
110
  [emit_direct_module_header(name_sym, inner_code) || emit_module_wrapper(name_sym, inner_code), next_i]
111
111
  else
112
- body, next_i = emit_form_run(forms, body_start, env, :module, header_form: form)
112
+ body, next_i = with_class_body do
113
+ emit_form_run(forms, body_start, env.child, :module, header_form: form)
114
+ end
113
115
  [emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body), next_i]
114
116
  end
115
117
  else
116
118
  name_sym, supers, = split_class_args(form.items[1..])
117
- body, next_i = emit_form_run(forms, body_start, env, :class, header_form: form)
119
+ body, next_i = with_class_body do
120
+ emit_form_run(forms, body_start, env.child, :class, header_form: form)
121
+ end
118
122
  code = emit_direct_class_header(name_sym, supers, body, env) ||
119
123
  emit_class_wrapper(name_sym, supers, env, body)
120
124
  [code, next_i]
@@ -170,14 +174,41 @@ module Kapusta
170
174
 
171
175
  def emit_sequence(forms, env, current_scope, allow_method_definitions:, result: true)
172
176
  current_env = env
173
- codes = []
177
+ entries = []
174
178
  forms.each_with_index do |form, index|
175
179
  code, current_env = emit_form_in_sequence(form, current_env, current_scope,
176
180
  allow_method_definitions:,
177
181
  result_needed: result && index == forms.length - 1)
178
- codes << code unless code.empty?
182
+ entries << [form, code] unless code.empty?
183
+ end
184
+ [join_definition_aware(entries, current_scope), current_env]
185
+ end
186
+
187
+ def join_definition_aware(entries, current_scope)
188
+ return '' if entries.empty?
189
+
190
+ result = +entries.first[1]
191
+ entries.each_cons(2) do |(prev_form, _), (curr_form, curr_code)|
192
+ sep = blank_between_definitions?(prev_form, curr_form, current_scope) ? "\n\n" : "\n"
193
+ result << sep << curr_code
194
+ end
195
+ result
196
+ end
197
+
198
+ def blank_between_definitions?(prev_form, curr_form, current_scope)
199
+ return false unless %i[toplevel module class].include?(current_scope)
200
+
201
+ definition_form?(prev_form) || definition_form?(curr_form)
202
+ end
203
+
204
+ def definition_form?(form)
205
+ return false unless form.is_a?(List) && !form.empty? && form.head.is_a?(Sym)
206
+
207
+ case form.head.name
208
+ when 'defn', 'class', 'module' then true
209
+ when 'fn', 'lambda', 'λ' then form.items[1].is_a?(Sym)
210
+ else false
179
211
  end
180
- [codes.join("\n"), current_env]
181
212
  end
182
213
 
183
214
  def sequence_statement_form?(form)
@@ -313,8 +344,30 @@ module Kapusta
313
344
  binding.is_a?(Env::MethodBinding)
314
345
  end
315
346
 
347
+ def in_class_body?
348
+ @class_body_depth.positive?
349
+ end
350
+
351
+ def with_class_body
352
+ @class_body_depth += 1
353
+ yield
354
+ ensure
355
+ @class_body_depth -= 1
356
+ end
357
+
358
+ def self_method_binding?(binding)
359
+ binding.is_a?(Env::SelfMethodBinding)
360
+ end
361
+
362
+ def callable_method_binding?(binding)
363
+ method_binding?(binding) || self_method_binding?(binding)
364
+ end
365
+
316
366
  def binding_value_code(binding)
317
- method_binding?(binding) ? "method(#{binding.ruby_name.to_sym.inspect})" : binding
367
+ return "method(#{binding.ruby_name.to_sym.inspect})" if method_binding?(binding)
368
+ return binding.ruby_name if self_method_binding?(binding)
369
+
370
+ binding
318
371
  end
319
372
 
320
373
  def parse_counted_for_bindings(bindings, env, current_scope)
@@ -28,6 +28,7 @@ module Kapusta
28
28
  @path = path
29
29
  @target = target
30
30
  @temp_index = 0
31
+ @class_body_depth = 0
31
32
  end
32
33
 
33
34
  def emit_file(forms)
@@ -139,6 +139,8 @@ module Kapusta
139
139
 
140
140
  def doto(forms)
141
141
  value = forms.first
142
+ return value if forms[1..].empty?
143
+
142
144
  temp = gensym('doto')
143
145
  body = forms[1..].map do |form|
144
146
  if form.is_a?(List)
@@ -147,7 +149,8 @@ module Kapusta
147
149
  List.new([form, temp])
148
150
  end
149
151
  end
150
- List.new([Sym.new('let'), Vec.new([temp, value]), *body, temp])
152
+ fn = List.new([Sym.new('fn'), Vec.new([temp]), *body])
153
+ List.new([Sym.new(':'), value, :tap, fn])
151
154
  end
152
155
 
153
156
  def gensym(prefix)
data/lib/kapusta/env.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Kapusta
4
4
  class Env
5
5
  MethodBinding = Struct.new(:ruby_name)
6
+ SelfMethodBinding = Struct.new(:ruby_name)
6
7
 
7
8
  def initialize(parent = nil)
8
9
  @parent = parent
@@ -555,30 +555,18 @@ module Kapusta
555
555
  lines = [base]
556
556
  args = list_raw_rest(list)
557
557
  semantic_length = semantic_items(args).length
558
+ hang_subsequent_args = hang_call_args?(list, indent)
558
559
 
559
560
  semantic_index = 0
561
+ hanging = nil
560
562
  args.each do |arg|
561
563
  if comment?(arg)
562
564
  lines << indent_block(render(arg, indent + INDENT), INDENT)
563
565
  next
564
- end
565
-
566
- if semantic_index.zero?
567
- first = render(
568
- arg,
569
- indent + base.length + 1,
570
- force_expand: semantic_length == 1 && fn_form?(arg)
571
- )
572
- first_line, *rest = first.lines(chomp: true)
573
- candidate = "#{base} #{first_line}"
574
-
575
- if lines.length == 1 && fits?(candidate, indent)
576
- lines[0] = candidate
577
- hanging = ' ' * (base.length + 1)
578
- rest.each { |line| lines << "#{hanging}#{line}" }
579
- else
580
- lines << indent_block(first, INDENT)
581
- end
566
+ elsif semantic_index.zero?
567
+ hanging = append_first_call_arg(lines, arg, base, indent, semantic_length)
568
+ elsif hanging && hang_subsequent_args
569
+ lines << prefix_continuation(hanging, render(arg, indent + hanging.length))
582
570
  else
583
571
  lines << indent_block(render(arg, indent + INDENT), INDENT)
584
572
  end
@@ -589,6 +577,48 @@ module Kapusta
589
577
  append_suffix(lines, ')')
590
578
  end
591
579
 
580
+ def append_first_call_arg(lines, arg, base, indent, semantic_length)
581
+ first = render(
582
+ arg,
583
+ indent + base.length + 1,
584
+ force_expand: semantic_length == 1 && fn_form?(arg)
585
+ )
586
+ first_line, *rest = first.lines(chomp: true)
587
+ candidate = "#{base} #{first_line}"
588
+
589
+ unless lines.length == 1 && fits?(candidate, indent)
590
+ lines << indent_block(first, INDENT)
591
+ return
592
+ end
593
+
594
+ hanging = ' ' * (base.length + 1)
595
+ lines[0] = candidate
596
+ rest.each { |line| lines << "#{hanging}#{line}" }
597
+ hanging
598
+ end
599
+
600
+ def hang_call_args?(list, indent)
601
+ return false unless operator_call?(list)
602
+
603
+ flat = flat_call_render(list)
604
+ flat && !fits?(flat, indent)
605
+ end
606
+
607
+ def operator_call?(list)
608
+ head = list_head(list)
609
+ head.is_a?(Sym) && head.name.match?(/\A[^\w.]+\z/)
610
+ end
611
+
612
+ def flat_call_render(list)
613
+ head = flat_render(list_head(list))
614
+ return unless head
615
+
616
+ rendered_args = semantic_items(list_raw_rest(list)).map { |arg| flat_render(arg) }
617
+ return if rendered_args.any?(&:nil?)
618
+
619
+ "(#{[head, *rendered_args].join(' ')})"
620
+ end
621
+
592
622
  def render_vec(vec, indent, layout: nil, top_level: false, force_expand: false)
593
623
  flat = flat_render(vec)
594
624
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
@@ -800,8 +830,7 @@ module Kapusta
800
830
  'fn', 'lambda', 'λ', 'macro'
801
831
  true
802
832
  else
803
- flat = flat_render(form)
804
- flat && flat.length > 40
833
+ false
805
834
  end
806
835
  end
807
836
 
@@ -17,7 +17,7 @@ module Kapusta
17
17
  return unless target
18
18
 
19
19
  case target.kind
20
- when :local, :toplevel_fn, :constant
20
+ when :local, :toplevel_fn, :constant, :method
21
21
  location_for_binding(uri, target.binding) if target.binding
22
22
  when :macro
23
23
  locations_for_macro(uri, target.binding, workspace_index)
@@ -32,6 +32,8 @@ module Kapusta
32
32
  case target.kind
33
33
  when :local
34
34
  rename_local(uri, target, new_name)
35
+ when :method
36
+ rename_method(uri, target, new_name)
35
37
  when :toplevel_fn, :free_toplevel
36
38
  return error('cross-file rename requires a workspace') unless workspace_index
37
39
 
@@ -128,8 +130,6 @@ module Kapusta
128
130
  end
129
131
 
130
132
  if binding
131
- return if binding.kind == :method
132
-
133
133
  return constant_target(walker, binding, seg) if %i[module class].include?(binding.kind)
134
134
 
135
135
  return local_target(walker, binding, seg)
@@ -138,7 +138,6 @@ module Kapusta
138
138
  if reference
139
139
  target = reference.target
140
140
  if target
141
- return if target.kind == :method
142
141
  return constant_target(walker, target, seg, sym:) if %i[module class].include?(target.kind)
143
142
 
144
143
  return local_target(walker, target, seg, sym:)
@@ -164,6 +163,7 @@ module Kapusta
164
163
  kind = case binding.kind
165
164
  when :toplevel_fn then :toplevel_fn
166
165
  when :macro, :macro_import then :macro
166
+ when :method then :method
167
167
  else :local
168
168
  end
169
169
  Target.new(
@@ -207,6 +207,22 @@ module Kapusta
207
207
  }
208
208
  end
209
209
 
210
+ def rename_method(uri, target, new_name)
211
+ return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
212
+ return error('cannot resolve binding') unless target.binding
213
+
214
+ walker = target.walker
215
+ binding = target.binding
216
+
217
+ occs = [binding]
218
+ walker.references.each do |r|
219
+ occs << r if r.target.equal?(binding)
220
+ end
221
+
222
+ edits = occs.map { |o| text_edit_first_segment(o, new_name) }
223
+ { changes: { uri => edits } }
224
+ end
225
+
210
226
  def rename_local(uri, target, new_name)
211
227
  return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
212
228
  return error('cannot resolve binding') unless target.binding
@@ -76,6 +76,17 @@ module Kapusta
76
76
 
77
77
  def walk_top(forms)
78
78
  walk_form_run(forms, 0, @root_scope)
79
+ resolve_late_references
80
+ end
81
+
82
+ def resolve_late_references
83
+ @references.each do |r|
84
+ next if r.target
85
+ next unless r.scope
86
+
87
+ target = r.scope.lookup(r.name)
88
+ r.target = target if target
89
+ end
79
90
  end
80
91
 
81
92
  def walk_form_run(forms, start, scope, header_target: nil)
@@ -164,7 +175,8 @@ module Kapusta
164
175
  if body.length == 1 && bodyless_header?(body[0])
165
176
  walk_bodyless_header(body[0], forms, body_start, scope)
166
177
  else
167
- walk_form_run(forms, body_start, scope, header_target: binding)
178
+ body_scope = make_scope(scope, :module)
179
+ walk_form_run(forms, body_start, body_scope, header_target: binding)
168
180
  end
169
181
  end
170
182
  when 'class'
@@ -172,7 +184,8 @@ module Kapusta
172
184
  supers&.items&.each { |item| walk_form(item, scope) }
173
185
  binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
174
186
  inside_class do
175
- walk_form_run(forms, body_start, scope, header_target: binding)
187
+ body_scope = make_scope(scope, :class)
188
+ walk_form_run(forms, body_start, body_scope, header_target: binding)
176
189
  end
177
190
  end
178
191
  end
@@ -406,7 +419,7 @@ module Kapusta
406
419
  else
407
420
  (scope == @root_scope ? :toplevel_fn : :fn_local)
408
421
  end
409
- binding = add_binding(name_sym, scope, kind, lexical: kind != :method)
422
+ binding = add_binding(name_sym, scope, kind, lexical: true)
410
423
  fn_scope.bindings[name_sym.name] = binding unless kind == :method
411
424
  end
412
425
  bind_param_vec(params, fn_scope)
@@ -558,10 +571,11 @@ module Kapusta
558
571
  add_constant_binding(name_sym, scope, kind) if name_sym.is_a?(Sym)
559
572
 
560
573
  body = list.items[body_start..] || []
574
+ body_scope = make_scope(scope, kind)
561
575
  if kind == :class
562
- inside_class { body.each { |form| walk_form(form, scope) } }
576
+ inside_class { body.each { |form| walk_form(form, body_scope) } }
563
577
  else
564
- inside_module_or_class { body.each { |form| walk_form(form, scope) } }
578
+ inside_module_or_class { body.each { |form| walk_form(form, body_scope) } }
565
579
  end
566
580
  end
567
581
 
data/lib/kapusta/lsp.rb CHANGED
@@ -15,8 +15,38 @@ module Kapusta
15
15
  METHOD_NOT_FOUND = -32_601
16
16
  FULL_SYNC = 1
17
17
 
18
+ def self.debug?
19
+ %w[1 true yes on].include?(ENV['KAPUSTA_LS_DEBUG'].to_s.downcase)
20
+ end
21
+
18
22
  def self.start(input: $stdin, output: $stdout, log: $stderr)
19
- new(input:, output:, log:).run
23
+ server = new(input:, output:, log:)
24
+ install_signal_handlers(log)
25
+ server.run
26
+ debug_log(log, 'run returned, calling exit!(0)')
27
+ exit!(0)
28
+ end
29
+
30
+ def self.install_signal_handlers(log)
31
+ %w[TERM INT HUP].each do |sig|
32
+ Signal.trap(sig) do
33
+ debug_log(log, "signal #{sig} received, exiting")
34
+ exit!(0)
35
+ rescue StandardError
36
+ exit!(0)
37
+ end
38
+ rescue ArgumentError
39
+ nil
40
+ end
41
+ end
42
+
43
+ def self.debug_log(log, message)
44
+ return unless debug?
45
+
46
+ log.write("kapusta-ls[debug pid=#{Process.pid}]: #{message}\n")
47
+ log.flush
48
+ rescue StandardError
49
+ nil
20
50
  end
21
51
 
22
52
  def self.uri_to_path(uri)
@@ -34,7 +64,7 @@ module Kapusta
34
64
  @input = input.binmode
35
65
  @output = output.binmode
36
66
  @log = log
37
- @debug = %w[1 true yes on].include?(ENV['KAPUSTA_LS_DEBUG'].to_s.downcase)
67
+ @debug = LSP.debug?
38
68
  @sources = {}
39
69
  @workspace_index = WorkspaceIndex.new
40
70
  @initialized = false
@@ -42,9 +72,11 @@ module Kapusta
42
72
  end
43
73
 
44
74
  def run
75
+ debug('run loop start')
45
76
  until (message = read_message).nil?
46
77
  handle(message)
47
78
  end
79
+ debug('stdin EOF, run loop exiting')
48
80
  end
49
81
 
50
82
  private
@@ -83,8 +115,14 @@ module Kapusta
83
115
 
84
116
  def write_message(payload)
85
117
  body = JSON.generate(payload)
118
+ tag = payload[:id] ? "response id=#{payload[:id]}" : "notify #{payload[:method]}"
119
+ debug("write begin (#{tag}, #{body.bytesize} bytes)")
86
120
  @output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
87
121
  @output.flush
122
+ debug("write done (#{tag})")
123
+ rescue Errno::EPIPE, IOError => e
124
+ debug("write failed (#{e.class}: #{e.message}), exiting")
125
+ exit!(0)
88
126
  end
89
127
 
90
128
  def handle(message)
@@ -92,9 +130,11 @@ module Kapusta
92
130
  id = message['id']
93
131
  params = message['params'] || {}
94
132
 
133
+ debug("dispatch begin method=#{method.inspect} id=#{id.inspect}")
95
134
  return handle_pre_init(method, id, params) unless @initialized || method == 'initialize' || method == 'exit'
96
135
 
97
136
  dispatch(method, id, params)
137
+ debug("dispatch end method=#{method.inspect} id=#{id.inspect}")
98
138
  rescue StandardError => e
99
139
  log("#{e.class}: #{e.message}")
100
140
  log(e.backtrace.first(5).join("\n"))
@@ -116,8 +156,12 @@ module Kapusta
116
156
  when 'initialized' then nil
117
157
  when 'shutdown'
118
158
  @shutdown = true
159
+ debug('shutdown received, replying and arming watchdog')
119
160
  reply(id, nil)
120
- when 'exit' then exit(@shutdown ? 0 : 1)
161
+ arm_shutdown_watchdog
162
+ when 'exit'
163
+ debug("exit notification received (shutdown=#{@shutdown}), calling exit!")
164
+ exit!(@shutdown ? 0 : 1)
121
165
  when 'textDocument/didOpen' then on_did_open(params)
122
166
  when 'textDocument/didChange' then on_did_change(params)
123
167
  when 'textDocument/didSave' then on_did_save(params)
@@ -300,14 +344,28 @@ module Kapusta
300
344
  notify('textDocument/publishDiagnostics', params)
301
345
  end
302
346
 
347
+ def arm_shutdown_watchdog(seconds: 2)
348
+ Thread.new do
349
+ sleep seconds
350
+ debug("shutdown watchdog firing after #{seconds}s, forcing exit")
351
+ exit!(0)
352
+ end
353
+ end
354
+
303
355
  def log(message)
304
356
  @log.puts "kapusta-ls: #{message}"
357
+ @log.flush
358
+ rescue StandardError
359
+ nil
305
360
  end
306
361
 
307
362
  def debug(message)
308
363
  return unless @debug
309
364
 
310
- @log.puts "kapusta-ls[debug]: #{message}"
365
+ @log.write("kapusta-ls[debug pid=#{Process.pid}]: #{message}\n")
366
+ @log.flush
367
+ rescue StandardError
368
+ nil
311
369
  end
312
370
  end
313
371
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.11.2'
4
+ VERSION = '0.13.0'
5
5
  end
@@ -76,6 +76,29 @@ RSpec.describe 'examples' do
76
76
  expect(run_example('ackermann.kap')).to eq("9\n61\n")
77
77
  end
78
78
 
79
+ it 'non-constant-local.kap' do
80
+ expect(run_example('non-constant-local.kap')).to eq(<<~OUT)
81
+ "nil"
82
+ 6
83
+ 10
84
+ 3
85
+ 1
86
+ OUT
87
+ end
88
+
89
+ it 'case-vs-match.kap' do
90
+ expect(run_example('case-vs-match.kap')).to eq(<<~OUT)
91
+ "case: packet[:ping, 42] seq 42"
92
+ "case: packet[:ping, 42] seq 42"
93
+ "case: other"
94
+ "case: packet[:ping, 42] seq 42"
95
+ "match: other"
96
+ "match: other"
97
+ "match: packet[:ping, nil] seq nil"
98
+ "match: packet[:ping, 42] seq 42"
99
+ OUT
100
+ end
101
+
79
102
  it 'accumulator.kap' do
80
103
  expect(run_example('accumulator.kap')).to eq("22\n")
81
104
  end
@@ -92,6 +115,30 @@ RSpec.describe 'examples' do
92
115
  expect(run_example('circle.kap')).to eq("78.53975\n31.4159\n")
93
116
  end
94
117
 
118
+ it 'arrange-coins.kap' do
119
+ expect(run_example('arrange-coins.kap')).to eq(<<~OUT)
120
+ 0
121
+ 0
122
+ 1
123
+ 1
124
+ 2
125
+ 3
126
+ 3
127
+ 6
128
+ 4
129
+ 10
130
+ OUT
131
+ end
132
+
133
+ it 'divisibility-stats.kap' do
134
+ expect(run_example('divisibility-stats.kap')).to eq(<<~OUT)
135
+ 10
136
+ 6
137
+ 33
138
+ 20
139
+ OUT
140
+ end
141
+
95
142
  it 'anagram.kap' do
96
143
  expect(run_example('anagram.kap')).to eq("true\ntrue\nfalse\n")
97
144
  end
@@ -444,15 +491,10 @@ RSpec.describe 'examples' do
444
491
  OUT
445
492
  end
446
493
 
447
- it 'underscore-patterns.kap on mruby keeps loose nil and strict fallback separate' do
448
- path = File.join(EXAMPLES_DIR, 'underscore-patterns.kap')
449
- ruby = compile_example('underscore-patterns.kap', target: :mruby3)
450
-
451
- expect(run_mruby_source(ruby, path:)).to eq(<<~OUT)
452
- 5
453
- nil
454
- 5
455
- "fallback"
494
+ it 'nested-nil-pattern.kap' do
495
+ expect(run_example('nested-nil-pattern.kap')).to eq(<<~OUT)
496
+ "got 42"
497
+ "other"
456
498
  OUT
457
499
  end
458
500
 
data/spec/lsp_spec.rb CHANGED
@@ -234,7 +234,7 @@ RSpec.describe Kapusta::LSP do
234
234
  end
235
235
  end
236
236
 
237
- it 'does not offer rename for method definitions after a bodyless class header' do
237
+ it 'offers rename for method definitions after a bodyless class header' do
238
238
  text = "(class Counter)\n(fn initialize [start] start)\n"
239
239
  responses = run(
240
240
  frame_initialize,
@@ -242,7 +242,60 @@ RSpec.describe Kapusta::LSP do
242
242
  frame_prepare_rename(uri: 'file:///x.kap', **cursor_at(text, 'initialize'))
243
243
  )
244
244
 
245
- expect(result_for(responses)['result']).to be_nil
245
+ expect(result_for(responses)['result']).to include('placeholder' => 'initialize')
246
+ end
247
+
248
+ it 'renames a class method and its bare-self call sites within a file' do
249
+ text = "(class Greeter)\n" \
250
+ "(fn initialize [name] (set @name name))\n" \
251
+ "(fn greeting [] (.. \"Hello, \" (label)))\n" \
252
+ "(fn label [] @name)\n"
253
+ responses = run(
254
+ frame_initialize,
255
+ frame_did_open('file:///x.kap', text),
256
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'label'), new_name: 'who')
257
+ )
258
+ edits = result_for(responses)['result']['documentChanges'].first['edits']
259
+
260
+ expect(edits.map { |e| e['newText'] }).to all(eq('who'))
261
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
262
+ .to contain_exactly([2, 31], [3, 4])
263
+ end
264
+
265
+ it 'jumps from a bare-self method call to the class method definition' do
266
+ text = "(class Greeter)\n" \
267
+ "(fn label [] 1)\n" \
268
+ "(fn greeting [] (label))\n"
269
+ responses = run(
270
+ frame_initialize,
271
+ frame_did_open('file:///x.kap', text),
272
+ frame_definition(uri: 'file:///x.kap', line: 2, character: 17)
273
+ )
274
+ result = result_for(responses)['result']
275
+
276
+ expect(result).to eq(
277
+ 'uri' => 'file:///x.kap',
278
+ 'range' => {
279
+ 'start' => { 'line' => 1, 'character' => 4 },
280
+ 'end' => { 'line' => 1, 'character' => 9 }
281
+ }
282
+ )
283
+ end
284
+
285
+ it 'renames a class method when the cursor is on a bare-self call site' do
286
+ text = "(class Greeter)\n" \
287
+ "(fn label [] 1)\n" \
288
+ "(fn greeting [] (label))\n"
289
+ responses = run(
290
+ frame_initialize,
291
+ frame_did_open('file:///x.kap', text),
292
+ frame_rename(uri: 'file:///x.kap', line: 2, character: 17, new_name: 'who')
293
+ )
294
+ edits = result_for(responses)['result']['documentChanges'].first['edits']
295
+
296
+ expect(edits.map { |e| e['newText'] }).to all(eq('who'))
297
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
298
+ .to contain_exactly([1, 4], [2, 17])
246
299
  end
247
300
 
248
301
  it 'does not let class methods shadow top-level function references' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -31,6 +31,8 @@ files:
31
31
  - examples/ackermann.kap
32
32
  - examples/anagram.kap
33
33
  - examples/anonymous-greeter.kap
34
+ - examples/arrange-coins.kap
35
+ - examples/array-sign.kap
34
36
  - examples/bank-account.kap
35
37
  - examples/baseball-game.kap
36
38
  - examples/best-time-to-buy-sell-stock.kap
@@ -40,6 +42,7 @@ files:
40
42
  - examples/blocks-and-kwargs.kap
41
43
  - examples/bst-iterator.kap
42
44
  - examples/calc.kap
45
+ - examples/case-vs-match.kap
43
46
  - examples/circle.kap
44
47
  - examples/classify-wallet.kap
45
48
  - examples/climbing-stairs.kap
@@ -49,9 +52,11 @@ files:
49
52
  - examples/counter.kap
50
53
  - examples/describe.kap
51
54
  - examples/destructure.kap
55
+ - examples/divisibility-stats.kap
52
56
  - examples/doto-hygiene.kap
53
57
  - examples/doto.kap
54
58
  - examples/egg-count.kap
59
+ - examples/equal-sums.kap
55
60
  - examples/even-squares.kap
56
61
  - examples/exceptions.kap
57
62
  - examples/factorial.kap
@@ -87,6 +92,8 @@ files:
87
92
  - examples/module-header.kap
88
93
  - examples/move-zeroes.kap
89
94
  - examples/mruby-runtime-examples.txt
95
+ - examples/nested-nil-pattern.kap
96
+ - examples/non-constant-local.kap
90
97
  - examples/number-of-1-bits.kap
91
98
  - examples/number-of-steps.kap
92
99
  - examples/or-patterns.kap