kapusta 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -11
  3. data/bin/check-all +6 -1
  4. data/bin/compile-examples +7 -2
  5. data/examples/anagram.kap +3 -3
  6. data/examples/app/args.kap +9 -0
  7. data/examples/arrange-coins.kap +3 -3
  8. data/examples/baseball-game.kap +5 -5
  9. data/examples/best-time-to-buy-sell-stock.kap +2 -2
  10. data/examples/binary-search.kap +2 -2
  11. data/examples/binary-to-decimal.kap +1 -1
  12. data/examples/blocks-and-kwargs.kap +2 -2
  13. data/examples/count-effects.kap +1 -1
  14. data/examples/count-items-matching-rule.kap +18 -0
  15. data/examples/doto-hygiene.kap +2 -2
  16. data/examples/doto.kap +2 -2
  17. data/examples/egg-count.kap +1 -1
  18. data/examples/exceptions.kap +1 -1
  19. data/examples/falling-drops.kap +7 -7
  20. data/examples/fennel-parity-examples.txt +3 -6
  21. data/examples/good-pairs.kap +18 -0
  22. data/examples/greet.kap +1 -1
  23. data/examples/happy-number.kap +2 -2
  24. data/examples/left-right-difference.kap +60 -0
  25. data/examples/length-of-last-word.kap +4 -4
  26. data/examples/manhattan-distance.kap +1 -1
  27. data/examples/maximum-subarray.kap +23 -7
  28. data/examples/minimum-start-value.kap +19 -0
  29. data/examples/move-zeroes.kap +2 -2
  30. data/examples/mruby-runtime-examples.txt +8 -2
  31. data/examples/non-constant-local.kap +1 -1
  32. data/examples/number-of-1-bits.kap +1 -1
  33. data/examples/number-of-steps.kap +1 -1
  34. data/examples/palindrome.kap +2 -2
  35. data/examples/pangram.kap +4 -4
  36. data/examples/pcall.kap +3 -3
  37. data/examples/pipeline.kap +4 -4
  38. data/examples/plus-one.kap +3 -3
  39. data/examples/raindrops.kap +1 -1
  40. data/examples/range-width.kap +14 -0
  41. data/examples/recent-counter.kap +6 -6
  42. data/examples/require-local-args.kap +9 -0
  43. data/examples/require-local.kap +7 -0
  44. data/examples/require-module-local.kap +4 -0
  45. data/examples/require-module.kap +4 -0
  46. data/examples/reverse-integer.kap +1 -1
  47. data/examples/roman-to-integer.kap +3 -3
  48. data/examples/running-sum.kap +20 -0
  49. data/examples/safe-lookup.kap +2 -2
  50. data/examples/single-number.kap +1 -1
  51. data/examples/stack.kap +14 -14
  52. data/examples/subtract-product-sum.kap +1 -1
  53. data/examples/summary-ranges.kap +45 -0
  54. data/examples/threading.kap +5 -5
  55. data/examples/tset.kap +1 -1
  56. data/examples/two-sum-hash.kap +3 -3
  57. data/examples/two-sum.kap +1 -1
  58. data/examples/ugly-number.kap +1 -1
  59. data/examples/underground-system.kap +39 -0
  60. data/examples/valid-parentheses-1.kap +6 -6
  61. data/exe/kapusta-ls +49 -2
  62. data/lib/kapusta/ast.rb +8 -0
  63. data/lib/kapusta/compiler/emitter/bindings.rb +111 -89
  64. data/lib/kapusta/compiler/emitter/collections.rb +32 -40
  65. data/lib/kapusta/compiler/emitter/control_flow.rb +33 -31
  66. data/lib/kapusta/compiler/emitter/expressions.rb +21 -5
  67. data/lib/kapusta/compiler/emitter/interop.rb +168 -48
  68. data/lib/kapusta/compiler/emitter/patterns.rb +12 -14
  69. data/lib/kapusta/compiler/emitter/support.rb +63 -81
  70. data/lib/kapusta/compiler/language.rb +522 -0
  71. data/lib/kapusta/compiler/lua_compat.rb +23 -28
  72. data/lib/kapusta/compiler/macro_expander.rb +30 -30
  73. data/lib/kapusta/compiler/macro_lowerer.rb +12 -24
  74. data/lib/kapusta/compiler/normalizer.rb +25 -17
  75. data/lib/kapusta/compiler.rb +3 -24
  76. data/lib/kapusta/env.rb +2 -2
  77. data/lib/kapusta/errors.rb +2 -1
  78. data/lib/kapusta/formatter/ast_helpers.rb +78 -0
  79. data/lib/kapusta/formatter/cli.rb +125 -0
  80. data/lib/kapusta/formatter/line_helpers.rb +44 -0
  81. data/lib/kapusta/formatter/validator.rb +32 -0
  82. data/lib/kapusta/formatter.rb +354 -325
  83. data/lib/kapusta/lsp/identifier.rb +1 -1
  84. data/lib/kapusta/lsp/rename.rb +21 -11
  85. data/lib/kapusta/lsp/scope_walker.rb +122 -212
  86. data/lib/kapusta/lsp/workspace_index.rb +17 -5
  87. data/lib/kapusta/reader.rb +4 -2
  88. data/lib/kapusta/version.rb +1 -1
  89. data/lib/kapusta.rb +39 -6
  90. data/spec/cli_spec.rb +13 -0
  91. data/spec/examples_errors_spec.rb +3 -1
  92. data/spec/examples_spec.rb +67 -15
  93. data/spec/formatter_spec.rb +246 -0
  94. data/spec/lsp_spec.rb +69 -0
  95. data/spec/require_spec.rb +294 -0
  96. metadata +20 -2
  97. data/examples/describe.kap +0 -9
