kapusta 0.4.1 → 0.7.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.
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ module Compiler
5
+ module LuaCompat
6
+ SPECIAL_FORMS = %w[pcall xpcall].freeze
7
+ ITERATOR_FORMS = %w[ipairs pairs].freeze
8
+
9
+ def self.special_form?(name)
10
+ SPECIAL_FORMS.include?(name)
11
+ end
12
+
13
+ def self.iterator_form?(name)
14
+ ITERATOR_FORMS.include?(name)
15
+ end
16
+
17
+ module Normalization
18
+ private
19
+
20
+ def normalize_lua_compat_form(name, items)
21
+ case name
22
+ when 'pcall' then normalize_lua_pcall(items)
23
+ when 'xpcall' then normalize_lua_xpcall(items)
24
+ end
25
+ end
26
+
27
+ def normalize_lua_pcall(items)
28
+ fn = items[1]
29
+ args = items[2..]
30
+ List.new([
31
+ Sym.new('try'),
32
+ List.new([Sym.new('values'), true, List.new([fn, *args])]),
33
+ List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
34
+ List.new([Sym.new('values'), false, Sym.new('e')])])
35
+ ])
36
+ end
37
+
38
+ def normalize_lua_xpcall(items)
39
+ fn = items[1]
40
+ handler = items[2]
41
+ args = items[3..]
42
+ List.new([
43
+ Sym.new('try'),
44
+ List.new([Sym.new('values'), true, List.new([fn, *args])]),
45
+ List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
46
+ List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
47
+ ])
48
+ end
49
+ end
50
+
51
+ module Emission
52
+ private
53
+
54
+ def emit_lua_compat_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
55
+ init_code, body_forms)
56
+ return unless lua_iterator_expr?(iter_expr)
57
+
58
+ case iter_expr.head.name
59
+ when 'ipairs'
60
+ emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
61
+ acc_var, init_code, body_forms)
62
+ when 'pairs'
63
+ emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
64
+ acc_var, init_code, body_forms)
65
+ end
66
+ end
67
+
68
+ def emit_lua_compat_iteration(iter_expr, binding_pats, env, current_scope, method:,
69
+ extra_block_param: nil, &block)
70
+ return unless lua_iterator_expr?(iter_expr)
71
+
72
+ case iter_expr.head.name
73
+ when 'ipairs'
74
+ emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope,
75
+ method:, extra_block_param:, &block)
76
+ when 'pairs'
77
+ emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope,
78
+ method:, extra_block_param:, &block)
79
+ end
80
+ end
81
+
82
+ def lua_iterator_expr?(expr)
83
+ expr.is_a?(List) && expr.head.is_a?(Sym) && LuaCompat.iterator_form?(expr.head.name)
84
+ end
85
+
86
+ def emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
87
+ init_code, body_forms)
88
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
89
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
90
+ if ignored_pattern?(binding_pats[0])
91
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
92
+ return inject_block(coll_code, "#{acc_var}, #{value_var}", init_code, value_bind || '', body_code)
93
+ end
94
+
95
+ index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
96
+ bind_code = [index_bind, value_bind].compact.join("\n")
97
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
98
+ inject_block("#{coll_code}.each_with_index", "#{acc_var}, (#{value_var}, #{index_var})",
99
+ init_code, bind_code, body_code)
100
+ end
101
+
102
+ def emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
103
+ init_code, body_forms)
104
+ key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
105
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
106
+ bind_code = [key_bind, value_bind].compact.join("\n")
107
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
108
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
109
+ inject_block(coll_code, "#{acc_var}, (#{key_var}, #{value_var})",
110
+ init_code, bind_code, body_code)
111
+ end
112
+
113
+ def emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
114
+ extra_block_param: nil, &block)
115
+ body_env = env.child
116
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
117
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
118
+ if ignored_pattern?(binding_pats[0])
119
+ bind_code = value_bind || ''
120
+ body_code = block.call(body_env)
121
+ params = extra_block_param ? "#{value_var}, #{extra_block_param}" : value_var
122
+ return iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
123
+ end
124
+
125
+ index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
126
+ bind_code = [index_bind, value_bind].compact.join("\n")
127
+ body_code = block.call(body_env)
128
+ receiver = method == 'each' ? "#{coll_code}.each_with_index" : "#{coll_code}.each_with_index.#{method}"
129
+ inner_params = "#{value_var}, #{index_var}"
130
+ params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
131
+ iteration_block("#{receiver} do |#{params}|", bind_code, body_code)
132
+ end
133
+
134
+ def emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
135
+ extra_block_param: nil, &block)
136
+ body_env = env.child
137
+ key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
138
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
139
+ bind_code = [key_bind, value_bind].compact.join("\n")
140
+ body_code = block.call(body_env)
141
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
142
+ inner_params = "#{key_var}, #{value_var}"
143
+ params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
144
+ iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -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
 
