kapusta 0.13.2 → 0.15.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 (100) 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/digit-tools.kap +24 -0
  16. data/examples/doto-hygiene.kap +2 -2
  17. data/examples/doto.kap +2 -2
  18. data/examples/egg-count.kap +1 -1
  19. data/examples/excel-column.kap +26 -0
  20. data/examples/exceptions.kap +1 -1
  21. data/examples/falling-drops.kap +7 -7
  22. data/examples/fennel-parity-examples.txt +3 -6
  23. data/examples/good-pairs.kap +18 -0
  24. data/examples/greet.kap +1 -1
  25. data/examples/happy-number.kap +2 -2
  26. data/examples/left-right-difference.kap +60 -0
  27. data/examples/length-of-last-word.kap +4 -4
  28. data/examples/logger-rate-limiter.kap +26 -0
  29. data/examples/manhattan-distance.kap +1 -1
  30. data/examples/maximum-subarray.kap +23 -7
  31. data/examples/minimum-start-value.kap +19 -0
  32. data/examples/move-zeroes.kap +2 -2
  33. data/examples/mruby-runtime-examples.txt +10 -2
  34. data/examples/non-constant-local.kap +1 -1
  35. data/examples/number-of-1-bits.kap +1 -1
  36. data/examples/number-of-steps.kap +1 -1
  37. data/examples/palindrome.kap +2 -2
  38. data/examples/pangram.kap +4 -4
  39. data/examples/pcall.kap +3 -3
  40. data/examples/pipeline.kap +4 -4
  41. data/examples/plus-one.kap +3 -3
  42. data/examples/raindrops.kap +1 -1
  43. data/examples/range-width.kap +14 -0
  44. data/examples/recent-counter.kap +6 -6
  45. data/examples/require-local-args.kap +9 -0
  46. data/examples/require-local.kap +7 -0
  47. data/examples/require-module-local.kap +4 -0
  48. data/examples/require-module.kap +4 -0
  49. data/examples/reverse-integer.kap +1 -1
  50. data/examples/roman-to-integer.kap +3 -3
  51. data/examples/running-sum.kap +20 -0
  52. data/examples/safe-lookup.kap +2 -2
  53. data/examples/single-number.kap +1 -1
  54. data/examples/stack.kap +14 -14
  55. data/examples/subtract-product-sum.kap +1 -1
  56. data/examples/summary-ranges.kap +45 -0
  57. data/examples/threading.kap +5 -5
  58. data/examples/tset.kap +1 -1
  59. data/examples/two-sum-hash.kap +3 -3
  60. data/examples/two-sum.kap +1 -1
  61. data/examples/ugly-number.kap +1 -1
  62. data/examples/underground-system.kap +39 -0
  63. data/examples/valid-parentheses-1.kap +6 -6
  64. data/exe/kapusta-ls +49 -2
  65. data/lib/kapusta/ast.rb +8 -0
  66. data/lib/kapusta/compiler/emitter/bindings.rb +111 -89
  67. data/lib/kapusta/compiler/emitter/collections.rb +32 -40
  68. data/lib/kapusta/compiler/emitter/control_flow.rb +33 -31
  69. data/lib/kapusta/compiler/emitter/expressions.rb +21 -5
  70. data/lib/kapusta/compiler/emitter/interop.rb +172 -48
  71. data/lib/kapusta/compiler/emitter/patterns.rb +12 -14
  72. data/lib/kapusta/compiler/emitter/support.rb +70 -81
  73. data/lib/kapusta/compiler/language.rb +533 -0
  74. data/lib/kapusta/compiler/lua_compat.rb +23 -28
  75. data/lib/kapusta/compiler/macro_expander.rb +30 -30
  76. data/lib/kapusta/compiler/macro_lowerer.rb +12 -24
  77. data/lib/kapusta/compiler/normalizer.rb +25 -17
  78. data/lib/kapusta/compiler.rb +3 -24
  79. data/lib/kapusta/env.rb +2 -2
  80. data/lib/kapusta/errors.rb +2 -1
  81. data/lib/kapusta/formatter/ast_helpers.rb +78 -0
  82. data/lib/kapusta/formatter/cli.rb +125 -0
  83. data/lib/kapusta/formatter/line_helpers.rb +44 -0
  84. data/lib/kapusta/formatter/validator.rb +32 -0
  85. data/lib/kapusta/formatter.rb +363 -325
  86. data/lib/kapusta/lsp/identifier.rb +1 -1
  87. data/lib/kapusta/lsp/rename.rb +21 -11
  88. data/lib/kapusta/lsp/scope_walker.rb +122 -212
  89. data/lib/kapusta/lsp/workspace_index.rb +17 -5
  90. data/lib/kapusta/reader.rb +4 -2
  91. data/lib/kapusta/version.rb +1 -1
  92. data/lib/kapusta.rb +39 -6
  93. data/spec/cli_spec.rb +13 -0
  94. data/spec/examples_errors_spec.rb +3 -1
  95. data/spec/examples_spec.rb +99 -15
  96. data/spec/formatter_spec.rb +246 -0
  97. data/spec/lsp_spec.rb +69 -0
  98. data/spec/require_spec.rb +294 -0
  99. metadata +23 -2
  100. 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
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,78 @@ 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)
191
+ when 'module' then render_module(list, indent)
280
192
  when 'catch' then render_catch(list, indent)