@@ -32,18 +32,16 @@ module Kapusta
32
32
  private
33
33
 
34
34
  def expand_top(form)
35
- if form.is_a?(List) && form.head.is_a?(Sym)
36
- case form.head.name
37
- when 'macro'
38
- register_macro_form(form.rest)
39
- return []
40
- when 'macros'
41
- register_macros_form(form.rest)
42
- return []
43
- when 'import-macros'
44
- handle_import_macros(form)
45
- return []
46
- end
35
+ case Language.list_head_name(form)
36
+ when 'macro'
37
+ register_macro_form(form.rest)
38
+ return []
39
+ when 'macros'
40
+ register_macros_form(form.rest)
41
+ return []
42
+ when 'import-macros'
43
+ handle_import_macros(form)
44
+ return []
47
45
  end
48
46
  [expand(form)]
49
47
  end
@@ -104,11 +102,11 @@ module Kapusta
104
102
  end
105
103
 
106
104
  def register_macro_form(args)
107
- name_sym, params, *body = args
108
- raise macro_error(:macro_name_must_be_symbol, name_sym) unless name_sym.is_a?(Sym)
109
- raise macro_error(:macro_params_must_be_vector, params) unless params.is_a?(Vec)
105
+ parsed = Language.parse_macro_definition_args(args)
106
+ raise macro_error(:macro_name_must_be_symbol, parsed.name) unless parsed.name.is_a?(Sym)
107
+ raise macro_error(:macro_params_must_be_vector, parsed.params) unless parsed.params.is_a?(Vec)
110
108
 
111
- register(name_sym.name, params, body)
109
+ register(parsed.name.name, parsed.params, parsed.body)
112
110
  end
113
111
 
114
112
  def register_macros_form(args)
@@ -116,11 +114,15 @@ module Kapusta
116
114
  raise macro_error(:macros_expects_hash, hash_lit) unless hash_lit.is_a?(HashLit)
117
115
 
118
116
  hash_lit.pairs.each do |key, value|
119
- raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect) unless fn_form?(value)
117
+ unless Language.function_head?(Language.list_head_name(value))
118
+ raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect)
119
+ end
120
+
121
+ parsed = Language.parse_function_form(value)
120
122
 
121
123
  name = key.to_s
