kapusta 0.3.0 → 0.5.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.
@@ -27,7 +27,8 @@ module Kapusta
27
27
 
28
28
  formatted = @files.map do |path|
29
29
  original = read_source(path)
30
- [path, original, format_source(original)]
30
+ validate_kapusta_source(original, path)
31
+ [path, original, format_source(original, path)]
31
32
  end
32
33
 
33
34
  case @mode
@@ -48,13 +49,17 @@ module Kapusta
48
49
  end
49
50
 
50
51
  0
51
- rescue Error => e
52
- warn e.message
52
+ rescue Kapusta::Error => e
53
+ warn e.formatted
53
54
  1
54
55
  end
55
56
 
56
57
  private
57
58
 
59
+ def validate_kapusta_source(source, path)
60
+ Kapusta::Compiler.compile(source, path:)
61
+ end
62
+
58
63
  def parse_args(argv)
59
64
  argv.each do |arg|
60
65
  case arg
@@ -99,7 +104,7 @@ module Kapusta
99
104
  $stdin.read
100
105
  end
101
106
 
102
- def format_source(source)
107
+ def format_source(source, path = nil)
103
108
  forms = Reader.read_all(source, preserve_comments: true)
104
109
  entries = top_level_entries(forms)
105
110
  return '' if entries.empty?
@@ -110,40 +115,39 @@ module Kapusta
110
115
  output << render_top_level_entry(entry)
111
116
  end
112
117
  output << "\n"
118
+ rescue Kapusta::Error => e
119
+ raise e.with_defaults(path:)
113
120
  rescue StandardError => e
114
- raise Error, e.message
121
+ raise Error.new(e.message, path:)
115
122
  end
116
123
 
117
- def separator_for(previous, current)
118
- if consecutive_requires?(previous, current) ||
119
- (groupable_top_level_form?(previous) && groupable_top_level_form?(current))
120
- "\n"
121
- else
122
- "\n\n"
123
- end
124
+ def separator_for(_previous, _current)
125
+ "\n"
124
126
  end
125
127
 
126
128
  def top_level_entries(forms)
127
129
  entries = []
128
130
  leading_comments = []
131
+ pending_blank = false
129
132
 
130
133
  forms.each do |form|
131
- if comment?(form)
134
+ if form.is_a?(BlankLine)
135
+ pending_blank = true
136
+ elsif comment?(form)
132
137
  leading_comments << form
133
138
  else
134
- entries << { comments: leading_comments, form: }
139
+ entries << { comments: leading_comments, form:, blank_before: pending_blank }
135
140
  leading_comments = []
141
+ pending_blank = false
136
142
  end
137
143
  end
138
144
 
139
- entries << { comments: leading_comments, form: nil } unless leading_comments.empty?
145
+ entries << { comments: leading_comments, form: nil, blank_before: pending_blank } unless leading_comments.empty?
140
146
  entries
141
147
  end
142
148
 
143
- def separator_for_entries(previous, current)
144
- return "\n" unless previous[:form] && current[:form]
145
-
146
- separator_for(previous[:form], current[:form])
149
+ def separator_for_entries(_previous, current)
150
+ current[:blank_before] ? "\n\n" : "\n"
147
151
  end
148
152
 
149
153
  def render_top_level_entry(entry)
@@ -156,6 +160,14 @@ module Kapusta
156
160
  form.is_a?(Comment)
157
161
  end
158
162
 
163
+ def blank_line?(form)
164
+ form.is_a?(BlankLine)
165
+ end
166
+
167
+ def non_semantic?(form)
168
+ comment?(form) || blank_line?(form)
169
+ end
170
+
159
171
  def render(form, indent, layout: nil, top_level: false, force_expand: false)
160
172
  flat = flat_render(form)
161
173
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(form, top_level:, layout:)
@@ -165,30 +177,65 @@ module Kapusta
165
177
  when List then render_list(form, indent, top_level:)
166
178
  when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
167
179
  when HashLit then render_hash(form, indent)
180
+ when Quasiquote then render_prefix('`', form.form, indent, force_expand:)
181
+ when Unquote then render_prefix(',', form.form, indent, force_expand:)
182
+ when UnquoteSplice then render_prefix(',@', form.form, indent, force_expand:)
168
183
  else
169
184
  flat || raise(Error, "cannot format form: #{form.inspect}")
170
185
  end
171
186
  end
172
187
 
188
+ def render_prefix(prefix, inner, indent, force_expand: false)
189
+ rendered = render(inner, indent + prefix.length, force_expand:)
190
+ lines = rendered.lines(chomp: true)
191
+ pad = ' ' * prefix.length
192
+ lines.each_with_index.map { |line, i| i.zero? ? "#{prefix}#{line}" : "#{pad}#{line}" }.join("\n")
193
+ end
194
+
173
195
  def flat_render(form)