281
193
  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)
194
+ when *CASE_HEADS then render_case_or_match(name, list, raw_args, indent)
195
+ when *Compiler::Language::PIPELINE_HEADS then render_pipeline(name, raw_args, indent)
289
196
  else
290
197
  render_call(list, indent)
291
198
  end
292
199
  end
293
200
 
201
+ def render_single_prefix_body_form(head, raw_args, indent)
202
+ raw_prefix, raw_body = split_raw_items(raw_args, 1)
203
+ render_prefix_body_form(head, raw_prefix, raw_body, indent)
204
+ end
205
+
206
+ def render_case_or_match(head, list, raw_args, indent)
207
+ return render_sequential_head_form(head, raw_args, indent) if contains_comments?(raw_args)
208
+
209
+ render_case(head, list_rest(list), indent)
210
+ end
211
+
294
212
  def render_fn(head, list, indent, top_level: false)
295
213
  args = list_rest(list)
296
214
  raw_args = list_raw_rest(list)
297
- prefix_length = args[0].is_a?(Sym) && args[1].is_a?(Vec) ? 2 : 1
215
+ prefix_length = Compiler::Language.parse_function_args(args)&.prefix_length || 1
298
216
  raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
299
217
  force = top_level || fn_body_has_quasi_list?(raw_body)
300
218
  render_prefix_body_form(head, raw_prefix, raw_body, indent, force_body_multiline: force)
@@ -312,11 +230,19 @@ module Kapusta
312
230
  def render_class(list, indent)
313
231
  args = list_rest(list)
314
232
  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)
233
+ prefix_length = Compiler::Language.parse_class_args(args).prefix_length
234
+ raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
317
235
  render_prefix_body_form('class', raw_prefix, raw_body, indent)
318
236
  end
319
237
 
238
+ def render_module(list, indent)
239
+ args = list_rest(list)
240
+ raw_args = list_raw_rest(list)
241
+ prefix_length = Compiler::Language.parse_module_args(args).prefix_length
242
+ raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
243
+ render_prefix_body_form('module', raw_prefix, raw_body, indent)
244
+ end
245
+
320
246
  def render_try(list, indent)
321
247
  args = list_rest(list)
322
248
  return render_sequential_head_form('try', list_raw_rest(list), indent) if contains_comments?(list_raw_rest(list))
@@ -341,10 +267,10 @@ module Kapusta
341
267
  end
342
268
 
343
269
  def render_let(list, indent)
344
- bindings = list_rest(list).first
270
+ parsed = Compiler::Language.parse_let_args(list_rest(list))
271
+ bindings = parsed.bindings
345
272
  raw_args = list_raw_rest(list)
346
273
  raw_prefix, raw_body = split_raw_items(raw_args, 1)
347
- body = list_rest(list).drop(1)
348
274
  unless bindings.is_a?(Vec)
349
275
  return render_prefix_body_form('let', raw_prefix, raw_body, indent,
350
276
  layouts: [:pairwise])
@@ -358,7 +284,7 @@ module Kapusta
358
284
  rendered_bindings = render_let_bindings(bindings, indent)