@@ -71,17 +89,19 @@ module Kapusta
71
89
  when 'macros'
72
90
  register_macros_form(list.rest)
73
91
  return List.new([Sym.new('do')])
92
+ when 'import-macros'
93
+ raise macro_error(:import_macros_unsupported, list)
74
94
  end
75
95
 
76
96
  key = lookup_key(name)
77
97
  if @macros.key?(key)
78
98
  args = list.rest
79
99
  result = invoke_macro(key, args)
80
- return expand(result)
100
+ return copy_position(expand(result), list)
81
101
  end
82
102
  end
83
103
 
84
- List.new(list.items.map { |item| expand(item) })
104
+ copy_position(List.new(list.items.map { |item| expand(item) }), list)
85
105
  end
86
106
 
87
107
  def lookup_key(name)
@@ -90,23 +110,23 @@ module Kapusta
90
110
 
91
111
  def register_macro_form(args)
92
112
  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)
113
+ raise macro_error(:macro_name_must_be_symbol, name_sym) unless name_sym.is_a?(Sym)
114
+ raise macro_error(:macro_params_must_be_vector, params) unless params.is_a?(Vec)
95
115
 
96
116
  register(name_sym.name, params, body)
97
117
  end
98
118
 
99
119
  def register_macros_form(args)
100
120
  hash_lit = args[0]
101
- raise Error, 'macros expects a hash literal' unless hash_lit.is_a?(HashLit)
121
+ raise macro_error(:macros_expects_hash, hash_lit) unless hash_lit.is_a?(HashLit)
102
122
 
103
123
  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)
124
+ raise macro_error(:macros_entry_must_be_fn, value, form: value.inspect) unless fn_form?(value)
105
125
 
106
126
  name = key.to_s
107
127
  params = value.items[1]
108
128
  body = value.items[2..]
109
- raise Error, 'macros entry params must be a vector' unless params.is_a?(Vec)
129
+ raise macro_error(:macros_entry_params_must_be_vector, params) unless params.is_a?(Vec)
110
130
 
111
131
  register(name, params, body)
112
132
  end
@@ -137,8 +157,9 @@ module Kapusta
137
157
  List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
138
158
  end
139
159
 
140
- ruby = Compiler.compile_forms([wrapped], path: "(macro #{name})")
141
- TOPLEVEL_BINDING.eval(ruby, "(macro #{name})", 1)
160
+ macro_path = @path || "(macro #{name})"
161
+ ruby = Compiler.compile_forms([wrapped], path: macro_path)
162
+ TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
142
163
  end
143
164
 
144
165
  def invoke_macro(key, args)
@@ -157,22 +178,33 @@ module Kapusta
157
178
 
158
179
  def lower(form)
159
180
  case form
160
- when Quasiquote then lower_quasi(form.form)
181
+ when Quasiquote then copy_position(lower_quasi(form.form), form)
161
182
  when Unquote, UnquoteSplice
162
- raise Error, 'unquote outside quasiquote'
183
+ raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
163
184
  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) })
185
+ raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
186
+ when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
187
+ when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
167
188
  when HashLit
168
- HashLit.new(form.entries.map do |entry|
169
- entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
170
- end)
189
+ copy_position(
190
+ HashLit.new(form.entries.map do |entry|
191
+ entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
192
+ end),
193
+ form
194
+ )
171
195
  else
172
196
  form
173
197
  end
174
198
  end
175
199
 
200
+ def copy_position(target, source)
201
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
202
+
203
+ target.line ||= source.line
204
+ target.column ||= source.column
205
+ target
206
+ end
207
+
176
208
  def lower_quasi(form)
177
209
  case form
178
210
  when AutoGensym then gensym_local_for(form.name)
@@ -182,9 +214,9 @@ module Kapusta
182
214
  when HashLit then lower_quasi_hash(form)
183
215
  when Unquote then lower(form.form)
184
216
  when UnquoteSplice
185
- raise Error, 'unquote-splice must appear inside a quoted list/vec'
217
+ raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
186
218
  when Quasiquote
187
- raise Error, 'nested quasiquote is not supported'
219
+ raise Error, Kapusta::Errors.format(:nested_quasiquote)
188
220
  else
189
221
  form
190
222
  end
@@ -3,6 +3,8 @@
3
3
  module Kapusta
4
4
  module Compiler
5
5
  class Normalizer
6
+ include LuaCompat::Normalization
7
+
6
8
  def normalize_all(forms)