122
- params = value.items[1]
123
- body = value.items[2..]
124
+ params = parsed&.named? ? value.items[1] : (parsed&.params || value.items[1])
125
+ body = parsed&.body || value.items[2..]
124
126
  raise macro_error(:macros_entry_params_must_be_vector, params) unless params.is_a?(Vec)
125
127
 
126
128
  register(name, params, body)
@@ -128,26 +130,24 @@ module Kapusta
128
130
  end
129
131
 
130
132
  def fn_form?(value)
131
- value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
133
+ Language.function_form?(value)
132
134
  end
133
135
 
134
136
  def handle_import_macros(form)
135
- args = form.rest
136
- destructure = args[0]
137
- module_arg = args[1]
138
- unless destructure.is_a?(HashLit) || destructure.is_a?(Sym)
137
+ parsed = Language.parse_import_macros_args(form.rest)
138
+ unless parsed.destructure.is_a?(HashLit) || parsed.destructure.is_a?(Sym)
139
139
  raise macro_error(:import_macros_destructure_invalid, form)
140
140
  end
141
- unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
141
+ unless parsed.module_arg.is_a?(Symbol) || parsed.module_arg.is_a?(String)
142
142
  raise macro_error(:import_macros_module_invalid, form)
143
143
  end
144
144
 
145
- module_label = MacroImporter.module_label(module_arg)
146
- exports = macro_importer.load(module_arg, form)
147
- if destructure.is_a?(HashLit)
148
- register_imported_macros(destructure, exports, module_label, form)
145
+ module_label = MacroImporter.module_label(parsed.module_arg)
146
+ exports = macro_importer.load(parsed.module_arg, form)
147
+ if parsed.destructure.is_a?(HashLit)
148
+ register_imported_macros(parsed.destructure, exports, module_label, form)
149
149
  else
150
- register_whole_module(destructure, exports)
150
+ register_whole_module(parsed.destructure, exports)
151
151
  end
152
152
  end
153
153
 
@@ -5,8 +5,6 @@ require_relative 'macro_gensym'
5
5
  module Kapusta
6
6
  module Compiler
7
7
  class MacroLowerer
8
- FN_HEADS = %w[fn lambda λ].freeze
9
-
10
8
  def self.compile(params:, body:, path:, error_class:)
11
9
  callable = new(error_class:).callable_form(params, body)
12
10
  ruby = Compiler.compile_forms([callable], path:)
@@ -56,21 +54,11 @@ module Kapusta
56
54
  private
57
55
 
58
56
  def lower_fn_form(form)
59
- items = form.items
60
- if items[1].is_a?(Sym) && items[2].is_a?(Vec)
61
- name_sym = items[1]
62
- params = items[2]
63
- body = items[3..] || []
64
- elsif items[1].is_a?(Vec)
65
- name_sym = nil
66
- params = items[1]
67
- body = items[2..] || []
68
- else
69
- return form
70
- end
57
+ parsed = Language.parse_function_form(form)
58
+ return form unless parsed
71
59
 
72
- head_items = name_sym ? [form.head, name_sym, params] : [form.head, params]
73
- List.new(head_items + lowered_body_with_gensyms(body))
60
+ head_items = parsed.named? ? [form.head, parsed.name, parsed.params] : [form.head, parsed.params]
61
+ List.new(head_items + lowered_body_with_gensyms(parsed.body))
74
62
  end
75
63
 
76
64
  def lowered_body_with_gensyms(body)
@@ -91,7 +79,7 @@ module Kapusta
91
79
  end
92
80
 
93
81
  def fn_form?(form)
94
- form.is_a?(List) && form.head.is_a?(Sym) && FN_HEADS.include?(form.head.name)
82
+ Language.function_form?(form)
95
83
  end
96
84
 
97
85
  def lower_quasi(form)
@@ -147,8 +135,8 @@ module Kapusta
147
135
  end
148
136
 
149
137
  def lower_quasi_item(item)
150
- if item.is_a?(Unquote) && unpack_call?(item.form)
151
- inner = lower(item.form.items[1])
138
+ if item.is_a?(Unquote) && (unpack = Language.parse_unpack_call(item.form))
139
+ inner = lower(unpack.value)
152
140
  List.new([Sym.new('.'), inner, 0])
