kapusta 0.13.2 → 0.14.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -11
  3. data/bin/check-all +6 -1
  4. data/bin/compile-examples +7 -2
  5. data/examples/anagram.kap +3 -3
  6. data/examples/app/args.kap +9 -0
  7. data/examples/arrange-coins.kap +3 -3
  8. data/examples/baseball-game.kap +5 -5
  9. data/examples/best-time-to-buy-sell-stock.kap +2 -2
  10. data/examples/binary-search.kap +2 -2
  11. data/examples/binary-to-decimal.kap +1 -1
  12. data/examples/blocks-and-kwargs.kap +2 -2
  13. data/examples/count-effects.kap +1 -1
  14. data/examples/count-items-matching-rule.kap +18 -0
  15. data/examples/doto-hygiene.kap +2 -2
  16. data/examples/doto.kap +2 -2
  17. data/examples/egg-count.kap +1 -1
  18. data/examples/exceptions.kap +1 -1
  19. data/examples/falling-drops.kap +7 -7
  20. data/examples/fennel-parity-examples.txt +3 -6
  21. data/examples/good-pairs.kap +18 -0
  22. data/examples/greet.kap +1 -1
  23. data/examples/happy-number.kap +2 -2
  24. data/examples/left-right-difference.kap +60 -0
  25. data/examples/length-of-last-word.kap +4 -4
  26. data/examples/manhattan-distance.kap +1 -1
  27. data/examples/maximum-subarray.kap +23 -7
  28. data/examples/minimum-start-value.kap +19 -0
  29. data/examples/move-zeroes.kap +2 -2
  30. data/examples/mruby-runtime-examples.txt +8 -2
  31. data/examples/non-constant-local.kap +1 -1
  32. data/examples/number-of-1-bits.kap +1 -1
  33. data/examples/number-of-steps.kap +1 -1
  34. data/examples/palindrome.kap +2 -2
  35. data/examples/pangram.kap +4 -4
  36. data/examples/pcall.kap +3 -3
  37. data/examples/pipeline.kap +4 -4
  38. data/examples/plus-one.kap +3 -3
  39. data/examples/raindrops.kap +1 -1
  40. data/examples/range-width.kap +14 -0
  41. data/examples/recent-counter.kap +6 -6
  42. data/examples/require-local-args.kap +9 -0
  43. data/examples/require-local.kap +7 -0
  44. data/examples/require-module-local.kap +4 -0
  45. data/examples/require-module.kap +4 -0
  46. data/examples/reverse-integer.kap +1 -1
  47. data/examples/roman-to-integer.kap +3 -3
  48. data/examples/running-sum.kap +20 -0
  49. data/examples/safe-lookup.kap +2 -2
  50. data/examples/single-number.kap +1 -1
  51. data/examples/stack.kap +14 -14
  52. data/examples/subtract-product-sum.kap +1 -1
  53. data/examples/summary-ranges.kap +45 -0
  54. data/examples/threading.kap +5 -5
  55. data/examples/tset.kap +1 -1
  56. data/examples/two-sum-hash.kap +3 -3
  57. data/examples/two-sum.kap +1 -1
  58. data/examples/ugly-number.kap +1 -1
  59. data/examples/underground-system.kap +39 -0
  60. data/examples/valid-parentheses-1.kap +6 -6
  61. data/exe/kapusta-ls +49 -2
  62. data/lib/kapusta/ast.rb +8 -0
  63. data/lib/kapusta/compiler/emitter/bindings.rb +111 -89
  64. data/lib/kapusta/compiler/emitter/collections.rb +32 -40
  65. data/lib/kapusta/compiler/emitter/control_flow.rb +33 -31
  66. data/lib/kapusta/compiler/emitter/expressions.rb +21 -5
  67. data/lib/kapusta/compiler/emitter/interop.rb +168 -48
  68. data/lib/kapusta/compiler/emitter/patterns.rb +12 -14
  69. data/lib/kapusta/compiler/emitter/support.rb +63 -81
  70. data/lib/kapusta/compiler/language.rb +522 -0
  71. data/lib/kapusta/compiler/lua_compat.rb +23 -28
  72. data/lib/kapusta/compiler/macro_expander.rb +30 -30
  73. data/lib/kapusta/compiler/macro_lowerer.rb +12 -24
  74. data/lib/kapusta/compiler/normalizer.rb +25 -17
  75. data/lib/kapusta/compiler.rb +3 -24
  76. data/lib/kapusta/env.rb +2 -2
  77. data/lib/kapusta/errors.rb +2 -1
  78. data/lib/kapusta/formatter/ast_helpers.rb +78 -0
  79. data/lib/kapusta/formatter/cli.rb +125 -0
  80. data/lib/kapusta/formatter/line_helpers.rb +44 -0
  81. data/lib/kapusta/formatter/validator.rb +32 -0
  82. data/lib/kapusta/formatter.rb +354 -325
  83. data/lib/kapusta/lsp/identifier.rb +1 -1
  84. data/lib/kapusta/lsp/rename.rb +21 -11
  85. data/lib/kapusta/lsp/scope_walker.rb +122 -212
  86. data/lib/kapusta/lsp/workspace_index.rb +17 -5
  87. data/lib/kapusta/reader.rb +4 -2
  88. data/lib/kapusta/version.rb +1 -1
  89. data/lib/kapusta.rb +39 -6
  90. data/spec/cli_spec.rb +13 -0
  91. data/spec/examples_errors_spec.rb +3 -1
  92. data/spec/examples_spec.rb +67 -15
  93. data/spec/formatter_spec.rb +246 -0
  94. data/spec/lsp_spec.rb +69 -0
  95. data/spec/require_spec.rb +294 -0
  96. metadata +20 -2
  97. data/examples/describe.kap +0 -9