174
196
  case form
175
197
  when Comment
176
198
  nil
199
+ when AutoGensym
200
+ "#{form.name}#"
177
201
  when Sym
178
202
  form.name
179
203
  when Vec
180
204
  return if contains_comments?(form.items)
205
+ return if multiline_in_source?(form)
206
+
207
+ rendered = form.items.map { |item| flat_render(item) }
208
+ return if rendered.any?(&:nil?)
181
209
 
182
- "[#{form.items.map { |item| flat_render(item) }.join(' ')}]"
210
+ "[#{rendered.join(' ')}]"
183
211
  when HashLit
184
212
  return if contains_comments?(form.entries)
213
+ return if multiline_in_source?(form)
214
+
215
+ rendered = form.pairs.map { |key, value| flat_hash_pair(key, value) }
216
+ return if rendered.any?(&:nil?)
185
217
 
186
- "{#{form.pairs.map { |key, value| flat_hash_pair(key, value) }.join(' ')}}"
218
+ "{#{rendered.join(' ')}}"
187
219
  when List
188
220
  return if contains_comments?(form.items)
189
221
  return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
190
-
191
- "(#{form.items.map { |item| flat_render(item) }.join(' ')})"
222
+ return if multiline_in_source?(form)
223
+ return if let_with_multiple_bindings?(form)
224
+ return if let_with_nested_binding_value?(form)
225
+
226
+ rendered = form.items.map { |item| flat_render(item) }
227
+ return if rendered.any?(&:nil?)
228
+
229
+ "(#{rendered.join(' ')})"
230
+ when Quasiquote
231
+ inner = flat_render(form.form)
232
+ inner ? "`#{inner}" : nil
233
+ when Unquote
234
+ inner = flat_render(form.form)
235
+ inner ? ",#{inner}" : nil
236
+ when UnquoteSplice
237
+ inner = flat_render(form.form)
238
+ inner ? ",@#{inner}" : nil
192
239
  when String, Numeric, true, false, nil
193
240
  form.inspect
194
241
  when Symbol
@@ -207,7 +254,7 @@ module Kapusta
207
254
  raw_args = list_raw_rest(list)
208
255
 
209
256
  case head_name
210
- when 'fn', 'lambda', 'λ' then render_fn(head_name, list, indent, top_level:)
257
+ when 'fn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
211
258
  when 'let' then render_let(list, indent)
212
259
  when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
213
260
  when 'try' then render_try(list, indent)
@@ -237,7 +284,12 @@ module Kapusta
237
284
  raw_args = list_raw_rest(list)
238
285
  prefix_length = args[0].is_a?(Sym) && args[1].is_a?(Vec) ? 2 : 1
239
286
  raw_prefix, raw_body = split_raw_items(raw_args, prefix_length)
240
- render_prefix_body_form(head, raw_prefix, raw_body, indent, force_body_multiline: top_level)
287
+ force = top_level || fn_body_has_quasi_list?(raw_body)
288
+ render_prefix_body_form(head, raw_prefix, raw_body, indent, force_body_multiline: force)
289
+ end
290
+
291
+ def fn_body_has_quasi_list?(body_forms)
292
+ body_forms.any? { |form| form.is_a?(Quasiquote) && form.form.is_a?(List) }
241
293
  end
242
294
 
243
295
  def render_catch(list, indent)
@@ -300,6 +352,35 @@ module Kapusta
300
352
  append_suffix(lines, ')')
301
353
  end
302
354
 
355
+ def append_prefix_form(lines, form, indent, current_first_line, inline_prefix, layouts, layout_index)
356
+ if blank_line?(form)
357
+ lines << ''
358
+ return [current_first_line, false, layout_index]
359
+ end
360
+ if comment?(form)
361
+ lines << indent_block(render(form, indent + INDENT), INDENT)
362
+ return [current_first_line, false, layout_index]
363
+ end
364
+
365
+ rendered = render(form, indent + current_first_line.length + 1, layout: layouts[layout_index])
366
+ rendered_lines = rendered.lines.map(&:chomp)
367
+ candidate_first = "#{current_first_line} #{rendered_lines.first}"
368
+
369
+ if inline_prefix && fits?(candidate_first, indent)
370
+ lines[-1] = candidate_first
371
+ if rendered_lines.length == 1
372
+ [candidate_first, true, layout_index + 1]
373
+ else
374
+ hanging = ' ' * (current_first_line.length + 1)
375
+ rendered_lines.drop(1).each { |line| lines << "#{hanging}#{line}" }
376
+ [current_first_line, false, layout_index + 1]
377
+ end
378
+ else
379
+ lines << indent_block(rendered, INDENT)
380
+ [current_first_line, false, layout_index + 1]
381
+ end
382
+ end
383
+
303
384
  def render_prefix_body_form(head, prefix_forms, body_forms, indent, layouts: [], force_body_multiline: false)
