kapusta 0.11.1 → 0.12.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: 85bcd0988a353fb851e5ff955c2b5196f39193e1c3c8fb6ca4a111d6260e774d
4
- data.tar.gz: 80efbc138038947b866eaac96ef21d5cdccd04a461a25f2e17cbd7ce925ceaec
3
+ metadata.gz: d5e4a7dc8e3d044580b58064b4632706c2115964847d674c23bbe39a625e0b6e
4
+ data.tar.gz: b17a395b81ab568829022273e5df9ee5a186c052d6a1e4c843df631dfe1d6f76
5
5
  SHA512:
6
- metadata.gz: 16d02e652b9221990ac027423e677a8f27c285d582c6bfb368f585140305e1c48dff5add4a8491a7996b5be99e34efd761e7fb280d8932fc6548622939b166b4
7
- data.tar.gz: 8ab10f9991b7029d9366e47d434a9e9166568b870369f1fe585007fbe2cf9b750c84e8eb3b8fb015db255977eef80fd3255750fa99006d577a9564b640d84319
6
+ metadata.gz: d3e8ca629130a695a67a1c210ab06869518e2d232084e01a8cb6d180094e2b10b0cbc3f65c191a9a7a6b1e70727f1563f28e3637264cc7776a58c2e16e554991
7
+ data.tar.gz: 71695c0adf8e37705c9b9b85263e495578bcdd0d442c2d56ad09b1a6386640003164adf98d932c499472ae81d5c3d4a909898b907417e6d6463384b855b2fa33
data/README.md CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  Kapusta is a Lisp for the Ruby runtime.
4
4
 
5
- It is inspired by [Fennel](https://fennel-lang.org). It is not intended to be production-ready like Clojure: that would be a lot of work, and Ruby is already a rich, elegant language.
5
+ It is inspired by [Fennel](https://fennel-lang.org). Kapusta aims to bring the simplicity and joy of Lisp to Ruby. Where Fennel uses Lua's stdlib and runtime, Kapusta uses Ruby's.
6
6
 
7
- Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. Where Fennel uses Lua's stdlib and runtime, Kapusta uses Ruby's. You can use it for small apps, LeetCode, DragonRuby, or maybe even Rails.
8
-
9
- For more information about Kapusta, see the official Fennel documentation and tutorials.
7
+ For more information about Kapusta, see the official Fennel documentation and tutorials, but replace Lua with Ruby.
10
8
 
11
9
  ## Features
12
10
 
@@ -41,14 +39,14 @@ exe/kapusta examples/fizzbuzz.kap
41
39
  or
42
40
 
43
41
  ```
44
- exe/kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
42
+ kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
45
43
  ruby examples/fizzbuzz.rb
46
44
  ```
47
45
 
48
- For mruby-compatible output, such as DragonRuby, use:
46
+ For mruby 3 compatible output, such as DragonRuby, use:
49
47
 
50
48
  ```
51
- exe/kapusta --compile --target=mruby examples/match.kap > examples/match-mruby.rb
49
+ kapusta --compile --target=mruby3 examples/match.kap > examples/match-mruby3.rb
52
50
  ```
53
51
 
54
52
  ## Use from Ruby
@@ -105,6 +103,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
105
103
  | `string.format`, `table.insert`, etc. | use Ruby methods and stdlib instead |
106
104
  | `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
107
105
  | `(print x)` is Lua's `print` (bare) | `(print x)` is Ruby's `p` (inspect-style) |
106
+ | `(.. "x: " nil)` errors at runtime | `(.. "x: " nil)` produces `"x: "` (Ruby `nil.to_s`) |
108
107
  | `with-open`, `tail!` | not provided |
109
108
 
110
109
  Kapusta-specific additions:
@@ -115,8 +114,8 @@ Kapusta-specific additions:
115
114
  - `ivar` or `@var` / `cvar` or `@@var` / `gvar` or `$var`
116
115
  - `try` / `catch` / `finally` plus `raise` for exceptions
117
116
  - `(ruby "...")` raw host escape hatch
118
- - a trailing symbol-keyed hash is emitted as Ruby keyword arguments
119
- - a final function literal argument is emitted as a Ruby block
117
+ - pass Ruby keyword arguments by ending a call with a symbol-keyed hash: `(File.open path "r" {:encoding "UTF-8"})`
118
+ - pass a Ruby block by ending a call with a `(fn ...)` or `#(...)` literal: `(File.open path "r" (fn [io] (: io :read)))`
120
119
 
121
120
  ## Format
122
121
 
@@ -130,7 +129,7 @@ Use `kapusta-ls` in the editor of your choice.
130
129
 
131
130
  ## Syntax highlight
132
131
 
133
- For Vim, you can use https://git.sr.ht/~m15a/vim-fennel-syntax
132
+ For Vim, you can use [`vim-syntax/`](https://github.com/evmorov/kapusta/tree/main/vim-syntax/).
134
133
 
135
134
  ## License
136
135
 
@@ -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]))
@@ -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))
data/lib/kapusta/cli.rb CHANGED
@@ -39,7 +39,7 @@ module Kapusta
39
39
  OptionParser.new do |parser|
40
40
  parser.banner = usage
41
41
  parser.on('-c', '--compile', 'Compile .kap to Ruby') { options.compile = true }
42
- parser.on('--target=TARGET', 'Compile for mruby') do |target|
42
+ parser.on('--target=TARGET', 'Compile for mruby3') do |target|
43
43
  options.target = Kapusta::Compiler.normalize_target(target)
44
44
  end
45
45
  parser.on('-h', '--help', 'Show this help') { options.help = true }
@@ -69,7 +69,7 @@ module Kapusta
69
69
  end