@@ -1,113 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../kapusta'
4
+ require_relative 'formatter/ast_helpers'
5
+ require_relative 'formatter/cli'
6
+ require_relative 'formatter/line_helpers'
7
+ require_relative 'formatter/validator'
4
8
 
5
9
  module Kapusta
6
10
  class Formatter
7
11
  MAX_WIDTH = 80
8
12
  INDENT = 2
9
13
  STDIN_PATH = '-'
10
-
11
- PIPELINE_FORMS = %w[-> ->> -?> -?>> doto].freeze
14
+ BODY_ONLY_HEADS = %w[do finally].freeze
15
+ SINGLE_PREFIX_BODY_HEADS = %w[
16
+ while when unless for each icollect collect fcollect accumulate faccumulate module
17
+ ].freeze
18
+ CASE_HEADS = %w[case match].freeze
19
+ private_constant :BODY_ONLY_HEADS, :SINGLE_PREFIX_BODY_HEADS, :CASE_HEADS
20
+ include ASTHelpers
21
+ include CLI
22
+ include LineHelpers
23
+ include Validator
12
24
 
13
25
  def self.format(source, path: nil)
14
26
  new([]).send(:format_source, source, path)
15
27
  end
16
28
 
17
- def initialize(argv)
18
- @mode = :stdout
19
- @files = []
20
- @version = false
21
- parse_args(argv)
22
- end
23
-
24
- def run
25
- if @version
26
- puts "kapfmt #{Kapusta::VERSION}"
27
- return 0
28
- end
29
-
30
- validate_args!
31
-
32
- formatted = @files.map do |path|
33
- original = read_source(path)
34
- validate_kapusta_source(original, path)
35
- [path, original, format_source(original, path)]
36
- end
37
-
38
- case @mode
39
- when :stdout
40
- $stdout.write(formatted.first[2])
41
- when :fix
42
- formatted.each do |path, _original, rewritten|
43
- raise Error, 'Cannot use --fix with stdin (-).' if stdin_path?(path)
44
-
45
- File.write(path, rewritten)
46
- end
47
- when :check
48
- dirty = formatted.reject { |_path, original, rewritten| original == rewritten }
49
- dirty.each do |path, _original, _rewritten|
50
- warn "Not formatted: #{path}"
51
- end
52
- return 1 unless dirty.empty?
53
- end
54
-
55
- 0
56
- rescue Kapusta::Error => e
57
- warn e.formatted
58
- 1
59
- end
60
-
61
29
  private
62
30
 
63
- def validate_kapusta_source(source, path)
64
- Kapusta::Compiler.compile(source, path:)
65
- end
66
-
67
- def parse_args(argv)
68
- argv.each do |arg|
69
- case arg
70
- when '--fix'
71
- ensure_mode!(:fix)
72
- when '--check'
73
- ensure_mode!(:check)
74
- when '--version', '-v'
75
- @version = true
76
- when '--help', '-h'
77
- print_help
78
- exit 0
79
- else
80
- @files << arg
81
- end
82
- end
83
- end
84
-
85
- def ensure_mode!(mode)
86
- raise Error, 'Use at most one of --fix or --check.' if @mode != :stdout && @mode != mode
87
-
88
- @mode = mode
89
- end
90
-
91
- def validate_args!
92
- raise Error, 'Usage: kapfmt [--fix] [--check] FILENAME...' if @files.empty?
93
- raise Error, 'stdin (-) may only be specified once.' if @files.count { |path| stdin_path?(path) } > 1
94
- raise Error, 'Cannot use --fix with stdin (-).' if @mode == :fix && @files.any? { |path| stdin_path?(path) }
95
-
96
- return unless @mode == :stdout && @files.length != 1
97
-
98
- raise Error, 'Without --fix or --check, kapfmt accepts exactly one file.'
99
- end
100
-
101
- def read_source(path)
102
- return File.read(path) unless stdin_path?(path)
103
-
104
- @stdin_read ||= false
105
- raise Error, 'stdin (-) may only be specified once.' if @stdin_read
106
-
107
- @stdin_read = true
108
- $stdin.read
109
- end
110
-
111
31
  def format_source(source, path = nil)
112
32
  forms = Reader.read_all(source, preserve_comments: true)
113
33
  entries = top_level_entries(forms)
@@ -125,10 +45,6 @@ module Kapusta
125
45
  raise Error.new(e.message, path:)
126
46
  end
127
47
 
128
- def separator_for(_previous, _current)
129
- "\n"
130
- end
131
-
132
48
  def top_level_entries(forms)
133
49
  entries = []
134
50
  leading_comments = []
@@ -160,18 +76,6 @@ module Kapusta
160
76
  parts.join("\n")
161
77
  end
162
78
 
163
- def comment?(form)
164
- form.is_a?(Comment)
165
- end
166
-
167
- def blank_line?(form)
168
- form.is_a?(BlankLine)
169
- end
170
-
171
- def non_semantic?(form)
172
- comment?(form) || blank_line?(form)
173
- end
174
-
175
79
  def render(form, indent, layout: nil, top_level: false, force_expand: false)
176
80
  flat = flat_render(form)
177
81
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(form, top_level:, layout:)
@@ -200,7 +104,11 @@ module Kapusta
200
104
  rendered = render(inner, indent + prefix.length, force_expand:)
201
105
  lines = rendered.lines(chomp: true)