359
285
  lines = rendered_bindings.lines(chomp: true)
360
286
  lines[0] = "(let #{lines[0]}"
361
- body.each do |form|
287
+ parsed.body.each do |form|
362
288
  lines << indent_block(render(form, indent + INDENT), INDENT)
363
289
  end
364
290
  append_suffix(lines, ')')
@@ -406,52 +332,50 @@ module Kapusta
406
332
  end
407
333
 
408
334
  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)
335
+ append_body_form(lines, form, indent, force_body_multiline:)
424
336
  end
425
337
 
426
338
  append_suffix(lines, ')')
427
339
  end
428
340
 
341
+ def append_body_form(lines, form, indent, force_body_multiline: false)
342
+ if blank_line?(form)
343
+ lines << ''
344
+ return
345
+ end
346
+ if comment?(form)
347
+ lines << indent_block(render(form, indent + INDENT), INDENT)
348
+ return
349
+ end
350
+
351
+ body = render(
352
+ form,
353
+ indent + INDENT,
354
+ force_expand: force_body_multiline && force_multiline_body?(form)
355
+ )
356
+ lines << indent_block(body, INDENT)
357
+ end
358
+
429
359
  def render_if(list, indent)
430
360
  args = list_rest(list)
431
361
  return render_sequential_head_form('if', list_raw_rest(list), indent) if contains_comments?(list_raw_rest(list))
432
362
 
433
363
  lines = []
434
- hanging = ' ' * '(if '.length
364
+ hanging = if_hanging
435
365
 
436
366
  if args.length == 3
437
367
  flat = flat_render(list)
438
368
  return flat if inline_three_arg_if?(args) && flat && fits?(flat, indent)
439
369
 
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))
370
+ append_if_form(lines, args[0], indent, '(if ')
371
+ append_if_form(lines, args[1], indent, hanging)
372
+ append_if_form(lines, args[2], indent, hanging)
443
373
  return append_suffix(lines, ')')
444
374
  end
445
375
 
446
376
  index = 0
447
377
  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
378
+ append_if_pair(lines, args[0], args[1], indent, '(if ')
455
379
  index = 2
456
380
  else
457
381
  lines << '(if'
@@ -460,16 +384,10 @@ module Kapusta
460
384
  while index < args.length
461
385
  remaining = args.length - index
462
386
  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
387
+ append_if_pair(lines, args[index], args[index + 1], indent, hanging)
470
388
  index += 2
471
389
  else
472
- lines << prefix_continuation(hanging, render(args[index], indent + '(if '.length))
390
+ append_if_form(lines, args[index], indent, hanging)
473
391
  index += 1
474
392
  end
475
393
  end
@@ -477,19 +395,44 @@ module Kapusta
477
395
  append_suffix(lines, ')')
478
396
  end
479
397
 
398
+ def append_if_pair(lines, condition, branch, indent, prefix)
399
+ pair = render_pair(condition, branch, if_value_indent(indent))
400
+ if pair
401
+ lines << "#{prefix}#{pair}"
402
+ else
403
+ append_if_form(lines, condition, indent, prefix)
404
+ append_if_form(lines, branch, indent, if_hanging)
405
+ end
406
+ end
407
+
408
+ def append_if_form(lines, form, indent, prefix)
409
+ lines << prefix_continuation(prefix, render(form, if_value_indent(indent)))
410
+ end
411
+
412
+ def if_value_indent(indent)
413
+ indent + '(if '.length
414
+ end
415
+
416
+ def if_hanging
417
+ ' ' * '(if '.length
418
+ end
419
+
480
420
  def prefix_continuation(prefix, rendered)
481
- first_line, *rest = rendered.lines(chomp: true)
421
+ prefix_lines(prefix, rendered.lines(chomp: true)).join("\n")
422
+ end
423
+
424
+ def prefix_lines(prefix, lines)
425
+ first_line, *rest = lines
482
426
  pad = ' ' * prefix.length
483
- ["#{prefix}#{first_line}", *rest.map { |line| "#{pad}#{line}" }].join("\n")
427
+ ["#{prefix}#{first_line}", *rest.map { |line| line.empty? ? '' : "#{pad}#{line}" }]
484
428
  end
485
429
 
