kapusta 0.4.1 → 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.
@@ -7,6 +7,8 @@ module Kapusta
7
7
  private
8
8
 
9
9
  def emit_lookup(args, env, current_scope)
10
+ emit_error!(:dot_no_args) if args.empty?
11
+
10
12
  object_code = emit_expr(args[0], env, current_scope)
11
13
  keys = args[1..].map { |arg| emit_expr(arg, env, current_scope) }
12
14
  return object_code if keys.empty?
@@ -91,7 +93,7 @@ module Kapusta
91
93
 
92
94
  def emit_module_wrapper(name_sym, body)
93
95
  segments = constant_segments(name_sym)
94
- emit_error!("invalid module name: #{name_sym.name}") unless segments
96
+ emit_error!(:invalid_module_name, name: name_sym.name) unless segments
95
97
  inner = build_nested_module(segments, body)
96
98
  ['(-> do', indent(inner), indent(segments.join('::')), 'end).call'].join("\n")
97
99
  end
@@ -105,7 +107,7 @@ module Kapusta
105
107
 
106
108
  def emit_class_wrapper(name_sym, supers, env, body)
107
109
  segments = constant_segments(name_sym)
108
- emit_error!("invalid class name: #{name_sym.name}") unless segments
110
+ emit_error!(:invalid_class_name, name: name_sym.name) unless segments
109
111
  super_code = class_super_code(supers, env)
110
112
  inner = build_nested_class(segments, super_code, body)
111
113
  ['(-> do', indent(inner), indent(segments.join('::')), 'end).call'].join("\n")
@@ -434,7 +436,7 @@ module Kapusta
434
436
  return 'ARGV' if name == 'ARGV'
435
437
  return name if name.match?(/\A[A-Z]/)
436
438
 
437
- emit_error!("undefined symbol: #{name}")
439
+ emit_error!(:undefined_symbol, name:)
438
440
  end
439
441
 
440
442
  def emit_gvar(sym)
@@ -458,7 +460,7 @@ module Kapusta
458
460
  const_path << segments[idx]
459
461
  idx += 1
460
462
  end
461
- emit_error!("bad multisym: #{segments.join('.')}") if const_path.empty?
463
+ emit_error!(:bad_multisym, path: segments.join('.')) if const_path.empty?
462
464
 
463
465
  [const_path.join('::'), segments[idx..]]
464
466
  end
@@ -10,6 +10,7 @@ module Kapusta
10
10
  if pattern.is_a?(Sym)
11
11
  return ['', env] if pattern.name == '_'
12
12
 
13
+ validate_binding_symbol!(pattern)
13
14
  ruby_name = define_local(env, pattern)
14
15
  return ["#{ruby_name} = #{value_code}", env]
15
16
  end
@@ -17,7 +18,26 @@ module Kapusta
17
18
  native = try_emit_native_pattern_bind(pattern, value_code, env)
18
19
  return native if native
19
20
 
20
- emit_error!("destructure pattern this compiler cannot translate: #{pattern.inspect}")
21
+ emit_error!(:destructure_unsupported, pattern: pattern.inspect)
22
+ end
23
+
24
+ def validate_binding_symbol!(sym)
25
+ name = sym.name
26
+ emit_error!(:shadowed_special, name:) if Compiler::SPECIAL_FORMS.include?(name)
27
+ return unless sym.is_a?(MacroSym)
28
+
29
+ emit_error!(:macro_unsafe_bind, name:)
30
+ end
31
+
32
+ def validate_destructure_pattern!(pattern)
33
+ items = pattern.items
34
+ items.each_with_index do |item, idx|
35
+ emit_error!(:bind_table_dots) if item.is_a?(Sym) && item.name == '...'
36
+ next unless item.is_a?(Sym) && item.name == '&'
37
+
38
+ emit_error!(:rest_not_last) if idx + 2 < items.length
39
+ emit_error!(:rest_not_last) if idx + 1 >= items.length
40
+ end
21
41
  end