202
106
  pad = ' ' * prefix.length
203
- lines.each_with_index.map { |line, i| i.zero? ? "#{prefix}#{line}" : "#{pad}#{line}" }.join("\n")
107
+ lines.each_with_index.map do |line, i|
108
+ next '' if line.empty?
109
+
110
+ i.zero? ? "#{prefix}#{line}" : "#{pad}#{line}"
111
+ end.join("\n")
204
112
  end
205
113
 
206
114
  def flat_render(form)
@@ -212,33 +120,11 @@ module Kapusta
212
120
  when Sym
213
121
  form.name
214
122
  when Vec
215
- return if contains_comments?(form.items)
216
- return if multiline_in_source?(form)
217
-
218
- rendered = form.items.map { |item| flat_render(item) }
219
- return if rendered.any?(&:nil?)
220
-
221
- "[#{rendered.join(' ')}]"
123
+ flat_render_vec(form)
222
124
  when HashLit
223
- return if contains_comments?(form.entries)
224
- return if multiline_in_source?(form)
225
-
226
- rendered = form.pairs.map { |key, value| flat_hash_pair(key, value) }
227
- return if rendered.any?(&:nil?)
228
-
229
- "{#{rendered.join(' ')}}"
125
+ flat_render_hash(form)
230
126
  when List
231
- return render_sigil(form) if form.sigil
232
- return if contains_comments?(form.items)
233
- return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
234
- return if multiline_in_source?(form)
235
- return if let_with_multiple_bindings?(form)
236
- return if let_with_nested_binding_value?(form)
237
-
238
- rendered = form.items.map { |item| flat_render(item) }
239
- return if rendered.any?(&:nil?)
240
-
241
- "(#{rendered.join(' ')})"
127
+ flat_render_list(form)
242
128
  when Quasiquote
243
129
  inner = flat_render(form.form)
244
130
  inner ? "`#{inner}" : nil
@@ -255,46 +141,77 @@ module Kapusta
255
141
  end
256
142
  end
257
143
 
144
+ def flat_render_vec(vec)
145
+ return if contains_comments?(vec.items)
146
+ return if multiline_in_source?(vec)
147
+
148
+ flat_delimited_render(vec.items, '[', ']') { |item| flat_render(item) }
149
+ end
150
+
151
+ def flat_render_hash(hash)
152
+ return if contains_comments?(hash.entries)
153
+ return if multiline_in_source?(hash)
154
+
155
+ flat_hash_render(hash.pairs)
156
+ end
157
+
158
+ def flat_render_list(list)
159
+ return render_sigil(list) if list.sigil
160
+ return if contains_comments?(list.items)
161
+ return flat_render_hashfn(list) if hashfn_literal?(list)
162
+ return if multiline_in_source?(list)
163
+ return if let_with_multiple_bindings?(list)
164
+ return if let_with_nested_binding_value?(list)
165
+
166
+ flat_delimited_render(list.items, '(', ')') { |item| flat_render(item) }
167
+ end
168
+
169
+ def flat_render_hashfn(list)
170
+ rendered = flat_render(semantic_items(list.items)[1])
171
+ "##{rendered}" if rendered
172
+ end
173
+
258
174
  def render_list(list, indent, top_level: false)
259
175
  return '()' if list.items.empty?
260
176
  return "##{render(semantic_items(list.items)[1], indent, top_level:)}" if hashfn_literal?(list)
261
177
 
262
- head = list_head(list)
263
- return render_generic_list(list, indent) unless head
178
+ return render_generic_list(list, indent) unless list_head(list)
264
179
 
265
- head_name = head.is_a?(Sym) ? head.name : nil
180
+ name = head_name(list)
266
181
  raw_args = list_raw_rest(list)
267
182
 
268
- case head_name
269
- when 'fn', 'defn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
183
+ case name
184
+ when *Compiler::Language::FUNCTION_DEFINITION_HEADS, 'macro'
185
+ render_fn(name, list, indent, top_level:)
270
186
  when 'let' then render_let(list, indent)
271
- when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
187
+ when *BODY_ONLY_HEADS then render_prefix_body_form(name, [], raw_args, indent)
272
188
  when 'try' then render_try(list, indent)
273
- when 'while', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect', 'accumulate', 'faccumulate'
274
- raw_prefix, raw_body = split_raw_items(raw_args, 1)
275
- render_prefix_body_form(head_name, raw_prefix, raw_body, indent)
276
- when 'module'
277
- raw_prefix, raw_body = split_raw_items(raw_args, 1)
278
- render_prefix_body_form('module', raw_prefix, raw_body, indent)
189
+ when *SINGLE_PREFIX_BODY_HEADS then render_single_prefix_body_form(name, raw_args, indent)
279
190
  when 'class' then render_class(list, indent)
280
191
  when 'catch' then render_catch(list, indent)
281
192
  when 'if' then render_if(list, indent)
282
- when 'case', 'match'
283
- if contains_comments?(raw_args)
284
- render_sequential_head_form(head_name, raw_args, indent)
285
- else
286
- render_case(head_name, list_rest(list), indent)
287
- end
288
- when *PIPELINE_FORMS then render_pipeline(head_name, raw_args, indent)
193
+ when *CASE_HEADS then render_case_or_match(name, list, raw_args, indent)
194
+ when *Compiler::Language::PIPELINE_HEADS then render_pipeline(name, raw_args, indent)
289
195
  else
290
196
  render_call(list, indent)
291
197
  end
292
198
  end
293
199
 