486
430
  def render_case(head, args, indent)
487
- subject = args.first
488
- clauses = args.drop(1)
431
+ parsed = Compiler::Language.parse_case_args(args)
489
432
  lines = ['(case']
490
433
 
491
- if subject
492
- rendered_subject = render(subject, indent + INDENT)
434
+ if parsed.subject
435
+ rendered_subject = render(parsed.subject, indent + INDENT)
493
436
  if single_line?(rendered_subject) && fits?("(#{head} #{rendered_subject}", indent)
494
437
  lines[0] = "(#{head} #{rendered_subject}"
495
438
  else
@@ -498,24 +441,27 @@ module Kapusta
498
441
  end
499
442
  end
500
443
 
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
444
+ parsed.arm_pairs.each { |arm| append_case_arm(lines, arm, indent) }
515
445
 
516
446
  append_suffix(lines, ')')
517
447
  end
518
448
 
449
+ def append_case_arm(lines, arm, indent)
450
+ pattern, value = arm
451
+ unless arm.length == 2
452
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
453
+ return
454
+ end
455
+
456
+ rendered_pair = render_pair(pattern, value, indent + INDENT)
457
+ if rendered_pair
458
+ lines << indent_block(rendered_pair, INDENT)
459
+ else
460
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
461
+ lines << indent_block(render(value, indent + INDENT), INDENT)
462
+ end
463
+ end
464
+
519
465
  def render_pipeline(head, args, indent)
520
466
  base = "(#{head}"
521
467
  lines = [base]
@@ -547,7 +493,7 @@ module Kapusta
547
493
  append_suffix(lines, ')')
548
494
  end
549
495
 
550
- def render_call(list, indent)
496
+ def render_call(list, indent, force_hang: false)
551
497
  head = flat_render(list_head(list))
552
498
  raise Error, "cannot format form head: #{list_head(list).inspect}" unless head
553
499
 
@@ -555,7 +501,7 @@ module Kapusta
555
501
  lines = [base]
556
502
  args = list_raw_rest(list)
557
503
  semantic_length = semantic_items(args).length
558
- hang_subsequent_args = hang_call_args?(list, indent)
504
+ hang_subsequent_args = force_hang || hang_call_args?(list, base, indent)
559
505
 
560
506
  semantic_index = 0
561
507
  hanging = nil
@@ -565,6 +511,8 @@ module Kapusta
565
511
  next
566
512
  elsif semantic_index.zero?
567
513
  hanging = append_first_call_arg(lines, arg, base, indent, semantic_length)
514
+ elsif append_packed_call_arg?(list, lines, arg, indent)
515
+ nil
568
516
  elsif hanging && hang_subsequent_args
569
517
  lines << prefix_continuation(hanging, render(arg, indent + hanging.length))
570
518
  else
@@ -597,33 +545,172 @@ module Kapusta
597
545
  hanging
598
546
  end
599
547
 