304
385
  line = "(#{head}"
305
386
  lines = [line]
@@ -308,26 +389,15 @@ module Kapusta
308
389
  inline_prefix = true
309
390
 
310
391
  prefix_forms.each do |form|
311
- if comment?(form)
312
- lines << indent_block(render(form, indent + INDENT), INDENT)
313
- inline_prefix = false
314
- next
315
- end
316
-
317
- rendered = render(form, indent + INDENT, layout: layouts[layout_index])
318
- layout_index += 1
319
- candidate = "#{current_first_line} #{rendered}"
320
-
321
- if inline_prefix && single_line?(rendered) && fits?(candidate, indent)
322
- current_first_line = candidate
323
- lines[0] = current_first_line
324
- else
325
- lines << indent_block(rendered, INDENT)
326
- inline_prefix = false
327
- end
392
+ current_first_line, inline_prefix, layout_index =
393
+ append_prefix_form(lines, form, indent, current_first_line, inline_prefix, layouts, layout_index)
328
394
  end
329
395
 
330
396
  body_forms.each do |form|
397
+ if blank_line?(form)
398
+ lines << ''
399
+ next
400
+ end
331
401
  if comment?(form)
332
402
  lines << indent_block(render(form, indent + INDENT), INDENT)
333
403
  next
@@ -356,8 +426,8 @@ module Kapusta
356
426
  return flat if inline_three_arg_if?(args) && flat && fits?(flat, indent)
357
427
 
358
428
  lines << "(if #{render(args[0], indent + '(if '.length)}"
359
- lines << "#{hanging}#{render(args[1], indent + '(if '.length)}"
360
- lines << "#{hanging}#{render(args[2], indent + '(if '.length)}"
429
+ lines << prefix_continuation(hanging, render(args[1], indent + '(if '.length))
430
+ lines << prefix_continuation(hanging, render(args[2], indent + '(if '.length))
361
431
  return append_suffix(lines, ')')
362
432
  end
363
433
 
@@ -368,7 +438,7 @@ module Kapusta
368
438
  lines << "(if #{first_pair}"
369
439
  else
370
440
  lines << "(if #{render(args[0], indent + '(if '.length)}"
371
- lines << "#{hanging}#{render(args[1], indent + '(if '.length)}"
441
+ lines << prefix_continuation(hanging, render(args[1], indent + '(if '.length))
372
442
  end
373
443
  index = 2
374
444
  else
@@ -382,12 +452,12 @@ module Kapusta
382
452
  if pair
383
453
  lines << "#{hanging}#{pair}"
384
454
  else
385
- lines << "#{hanging}#{render(args[index], indent + '(if '.length)}"
386
- lines << "#{hanging}#{render(args[index + 1], indent + '(if '.length)}"
455
+ lines << prefix_continuation(hanging, render(args[index], indent + '(if '.length))
456
+ lines << prefix_continuation(hanging, render(args[index + 1], indent + '(if '.length))
387
457
  end
388
458
  index += 2
389
459
  else
390
- lines << "#{hanging}#{render(args[index], indent + '(if '.length)}"
460
+ lines << prefix_continuation(hanging, render(args[index], indent + '(if '.length))
391
461
  index += 1
392
462
  end
393
463
  end
@@ -395,6 +465,12 @@ module Kapusta
395
465
  append_suffix(lines, ')')
396
466
  end
397
467
 
468
+ def prefix_continuation(prefix, rendered)
469
+ first_line, *rest = rendered.lines(chomp: true)
470
+ pad = ' ' * prefix.length
471
+ ["#{prefix}#{first_line}", *rest.map { |line| "#{pad}#{line}" }].join("\n")
472
+ end
473
+
398
474
  def render_case(head, args, indent)
399
475
  subject = args.first
400
476
  clauses = args.drop(1)
@@ -505,15 +581,44 @@ module Kapusta
505
581
  flat = flat_render(vec)
506
582
  return flat if !force_expand && flat && fits?(flat, indent) && allow_flat?(vec, top_level:, layout:)
507
583
 