153
141
  else
154
142
  lower_quasi(item)
@@ -159,13 +147,13 @@ module Kapusta
159
147
  last = items.last
160
148
  return unless last
161
149
  return lower(last.form) if last.is_a?(UnquoteSplice)
162
- return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
163
150
 
164
- nil
165
- end
151
+ if last.is_a?(Unquote)
152
+ unpack = Language.parse_unpack_call(last.form)
153
+ return lower(unpack.value) if unpack
154
+ end
166
155
 
167
- def unpack_call?(form)
168
- form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
156
+ nil
169
157
  end
170
158
 
171
159
  def gensym_local_for(prefix)
@@ -41,27 +41,27 @@ module Kapusta
41
41
 
42
42
  case head.name
43
43
  when 'when'
44
- raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
44
+ parsed = Language.parse_conditional_body_args(items[1..])
45
+ raise compiler_error(:when_no_body, list, form: head.name) unless parsed.body?
45
46
 
46
- cond = items[1]
47
- body = wrap_do(items[2..])
48
- Kapusta.copy_position(List.new([Sym.new('if'), cond, body]), list)
47
+ Kapusta.copy_position(List.new([Sym.new('if'), parsed.condition, wrap_do(parsed.body)]), list)
49
48
  when 'unless'
50
- raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
49
+ parsed = Language.parse_conditional_body_args(items[1..])
50
+ raise compiler_error(:when_no_body, list, form: head.name) unless parsed.body?
51
51
 
52
- cond = items[1]
53
- body = wrap_do(items[2..])
54
- Kapusta.copy_position(List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body]), list)
52
+ negated = List.new([Sym.new('not'), parsed.condition])
53
+ Kapusta.copy_position(List.new([Sym.new('if'), negated, wrap_do(parsed.body)]), list)
55
54
  when 'tset'
56
- raise compiler_error(:tset_no_value, list) if items.length < 4
55
+ parsed = Language.parse_tset_args(items[1..])
56
+ raise compiler_error(:tset_no_value, list) unless parsed
57
57
 
58
58
  Kapusta.copy_position(
59
- List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
59
+ List.new([Sym.new('set'), List.new([Sym.new(':'), parsed.table, parsed.key]), parsed.value]),
60
60
  list
61
61
  )
62
62
  when *LuaCompat::SPECIAL_FORMS
63
63
  normalize_lua_compat_form(head.name, items)
64
- when '->', '->>', '-?>', '-?>>'
64
+ when *Language::THREAD_HEADS
65
65
  Kapusta.copy_position(normalize(thread(items[1..], head.name)), list)
66
66
  when 'doto'
67
67
  Kapusta.copy_position(normalize(doto(items[1..])), list)
@@ -85,8 +85,8 @@ module Kapusta
85
85
 
86
86
  def thread(forms, kind)
87
87
  value = forms.first
88
- short = %w[-?> -?>>].include?(kind)
89
- position = %w[-> -?>].include?(kind) ? :first : :last
88
+ short = Language.short_pipeline_head?(kind)
89
+ position = Language.thread_first_head?(kind) ? :first : :last
90
90
 
91
91
  return thread_short(forms, position) if short
92
92
 
@@ -124,9 +124,9 @@ module Kapusta
124
124
  def thread_step(memo, form, position)
125
125
  if form.is_a?(List)
126
126
  if position == :first
127
- List.new([form.items[0], memo, *form.items[1..]])
127
+ prepend_call_arg(form, memo)
128
128
  else
129
- List.new([*form.items, memo])
129
+ append_call_arg(form, memo)
130
130
  end
131
131
  else
132
132
  List.new([form, memo])
@@ -144,19 +144,27 @@ module Kapusta
144
144
  temp = gensym('doto')
145
145
  body = forms[1..].map do |form|
146
146
  if form.is_a?(List)
147
- List.new([form.items[0], temp, *form.items[1..]])
147
+ prepend_call_arg(form, temp)
148
148
  else