22
42
 
23
43
  def try_emit_native_pattern_bind(pattern, value_code, env)
@@ -32,6 +52,7 @@ module Kapusta
32
52
  end
33
53
 
34
54
  def try_emit_native_vec_bind(pattern, value_code, env)
55
+ validate_destructure_pattern!(pattern)
35
56
  parts = []
36
57
  deferred = []
37
58
  current_env = env
@@ -6,8 +6,27 @@ module Kapusta
6
6
  module Support
7
7
  private
8
8
 
9
- def emit_error!(message)
10
- raise Error, "#{@path}: #{message}"
9
+ def emit_error!(code, **args)
10
+ form = current_form
11
+ line = form.respond_to?(:line) ? form.line : nil
12
+ column = form.respond_to?(:column) ? form.column : nil
13
+ raise Error.new(Kapusta::Errors.format(code, **args), path: @path, line:, column:)
14
+ end
15
+
16
+ def with_current_form(form)
17
+ @form_stack ||= []
18
+ @form_stack.push(form) if positionable?(form)
19
+ yield
20
+ ensure
21
+ @form_stack.pop if positionable?(form)
22
+ end
23
+
24
+ def current_form
25
+ (@form_stack ||= []).last
26
+ end
27
+
28
+ def positionable?(form)
29
+ form.respond_to?(:line) && form.respond_to?(:column)
11
30
  end
12
31
 
13
32
  def emit_forms_with_headers(forms, env, current_scope, result: true)
@@ -67,27 +86,29 @@ module Kapusta
67
86
  end
68
87
 
69
88
  def emit_form_in_sequence(form, env, current_scope, allow_method_definitions:, result_needed: true)
70
- if allow_method_definitions &&
71
- method_definition_form?(form) &&
72
- %i[toplevel module class].include?(current_scope)
73
- code, env = emit_definition_form(form, env, current_scope)
74
- return [code, env] if code
75
- end
89
+ with_current_form(form) do
90
+ if allow_method_definitions &&
91
+ method_definition_form?(form) &&
92
+ %i[toplevel module class].include?(current_scope)
93
+ code, env = emit_definition_form(form, env, current_scope)
94
+ return [code, env] if code
95
+ end
76
96
 
77
- if named_function_form?(form)
78
- emit_named_fn_assignment(form, env, current_scope)
79
- elsif local_form?(form)
80
- code, env = emit_local_form(form, env, current_scope)
81
- code = code.delete_suffix("\nnil") unless result_needed
82
- [code, env]
83
- elsif do_form?(form)
84
- emit_do_form(form.rest, env, current_scope, result_needed:)
85
- elsif sequence_statement_form?(form)
86
- emit_sequence_statement_form(form, env, current_scope, result_needed:)
87
- elsif set_new_local_form?(form, env)
88
- emit_set_form(form, env, current_scope)
89
- else
90
- [emit_expr(form, env, current_scope), env]
97
+ if named_function_form?(form)
98
+ emit_named_fn_assignment(form, env, current_scope)
99
+ elsif local_form?(form)
100
+ code, env = emit_local_form(form, env, current_scope)
101
+ code = code.delete_suffix("\nnil") unless result_needed
102
+ [code, env]
103
+ elsif do_form?(form)
104
+ emit_do_form(form.rest, env, current_scope, result_needed:)
105
+ elsif sequence_statement_form?(form)
106
+ emit_sequence_statement_form(form, env, current_scope, result_needed:)
107
+ elsif set_new_local_form?(form, env)
108
+ emit_set_form(form, env, current_scope)
109
+ else
110
+ [emit_expr(form, env, current_scope), env]
111
+ end
91
112
  end
92
113
  end
93
114
 
@@ -228,6 +249,7 @@ module Kapusta
228
249
  end
229
250
 
230
251
  def parse_counted_for_bindings(bindings, env, current_scope)
252
+ emit_error!(:counted_no_range) if bindings.length < 3
231
253
  name_sym = bindings[0]