508
- if layout == :pairwise && !contains_comments?(vec.items)
509
- render_pairwise_vec(vec, indent)
510
- else
511
- lines = ['[']
512
- vec.items.each do |item|
513
- lines << indent_block(render(item, indent + INDENT), INDENT)
584
+ return render_pairwise_vec(vec, indent) if layout == :pairwise && !contains_comments?(vec.items)
585
+ return render_filled_vec(vec, indent) if !contains_comments?(vec.items) && !vec.items.empty?
586
+
587
+ lines = ['[']
588
+ vec.items.each do |item|
589
+ lines << indent_block(render(item, indent + INDENT), INDENT)
590
+ end
591
+ append_suffix(lines, ']')
592
+ end
593
+
594
+ def render_filled_vec(vec, indent)
595
+ output_lines = ['[']
596
+
597
+ vec.items.each_with_index do |item, idx|
598
+ if idx.zero?
599
+ item_col = output_lines.last.length
600
+ rendered_lines = render(item, indent + item_col).lines.map(&:chomp)
601
+ output_lines[-1] += rendered_lines.first
602
+ rendered_lines.drop(1).each { |line| output_lines << ((' ' * item_col) + line) }
603
+ next
604
+ end
605
+
606
+ inline_col = output_lines.last.length + 1
607
+ flat = flat_render(item)
608
+
609
+ if flat && indent + inline_col + flat.length <= MAX_WIDTH
610
+ output_lines[-1] += " #{flat}"
611
+ elsif flat && indent + 1 + flat.length <= MAX_WIDTH
612
+ output_lines << " #{flat}"
613
+ else
614
+ rendered_lines = render(item, indent + inline_col).lines.map(&:chomp)
615
+ output_lines[-1] += " #{rendered_lines.first}"
616
+ rendered_lines.drop(1).each { |line| output_lines << ((' ' * inline_col) + line) }
514
617
  end
515
- append_suffix(lines, ']')
516
618
  end
619
+
620
+ output_lines[-1] += ']'
621
+ output_lines.join("\n")
517
622
  end
518
623
 
519
624
  def render_pairwise_vec(vec, indent)
@@ -567,33 +672,45 @@ module Kapusta
567
672
  def render_hash(hash, indent)
568
673
  flat = flat_render(hash)
569
674
  return flat if flat && fits?(flat, indent)
675
+ return '{}' if hash.entries.empty?
570
676
 
571
- lines = ['{']
677
+ output_lines = []
572
678
 
573
- hash.entries.each do |entry|
679
+ hash.entries.each_with_index do |entry, idx|
574
680
  if comment?(entry)
575
- lines << indent_block(render(entry, indent + INDENT), INDENT)
576
- else
577
- key, value = entry
578
- pair = flat_hash_pair(key, value)
579
- if pair && fits?(pair, indent + INDENT)
580
- lines << indent_block(pair, INDENT)
581
- else
582
- lines << indent_block(render_hash_key(key), INDENT)
583
- lines << indent_block(render(value, indent + INDENT), INDENT)
584
- end
681
+ output_lines << "#{idx.zero? ? '{' : ' '}#{render(entry, indent + 1)}"
682
+ next
683
+ end
684
+
685
+ key, value = entry
686
+ first_pair = output_lines.empty?
687
+ if hash_shorthand?(key, value)
688
+ output_lines << "#{first_pair ? '{' : ' '}: #{value.name}"
689
+ next
585
690
  end
691
+
692
+ key_str = render_hash_key(key)
693
+ value_col = indent + 1 + key_str.length + 1
694
+ rendered_value = render(value, value_col)
695
+ value_lines = rendered_value.lines(chomp: true)
696
+
697
+ prefix = "#{first_pair ? '{' : ' '}#{key_str} "
698
+ output_lines << "#{prefix}#{value_lines.first}"
699
+ pad = ' ' * prefix.length
700
+ value_lines.drop(1).each { |line| output_lines << "#{pad}#{line}" }
586
701
  end
587
702
 
588
- append_suffix(lines, '}')
703
+ output_lines[-1] = "#{output_lines[-1]}}"
704
+ output_lines.join("\n")
589
705
  end
590
706
 
591
707
  def flat_hash_pair(key, value)
592
- if hash_shorthand?(key, value)
593
- ": #{value.name}"
594
- else
595
- "#{render_hash_key(key)} #{flat_render(value)}"
596
- end
708
+ return ": #{value.name}" if hash_shorthand?(key, value)
709
+
710
+ rendered_value = flat_render(value)
711
+ return unless rendered_value
712
+
713
+ "#{render_hash_key(key)} #{rendered_value}"
597
714
  end
598
715
 
599
716
  def render_hash_key(key)