600
- def hang_call_args?(list, indent)
601
- return false unless operator_call?(list)
548
+ def append_packed_call_arg?(list, lines, arg, indent)
549
+ return false unless packable_call_arg?(list, arg)
550
+
551
+ packed = packed_call_arg(lines.last, list, arg, indent)
552
+ if packed
553
+ lines[-1] = packed.first
554
+ lines.concat(packed.drop(1))
555
+ return true
556
+ end
557
+
558
+ false
559
+ end
560
+
561
+ def packable_call_arg?(list, arg)
562
+ return false if fn_form?(arg)
563
+ return true if packable_collection_call_arg?(list, arg)
564
+
565
+ pack_call_args?(list) && flat_render(arg)
566
+ end
567
+
568
+ def packable_collection_call_arg?(list, arg)
569
+ return true if arg.is_a?(Vec)
570
+
571
+ arg.is_a?(HashLit) && packable_hash_call_arg?(list)
572
+ end
573
+
574
+ def packed_call_arg(current_line, list, arg, indent)
575
+ packable_call_arg_renderings(list, arg, indent + current_line.length + 1).each do |rendered|
576
+ first_line, *rest = rendered.lines(chomp: true)
577
+ candidate = "#{current_line} #{first_line}"
578
+ next unless inline_arg_fits?(candidate, indent)
579
+
580
+ hanging = ' ' * (current_line.length + 1)
581
+ return [candidate, *rest.map { |line| "#{hanging}#{line}" }]
582
+ end
583
+
584
+ nil
585
+ end
586
+
587
+ def packable_call_arg_renderings(list, arg, indent)
588
+ [
589
+ flat_collection_render(arg),
590
+ flat_render(arg),
591
+ multiline_packable_call_arg_rendering(list, arg, indent)
592
+ ].compact.uniq
593
+ end
594
+
595
+ def multiline_packable_call_arg_rendering(list, arg, indent)
596
+ return render(arg, indent) if arg.is_a?(Vec)
597
+ return render(arg, indent) if local_hash_call_arg?(list, arg)
598
+
599
+ nil
600
+ end
601
+
602
+ def local_hash_call_arg?(list, arg)
603
+ arg.is_a?(HashLit) && head_name(list) == 'local'
604
+ end
605
+
606
+ def flat_collection_render(form)
607
+ case form
608
+ when Vec
609
+ flat_delimited_render(form.items, '[', ']') { |item| flat_render(item) }
610
+ when HashLit
611
+ return if contains_comments?(form.entries)
612
+
613
+ flat_hash_render(form.pairs)
614
+ end
615
+ end
616
+
617
+ def flat_delimited_render(items, open, close)
618
+ return if contains_comments?(items)
619
+
620
+ rendered = items.map do |item|
621
+ item.is_a?(Array) ? yield(*item) : yield(item)
622
+ end
623
+ return if rendered.any?(&:nil?)
624
+
625
+ "#{open}#{rendered.join(' ')}#{close}"
626
+ end
627
+
628
+ def flat_hash_render(pairs)
629
+ rendered = pairs.map { |pair| flat_hash_pair(pair) }
630
+ return if rendered.any?(&:nil?)
631
+
632
+ "{#{rendered.join(' ')}}"
633
+ end
634
+
635
+ def hang_call_args?(list, base, indent)
636
+ return true if source_hangs_call_args?(list, base)
637
+ return true if set_function_value?(list)
602
638
 
603
639
  flat = flat_call_render(list)
604
- flat && !fits?(flat, indent)
640
+ return false unless flat
641
+
642
+ overflowing = !fits?(flat, indent)
643
+ return true if overflowing && hanging_overflow_call?(list)
644
+
645
+ operator_call?(list) && overflowing
646
+ end
647
+
648
+ def pack_call_args?(list)
649
+ head_name(list)&.match?(/\A[a-z0-9_-][\w-]*\./)
650
+ end
651
+
652
+ def packable_hash_call_arg?(list)
653
+ head_name(list) == 'local' || pack_call_args?(list)
654
+ end
655
+
656
+ def hanging_overflow_call?(list)
657
+ hash_first_call_arg?(list) || pack_call_args?(list)
658
+ end
659
+
660
+ def hash_first_call_arg?(list)
661
+ list_rest(list).first.is_a?(HashLit)
662
+ end
663
+
664
+ def set_function_value?(list)
665
+ head_name(list) == 'set' && fn_form?(list_rest(list)[1])
666
+ end
667
+
668
+ def source_hangs_call_args?(list, base)
669
+ return false unless list.respond_to?(:column) && list.column
670
+
671
+ args = list_rest(list)
672
+ return false if args.length < 2
673
+
674
+ expected_column = list.column + base.length + 1
675
+ args.drop(1).all? do |arg|
676
+ arg.respond_to?(:column) && arg.column == expected_column
677
+ end
605
678
  end
606
679
 
607
680
  def operator_call?(list)
608
- head = list_head(list)
609
- head.is_a?(Sym) && head.name.match?(/\A[^\w.]+\z/)
681
+ head_name(list)&.match?(/\A[^\w.]+\z/)
682
+ end
683
+
684
+ def ordinary_call_form?(list)
685
+ name = head_name(list)
686
+ name && !Compiler::Language::SPECIAL_FORMS.include?(name)
610
687
  end
611
688
 
612
689
  def flat_call_render(list)
613
690
  head = flat_render(list_head(list))
614
691
  return unless head
615
692
 