70
70
 
71
71
  def self.usage
72
- 'usage: kapusta [--compile|-c] [--target=mruby] <file.kap> | kapusta <file.kap> [args...]'
72
+ 'usage: kapusta [--compile|-c] [--target=mruby3] <file.kap> | kapusta <file.kap> [args...]'
73
73
  end
74
74
  end
75
75
  end
@@ -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]
@@ -178,7 +178,7 @@ module Kapusta
178
178
 
179
179
  def emit_toplevel_method_bridge(ruby_name)
180
180
  method_name = ruby_name.to_sym.inspect
181
- if mruby_target?
181
+ if mruby3_target?
182
182
  return [
183
183
  "define_singleton_method(#{method_name}) do |*args|",
184
184
  indent("Object.instance_method(#{method_name}).bind(self).call(*args)"),
@@ -296,7 +296,9 @@ module Kapusta
296
296
 
297
297
  if target.is_a?(Sym)
298
298
  validate_binding_symbol!(target)
299
- if allow_constant && form.head.name == 'local' && (constant_name = constant_name_for(target.name))
299
+ if allow_constant && form.head.name == 'local' &&
300
+ constant_value?(form.items[2]) &&
301
+ (constant_name = constant_name_for(target.name))
300
302
  env.define(target.name, constant_name)
301
303
  mark_mutability(env, target.name, mutable: false)
302
304
  return ["#{constant_name} = #{value_code}\nnil", env]
@@ -316,6 +318,13 @@ module Kapusta
316
318
  candidate if candidate.match?(/\A[A-Z][A-Z0-9_]*\z/)
317
319
  end
318
320
 
321
+ def constant_value?(value_form)
322
+ case value_form
323
+ when Numeric, String, ::Symbol, true, false, nil then true
324
+ else false
325
+ end
326
+ end
327
+
319
328
  def check_destructure_value!(pattern, value_form)
320
329
  return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
321
330
 
@@ -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)
@@ -96,7 +96,7 @@ module Kapusta
96
96
  end
97
97
 
98
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?
99
+ return try_emit_compat_case(value_var, clauses, env, current_scope, mode) if mruby3_target?
100
100
 
101
101
  try_emit_native_case(value_var, clauses, env, current_scope, mode) ||
102
102
  try_emit_compat_case(value_var, clauses, env, current_scope, mode)
@@ -25,8 +25,8 @@ module Kapusta
25
25
  (@form_stack ||= []).last
26
26
  end
27
27
 
28
- def mruby_target?
29
- @target == :mruby
28
+ def mruby3_target?
29
+ @target == :mruby3
30
30
  end
31
31
 
32
32
  def positionable?(form)
@@ -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)
@@ -61,7 +61,7 @@ module Kapusta
61
61
  def self.normalize_target(target)
62
62
  case target
63
63
  when nil then nil
64
- when :mruby, 'mruby' then :mruby
64
+ when :mruby3, 'mruby3', :mruby, 'mruby' then :mruby3
65
65
  else
66
66
  raise Error, Kapusta::Errors.format(:unknown_target, target: target.inspect)
67
67
  end
@@ -64,7 +64,7 @@ module Kapusta
64
64
  unexpected_eof: 'unexpected eof',
65
65
  unexpected_vararg: 'unexpected vararg',
66
66
  unknown_special_form: 'unknown special form: %{name}',
67
- unknown_target: 'unknown target %{target}; only mruby is supported',
67
+ unknown_target: 'unknown target %{target}; only mruby3 is supported',
68
68
  unquote_outside_quasiquote: 'unquote outside quasiquote',
69
69
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
70
70
  unterminated_string: 'unterminated string',
@@ -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
 
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.1'
4
+ VERSION = '0.12.0'
5
5
  end
data/spec/cli_spec.rb CHANGED
@@ -66,11 +66,11 @@ RSpec.describe Kapusta::CLI do
66
66
  end
67
67
  end
68
68
 
69
- it 'compiles case and match forms for mruby with --target=mruby' do
69
+ it 'compiles case and match forms for mruby3 with --target=mruby3' do
70
70
  path = File.expand_path('../examples/match.kap', __dir__)
71
71
 
72
72
  ruby = capture_stdout do
73
- described_class.start(['--compile', '--target=mruby', path])
73
+ described_class.start(['--compile', '--target=mruby3', path])
74
74
  end
75
75
 
76
76
  expect(ruby).not_to match(/^\s*in\b/)
@@ -98,14 +98,14 @@ RSpec.describe Kapusta::CLI do
98
98
  .to raise_error(SystemExit)
99
99
  end
100
100
 
101
- expect(error_output).to include('unknown target "mri"; only mruby is supported')
101
+ expect(error_output).to include('unknown target "mri"; only mruby3 is supported')
102
102
  end
103
103
 
104
104
  it 'rejects target without compile mode' do
105
105
  path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
106
106
 
107
107
  error_output = capture_stderr do
108
- expect { described_class.start(['--target=mruby', path]) }
108
+ expect { described_class.start(['--target=mruby3', path]) }
109
109
  .to raise_error(SystemExit)
110
110
  end
111
111
 
@@ -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: :mruby)
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
 
@@ -699,7 +741,7 @@ RSpec.describe 'mruby runtime examples' do
699
741
  if mruby_status.success? && mruby_stdout == expected
700
742
  expect(run_mruby_source(ruby, path:)).to eq(expected)
701
743
  else
702
- mruby_ruby = compile_example(name, target: :mruby)
744
+ mruby_ruby = compile_example(name, target: :mruby3)
703
745
 
704
746
  if mruby_ruby == ruby
705
747
  expect(mruby_status).to be_success
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.1
4
+ version: 0.12.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