@@ -642,16 +759,17 @@ module Kapusta
642
759
  items[0].name == 'hashfn'
643
760
  end
644
761
 
645
- def allow_flat?(form, top_level:, layout:)
762
+ def allow_flat?(form, top_level: false, layout: nil)
646
763
  return false if layout == :pairwise && form.is_a?(Vec) && semantic_items(form.items).length > 2
647
764
  return true unless form.is_a?(List)
765
+ return true if !multiline_in_source?(form) && form.respond_to?(:multiline_source)
648
766
 
649
767
  head = list_head(form)
650
768
  return true unless head.is_a?(Sym)
651
769
 
652
770
  case head.name
653
- when 'fn', 'lambda', 'λ', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect', 'accumulate',
654
- 'faccumulate'
771
+ when 'fn', 'lambda', 'λ', 'macro', 'when', 'unless', 'for', 'each', 'icollect', 'collect', 'fcollect',
772
+ 'accumulate', 'faccumulate'
655
773
  !top_level
656
774
  else
657
775
  !%w[let case match try catch finally do -> ->> -?> -?>> doto].include?(head.name)
@@ -659,14 +777,16 @@ module Kapusta
659
777
  end
660
778
 
661
779
  def force_multiline_body?(form)
780
+ return force_multiline_body?(form.form) if form.is_a?(Quasiquote)
662
781
  return false unless form.is_a?(List)
782
+ return true if multiline_in_source?(form)
663
783
 
664
784
  head = list_head(form)
665
785
  return false unless head.is_a?(Sym)
666
786
 
667
787
  case head.name
668
788
  when 'if', 'case', 'match', 'let', 'try', 'catch', 'finally', 'do', 'for', '->', '->>', '-?>', '-?>>', 'doto',
669
- 'fn', 'lambda', 'λ'
789
+ 'fn', 'lambda', 'λ', 'macro'
670
790
  true
671
791
  else
672
792
  flat = flat_render(form)
@@ -674,27 +794,43 @@ module Kapusta
674
794
  end
675
795
  end
676
796
 
677
- def consecutive_requires?(previous, current)
678
- require_form?(previous) && require_form?(current)
797
+ def multiline_in_source?(form)
798
+ form.respond_to?(:multiline_source) && form.multiline_source
679
799
  end
680
800
 
681
- def groupable_top_level_form?(form)
682
- return true if require_form?(form)
683
- return false unless form.is_a?(List) && flat_render(form)
801
+ def let_with_multiple_bindings?(form)
802
+ head = list_head(form)
803
+ return false unless head.is_a?(Sym) && head.name == 'let'
804
+
805
+ bindings = semantic_items(form.items)[1]
806
+ return false unless bindings.is_a?(Vec)
807
+
808
+ semantic_items(bindings.items).length > 2
809
+ end
684
810
 
811
+ def let_with_nested_binding_value?(form)
685
812
  head = list_head(form)
686
- return false unless head.is_a?(Sym)
813
+ return false unless head.is_a?(Sym) && head.name == 'let'
814
+
815
+ bindings = semantic_items(form.items)[1]
816
+ return false unless bindings.is_a?(Vec)
687
817
 
688
- !%w[fn module class let].include?(head.name)
818
+ semantic_items(bindings.items).each_slice(2).any? do |_pattern, value|
819
+ value && contains_collection?(value)
820
+ end
689
821
  end
690
822
 
691
- def require_form?(form)
692
- return false unless form.is_a?(List)
823
+ def contains_collection?(form)
824
+ case form
825
+ when List then semantic_items(form.items).any? { |item| collection?(item) }
826
+ when Vec then form.items.any? { |item| collection?(item) }
827
+ when HashLit then form.pairs.any? { |k, v| collection?(k) || collection?(v) }
828
+ else false
829
+ end
830
+ end
693
831
 
694
- items = semantic_items(form.items)
695
- items.length == 2 &&
696
- items[0].is_a?(Sym) &&
697
- items[0].name == 'require'
832
+ def collection?(form)
833
+ form.is_a?(List) || form.is_a?(Vec) || form.is_a?(HashLit)
698
834
  end
699
835
 
700
836
  def fn_form?(form)
@@ -784,11 +920,11 @@ module Kapusta
784
920
  end
785
921
 
786
922
  def contains_comments?(items)
787
- items.any? { |item| comment?(item) }
923
+ items.any? { |item| non_semantic?(item) }
788
924
  end
789
925
 
790
926
  def semantic_items(items)
791
- items.reject { |item| comment?(item) }
927
+ items.reject { |item| non_semantic?(item) }
792
928
  end
793
929
 
794
930
  def list_head(list)