616
- rendered_args = semantic_items(list_raw_rest(list)).map { |arg| flat_render(arg) }
693
+ rendered_args = semantic_items(list_raw_rest(list)).map { |arg| flat_call_arg_render(arg) }
617
694
  return if rendered_args.any?(&:nil?)
618
695
 
619
696
  "(#{[head, *rendered_args].join(' ')})"
620
697
  end
621
698
 
699
+ def flat_call_arg_render(arg)
700
+ collection = flat_collection_render(arg)
701
+ return collection if collection
702
+
703
+ flat_render(arg)
704
+ end
705
+
622
706
  def render_vec(vec, indent, layout: nil, top_level: false, force_expand: false)
623
707
  flat = flat_render(vec)
624
708
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
625
709
 
626
710
  return render_pairwise_vec(vec, indent) if layout == :pairwise && !contains_comments?(vec.items)
711
+ if multiline_in_source?(vec) && multiline_vec_items_should_separate?(vec) && !contains_comments?(vec.items)
712
+ return render_multiline_vec(vec, indent)
713
+ end
627
714
  return render_filled_vec(vec, indent) if !contains_comments?(vec.items) && !vec.items.empty?
628
715
 
629
716
  lines = ['[']
@@ -633,6 +720,42 @@ module Kapusta
633
720
  append_suffix(lines, ']')
634
721
  end
635
722
 
723
+ def render_multiline_vec(vec, indent)
724
+ return '[]' if vec.items.empty?
725
+
726
+ lines = []
727
+ vec.items.each_with_index do |item, idx|
728
+ prefix = idx.zero? ? '[' : ' '
729
+ lines.concat(prefix_lines(prefix, render_multiline_vec_item(item, indent + 1).lines.map(&:chomp)))
730
+ end
731
+ lines[-1] = "#{lines[-1]}]"
732
+ lines.join("\n")
733
+ end
734
+
735
+ def render_multiline_vec_item(item, indent)
736
+ return render_call(item, indent, force_hang: true) if hanging_multiline_vec_call_item?(item)
737
+
738
+ render(item, indent)
739
+ end
740
+
741
+ def hanging_multiline_vec_call_item?(item)
742
+ item.is_a?(List) && multiline_in_source?(item) && ordinary_call_form?(item)
743
+ end
744
+
745
+ def multiline_vec_items_should_separate?(vec)
746
+ multiline_vec_items_on_separate_lines?(vec) || semantic_items(vec.items).all? { |item| collection?(item) }
747
+ end
748
+
749
+ def multiline_vec_items_on_separate_lines?(vec)
750
+ items = semantic_items(vec.items)
751
+ return false if items.length < 2
752
+
753
+ lines = items.filter_map { |item| item.line if item.respond_to?(:line) }
754
+ return true if lines.length < 2
755
+
756
+ lines.uniq.length == lines.length
757
+ end
758
+
636
759
  def render_filled_vec(vec, indent)
637
760
  output_lines = ['[']
638
761
 
@@ -688,16 +811,16 @@ module Kapusta
688
811
  def render_let_bindings(bindings, indent)
689
812
  return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
690
813
 
691
- hanging = render_hanging_pairwise_vec(bindings)
814
+ hanging = render_hanging_pairwise_vec(bindings, indent)
692
815
  hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
693
816
  end
694
817
 
695
- def render_hanging_pairwise_vec(vec)
818
+ def render_hanging_pairwise_vec(vec, indent)
696
819
  pairs = vec.items.each_slice(2).to_a
697
820
  return unless pairs.all? { |pair| pair.length == 2 }
698
821
 
699
822
  rendered_pairs = pairs.map do |left, right|
700
- render_binding_pair(left, right)
823
+ render_binding_pair(left, right, indent)
701
824
  end
702
825
  return if rendered_pairs.any?(&:nil?)
703
826
 
@@ -725,7 +848,7 @@ module Kapusta
725
848
 
726
849
  key, value = entry
727
850
  first_pair = output_lines.empty?
728
- if hash_shorthand?(key, value)
851
+ if hash_pair_shorthand?(entry)
729
852
  output_lines << "#{first_pair ? '{' : ' '}: #{value.name}"
730
853
  next
731
854
  end
@@ -745,8 +868,9 @@ module Kapusta
745
868
  output_lines.join("\n")
746
869
  end
747
870
 