7
9
  forms.map { |form| normalize(form) }
8
10
  end
@@ -10,9 +12,12 @@ module Kapusta
10
12
  def normalize(form)
11
13
  case form
12
14
  when List then normalize_list(form)
13
- when Vec then Vec.new(form.items.map { |item| normalize(item) })
15
+ when Vec then inherit_position(Vec.new(form.items.map { |item| normalize(item) }), form)
14
16
  when HashLit
15
- HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] })
17
+ inherit_position(
18
+ HashLit.new(form.pairs.map { |key, value| [normalize_hash_key(key), normalize(value)] }),
19
+ form
20
+ )
16
21
  else
17
22
  form
18
23
  end
@@ -32,47 +37,53 @@ module Kapusta
32
37
 
33
38
  head = list.head
34
39
  items = list.items.map { |item| normalize(item) }
35
- return List.new(items) unless head.is_a?(Sym)
40
+ return inherit_position(List.new(items), list) unless head.is_a?(Sym)
36
41
 
37
42
  case head.name
38
43
  when 'when'
44
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
45
+
39
46
  cond = items[1]
40
47
  body = wrap_do(items[2..])
41
- List.new([Sym.new('if'), cond, body])
48
+ inherit_position(List.new([Sym.new('if'), cond, body]), list)
42
49
  when 'unless'
50
+ raise compiler_error(:when_no_body, list, form: head.name) if items[2..].empty?
51
+
43
52
  cond = items[1]
44
53
  body = wrap_do(items[2..])
45
- List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body])
54
+ inherit_position(List.new([Sym.new('if'), List.new([Sym.new('not'), cond]), body]), list)
46
55
  when 'tset'
47
- List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]])
48
- when 'pcall'
49
- fn = items[1]
50
- args = items[2..]
51
- List.new([
52
- Sym.new('try'),
53
- List.new([Sym.new('values'), true, List.new([fn, *args])]),
54
- List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
55
- List.new([Sym.new('values'), false, Sym.new('e')])])
56
- ])
57
- when 'xpcall'
58
- fn = items[1]
59
- handler = items[2]
60
- args = items[3..]
61
- List.new([
62
- Sym.new('try'),
63
- List.new([Sym.new('values'), true, List.new([fn, *args])]),
64
- List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
65
- List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
66
- ])
56
+ raise compiler_error(:tset_no_value, list) if items.length < 4
57
+
58
+ inherit_position(
59
+ List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
60
+ list
61
+ )
62
+ when *LuaCompat::SPECIAL_FORMS
63
+ normalize_lua_compat_form(head.name, items)
67
64
  when '->', '->>', '-?>', '-?>>'
68
- normalize(thread(items[1..], head.name))
65
+ inherit_position(normalize(thread(items[1..], head.name)), list)
69
66
  when 'doto'
70
- normalize(doto(items[1..]))
67
+ inherit_position(normalize(doto(items[1..])), list)
71
68
  else
72
- List.new(items)
69
+ inherit_position(List.new(items), list)
73
70
  end
74
71
  end
75
72
 
73
+ def inherit_position(target, source)
74
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
75
+
76
+ target.line ||= source.line
77
+ target.column ||= source.column
78
+ target
79
+ end
80
+
81
+ def compiler_error(code, form, **args)
82
+ line = form.respond_to?(:line) ? form.line : nil
83
+ column = form.respond_to?(:column) ? form.column : nil
84
+ Compiler::Error.new(Kapusta::Errors.format(code, **args), line:, column:)
85
+ end
86
+
76
87
  def wrap_do(forms)
77
88
  return if forms.empty?
78
89
  return forms.first if forms.length == 1
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'error'
4
+ require_relative 'compiler/lua_compat'
4
5
  require_relative 'compiler/normalizer'
5
6
  require_relative 'compiler/emitter'
6
7
  require_relative 'compiler/macro_expander'
@@ -8,8 +9,8 @@ require_relative 'compiler/macro_expander'
8
9
  module Kapusta
9
10
  module Compiler
10
11
  class Error < Kapusta::Error; end