232
254
  loop_env = env.child
233
255
  ruby_name = define_local(loop_env, name_sym.name)
@@ -19,8 +19,9 @@ module Kapusta
19
19
  end
20
20
  end
21
21
 
22
- def initialize
22
+ def initialize(path: nil)
23
23
  @macros = {}
24
+ @path = path
24
25
  end
25
26
 
26
27
  def expand_all(forms)
@@ -39,25 +40,42 @@ module Kapusta
39
40
  register_macros_form(form.rest)
40
41
  return []
41
42
  when 'import-macros'
42
- raise Error, 'import-macros is not yet supported'
43
+ raise macro_error(:import_macros_unsupported, form)
43
44
  end
44
45
  end
45
46
  [expand(form)]
46
47
  end
47
48
 
49
+ def macro_error(code, form, **args)
50
+ line = form.respond_to?(:line) ? form.line : nil
51
+ column = form.respond_to?(:column) ? form.column : nil
52
+ Error.new(Kapusta::Errors.format(code, **args), path: @path, line:, column:)
53
+ end
54
+
48
55
  def expand(form)
49
56
  case form
50
57
  when List then expand_list(form)
51
- when Vec then Vec.new(form.items.map { |item| expand(item) })
58
+ when Vec then copy_position(Vec.new(form.items.map { |item| expand(item) }), form)
52
59
  when HashLit
53
- HashLit.new(form.entries.map do |entry|
54
- entry.is_a?(Array) ? [expand(entry[0]), expand(entry[1])] : entry
55
- end)
60
+ copy_position(
61
+ HashLit.new(form.entries.map do |entry|
62
+ entry.is_a?(Array) ? [expand(entry[0]), expand(entry[1])] : entry
63
+ end),
64
+ form
65
+ )
56
66
  else
57
67
  form
58
68
  end
59
69
  end
60
70
 
71
+ def copy_position(target, source)
72
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
73
+
74
+ target.line ||= source.line
75
+ target.column ||= source.column
76
+ target
77
+ end
78
+
61
79
  def expand_list(list)
62
80
  return list if list.empty?
63
81
 
@@ -77,11 +95,11 @@ module Kapusta
77
95
  if @macros.key?(key)
78
96
  args = list.rest
79
97
  result = invoke_macro(key, args)
80
- return expand(result)
98
+ return copy_position(expand(result), list)
81
99
  end
82
100
  end
83
101
 
84
- List.new(list.items.map { |item| expand(item) })
102
+ copy_position(List.new(list.items.map { |item| expand(item) }), list)
85
103
  end
86
104
 
87
105
  def lookup_key(name)
@@ -90,23 +108,23 @@ module Kapusta
90
108
 
91
109
  def register_macro_form(args)
92
110
  name_sym, params, *body = args
93
- raise Error, 'macro name must be a symbol' unless name_sym.is_a?(Sym)
94
- raise Error, 'macro params must be a vector' unless params.is_a?(Vec)
111
+ raise macro_error(:macro_name_must_be_symbol, name_sym) unless name_sym.is_a?(Sym)
112
+ raise macro_error(:macro_params_must_be_vector, params) unless params.is_a?(Vec)
95
113
 
96
114
  register(name_sym.name, params, body)
97
115
  end
98
116
 
99
117
  def register_macros_form(args)
100
118
  hash_lit = args[0]
101
- raise Error, 'macros expects a hash literal' unless hash_lit.is_a?(HashLit)
119
+ raise macro_error(:macros_expects_hash, hash_lit) unless hash_lit.is_a?(HashLit)
102
120
 
103
121
  hash_lit.pairs.each do |key, value|
104
- raise Error, "macros entry value must be a fn form, got #{value.inspect}" unless fn_form?(value)
122
+ raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect) unless fn_form?(value)
105
123
 
106
124
  name = key.to_s
107
125
  params = value.items[1]
108
126
  body = value.items[2..]