149
149
  List.new([form, temp])
150
150
  end
151
151
  end
152
152
  fn = List.new([Sym.new('fn'), Vec.new([temp]), *body])
153
- List.new([Sym.new(':'), value, :tap, fn])
153
+ List.new([Sym.new('.'), value, :tap, fn])
154
154
  end
155
155
 
156
156
  def gensym(prefix)
157
157
  @gensym_index = (@gensym_index || 0) + 1
158
158
  GeneratedSym.new("#{prefix}_#{@gensym_index}", @gensym_index)
159
159
  end
160
+
161
+ def prepend_call_arg(form, value)
162
+ List.new([form.head, value, *form.rest])
163
+ end
164
+
165
+ def append_call_arg(form, value)
166
+ List.new([*form.items, value])
167
+ end
160
168
  end
161
169
  end
162
170
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'error'
4
4
  require_relative 'compiler/lua_compat'
5
+ require_relative 'compiler/language'
5
6
  require_relative 'compiler/normalizer'
6
7
  require_relative 'compiler/emitter'
7
8
  require_relative 'compiler/macro_expander'
@@ -9,30 +10,8 @@ require_relative 'compiler/macro_expander'
9
10
  module Kapusta
10
11
  module Compiler
11
12
  class Error < Kapusta::Error; end
12
- CORE_SPECIAL_FORMS = %w[
13
- fn defn lambda λ let local var global set if when unless case match
14
- while for each do values
15
- -> ->> -?> -?>> doto
16
- icollect collect fcollect accumulate faccumulate
17
- hashfn
18
- . ?. :
19
- ..
20
- length
21
- require
22
- module class end
23
- try catch finally
24
- raise
25
- ivar cvar gvar
26
- ruby
27
- tset
28
- and or not
29
- = not= < <= > >=
30
- + - * / %
31
- print
32
- macro macros import-macros
33
- quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
34
- ].freeze
35
- SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
13
+ CORE_SPECIAL_FORMS = Language::CORE_SPECIAL_FORMS
14
+ SPECIAL_FORMS = Language::SPECIAL_FORMS
36
15
 
37
16
  def self.compile(source, path: '(kapusta)', target: nil)
38
17
  forms = Reader.read_all(source)
data/lib/kapusta/env.rb CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Kapusta
4
4
  class Env
5
- MethodBinding = Struct.new(:ruby_name)
6
- SelfMethodBinding = Struct.new(:ruby_name)
5
+ MethodBinding = Struct.new(:ruby_name, :multi_return)
6
+ SelfMethodBinding = Struct.new(:ruby_name, :multi_return)
7
7
 
8
8
  def initialize(parent = nil)
9
9
  @parent = parent
