kapusta 0.1.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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +10 -0
  4. data/README.md +58 -0
  5. data/Rakefile +10 -0
  6. data/bin/console +8 -0
  7. data/bin/setup +4 -0
  8. data/examples/accumulator.kap +16 -0
  9. data/examples/ackermann.kap +7 -0
  10. data/examples/anagram.kap +13 -0
  11. data/examples/binary-search.kap +16 -0
  12. data/examples/block-sort.kap +3 -0
  13. data/examples/calc.kap +10 -0
  14. data/examples/counter.kap +19 -0
  15. data/examples/describe.kap +9 -0
  16. data/examples/destructure.kap +4 -0
  17. data/examples/doto.kap +2 -0
  18. data/examples/egg-count.kap +10 -0
  19. data/examples/even-squares.kap +7 -0
  20. data/examples/exceptions.kap +14 -0
  21. data/examples/factorial.kap +8 -0
  22. data/examples/fib.kap +4 -0
  23. data/examples/fizzbuzz.kap +7 -0
  24. data/examples/gcd.kap +5 -0
  25. data/examples/greet.kap +2 -0
  26. data/examples/hashfn.kap +4 -0
  27. data/examples/kwargs.kap +1 -0
  28. data/examples/leap-year.kap +5 -0
  29. data/examples/match.kap +9 -0
  30. data/examples/min-max.kap +11 -0
  31. data/examples/module-header.kap +6 -0
  32. data/examples/palindrome.kap +8 -0
  33. data/examples/pangram.kap +9 -0
  34. data/examples/pcall.kap +9 -0
  35. data/examples/pipeline.kap +6 -0
  36. data/examples/points.kap +9 -0
  37. data/examples/primes.kap +8 -0
  38. data/examples/raindrops.kap +13 -0
  39. data/examples/record.kap +6 -0
  40. data/examples/regex.kap +9 -0
  41. data/examples/ruby-eval.kap +1 -0
  42. data/examples/safe-lookup.kap +6 -0
  43. data/examples/scopes.kap +18 -0
  44. data/examples/shapes.kap +9 -0
  45. data/examples/squares.kap +3 -0
  46. data/examples/stack.kap +19 -0
  47. data/examples/sum.kap +3 -0
  48. data/examples/tset.kap +4 -0
  49. data/examples/two-sum.kap +17 -0
  50. data/exe/kapfmt +6 -0
  51. data/exe/kapusta +6 -0
  52. data/kapfmt +4 -0
  53. data/kapusta.gemspec +25 -0
  54. data/lib/kapusta/ast.rb +76 -0
  55. data/lib/kapusta/cli.rb +61 -0
  56. data/lib/kapusta/compiler/emitter/bindings.rb +178 -0
  57. data/lib/kapusta/compiler/emitter/collections.rb +245 -0
  58. data/lib/kapusta/compiler/emitter/control_flow.rb +168 -0
  59. data/lib/kapusta/compiler/emitter/expressions.rb +107 -0
  60. data/lib/kapusta/compiler/emitter/interop.rb +277 -0
  61. data/lib/kapusta/compiler/emitter/patterns.rb +105 -0
  62. data/lib/kapusta/compiler/emitter/support.rb +169 -0
  63. data/lib/kapusta/compiler/emitter.rb +45 -0
  64. data/lib/kapusta/compiler/normalizer.rb +122 -0
  65. data/lib/kapusta/compiler/runtime.rb +583 -0
  66. data/lib/kapusta/compiler.rb +47 -0
  67. data/lib/kapusta/env.rb +42 -0
  68. data/lib/kapusta/formatter.rb +685 -0
  69. data/lib/kapusta/reader.rb +215 -0
  70. data/lib/kapusta/support.rb +7 -0
  71. data/lib/kapusta/version.rb +5 -0
  72. data/lib/kapusta.rb +30 -0
  73. data/spec/cli_spec.rb +77 -0
  74. data/spec/examples_spec.rb +258 -0
  75. data/spec/formatter_spec.rb +176 -0
  76. data/spec/spec_helper.rb +12 -0
  77. metadata +119 -0