200
+ def render_single_prefix_body_form(head, raw_args, indent)
201
+ raw_prefix, raw_body = split_raw_items(raw_args, 1)
202
+ render_prefix_body_form(head, raw_prefix, raw_body, indent)
203
+ end
204
+
205
+ def render_case_or_match(head, list, raw_args, indent)
206
+ return render_sequential_head_form(head, raw_args, indent) if contains_comments?(raw_args)
207
+
208
+ render_case(head, list_rest(list), indent)
209
+ end
210
+
294
211
  def render_fn(head, list, indent, top_level: false)
295
212
  args = list_rest(list)
296
213
  raw_args = list_raw_rest(list)
297
- prefix_length = args[0].is_a?(Sym) && args[1].is_a?(Vec) ? 2 : 1
214
+ prefix_length = Compiler::Language.parse_function_args(args)&.prefix_length || 1
298
215
  raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
299
216
  force = top_level || fn_body_has_quasi_list?(raw_body)
300
217
  render_prefix_body_form(head, raw_prefix, raw_body, indent, force_body_multiline: force)
@@ -312,8 +229,8 @@ module Kapusta
312
229
  def render_class(list, indent)
313
230
  args = list_rest(list)
314
231
  raw_args = list_raw_rest(list)
315
- prefix = args[1].is_a?(Vec) ? args.take(2) : args.take(1)
316
- raw_prefix, raw_body = split_raw_items(raw_args, prefix.length)
232
+ prefix_length = Compiler::Language.parse_class_args(args).prefix_length
233
+ raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
317
234
  render_prefix_body_form('class', raw_prefix, raw_body, indent)
318
235
  end
319
236
 
@@ -341,10 +258,10 @@ module Kapusta
341
258
  end
342
259
 
343
260
  def render_let(list, indent)
344
- bindings = list_rest(list).first
261
+ parsed = Compiler::Language.parse_let_args(list_rest(list))
262
+ bindings = parsed.bindings
345
263
  raw_args = list_raw_rest(list)
346
264
  raw_prefix, raw_body = split_raw_items(raw_args, 1)
347
- body = list_rest(list).drop(1)
348
265
  unless bindings.is_a?(Vec)
349
266
  return render_prefix_body_form('let', raw_prefix, raw_body, indent,
350
267
  layouts: [:pairwise])
@@ -358,7 +275,7 @@ module Kapusta
358
275
  rendered_bindings = render_let_bindings(bindings, indent)
359
276
  lines = rendered_bindings.lines(chomp: true)
360
277
  lines[0] = "(let #{lines[0]}"
361
- body.each do |form|
278
+ parsed.body.each do |form|
362
279
  lines << indent_block(render(form, indent + INDENT), INDENT)
363
280
  end
364
281
  append_suffix(lines, ')')
@@ -406,52 +323,50 @@ module Kapusta
406
323
  end
407
324
 
408
325
  body_forms.each do |form|
409
- if blank_line?(form)
410
- lines << ''
411
- next
412
- end
413
- if comment?(form)
414
- lines << indent_block(render(form, indent + INDENT), INDENT)
415
- next
416
- end
417
-
418
- body = render(
419
- form,
420
- indent + INDENT,
421
- force_expand: force_body_multiline && force_multiline_body?(form)
422
- )
423
- lines << indent_block(body, INDENT)
326
+ append_body_form(lines, form, indent, force_body_multiline:)
424
327
  end
425
328
 
426
329
  append_suffix(lines, ')')
427
330
  end
428
331
 
332
+ def append_body_form(lines, form, indent, force_body_multiline: false)
333
+ if blank_line?(form)
334
+ lines << ''
335
+ return
336
+ end
337
+ if comment?(form)
338
+ lines << indent_block(render(form, indent + INDENT), INDENT)
339
+ return
340
+ end
341
+
342
+ body = render(
343
+ form,
344
+ indent + INDENT,
345
+ force_expand: force_body_multiline && force_multiline_body?(form)
346
+ )
347
+ lines << indent_block(body, INDENT)
348
+ end
349
+
429
350
  def render_if(list, indent)
430
351
  args = list_rest(list)
431
352
  return render_sequential_head_form('if', list_raw_rest(list), indent) if contains_comments?(list_raw_rest(list))
432
353
 
433
354
  lines = []
434
- hanging = ' ' * '(if '.length
355
+ hanging = if_hanging
435
356
 
436
357
  if args.length == 3
437
358
  flat = flat_render(list)
438
359
  return flat if inline_three_arg_if?(args) && flat && fits?(flat, indent)
439
360
 
440
- lines << "(if #{render(args[0], indent + '(if '.length)}"
441
- lines << prefix_continuation(hanging, render(args[1], indent + '(if '.length))
442
- lines << prefix_continuation(hanging, render(args[2], indent + '(if '.length))
361
+ append_if_form(lines, args[0], indent, '(if ')
362
+ append_if_form(lines, args[1], indent, hanging)
363
+ append_if_form(lines, args[2], indent, hanging)
443
364
  return append_suffix(lines, ')')
444
365
  end
445
366
 
446
367
  index = 0
447
368
  if args.length >= 2
448
- first_pair = render_pair(args[0], args[1], indent + '(if '.length)
449
- if first_pair
450
- lines << "(if #{first_pair}"
451
- else
452
- lines << "(if #{render(args[0], indent + '(if '.length)}"
453
- lines << prefix_continuation(hanging, render(args[1], indent + '(if '.length))
454
- end
369
+ append_if_pair(lines, args[0], args[1], indent, '(if ')
455
370
  index = 2
456
371
  else
457
372
  lines << '(if'