748
- def flat_hash_pair(key, value)
749
- return ": #{value.name}" if hash_shorthand?(key, value)
871
+ def flat_hash_pair(pair)
872
+ key, value = pair
873
+ return ": #{value.name}" if hash_pair_shorthand?(pair)
750
874
 
751
875
  rendered_value = flat_render(value)
752
876
  return unless rendered_value
@@ -772,14 +896,15 @@ module Kapusta
772
896
  fits?(pair, indent) ? pair : nil
773
897
  end
774
898
 
775
- def render_binding_pair(left, right)
899
+ def render_binding_pair(left, right, indent)
776
900
  left_rendered = flat_render(left)
777
901
  return unless left_rendered
778
902
 
779
- right_rendered = render(right, '(let ['.length + left_rendered.length + 1)
903
+ right_indent = indent + '(let ['.length + left_rendered.length + 1
904
+ right_rendered = render(right, right_indent)
780
905
  first_line, *rest = right_rendered.lines(chomp: true)
781
906
  pair = "#{left_rendered} #{first_line}"
782
- return unless pair.length <= MAX_WIDTH
907
+ return unless fits?(pair, indent + '(let ['.length)
783
908
 
784
909
  return pair if rest.empty?
785
910
 
@@ -787,8 +912,8 @@ module Kapusta
787
912
  ([pair] + rest.map { |line| "#{continuation}#{line}" }).join("\n")
788
913
  end
789
914
 
790
- def hash_shorthand?(key, value)
791
- key.is_a?(Symbol) && value.is_a?(Sym) && key == Kapusta.kebab_to_snake(value.name).to_sym
915
+ def hash_pair_shorthand?(pair)
916
+ pair.respond_to?(:shorthand?) && pair.shorthand?
792
917
  end
793
918
 
794
919
  def hashfn_literal?(form)
@@ -809,11 +934,10 @@ module Kapusta
809
934
  return true unless head.is_a?(Sym)
810
935
 
811
936
  case head.name
812
- when 'fn', 'lambda', 'λ', 'macro', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect',
813
- 'accumulate', 'faccumulate'
937
+ when *Compiler::Language::FLAT_BODY_HEADS
814
938
  !top_level
815
939
  else
816
- !%w[let case match try catch finally do -> ->> -?> -?>> doto].include?(head.name)
940
+ !Compiler::Language.never_flat_head?(head.name)
817
941
  end
818
942
  end
819
943
 
@@ -826,18 +950,13 @@ module Kapusta
826
950
  return false unless head.is_a?(Sym)
827
951
 
828
952
  case head.name
829
- when 'if', 'case', 'match', 'let', 'try', 'catch', 'finally', 'do', 'for', '->', '->>', '-?>', '-?>>', 'doto',
830
- 'fn', 'lambda', 'λ', 'macro'
953
+ when *Compiler::Language::MULTILINE_BODY_HEADS
831
954
  true
832
955
  else
833
956
  false
834
957
  end
835
958
  end
836
959
 
837
- def multiline_in_source?(form)
838
- form.respond_to?(:multiline_source) && form.multiline_source
839
- end
840
-
841
960
  def let_with_multiple_bindings?(form)
842
961
  head = list_head(form)
843
962
  return false unless head.is_a?(Sym) && head.name == 'let'
@@ -860,25 +979,12 @@ module Kapusta
860
979
  end
861
980
  end
862
981
 
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
982
  def fn_form?(form)
877
983
  return false unless form.is_a?(List)
878
984
 
879
985
  head = list_head(form)
880
986
  head.is_a?(Sym) &&
881
- %w[fn lambda λ].include?(head.name)
987
+ Compiler::Language.function_head?(head.name)
882
988
  end
883
989
 
884
990
  def inline_three_arg_if?(args)
@@ -897,33 +1003,6 @@ module Kapusta
897
1003
  end
898
1004
  end
899
1005
 
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
1006
  def render_generic_list(list, indent)
928
1007
  lines = ['(']
929
1008
  list.items.each do |item|
@@ -959,47 +1038,6 @@ module Kapusta
959
1038
  append_suffix(lines, ')')
960
1039
  end
961
1040
 
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
1041
  class Error < Kapusta::Error; end
1004
1042
  end
1005
1043
  end