109
- raise Error, 'macros entry params must be a vector' unless params.is_a?(Vec)
127
+ raise macro_error(:macros_entry_params_must_be_vector, params) unless params.is_a?(Vec)
110
128
 
111
129
  register(name, params, body)
112
130
  end
@@ -137,8 +155,9 @@ module Kapusta
137
155
  List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
138
156
  end
139
157
 
140
- ruby = Compiler.compile_forms([wrapped], path: "(macro #{name})")
141
- TOPLEVEL_BINDING.eval(ruby, "(macro #{name})", 1)
158
+ macro_path = @path || "(macro #{name})"
159
+ ruby = Compiler.compile_forms([wrapped], path: macro_path)
160
+ TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
142
161
  end
143
162
 
144
163
  def invoke_macro(key, args)
@@ -157,22 +176,33 @@ module Kapusta
157
176
 
158
177
  def lower(form)
159
178
  case form
160
- when Quasiquote then lower_quasi(form.form)
179
+ when Quasiquote then copy_position(lower_quasi(form.form), form)
161
180
  when Unquote, UnquoteSplice
162
- raise Error, 'unquote outside quasiquote'
181
+ raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
163
182
  when AutoGensym
164
- raise Error, "auto-gensym #{form.name}# outside quasiquote"
165
- when List then List.new(form.items.map { |item| lower(item) })
166
- when Vec then Vec.new(form.items.map { |item| lower(item) })
183
+ raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
184
+ when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
185
+ when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
167
186
  when HashLit
168
- HashLit.new(form.entries.map do |entry|
169
- entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
170
- end)
187
+ copy_position(
188
+ HashLit.new(form.entries.map do |entry|
189
+ entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
190
+ end),
191
+ form
192
+ )
171
193
  else
172
194
  form
173
195
  end
174
196
  end
175
197
 
198
+ def copy_position(target, source)
199
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
200
+
201
+ target.line ||= source.line
202
+ target.column ||= source.column
203
+ target
204
+ end
205
+
176
206
  def lower_quasi(form)
177
207
  case form
178
208
  when AutoGensym then gensym_local_for(form.name)
@@ -182,9 +212,9 @@ module Kapusta
182
212
  when HashLit then lower_quasi_hash(form)
183
213
  when Unquote then lower(form.form)
184
214
  when UnquoteSplice
185
- raise Error, 'unquote-splice must appear inside a quoted list/vec'
215
+ raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
186
216
  when Quasiquote
187
- raise Error, 'nested quasiquote is not supported'
217
+ raise Error, Kapusta::Errors.format(:nested_quasiquote)
188
218
  else
189
219
  form
190
220
  end
@@ -10,9 +10,12 @@ module Kapusta
10
10
  def normalize(form)
11
11
  case form
12
12
  when List then normalize_list(form)
13
- when Vec then Vec.new(form.items.map { |item| normalize(item) })
13
+ when Vec then inherit_position(Vec.new(form.items.map { |item| normalize(item) }), form)
14
14
  when HashLit
15
- HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] })
15
+ inherit_position(
16
+ HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] }),
17
+ form
18
+ )
16
19
  else
17
20
  form
18
21
  end
@@ -32,19 +35,28 @@ module Kapusta
32
35
 
33
36
  head = list.head
34
37
  items = list.items.map { |item| normalize(item) }
35
- return List.new(items) unless head.is_a?(Sym)
38
+ return inherit_position(List.new(items), list) unless head.is_a?(Sym)
36
39
 
37
40
  case head.name
38
41
  when 'when'
42
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
43
+
39
44
  cond = items[1]
40
45
  body = wrap_do(items[2..])
41
- List.new([Sym.new('if'), cond, body])
46
+ inherit_position(List.new([Sym.new('if'), cond, body]), list)
42
47
  when 'unless'
48
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
49
+
43
50
  cond = items[1]
44
51
  body = wrap_do(items[2..])