@@ -460,16 +375,10 @@ module Kapusta
460
375
  while index < args.length
461
376
  remaining = args.length - index
462
377
  if remaining >= 2
463
- pair = render_pair(args[index], args[index + 1], indent + '(if '.length)
464
- if pair
465
- lines << "#{hanging}#{pair}"
466
- else
467
- lines << prefix_continuation(hanging, render(args[index], indent + '(if '.length))
468
- lines << prefix_continuation(hanging, render(args[index + 1], indent + '(if '.length))
469
- end
378
+ append_if_pair(lines, args[index], args[index + 1], indent, hanging)
470
379
  index += 2
471
380
  else
472
- lines << prefix_continuation(hanging, render(args[index], indent + '(if '.length))
381
+ append_if_form(lines, args[index], indent, hanging)
473
382
  index += 1
474
383
  end
475
384
  end
@@ -477,19 +386,44 @@ module Kapusta
477
386
  append_suffix(lines, ')')
478
387
  end
479
388
 
389
+ def append_if_pair(lines, condition, branch, indent, prefix)
390
+ pair = render_pair(condition, branch, if_value_indent(indent))
391
+ if pair
392
+ lines << "#{prefix}#{pair}"
393
+ else
394
+ append_if_form(lines, condition, indent, prefix)
395
+ append_if_form(lines, branch, indent, if_hanging)
396
+ end
397
+ end
398
+
399
+ def append_if_form(lines, form, indent, prefix)
400
+ lines << prefix_continuation(prefix, render(form, if_value_indent(indent)))
401
+ end
402
+
403
+ def if_value_indent(indent)
404
+ indent + '(if '.length
405
+ end
406
+
407
+ def if_hanging
408
+ ' ' * '(if '.length
409
+ end
410
+
480
411
  def prefix_continuation(prefix, rendered)
481
- first_line, *rest = rendered.lines(chomp: true)
412
+ prefix_lines(prefix, rendered.lines(chomp: true)).join("\n")
413
+ end
414
+
415
+ def prefix_lines(prefix, lines)
416
+ first_line, *rest = lines
482
417
  pad = ' ' * prefix.length
483
- ["#{prefix}#{first_line}", *rest.map { |line| "#{pad}#{line}" }].join("\n")
418
+ ["#{prefix}#{first_line}", *rest.map { |line| line.empty? ? '' : "#{pad}#{line}" }]
484
419
  end
485
420
 
486
421
  def render_case(head, args, indent)
487
- subject = args.first
488
- clauses = args.drop(1)
422
+ parsed = Compiler::Language.parse_case_args(args)
489
423
  lines = ['(case']
490
424
 
491
- if subject
492
- rendered_subject = render(subject, indent + INDENT)
425
+ if parsed.subject
426
+ rendered_subject = render(parsed.subject, indent + INDENT)
493
427
  if single_line?(rendered_subject) && fits?("(#{head} #{rendered_subject}", indent)
494
428
  lines[0] = "(#{head} #{rendered_subject}"
495
429
  else
@@ -498,24 +432,27 @@ module Kapusta
498
432
  end
499
433
  end
500
434
 
501
- clauses.each_slice(2) do |pair|
502
- pattern, value = pair
503
- if pair.length == 2
504
- pair = render_pair(pattern, value, indent + INDENT)
505
- if pair
506
- lines << indent_block(pair, INDENT)
507
- else
508
- lines << indent_block(render(pattern, indent + INDENT), INDENT)
509
- lines << indent_block(render(value, indent + INDENT), INDENT)
510
- end
511
- else
512
- lines << indent_block(render(pattern, indent + INDENT), INDENT)
513
- end
514
- end
435
+ parsed.arm_pairs.each { |arm| append_case_arm(lines, arm, indent) }
515
436
 
516
437
  append_suffix(lines, ')')
517
438
  end
518
439
 
440
+ def append_case_arm(lines, arm, indent)
441
+ pattern, value = arm
442
+ unless arm.length == 2
443
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
444
+ return
445
+ end
446
+
447
+ rendered_pair = render_pair(pattern, value, indent + INDENT)
448
+ if rendered_pair
449
+ lines << indent_block(rendered_pair, INDENT)
450
+ else
451
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
452
+ lines << indent_block(render(value, indent + INDENT), INDENT)
453
+ end
454
+ end
455
+
519
456
  def render_pipeline(head, args, indent)
520
457
  base = "(#{head}"
521
458
  lines = [base]
@@ -547,7 +484,7 @@ module Kapusta
547
484
  append_suffix(lines, ')')
548
485
  end
549
486
 
550
- def render_call(list, indent)
487
+ def render_call(list, indent, force_hang: false)
551
488
  head = flat_render(list_head(list))
552
489
  raise Error, "cannot format form head: #{list_head(list).inspect}" unless head
553
490
 
@@ -555,7 +492,7 @@ module Kapusta
555
492
  lines = [base]
556
493
  args = list_raw_rest(list)
557
494
  semantic_length = semantic_items(args).length
558
- hang_subsequent_args = hang_call_args?(list, indent)
495
+ hang_subsequent_args = force_hang || hang_call_args?(list, base, indent)
559
496
 
560
497
  semantic_index = 0
561
498
  hanging = nil
@@ -565,6 +502,8 @@ module Kapusta
565
502
  next
566
503
  elsif semantic_index.zero?
567
504
  hanging = append_first_call_arg(lines, arg, base, indent, semantic_length)
505
+ elsif append_packed_call_arg?(list, lines, arg, indent)
506
+ nil
568
507
  elsif hanging && hang_subsequent_args