11
- SPECIAL_FORMS = %w[
12
- fn lambda λ let local var set if when unless case match
12
+ CORE_SPECIAL_FORMS = %w[
13
+ fn lambda λ let local var global set if when unless case match
13
14
  while for each do values
14
15
  -> ->> -?> -?>> doto
15
16
  icollect collect fcollect accumulate faccumulate
@@ -23,7 +24,7 @@ module Kapusta
23
24
  raise
24
25
  ivar cvar gvar
25
26
  ruby
26
- tset pcall xpcall
27
+ tset
27
28
  and or not
28
29
  = not= < <= > >=
29
30
  + - * / %
@@ -31,16 +32,21 @@ module Kapusta
31
32
  macro macros import-macros
32
33
  quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
33
34
  ].freeze
35
+ SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
34
36
 
35
37
  def self.compile(source, path: '(kapusta)')
36
38
  forms = Reader.read_all(source)
37
- expanded = MacroExpander.new.expand_all(forms)
39
+ expanded = MacroExpander.new(path:).expand_all(forms)
38
40
  compile_forms(expanded, path:)
41
+ rescue Kapusta::Error => e
42
+ raise e.with_defaults(path:)
39
43
  end
40
44
 
41
45
  def self.compile_forms(forms, path: '(kapusta)')
42
46
  normalized = Normalizer.new.normalize_all(forms)
43
47
  Emitter.new(path:).emit_file(normalized)
48
+ rescue Kapusta::Error => e
49
+ raise e.with_defaults(path:)
44
50
  end
45
51
 
46
52
  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,70 @@
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_no_subject: 'missing subject',
18
+ case_odd_patterns: 'expected even number of pattern/body pairs',
19
+ case_unsupported: 'case/match clauses use patterns this compiler cannot translate',
20
+ could_not_destructure_literal: 'could not destructure literal',
21
+ could_not_read_number: 'could not read number "%{token}"',
22
+ counted_no_range: 'expected range to include start and stop',
23
+ destructure_unsupported: 'destructure pattern this compiler cannot translate: %{pattern}',
24
+ dot_no_args: 'expected table argument',
25
+ each_no_binding: 'expected binding table',
26
+ empty_call: 'expected a function, macro, or special to call',
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
+ tset_no_value: 'tset: expected table, key, and value arguments',
50
+ unclosed_delimiter: "unclosed opening delimiter '%{char}'",
51
+ undefined_symbol: 'undefined symbol: %{name}',
52
+ unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
53
+ unexpected_eof: 'unexpected eof',
54
+ unexpected_vararg: 'unexpected vararg',
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_not_last: 'expected vararg as last parameter',
60
+ vararg_with_operator: 'tried to use vararg with operator',
61
+ when_no_body: '%{form}: expected body'
62
+ }.freeze
63
+
64
+ def self.format(code, **args)
65
+ template = MESSAGES.fetch(code) { raise ArgumentError, "unknown error code: #{code.inspect}" }
66
+ args.empty? ? template.dup : (template % args)
67
+ end
68
+ end
69
+ # rubocop:enable Style/FormatStringToken
70
+ end
@@ -10,6 +10,10 @@ module Kapusta
10
10
 
11
11
  PIPELINE_FORMS = %w[-> ->> -?> -?>> doto].freeze
12
12
 
13
+ def self.format(source, path: nil)
14
+ new([]).send(:format_source, source, path)
15
+ end
16
+
13
17
  def initialize(argv)
14
18
  @mode = :stdout
15
19
  @files = []
@@ -27,7 +31,8 @@ module Kapusta
27
31
 
28
32
  formatted = @files.map do |path|
29
33
  original = read_source(path)
30
- [path, original, format_source(original)]
34
+ validate_kapusta_source(original, path)
35
+ [path, original, format_source(original, path)]
31
36
  end
32
37
 
33
38
  case @mode
@@ -48,13 +53,17 @@ module Kapusta
48
53
  end
49
54
 
50
55
  0
51
- rescue Error => e
52
- warn e.message
56
+ rescue Kapusta::Error => e
57
+ warn e.formatted
53
58
  1
54
59
  end
55
60
 
56
61
  private
57
62
 
63
+ def validate_kapusta_source(source, path)
64
+ Kapusta::Compiler.compile(source, path:)
65
+ end
66
+
58
67
  def parse_args(argv)
59
68
  argv.each do |arg|
60
69
  case arg
@@ -99,7 +108,7 @@ module Kapusta
99
108
  $stdin.read
100
109
  end
101
110
 
102
- def format_source(source)
111
+ def format_source(source, path = nil)
103
112
  forms = Reader.read_all(source, preserve_comments: true)
104
113
  entries = top_level_entries(forms)
105
114
  return '' if entries.empty?
@@ -110,8 +119,10 @@ module Kapusta
110
119
  output << render_top_level_entry(entry)
111
120
  end
112
121
  output << "\n"
122
+ rescue Kapusta::Error => e
123
+ raise e.with_defaults(path:)
113
124
  rescue StandardError => e
114
- raise Error, e.message
125
+ raise Error.new(e.message, path:)
115
126
  end
116
127
 
117
128
  def separator_for(_previous, _current)