45
- List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body])
52
+ inherit_position(List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body]), list)
46
53
  when 'tset'
47
- List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]])
54
+ raise compiler_error(:tset_no_value, list) if items.length < 4
55
+
56
+ inherit_position(
57
+ List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
58
+ list
59
+ )
48
60
  when 'pcall'
49
61
  fn = items[1]
50
62
  args = items[2..]
@@ -65,14 +77,28 @@ module Kapusta
65
77
  List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
66
78
  ])
67
79
  when '->', '->>', '-?>', '-?>>'
68
- normalize(thread(items[1..], head.name))
80
+ inherit_position(normalize(thread(items[1..], head.name)), list)
69
81
  when 'doto'
70
- normalize(doto(items[1..]))
82
+ inherit_position(normalize(doto(items[1..])), list)
71
83
  else
72
- List.new(items)
84
+ inherit_position(List.new(items), list)
73
85
  end
74
86
  end
75
87
 
88
+ def inherit_position(target, source)
89
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
90
+
91
+ target.line ||= source.line
92
+ target.column ||= source.column
93
+ target
94
+ end
95
+
96
+ def compiler_error(code, form, **args)
97
+ line = form.respond_to?(:line) ? form.line : nil
98
+ column = form.respond_to?(:column) ? form.column : nil
99
+ Compiler::Error.new(Kapusta::Errors.format(code, **args), line:, column:)
100
+ end
101
+
76
102
  def wrap_do(forms)
77
103
  return if forms.empty?
78
104
  return forms.first if forms.length == 1
@@ -9,7 +9,7 @@ module Kapusta
9
9
  module Compiler
10
10
  class Error < Kapusta::Error; end