@@ -0,0 +1,685 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../kapusta'
4
+
5
+ module Kapusta
6
+ class Formatter
7
+ MAX_WIDTH = 80
8
+ INDENT = 2
9
+ STDIN_PATH = '-'
10
+
11
+ PIPELINE_FORMS = %w[-> ->> -?> -?>> doto].freeze
12
+
13
+ def initialize(argv)
14
+ @mode = :stdout
15
+ @files = []
16
+ parse_args(argv)
17
+ end
18
+
19
+ def run
20
+ validate_args!
21
+
22
+ formatted = @files.map do |path|
23
+ original = read_source(path)
24
+ [path, original, format_source(original)]
25
+ end
26
+
27
+ case @mode
28
+ when :stdout
29
+ $stdout.write(formatted.first[2])
30
+ when :fix
31
+ formatted.each do |path, _original, rewritten|
32
+ raise Error, 'Cannot use --fix with stdin (-).' if stdin_path?(path)
33
+
34
+ File.write(path, rewritten)
35
+ end
36
+ when :check
37
+ dirty = formatted.reject { |_path, original, rewritten| original == rewritten }
38
+ dirty.each do |path, _original, _rewritten|
39
+ warn "Not formatted: #{path}"
40
+ end
41
+ return 1 unless dirty.empty?
42
+ end
43
+
44
+ 0
45
+ rescue Error => e
46
+ warn e.message
47
+ 1
48
+ end
49
+
50
+ private
51
+
52
+ def parse_args(argv)
53
+ argv.each do |arg|
54
+ case arg
55
+ when '--fix'
56
+ ensure_mode!(:fix)
57
+ when '--check'
58
+ ensure_mode!(:check)
59
+ when '--help', '-h'
60
+ print_help
61
+ exit 0
62
+ else
63
+ @files << arg
64
+ end
65
+ end
66
+ end
67
+
68
+ def ensure_mode!(mode)
69
+ raise Error, 'Use at most one of --fix or --check.' if @mode != :stdout && @mode != mode
70
+
71
+ @mode = mode
72
+ end
73
+
74
+ def validate_args!
75
+ raise Error, 'Usage: kapfmt [--fix] [--check] FILENAME...' if @files.empty?
76
+ raise Error, 'stdin (-) may only be specified once.' if @files.count { |path| stdin_path?(path) } > 1
77
+ raise Error, 'Cannot use --fix with stdin (-).' if @mode == :fix && @files.any? { |path| stdin_path?(path) }
78
+
79
+ return unless @mode == :stdout && @files.length != 1
80
+
81
+ raise Error, 'Without --fix or --check, kapfmt accepts exactly one file.'
82
+ end
83
+
84
+ def read_source(path)
85
+ return File.read(path) unless stdin_path?(path)
86
+
87
+ @stdin_read ||= false
88
+ raise Error, 'stdin (-) may only be specified once.' if @stdin_read
89
+
90
+ @stdin_read = true
91
+ $stdin.read
92
+ end
93
+
94
+ def format_source(source)
95
+ reject_comments!(source)
96
+
97
+ forms = Reader.read_all(source)
98
+ rendered = forms.map { |form| render(form, 0, top_level: true) }
99
+ return '' if rendered.empty?
100
+
101
+ output = +''
102
+ rendered.each_with_index do |form, index|
103
+ output << separator_for(forms[index - 1], forms[index]) unless index.zero?
104
+ output << form
105
+ end
106
+ output << "\n"
107
+ rescue StandardError => e
108
+ raise Error, e.message
109
+ end
110
+
111
+ def separator_for(previous, current)
112
+ if consecutive_requires?(previous, current) ||
113
+ (groupable_top_level_form?(previous) && groupable_top_level_form?(current))
114
+ "\n"
115
+ else
116
+ "\n\n"
117
+ end
118
+ end
119
+
120
+ def reject_comments!(source)
121
+ index = 0
122
+
123
+ while index < source.length
124
+ char = source[index]
125
+
126
+ if char == '"'
127
+ index = consume_string(source, index)
128
+ elsif char == ';'
129
+ raise Error, 'kapfmt does not support comments yet.'
130
+ else
131
+ index += 1
132
+ end
133
+ end
134
+ end
135
+
136
+ def consume_string(source, start)
137
+ index = start + 1
138
+
139
+ while index < source.length
140
+ char = source[index]
141
+ index += 1
142
+
143
+ if char == '\\'
144
+ index += 1
145
+ elsif char == '"'
146
+ break
147
+ end
148
+ end
149
+
150
+ index
151
+ end
152
+
153
+ def render(form, indent, layout: nil, top_level: false, force_expand: false)
154
+ flat = flat_render(form)
155
+ return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(form, top_level:, layout:)
156
+
157
+ case form
158
+ when List then render_list(form, indent, top_level:)
159
+ when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
160
+ when HashLit then render_hash(form, indent)
161
+ else
162
+ flat || raise(Error, "cannot format form: #{form.inspect}")
163
+ end
164
+ end
165
+
166
+ def flat_render(form)
167
+ case form
168
+ when Sym
169
+ form.name
170
+ when Vec
171
+ "[#{form.items.map { |item| flat_render(item) }.join(' ')}]"
172
+ when HashLit
173
+ "{#{form.pairs.map { |key, value| flat_hash_pair(key, value) }.join(' ')}}"
174
+ when List
175
+ return "##{flat_render(form.items[1])}" if hashfn_literal?(form)
176
+
177
+ "(#{form.items.map { |item| flat_render(item) }.join(' ')})"
178
+ when String, Numeric, true, false, nil
179
+ form.inspect
180
+ when Symbol
181
+ ":#{form.to_s.tr('_', '-')}"
182
+ end
183
+ end
184
+
185
+ def render_list(list, indent, top_level: false)
186
+ return '()' if list.empty?
187
+ return "##{render(list.items[1], indent, top_level:)}" if hashfn_literal?(list)
188
+
189
+ head = list.head
190
+ head_name = head.is_a?(Sym) ? head.name : nil
191
+
192
+ case head_name
193
+ when 'fn', 'lambda', 'λ' then render_fn(head_name, list, indent, top_level:)
194
+ when 'let' then render_let(list, indent)
195
+ when 'do', 'finally' then render_prefix_body_form(head_name, [], list.rest, indent)
196
+ when 'try' then render_try(list, indent)
197
+ when 'while', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect', 'accumulate', 'faccumulate'
198
+ render_prefix_body_form(head_name, list.rest.take(1), list.rest.drop(1), indent)
199
+ when 'module' then render_prefix_body_form('module', list.rest.take(1), list.rest.drop(1), indent)
200
+ when 'class' then render_class(list, indent)
201
+ when 'catch' then render_catch(list, indent)
202
+ when 'if' then render_if(list, indent)
203
+ when 'case', 'match' then render_case(head_name, list.rest, indent)
204
+ when *PIPELINE_FORMS then render_pipeline(head_name, list.rest, indent)
205
+ else
206
+ render_call(list, indent)
207
+ end
208
+ end
209
+
210
+ def render_fn(head, list, indent, top_level: false)
211
+ args = list.rest
212
+ if args[0].is_a?(Sym) && args[1].is_a?(Vec)
213
+ render_prefix_body_form(head, args.take(2), args.drop(2), indent, force_body_multiline: top_level)
214
+ else
215
+ render_prefix_body_form(head, args.take(1), args.drop(1), indent, force_body_multiline: top_level)
216
+ end
217
+ end
218
+
219
+ def render_catch(list, indent)
220
+ args = list.rest
221
+ prefix = args.take(2)
222
+ body = args.drop(2)
223
+ render_prefix_body_form('catch', prefix, body, indent)
224
+ end
225
+
226
+ def render_class(list, indent)
227
+ args = list.rest
228
+ prefix = args[1].is_a?(Vec) ? args.take(2) : args.take(1)
229
+ body = args.drop(prefix.length)
230
+ render_prefix_body_form('class', prefix, body, indent)
231
+ end
232
+
233
+ def render_try(list, indent)
234
+ args = list.rest
235
+ lines = ['(try']
236
+
237
+ if args.any?
238
+ first = render(args.first, indent + '(try '.length)
239
+ candidate = "(try #{first}"
240
+ if single_line?(first) && fits?(candidate, indent)
241
+ lines[0] = candidate
242
+ else
243
+ lines << indent_block(first, INDENT)
244
+ end
245
+ end
246
+
247
+ args.drop(1).each do |form|
248
+ lines << indent_block(render(form, indent + INDENT), INDENT)
249
+ end
250
+
251
+ append_suffix(lines, ')')
252
+ end
253
+
254
+ def render_let(list, indent)
255
+ bindings = list.rest.first
256
+ body = list.rest.drop(1)
257
+ unless bindings.is_a?(Vec)
258
+ return render_prefix_body_form('let', list.rest.take(1), body, indent,
259
+ layouts: [:pairwise])
260
+ end
261
+
262
+ rendered_bindings = render_let_bindings(bindings, indent)
263
+ lines = rendered_bindings.lines(chomp: true)
264
+ lines[0] = "(let #{lines[0]}"
265
+ body.each do |form|
266
+ lines << indent_block(render(form, indent + INDENT), INDENT)
267
+ end
268
+ append_suffix(lines, ')')
269
+ end
270
+
271
+ def render_prefix_body_form(head, prefix_forms, body_forms, indent, layouts: [], force_body_multiline: false)
272
+ line = "(#{head}"
273
+ lines = [line]
274
+ current_first_line = line
275
+
276
+ prefix_forms.each_with_index do |form, index|
277
+ rendered = render(form, indent + INDENT, layout: layouts[index])
278
+ candidate = "#{current_first_line} #{rendered}"
279
+
280
+ if single_line?(rendered) && fits?(candidate, indent)
281
+ current_first_line = candidate
282
+ lines[0] = current_first_line
283
+ else
284
+ lines << indent_block(rendered, INDENT)
285
+ end
286
+ end
287
+
288
+ body_forms.each do |form|
289
+ body = render(
290
+ form,
291
+ indent + INDENT,
292
+ force_expand: force_body_multiline && force_multiline_body?(form)
293
+ )
294
+ lines << indent_block(body, INDENT)
295
+ end
296
+
297
+ append_suffix(lines, ')')
298
+ end
299
+
300
+ def render_if(list, indent)
301
+ args = list.rest
302
+ lines = []
303
+ hanging = ' ' * '(if '.length
304
+
305
+ if args.length == 3
306
+ flat = flat_render(list)
307
+ return flat if inline_three_arg_if?(args) && flat && fits?(flat, indent)
308
+
309
+ lines << "(if #{render(args[0], indent + '(if '.length)}"
310
+ lines << "#{hanging}#{render(args[1], indent + '(if '.length)}"
311
+ lines << "#{hanging}#{render(args[2], indent + '(if '.length)}"
312
+ return append_suffix(lines, ')')
313
+ end
314
+
315
+ index = 0
316
+ if args.length >= 2
317
+ first_pair = render_pair(args[0], args[1], indent + '(if '.length)
318
+ if first_pair
319
+ lines << "(if #{first_pair}"
320
+ else
321
+ lines << "(if #{render(args[0], indent + '(if '.length)}"
322
+ lines << "#{hanging}#{render(args[1], indent + '(if '.length)}"
323
+ end
324
+ index = 2
325
+ else
326
+ lines << '(if'
327
+ end
328
+
329
+ while index < args.length
330
+ remaining = args.length - index
331
+ if remaining >= 2
332
+ pair = render_pair(args[index], args[index + 1], indent + '(if '.length)
333
+ if pair
334
+ lines << "#{hanging}#{pair}"
335
+ else
336
+ lines << "#{hanging}#{render(args[index], indent + '(if '.length)}"
337
+ lines << "#{hanging}#{render(args[index + 1], indent + '(if '.length)}"
338
+ end
339
+ index += 2
340
+ else
341
+ lines << "#{hanging}#{render(args[index], indent + '(if '.length)}"
342
+ index += 1
343
+ end
344
+ end
345
+
346
+ append_suffix(lines, ')')
347
+ end
348
+
349
+ def render_case(head, args, indent)
350
+ subject = args.first
351
+ clauses = args.drop(1)
352
+ lines = ['(case']
353
+
354
+ if subject
355
+ rendered_subject = render(subject, indent + INDENT)
356
+ if single_line?(rendered_subject) && fits?("(#{head} #{rendered_subject}", indent)
357
+ lines[0] = "(#{head} #{rendered_subject}"
358
+ else
359
+ lines[0] = "(#{head}"
360
+ lines << indent_block(rendered_subject, INDENT)
361
+ end
362
+ end
363
+
364
+ clauses.each_slice(2) do |pair|
365
+ pattern, value = pair
366
+ if pair.length == 2
367
+ pair = render_pair(pattern, value, indent + INDENT)
368
+ if pair
369
+ lines << indent_block(pair, INDENT)
370
+ else
371
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
372
+ lines << indent_block(render(value, indent + INDENT), INDENT)
373
+ end
374
+ else
375
+ lines << indent_block(render(pattern, indent + INDENT), INDENT)
376
+ end
377
+ end
378
+
379
+ append_suffix(lines, ')')
380
+ end
381
+
382
+ def render_pipeline(head, args, indent)
383
+ base = "(#{head}"
384
+ lines = [base]
385
+ hanging = ' ' * (base.length + 1)
386
+
387
+ args.each_with_index do |form, index|
388
+ rendered = render(form, indent + base.length + 1)
389
+ if index.zero?
390
+ first_line, *rest = rendered.lines(chomp: true)
391
+ candidate = "#{base} #{first_line}"
392
+ if fits?(candidate, indent)
393
+ lines[0] = candidate
394
+ rest.each { |line| lines << "#{hanging}#{line}" }
395
+ else
396
+ lines << indent_block(rendered, INDENT)
397
+ end
398
+ else
399
+ lines << "#{hanging}#{rendered}"
400
+ end
401
+ end
402
+
403
+ append_suffix(lines, ')')
404
+ end
405
+
406
+ def render_call(list, indent)
407
+ head = flat_render(list.head)
408
+ raise Error, "cannot format form head: #{list.head.inspect}" unless head
409
+
410
+ base = "(#{head}"
411
+ lines = [base]
412
+ args = list.rest
413
+
414
+ unless args.empty?
415
+ first = render(
416
+ args.first,
417
+ indent + base.length + 1,
418
+ force_expand: args.length == 1 && fn_form?(args.first)
419
+ )
420
+ first_line, *rest = first.lines(chomp: true)
421
+ candidate = "#{base} #{first_line}"
422
+
423
+ if fits?(candidate, indent)
424
+ lines[0] = candidate
425
+ hanging = ' ' * (base.length + 1)
426
+ rest.each { |line| lines << "#{hanging}#{line}" }
427
+ else
428
+ lines << indent_block(first, INDENT)
429
+ end
430
+
431
+ args.drop(1).each do |arg|
432
+ lines << indent_block(render(arg, indent + INDENT), INDENT)
433
+ end
434
+ end
435
+
436
+ append_suffix(lines, ')')
437
+ end
438
+
439
+ def render_vec(vec, indent, layout: nil, top_level: false, force_expand: false)
440
+ flat = flat_render(vec)
441
+ return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
442
+
443
+ if layout == :pairwise
444
+ render_pairwise_vec(vec, indent)
445
+ else
446
+ lines = ['[']
447
+ vec.items.each do |item|
448
+ lines << indent_block(render(item, indent + INDENT), INDENT)
449
+ end
450
+ lines << ']'
451
+ lines.join("\n")
452
+ end
453
+ end
454
+
455
+ def render_pairwise_vec(vec, indent)
456
+ lines = ['[']
457
+
458
+ vec.items.each_slice(2) do |left, right|
459
+ if right
460
+ pair = render_pair(left, right, indent + INDENT)
461
+ if pair
462
+ lines << indent_block(pair, INDENT)
463
+ else
464
+ lines << indent_block(render(left, indent + INDENT), INDENT)
465
+ lines << indent_block(render(right, indent + INDENT), INDENT)
466
+ end
467
+ else
468
+ lines << indent_block(render(left, indent + INDENT), INDENT)
469
+ end
470
+ end
471
+
472
+ lines << ']'
473
+ lines.join("\n")
474
+ end
475
+
476
+ def render_let_bindings(bindings, indent)
477
+ return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
478
+
479
+ hanging = render_hanging_pairwise_vec(bindings)
480
+ hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
481
+ end
482
+
483
+ def render_hanging_pairwise_vec(vec)
484
+ pairs = vec.items.each_slice(2).to_a
485
+ rendered_pairs = pairs.map do |left, right|
486
+ return nil unless right
487
+
488
+ render_binding_pair(left, right)
489
+ end
490
+ return nil if rendered_pairs.any?(&:nil?)
491
+
492
+ lines = ["[#{rendered_pairs.first}"]
493
+ continuation = ' ' * '(let ['.length
494
+ rendered_pairs.drop(1).each do |pair|
495
+ lines << "#{continuation}#{pair}"
496
+ end
497
+ lines[-1] = "#{lines[-1]}]"
498
+ lines.join("\n")
499
+ end
500
+
501
+ def render_hash(hash, indent)
502
+ flat = flat_render(hash)
503
+ return flat if flat && fits?(flat, indent)
504
+
505
+ lines = ['{']
506
+
507
+ hash.pairs.each do |key, value|
508
+ pair = flat_hash_pair(key, value)
509
+ if fits?(pair, indent + INDENT)
510
+ lines << indent_block(pair, INDENT)
511
+ else
512
+ lines << indent_block(render_hash_key(key), INDENT)
513
+ lines << indent_block(render(value, indent + INDENT), INDENT)
514
+ end
515
+ end
516
+
517
+ lines << '}'
518
+ lines.join("\n")
519
+ end
520
+
521
+ def flat_hash_pair(key, value)
522
+ if hash_shorthand?(key, value)
523
+ ": #{value.name}"
524
+ else
525
+ "#{render_hash_key(key)} #{flat_render(value)}"
526
+ end
527
+ end
528
+
529
+ def render_hash_key(key)
530
+ return ":#{key.to_s.tr('_', '-')}" if key.is_a?(Symbol)
531
+
532
+ rendered = flat_render(key)
533
+ raise Error, "cannot format hash key: #{key.inspect}" unless rendered
534
+
535
+ rendered
536
+ end
537
+
538
+ def render_pair(left, right, indent)
539
+ left_rendered = flat_render(left) || render(left, indent)
540
+ right_rendered = flat_render(right) || render(right, indent)
541
+ return nil unless single_line?(left_rendered) && single_line?(right_rendered)
542
+
543
+ pair = "#{left_rendered} #{right_rendered}"
544
+ fits?(pair, indent) ? pair : nil
545
+ end
546
+
547
+ def render_binding_pair(left, right)
548
+ left_rendered = flat_render(left)
549
+ return nil unless left_rendered
550
+
551
+ right_rendered = render(right, '(let ['.length + left_rendered.length + 1)
552
+ first_line, *rest = right_rendered.lines(chomp: true)
553
+ pair = "#{left_rendered} #{first_line}"
554
+ return nil unless pair.length <= MAX_WIDTH
555
+
556
+ return pair if rest.empty?
557
+
558
+ continuation = ' ' * ('(let ['.length + left_rendered.length + 1)
559
+ ([pair] + rest.map { |line| "#{continuation}#{line}" }).join("\n")
560
+ end
561
+
562
+ def hash_shorthand?(key, value)
563
+ key.is_a?(Symbol) && value.is_a?(Sym) && key == Kapusta.kebab_to_snake(value.name).to_sym
564
+ end
565
+
566
+ def hashfn_literal?(form)
567
+ form.is_a?(List) &&
568
+ form.items.length == 2 &&
569
+ form.items[0].is_a?(Sym) &&
570
+ form.items[0].name == 'hashfn'
571
+ end
572
+
573
+ def allow_flat?(form, top_level:, layout:)
574
+ return false if layout == :pairwise && form.is_a?(Vec) && form.items.length > 2
575
+ return true unless form.is_a?(List)
576
+
577
+ head = form.head
578
+ return true unless head.is_a?(Sym)
579
+
580
+ case head.name
581
+ when 'fn', 'lambda', 'λ', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect', 'accumulate',
582
+ 'faccumulate'
583
+ !top_level
584
+ else
585
+ !%w[let case match try catch finally do -> ->> -?> -?>> doto].include?(head.name)
586
+ end
587
+ end
588
+
589
+ def force_multiline_body?(form)
590
+ return false unless form.is_a?(List) && form.head.is_a?(Sym)
591
+
592
+ case form.head.name
593
+ when 'if', 'case', 'match', 'let', 'try', 'catch', 'finally', 'do', 'for', '->', '->>', '-?>', '-?>>', 'doto',
594
+ 'fn', 'lambda', 'λ'
595
+ true
596
+ else
597
+ flat = flat_render(form)
598
+ flat && flat.length > 40
599
+ end
600
+ end
601
+
602
+ def fn_body(form)
603
+ args = form.rest
604
+ if args[0].is_a?(Sym) && args[1].is_a?(Vec)
605
+ args.drop(2)
606
+ else
607
+ args.drop(1)
608
+ end
609
+ end
610
+
611
+ def consecutive_requires?(previous, current)
612
+ require_form?(previous) && require_form?(current)
613
+ end
614
+
615
+ def groupable_top_level_form?(form)
616
+ return true if require_form?(form)
617
+ return false unless form.is_a?(List) && flat_render(form)
618
+
619
+ head = form.head
620
+ return false unless head.is_a?(Sym)
621
+
622
+ !%w[fn module class let].include?(head.name)
623
+ end
624
+
625
+ def require_form?(form)
626
+ form.is_a?(List) &&
627
+ form.items.length == 2 &&
628
+ form.head.is_a?(Sym) &&
629
+ form.head.name == 'require'
630
+ end
631
+
632
+ def fn_form?(form)
633
+ form.is_a?(List) &&
634
+ form.head.is_a?(Sym) &&
635
+ %w[fn lambda λ].include?(form.head.name)
636
+ end
637
+
638
+ def inline_three_arg_if?(args)
639
+ then_branch = args[1]
640
+ else_branch = args[2]
641
+
642
+ atomish?(then_branch) || atomish?(else_branch)
643
+ end
644
+
645
+ def atomish?(form)
646
+ case form
647
+ when Sym, String, Numeric, true, false, nil, Symbol
648
+ true
649
+ else
650
+ false
651
+ end
652
+ end
653
+
654
+ def stdin_path?(path)
655
+ path == STDIN_PATH
656
+ end
657
+
658
+ def fits?(text, indent)
659
+ !text.include?("\n") && indent + text.length <= MAX_WIDTH
660
+ end
661
+
662
+ def single_line?(text)
663
+ !text.include?("\n")
664
+ end
665
+
666
+ def indent_block(text, amount)
667
+ prefix = ' ' * amount
668
+ text.lines.map { |line| "#{prefix}#{line}" }.join
669
+ end
670
+
671
+ def append_suffix(lines, suffix)
672
+ updated = lines.dup
673
+ updated[-1] = "#{updated[-1]}#{suffix}"
674
+ updated.join("\n")
675
+ end
676
+
677
+ def print_help
678
+ puts 'Usage: kapfmt [--fix] [--check] FILENAME...'
679
+ puts
680
+ puts 'Formats Kapusta source using the built-in Kapusta reader and pretty-printer.'
681
+ end
682
+
683
+ class Error < StandardError; end
684
+ end
685
+ end