569
508
  lines << prefix_continuation(hanging, render(arg, indent + hanging.length))
570
509
  else
@@ -597,33 +536,172 @@ module Kapusta
597
536
  hanging
598
537
  end
599
538
 
600
- def hang_call_args?(list, indent)
601
- return false unless operator_call?(list)
539
+ def append_packed_call_arg?(list, lines, arg, indent)
540
+ return false unless packable_call_arg?(list, arg)
541
+
542
+ packed = packed_call_arg(lines.last, list, arg, indent)
543
+ if packed
544
+ lines[-1] = packed.first
545
+ lines.concat(packed.drop(1))
546
+ return true
547
+ end
548
+
549
+ false
550
+ end
551
+
552
+ def packable_call_arg?(list, arg)
553
+ return false if fn_form?(arg)
554
+ return true if packable_collection_call_arg?(list, arg)
555
+
556
+ pack_call_args?(list) && flat_render(arg)
557
+ end
558
+
559
+ def packable_collection_call_arg?(list, arg)
560
+ return true if arg.is_a?(Vec)
561
+
562
+ arg.is_a?(HashLit) && packable_hash_call_arg?(list)
563
+ end
564
+
565
+ def packed_call_arg(current_line, list, arg, indent)
566
+ packable_call_arg_renderings(list, arg, indent + current_line.length + 1).each do |rendered|
567
+ first_line, *rest = rendered.lines(chomp: true)
568
+ candidate = "#{current_line} #{first_line}"
569
+ next unless inline_arg_fits?(candidate, indent)
570
+
571
+ hanging = ' ' * (current_line.length + 1)
572
+ return [candidate, *rest.map { |line| "#{hanging}#{line}" }]
573
+ end
574
+
575
+ nil
576
+ end
577
+
578
+ def packable_call_arg_renderings(list, arg, indent)
579
+ [
580
+ flat_collection_render(arg),
581
+ flat_render(arg),
582
+ multiline_packable_call_arg_rendering(list, arg, indent)
583
+ ].compact.uniq
584
+ end
585
+
586
+ def multiline_packable_call_arg_rendering(list, arg, indent)
587
+ return render(arg, indent) if arg.is_a?(Vec)
588
+ return render(arg, indent) if local_hash_call_arg?(list, arg)
589
+
590
+ nil
591
+ end
592
+
593
+ def local_hash_call_arg?(list, arg)
594
+ arg.is_a?(HashLit) && head_name(list) == 'local'
595
+ end
596
+
597
+ def flat_collection_render(form)
598
+ case form
599
+ when Vec
600
+ flat_delimited_render(form.items, '[', ']') { |item| flat_render(item) }
601
+ when HashLit
602
+ return if contains_comments?(form.entries)
603
+
604
+ flat_hash_render(form.pairs)
605
+ end
606
+ end
607
+
608
+ def flat_delimited_render(items, open, close)
609
+ return if contains_comments?(items)
610
+
611
+ rendered = items.map do |item|
612
+ item.is_a?(Array) ? yield(*item) : yield(item)
613
+ end
614
+ return if rendered.any?(&:nil?)
615
+
616
+ "#{open}#{rendered.join(' ')}#{close}"
617
+ end
618
+
619
+ def flat_hash_render(pairs)
620
+ rendered = pairs.map { |pair| flat_hash_pair(pair) }
621
+ return if rendered.any?(&:nil?)
622
+
623
+ "{#{rendered.join(' ')}}"
624
+ end
625
+
626
+ def hang_call_args?(list, base, indent)
627
+ return true if source_hangs_call_args?(list, base)
628
+ return true if set_function_value?(list)
602
629
 
603
630
  flat = flat_call_render(list)
604
- flat && !fits?(flat, indent)
631
+ return false unless flat
632
+
633
+ overflowing = !fits?(flat, indent)
634
+ return true if overflowing && hanging_overflow_call?(list)
635
+
636
+ operator_call?(list) && overflowing
637
+ end
638
+
639
+ def pack_call_args?(list)
640
+ head_name(list)&.match?(/\A[a-z0-9_-][\w-]*\./)
641
+ end
642
+
643
+ def packable_hash_call_arg?(list)
644
+ head_name(list) == 'local' || pack_call_args?(list)
645
+ end
646
+
647
+ def hanging_overflow_call?(list)
648
+ hash_first_call_arg?(list) || pack_call_args?(list)
649
+ end
650
+
651
+ def hash_first_call_arg?(list)
652
+ list_rest(list).first.is_a?(HashLit)
653
+ end
654
+
655
+ def set_function_value?(list)
656
+ head_name(list) == 'set' && fn_form?(list_rest(list)[1])
657
+ end
658
+
659
+ def source_hangs_call_args?(list, base)
660
+ return false unless list.respond_to?(:column) && list.column
661
+
662
+ args = list_rest(list)
663
+ return false if args.length < 2
664
+
665
+ expected_column = list.column + base.length + 1
666
+ args.drop(1).all? do |arg|
667
+ arg.respond_to?(:column) && arg.column == expected_column
668
+ end
605
669
  end
606
670
 
607
671
  def operator_call?(list)
608
- head = list_head(list)
609
- head.is_a?(Sym) && head.name.match?(/\A[^\w.]+\z/)
672
+ head_name(list)&.match?(/\A[^\w.]+\z/)
673
+ end
674
+
675
+ def ordinary_call_form?(list)
676
+ name = head_name(list)
677
+ name && !Compiler::Language::SPECIAL_FORMS.include?(name)
610
678
  end
611
679
 