11
11
  SPECIAL_FORMS = %w[
12
- fn lambda λ let local var set if when unless case match
12
+ fn lambda λ let local var global set if when unless case match
13
13
  while for each do values
14
14
  -> ->> -?> -?>> doto
15
15
  icollect collect fcollect accumulate faccumulate
@@ -34,13 +34,17 @@ module Kapusta
34
34
 
35
35
  def self.compile(source, path: '(kapusta)')
36
36
  forms = Reader.read_all(source)
37
- expanded = MacroExpander.new.expand_all(forms)
37
+ expanded = MacroExpander.new(path:).expand_all(forms)
38
38
  compile_forms(expanded, path:)
39
+ rescue Kapusta::Error => e
40
+ raise e.with_defaults(path:)
39
41
  end
40
42
 
41
43
  def self.compile_forms(forms, path: '(kapusta)')
42
44
  normalized = Normalizer.new.normalize_all(forms)
43
45
  Emitter.new(path:).emit_file(normalized)
46
+ rescue Kapusta::Error => e
47
+ raise e.with_defaults(path:)
44
48
  end
45
49
 
46
50
  def self.run(source, path: '(kapusta)')
data/lib/kapusta/error.rb CHANGED
@@ -1,5 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- class Error < StandardError; end
4
+ class Error < StandardError
5
+ attr_reader :path, :line, :column, :reason
6
+
7
+ def initialize(reason, path: nil, line: nil, column: nil)
8
+ @reason = reason
9
+ @path = path
10
+ @line = line
11
+ @column = column
12
+ super(formatted)
13
+ end
14
+
15
+ def formatted
16
+ prefix = [path, line, column].compact.join(':')
17
+ prefix.empty? ? reason : "#{prefix}: #{reason}"
18
+ end
19
+
20
+ def with_defaults(path: nil, line: nil, column: nil)
21
+ copy = self.class.new(@reason,
22
+ path: @path || path,
23
+ line: @line || line,
24
+ column: @column || column)
25
+ copy.set_backtrace(backtrace) if backtrace
26
+ copy
27
+ end
28
+ end
5
29
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ # rubocop:disable Style/FormatStringToken
5
+ module Errors
6
+ MESSAGES = {
7
+ accumulate_no_iterator: 'expected initial value and iterator binding table',
8
+ auto_gensym_outside_quasiquote: 'auto-gensym %{name}# outside quasiquote',
9
+ bad_multisym: 'bad multisym: %{path}',
10
+ bad_set_target: 'bad set target: %{target}',
11
+ bad_shorthand: 'bad shorthand',
12
+ bind_table_dots: 'unable to bind table ...',
13
+ cannot_call_literal: 'cannot call literal value %{value}',
14
+ cannot_emit_form: 'cannot emit form: %{form}',
15
+ cannot_set_method_binding: 'cannot set method binding: %{name}',
16
+ case_no_patterns: 'expected at least one pattern/body pair',
17
+ case_odd_patterns: 'expected even number of pattern/body pairs',
18
+ case_unsupported: 'case/match clauses use patterns this compiler cannot translate',
19
+ could_not_destructure_literal: 'could not destructure literal',
20
+ could_not_read_number: 'could not read number "%{token}"',
21
+ counted_no_range: 'expected range to include start and stop',
22
+ destructure_unsupported: 'destructure pattern this compiler cannot translate: %{pattern}',
23
+ dot_no_args: 'expected table argument',
24
+ each_no_binding: 'expected binding table',
25
+ empty_call: 'expected a function, macro, or special to call',
26
+ empty_token: 'empty token',
27
+ expected_var: 'expected var %{name}',
28
+ fn_no_params: 'expected parameters table',
29
+ global_arity: 'expected name and value',
30
+ global_non_symbol_name: 'unable to bind %{type} %{value}',
31
+ icollect_no_iterator: 'expected iterator binding table',
32
+ if_no_body: 'expected condition and body',
33
+ import_macros_unsupported: 'import-macros is not yet supported',
34
+ invalid_class_name: 'invalid class name: %{name}',
35
+ invalid_module_name: 'invalid module name: %{name}',
36
+ let_no_body: 'expected body expression',
37
+ let_odd_bindings: 'expected even number of name/value bindings',
38
+ local_arity: '%{form}: expected name and value',
39
+ macro_name_must_be_symbol: 'macro name must be a symbol',
40
+ macro_params_must_be_vector: 'macro params must be a vector',
41
+ macro_unsafe_bind: 'macro tried to bind %{name} without gensym',
42
+ macros_entry_must_be_fn: 'macros entry value must be a fn form, got %{form}',
43
+ macros_entry_params_must_be_vector: 'macros entry params must be a vector',
44
+ macros_expects_hash: 'macros expects a hash literal',
45
+ nested_quasiquote: 'nested quasiquote is not supported',
46
+ odd_forms_in_hash: 'odd number of forms in hash',
47
+ rest_not_last: 'expected rest argument before last parameter',
48
+ shadowed_special: 'local %{name} was overshadowed by a special form or macro',
49
+ special_must_be_toplevel: '%{name} must appear at the top level and is consumed by the macro expander',
50
+ tset_no_value: 'tset: expected table, key, and value arguments',
51
+ unclosed_delimiter: "unclosed opening delimiter '%{char}'",
52
+ undefined_symbol: 'undefined symbol: %{name}',
53
+ unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
54
+ unexpected_eof: 'unexpected eof',
55
+ unknown_special_form: 'unknown special form: %{name}',
56
+ unquote_outside_quasiquote: 'unquote outside quasiquote',
57
+ unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
58
+ unterminated_string: 'unterminated string',
59
+ vararg_with_operator: 'tried to use vararg with operator',
60
+ when_no_body: '%{form}: expected body'
61
+ }.freeze
62
+
63
+ def self.format(code, **args)
64
+ template = MESSAGES.fetch(code) { raise ArgumentError, "unknown error code: #{code.inspect}" }
65
+ args.empty? ? template.dup : (template % args)
66
+ end
67
+ end
68
+ # rubocop:enable Style/FormatStringToken
69
+ end
@@ -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,8 +115,10 @@ 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
124
  def separator_for(_previous, _current)