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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/Gemfile +10 -0
- data/README.md +58 -0
- data/Rakefile +10 -0
- data/bin/console +8 -0
- data/bin/setup +4 -0
- data/examples/accumulator.kap +16 -0
- data/examples/ackermann.kap +7 -0
- data/examples/anagram.kap +13 -0
- data/examples/binary-search.kap +16 -0
- data/examples/block-sort.kap +3 -0
- data/examples/calc.kap +10 -0
- data/examples/counter.kap +19 -0
- data/examples/describe.kap +9 -0
- data/examples/destructure.kap +4 -0
- data/examples/doto.kap +2 -0
- data/examples/egg-count.kap +10 -0
- data/examples/even-squares.kap +7 -0
- data/examples/exceptions.kap +14 -0
- data/examples/factorial.kap +8 -0
- data/examples/fib.kap +4 -0
- data/examples/fizzbuzz.kap +7 -0
- data/examples/gcd.kap +5 -0
- data/examples/greet.kap +2 -0
- data/examples/hashfn.kap +4 -0
- data/examples/kwargs.kap +1 -0
- data/examples/leap-year.kap +5 -0
- data/examples/match.kap +9 -0
- data/examples/min-max.kap +11 -0
- data/examples/module-header.kap +6 -0
- data/examples/palindrome.kap +8 -0
- data/examples/pangram.kap +9 -0
- data/examples/pcall.kap +9 -0
- data/examples/pipeline.kap +6 -0
- data/examples/points.kap +9 -0
- data/examples/primes.kap +8 -0
- data/examples/raindrops.kap +13 -0
- data/examples/record.kap +6 -0
- data/examples/regex.kap +9 -0
- data/examples/ruby-eval.kap +1 -0
- data/examples/safe-lookup.kap +6 -0
- data/examples/scopes.kap +18 -0
- data/examples/shapes.kap +9 -0
- data/examples/squares.kap +3 -0
- data/examples/stack.kap +19 -0
- data/examples/sum.kap +3 -0
- data/examples/tset.kap +4 -0
- data/examples/two-sum.kap +17 -0
- data/exe/kapfmt +6 -0
- data/exe/kapusta +6 -0
- data/kapfmt +4 -0
- data/kapusta.gemspec +25 -0
- data/lib/kapusta/ast.rb +76 -0
- data/lib/kapusta/cli.rb +61 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +178 -0
- data/lib/kapusta/compiler/emitter/collections.rb +245 -0
- data/lib/kapusta/compiler/emitter/control_flow.rb +168 -0
- data/lib/kapusta/compiler/emitter/expressions.rb +107 -0
- data/lib/kapusta/compiler/emitter/interop.rb +277 -0
- data/lib/kapusta/compiler/emitter/patterns.rb +105 -0
- data/lib/kapusta/compiler/emitter/support.rb +169 -0
- data/lib/kapusta/compiler/emitter.rb +45 -0
- data/lib/kapusta/compiler/normalizer.rb +122 -0
- data/lib/kapusta/compiler/runtime.rb +583 -0
- data/lib/kapusta/compiler.rb +47 -0
- data/lib/kapusta/env.rb +42 -0
- data/lib/kapusta/formatter.rb +685 -0
- data/lib/kapusta/reader.rb +215 -0
- data/lib/kapusta/support.rb +7 -0
- data/lib/kapusta/version.rb +5 -0
- data/lib/kapusta.rb +30 -0
- data/spec/cli_spec.rb +77 -0
- data/spec/examples_spec.rb +258 -0
- data/spec/formatter_spec.rb +176 -0
- data/spec/spec_helper.rb +12 -0
- 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
|