612
680
  def flat_call_render(list)
613
681
  head = flat_render(list_head(list))
614
682
  return unless head
615
683
 
616
- rendered_args = semantic_items(list_raw_rest(list)).map { |arg| flat_render(arg) }
684
+ rendered_args = semantic_items(list_raw_rest(list)).map { |arg| flat_call_arg_render(arg) }
617
685
  return if rendered_args.any?(&:nil?)
618
686
 
619
687
  "(#{[head, *rendered_args].join(' ')})"
620
688
  end
621
689
 
690
+ def flat_call_arg_render(arg)
691
+ collection = flat_collection_render(arg)
692
+ return collection if collection
693
+
694
+ flat_render(arg)
695
+ end
696
+
622
697
  def render_vec(vec, indent, layout: nil, top_level: false, force_expand: false)
623
698
  flat = flat_render(vec)
624
699
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
625
700
 
626
701
  return render_pairwise_vec(vec, indent) if layout == :pairwise && !contains_comments?(vec.items)
702
+ if multiline_in_source?(vec) && multiline_vec_items_should_separate?(vec) && !contains_comments?(vec.items)
703
+ return render_multiline_vec(vec, indent)
704
+ end
627
705
  return render_filled_vec(vec, indent) if !contains_comments?(vec.items) && !vec.items.empty?
628
706
 
629
707
  lines = ['[']
@@ -633,6 +711,42 @@ module Kapusta
633
711
  append_suffix(lines, ']')
634
712
  end
635
713
 
714
+ def render_multiline_vec(vec, indent)
715
+ return '[]' if vec.items.empty?
716
+
717
+ lines = []
718
+ vec.items.each_with_index do |item, idx|
719
+ prefix = idx.zero? ? '[' : ' '
720
+ lines.concat(prefix_lines(prefix, render_multiline_vec_item(item, indent + 1).lines.map(&:chomp)))
721
+ end
722
+ lines[-1] = "#{lines[-1]}]"
723
+ lines.join("\n")
724
+ end
725
+
726
+ def render_multiline_vec_item(item, indent)
727
+ return render_call(item, indent, force_hang: true) if hanging_multiline_vec_call_item?(item)
728
+
729
+ render(item, indent)
730
+ end
731
+
732
+ def hanging_multiline_vec_call_item?(item)
733
+ item.is_a?(List) && multiline_in_source?(item) && ordinary_call_form?(item)
734
+ end
735
+
736
+ def multiline_vec_items_should_separate?(vec)
737
+ multiline_vec_items_on_separate_lines?(vec) || semantic_items(vec.items).all? { |item| collection?(item) }
738
+ end
739
+
740
+ def multiline_vec_items_on_separate_lines?(vec)
741
+ items = semantic_items(vec.items)
742
+ return false if items.length < 2
743
+
744
+ lines = items.filter_map { |item| item.line if item.respond_to?(:line) }
745
+ return true if lines.length < 2
746
+
747
+ lines.uniq.length == lines.length
748
+ end
749
+
636
750
  def render_filled_vec(vec, indent)
637
751
  output_lines = ['[']
638
752
 
@@ -688,16 +802,16 @@ module Kapusta
688
802
  def render_let_bindings(bindings, indent)
689
803
  return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
690
804
 
691
- hanging = render_hanging_pairwise_vec(bindings)
805
+ hanging = render_hanging_pairwise_vec(bindings, indent)
692
806
  hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
693
807
  end
694
808
 
695
- def render_hanging_pairwise_vec(vec)
809
+ def render_hanging_pairwise_vec(vec, indent)
696
810
  pairs = vec.items.each_slice(2).to_a
697
811
  return unless pairs.all? { |pair| pair.length == 2 }
698
812
 
699
813
  rendered_pairs = pairs.map do |left, right|
700
- render_binding_pair(left, right)
814
+ render_binding_pair(left, right, indent)
701
815
  end
702
816
  return if rendered_pairs.any?(&:nil?)
703
817
 
@@ -725,7 +839,7 @@ module Kapusta
725
839
 
726
840
  key, value = entry
727
841
  first_pair = output_lines.empty?
728
- if hash_shorthand?(key, value)
842
+ if hash_pair_shorthand?(entry)
729
843
  output_lines << "#{first_pair ? '{' : ' '}: #{value.name}"
730
844
  next
731
845
  end
@@ -745,8 +859,9 @@ module Kapusta
745
859
  output_lines.join("\n")
746
860
  end
747
861
 
748
- def flat_hash_pair(key, value)
749
- return ": #{value.name}" if hash_shorthand?(key, value)
862
+ def flat_hash_pair(pair)
863
+ key, value = pair
864
+ return ": #{value.name}" if hash_pair_shorthand?(pair)
750
865
 
751
866
  rendered_value = flat_render(value)
752
867
  return unless rendered_value
@@ -772,14 +887,15 @@ module Kapusta
772
887
  fits?(pair, indent) ? pair : nil
773
888
  end
774
889
 
775
- def render_binding_pair(left, right)
890
+ def render_binding_pair(left, right, indent)
776
891
  left_rendered = flat_render(left)
777
892
  return unless left_rendered
778
893
 
779
- right_rendered = render(right, '(let ['.length + left_rendered.length + 1)
894
+ right_indent = indent + '(let ['.length + left_rendered.length + 1
895
+ right_rendered = render(right, right_indent)
780
896
  first_line, *rest = right_rendered.lines(chomp: true)
781
897
  pair = "#{left_rendered} #{first_line}"
782
- return unless pair.length <= MAX_WIDTH
898
+ return unless fits?(pair, indent + '(let ['.length)
783
899
 