@@ -6,7 +6,7 @@ module Kapusta
6
6
  MESSAGES = {
7
7
  accumulate_no_iterator: 'expected initial value and iterator binding table',
8
8
  auto_gensym_outside_quasiquote: 'auto-gensym %{name}# outside quasiquote',
9
- bad_multisym: 'bad multisym: %{path}',
9
+ bad_multisym: 'bad multisym: %{path}; unresolved root %{segment}; %{suggestion}',
10
10
  bad_set_target: 'bad set target: %{target}',
11
11
  bad_shorthand: 'bad shorthand',
12
12
  bind_table_dots: 'unable to bind table ...',
@@ -42,6 +42,7 @@ module Kapusta
42
42
  import_macros_module_no_exports: 'import-macros: module %{module} has no export table',
43
43
  import_macros_module_not_found: 'import-macros: module %{module} not found',
44
44
  invalid_class_name: 'invalid class name: %{name}',
45
+ invalid_header_body_form: '%{scope} body form must be a declaration or known special form: %{name}',
45
46
  invalid_module_name: 'invalid module name: %{name}',
46
47
  let_no_body: 'expected body expression',
47
48
  let_odd_bindings: 'expected even number of name/value bindings',
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ class Formatter
5
+ module ASTHelpers
6
+ private
7
+
8
+ def comment?(form)
9
+ form.is_a?(Comment)
10
+ end
11
+
12
+ def blank_line?(form)
13
+ form.is_a?(BlankLine)
14
+ end
15
+
16
+ def non_semantic?(form)
17
+ comment?(form) || blank_line?(form)
18
+ end
19
+
20
+ def contains_comments?(items)
21
+ items.any? { |item| non_semantic?(item) }
22
+ end
23
+
24
+ def semantic_items(items)
25
+ items.reject { |item| non_semantic?(item) }
26
+ end
27
+
28
+ def list_head(list)
29
+ semantic_items(list.items).first
30
+ end
31
+
32
+ def head_name(list)
33
+ head = list_head(list)
34
+ head.name if head.is_a?(Sym)
35
+ end
36
+
37
+ def list_rest(list)
38
+ semantic_items(list.items).drop(1)
39
+ end
40
+
41
+ def list_raw_rest(list)
42
+ index = list.items.index { |item| !non_semantic?(item) }
43
+ return list.items if index.nil?
44
+
45
+ list.items[(index + 1)..] || []
46
+ end
47
+
48
+ def split_raw_items(items, semantic_count)
49
+ split_index = 0
50
+ seen = 0
51
+
52
+ while split_index < items.length && seen < semantic_count
53
+ seen += 1 unless non_semantic?(items[split_index])
54
+ split_index += 1
55
+ end
56
+
57
+ [items.take(split_index), items.drop(split_index)]
58
+ end
59
+
60
+ def multiline_in_source?(form)
61
+ form.respond_to?(:multiline_source) && form.multiline_source
62
+ end
63
+
64
+ def contains_collection?(form)
65
+ case form
66
+ when List then semantic_items(form.items).any? { |item| collection?(item) }
67
+ when Vec then form.items.any? { |item| collection?(item) }
68
+ when HashLit then form.pairs.any? { |k, v| collection?(k) || collection?(v) }
69
+ else false
70
+ end
71
+ end
72
+
73
+ def collection?(form)
74
+ form.is_a?(List) || form.is_a?(Vec) || form.is_a?(HashLit)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ class Formatter
5
+ module CLI
6
+ def initialize(argv)
7
+ @mode = :stdout
8
+ @files = []
9
+ @version = false
10
+ parse_args(argv)
11
+ end
12
+
13
+ def run
14
+ return print_version if @version
15
+
16
+ validate_args!
17
+ apply_mode(formatted_files)
18
+ rescue Kapusta::Error => e
19
+ warn e.formatted
20
+ 1
21
+ end
22
+
23
+ private
24
+
25
+ def print_version
26
+ puts "kapfmt #{Kapusta::VERSION}"
27
+ 0
28
+ end
29
+
30
+ def formatted_files
31
+ @files.map { |path| formatted_file(path) }
32
+ end
33
+
34
+ def formatted_file(path)
35
+ original = read_source(path)
36
+ validate_kapusta_source(original, path)
37
+ [path, original, format_source(original, path)]
38
+ end
39
+
40
+ def apply_mode(formatted)
41
+ case @mode
42
+ when :stdout
43
+ $stdout.write(formatted.first[2])
44
+ when :fix
45
+ fix_files(formatted)
46
+ when :check
47
+ return check_files(formatted)
48
+ end
49
+
50
+ 0
51
+ end
52
+
53
+ def fix_files(formatted)
54
+ formatted.each do |path, _original, rewritten|
55
+ raise Error, 'Cannot use --fix with stdin (-).' if stdin_path?(path)
56
+
57
+ File.write(path, rewritten)
58
+ end
59
+ end
60
+
61
+ def check_files(formatted)
62
+ dirty = formatted.reject { |_path, original, rewritten| original == rewritten }
63
+ dirty.each do |path, _original, _rewritten|
64
+ warn "Not formatted: #{path}"
65
+ end
66
+
67
+ dirty.empty? ? 0 : 1
68
+ end
69
+
70
+ def parse_args(argv)
71
+ argv.each do |arg|
72
+ case arg
73
+ when '--fix'
74
+ ensure_mode!(:fix)
75
+ when '--check'
76
+ ensure_mode!(:check)
77
+ when '--version', '-v'
78
+ @version = true
79
+ when '--help', '-h'
80
+ print_help
81
+ exit 0
82
+ else
83
+ @files << arg
84
+ end
85
+ end
86
+ end
87
+
88
+ def ensure_mode!(mode)
89
+ raise Error, 'Use at most one of --fix or --check.' if @mode != :stdout && @mode != mode
90
+
91
+ @mode = mode
92
+ end
93
+
94
+ def validate_args!
95
+ raise Error, 'Usage: kapfmt [--fix] [--check] FILENAME...' if @files.empty?
96
+ raise Error, 'stdin (-) may only be specified once.' if @files.count { |path| stdin_path?(path) } > 1
97
+ raise Error, 'Cannot use --fix with stdin (-).' if @mode == :fix && @files.any? { |path| stdin_path?(path) }
98
+
99
+ return unless @mode == :stdout && @files.length != 1
100
+
101
+ raise Error, 'Without --fix or --check, kapfmt accepts exactly one file.'
102
+ end
103
+
104
+ def read_source(path)
105
+ return File.read(path) unless stdin_path?(path)
106
+
107
+ @stdin_read ||= false
108
+ raise Error, 'stdin (-) may only be specified once.' if @stdin_read
109
+
110
+ @stdin_read = true
111
+ $stdin.read
112
+ end
113
+
114
+ def stdin_path?(path)
115
+ path == STDIN_PATH
116
+ end
117
+
118
+ def print_help
119
+ puts 'Usage: kapfmt [--fix] [--check] FILENAME...'
120
+ puts
121
+ puts 'Formats Kapusta source using the built-in Kapusta reader and pretty-printer.'
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ class Formatter
5
+ module LineHelpers
6
+ private
7
+
8
+ def fits?(text, indent)
9
+ fits_within?(text, indent, MAX_WIDTH)
10
+ end
11
+
12
+ def inline_arg_fits?(text, indent)
13
+ fits_within?(text, indent, MAX_WIDTH - 1)
14
+ end
15
+
16
+ def fits_within?(text, indent, width)
17
+ !text.include?("\n") && indent + text.length <= width
18
+ end
19
+
20
+ def single_line?(text)
21
+ !text.include?("\n")
22
+ end
23
+
24
+ def indent_block(text, amount)
25
+ prefix = ' ' * amount
26
+ text.lines.map { |line| line.strip.empty? ? blank_line_for(line) : "#{prefix}#{line}" }.join
27
+ end
28
+
29
+ def blank_line_for(line)
30
+ line.end_with?("\n") ? "\n" : ''
31
+ end
32
+
33
+ def append_suffix(lines, suffix)
34
+ updated = lines.dup
35
+ if updated[-1].lstrip.start_with?(';')
36
+ updated << suffix
37
+ else
38
+ updated[-1] = "#{updated[-1]}#{suffix}"
39
+ end
40
+ updated.join("\n")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ class Formatter
5
+ module Validator
6
+ private
7
+
8
+ def validate_kapusta_source(source, path)
9
+ return validate_macro_module_source(source, path) if macro_module_path?(path)
10
+
11
+ Kapusta::Compiler.compile(source, path:)
12
+ end
13
+
14
+ def validate_macro_module_source(source, path)
15
+ forms = Reader.read_all(source)
16
+ raise Error, 'macro module has no export table' unless forms.last.is_a?(HashLit)
17
+
18
+ processed = forms.map do |form|
19
+ Compiler::MacroLowerer.lower_module_form(form, error_class: Error)
20
+ end
21
+ wrapper = List.new([List.new([Sym.new('fn'), Vec.new([]), *processed])])
22
+ Compiler.compile_forms([wrapper], path:)
23
+ rescue Kapusta::Error => e
24
+ raise e.with_defaults(path:)
25
+ end
26
+
27
+ def macro_module_path?(path)
28
+ path && File.extname(path) == '.kapm'
29
+ end
30
+ end
31
+ end
32
+ end