784
900
  return pair if rest.empty?
785
901
 
@@ -787,8 +903,8 @@ module Kapusta
787
903
  ([pair] + rest.map { |line| "#{continuation}#{line}" }).join("\n")
788
904
  end
789
905
 
790
- def hash_shorthand?(key, value)
791
- key.is_a?(Symbol) && value.is_a?(Sym) && key == Kapusta.kebab_to_snake(value.name).to_sym
906
+ def hash_pair_shorthand?(pair)
907
+ pair.respond_to?(:shorthand?) && pair.shorthand?
792
908
  end
793
909
 
794
910
  def hashfn_literal?(form)
@@ -809,11 +925,10 @@ module Kapusta
809
925
  return true unless head.is_a?(Sym)
810
926
 
811
927
  case head.name
812
- when 'fn', 'lambda', 'λ', 'macro', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect',
813
- 'accumulate', 'faccumulate'
928
+ when *Compiler::Language::FLAT_BODY_HEADS
814
929
  !top_level
815
930
  else
816
- !%w[let case match try catch finally do -> ->> -?> -?>> doto].include?(head.name)
931
+ !Compiler::Language.never_flat_head?(head.name)
817
932
  end
818
933
  end
819
934
 
@@ -826,18 +941,13 @@ module Kapusta
826
941
  return false unless head.is_a?(Sym)
827
942
 
828
943
  case head.name
829
- when 'if', 'case', 'match', 'let', 'try', 'catch', 'finally', 'do', 'for', '->', '->>', '-?>', '-?>>', 'doto',
830
- 'fn', 'lambda', 'λ', 'macro'
944
+ when *Compiler::Language::MULTILINE_BODY_HEADS
831
945
  true
832
946
  else
833
947
  false
834
948
  end
835
949
  end
836
950
 
837
- def multiline_in_source?(form)
838
- form.respond_to?(:multiline_source) && form.multiline_source
839
- end
840
-
841
951
  def let_with_multiple_bindings?(form)
842
952
  head = list_head(form)
843
953
  return false unless head.is_a?(Sym) && head.name == 'let'
@@ -860,25 +970,12 @@ module Kapusta
860
970
  end
861
971
  end
862
972
 
863
- def contains_collection?(form)
864
- case form
865
- when List then semantic_items(form.items).any? { |item| collection?(item) }
866
- when Vec then form.items.any? { |item| collection?(item) }
867
- when HashLit then form.pairs.any? { |k, v| collection?(k) || collection?(v) }
868
- else false
869
- end
870
- end
871
-
872
- def collection?(form)
873
- form.is_a?(List) || form.is_a?(Vec) || form.is_a?(HashLit)
874
- end
875
-
876
973
  def fn_form?(form)
877
974
  return false unless form.is_a?(List)
878
975
 
879
976
  head = list_head(form)
880
977
  head.is_a?(Sym) &&
881
- %w[fn lambda λ].include?(head.name)
978
+ Compiler::Language.function_head?(head.name)
882
979
  end
883
980
 
884
981
  def inline_three_arg_if?(args)
@@ -897,33 +994,6 @@ module Kapusta
897
994
  end
898
995
  end
899
996
 
900
- def stdin_path?(path)
901
- path == STDIN_PATH
902
- end
903
-
904
- def fits?(text, indent)
905
- !text.include?("\n") && indent + text.length <= MAX_WIDTH
906
- end
907
-
908
- def single_line?(text)
909
- !text.include?("\n")
910
- end
911
-
912
- def indent_block(text, amount)
913
- prefix = ' ' * amount
914
- text.lines.map { |line| "#{prefix}#{line}" }.join
915
- end
916
-
917
- def append_suffix(lines, suffix)
918
- updated = lines.dup
919
- if updated[-1].lstrip.start_with?(';')
920
- updated << suffix
921
- else
922
- updated[-1] = "#{updated[-1]}#{suffix}"
923
- end
924
- updated.join("\n")
925
- end
926
-
927
997
  def render_generic_list(list, indent)
928
998
  lines = ['(']
929
999
  list.items.each do |item|
@@ -959,47 +1029,6 @@ module Kapusta
959
1029
  append_suffix(lines, ')')
960
1030
  end
961
1031
 
962
- def contains_comments?(items)
963
- items.any? { |item| non_semantic?(item) }
964
- end
965
-
966
- def semantic_items(items)
967
- items.reject { |item| non_semantic?(item) }
968
- end
969
-
970
- def list_head(list)
971
- semantic_items(list.items).first
972
- end
973
-
974
- def list_rest(list)
975
- semantic_items(list.items).drop(1)
976
- end
977
-
978
- def list_raw_rest(list)
979
- index = list.items.index { |item| !comment?(item) }
980
- return list.items if index.nil?
981
-
982
- list.items[(index + 1)..] || []
983
- end
984
-
985
- def split_raw_items(items, semantic_count)
986
- split_index = 0
987
- seen = 0
988
-
989
- while split_index < items.length && seen < semantic_count
990
- seen += 1 unless comment?(items[split_index])
991
- split_index += 1
992
- end
993
-
994
- [items.take(split_index), items.drop(split_index)]
995
- end
996
-
997
- def print_help
998
- puts 'Usage: kapfmt [--fix] [--check] FILENAME...'
999
- puts
1000
- puts 'Formats Kapusta source using the built-in Kapusta reader and pretty-printer.'
1001
- end
1002
-
1003
1032
  class Error < Kapusta::Error; end
1004
1033
  end
1005
1034
  end