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.
@@ -72,7 +72,7 @@ module Kapusta
72
72
 
73
73
  def read_next_item
74
74
  skip_ws
75
- raise Error, 'unexpected eof' if eof?
75
+ raise reader_error(:unexpected_eof, source_position) if eof?
76
76
 
77
77
  return read_comment if @preserve_comments && peek == ';'
78
78
 
@@ -81,10 +81,11 @@ module Kapusta
81
81
 
82
82
  def read_form
83
83
  skip_ws
84
- raise Error, 'unexpected eof' if eof?
84
+ raise reader_error(:unexpected_eof, source_position) if eof?
85
85
 
86
86
  return read_comment if @preserve_comments && peek == ';'
87
87
 
88
+ position = source_position
88
89
  form =
89
90
  case peek
90
91
  when '(' then read_list
@@ -99,9 +100,18 @@ module Kapusta
99
100
  read_atom
100
101
  end
101
102
 
103
+ attach_position(form, position)
102
104
  read_postfix(form)
103
105
  end
104
106
 
107
+ def attach_position(form, position)
108
+ return form unless form.respond_to?(:line=)
109
+
110
+ form.line ||= position[0]
111
+ form.column ||= position[1]
112
+ form
113
+ end
114
+
105
115
  def read_quasiquote
106
116
  advance
107
117
  Quasiquote.new(read_form)
@@ -133,6 +143,8 @@ module Kapusta
133
143
  advance
134
144
  list = List.new(items)
135
145
  list.multiline_source = closing_position[0] != opening_position[0]
146
+ list.line = opening_position[0]
147
+ list.column = opening_position[1]
136
148
  list
137
149
  end
138
150
 
@@ -152,6 +164,8 @@ module Kapusta
152
164
  advance
153
165
  vec = Vec.new(items)
154
166
  vec.multiline_source = closing_position[0] != opening_position[0]
167
+ vec.line = opening_position[0]
168
+ vec.column = opening_position[1]
155
169
  vec
156
170
  end
157
171
 
@@ -180,14 +194,17 @@ module Kapusta
180
194
  closing_position = source_position
181
195
  advance
182
196
 
183
- raise Error, 'odd number of forms in hash' unless pending.empty?
197
+ raise reader_error(:odd_forms_in_hash, opening_position) unless pending.empty?
184
198
 
185
199
  hash = HashLit.new(entries)
186
200
  hash.multiline_source = closing_position[0] != opening_position[0]
201
+ hash.line = opening_position[0]
202
+ hash.column = opening_position[1]
187
203
  hash
188
204
  end
189
205
 
190
206
  def read_string
207
+ opening_position = source_position
191
208
  advance
192
209
  buffer = +''
193
210
  until eof? || peek == '"'
@@ -211,7 +228,7 @@ module Kapusta
211
228
  buffer << advance
212
229
  end
213
230
  end
214
- raise Error, 'unterminated string' if eof?
231
+ raise reader_error(:unterminated_string, opening_position) if eof?
215
232
 
216
233
  advance
217
234
  buffer
@@ -249,22 +266,25 @@ module Kapusta
249
266
  end
250
267
 
251
268
  def read_atom
269
+ position = source_position
252
270
  start = @pos
253
271
  advance until delim?(peek)
254
272
  token = @src[start...@pos]
255
- raise Error, 'empty token' if token.empty?
273
+ raise reader_error(:empty_token, position) if token.empty?
256
274
 
257
- parse_atom(token)
275
+ parse_atom(token, position)
258
276
  end
259
277
 
260
278
  def unexpected_closing_delim(char)
261
- line, column = source_position
262
- Error.new("unexpected closing delimiter '#{char}' at line #{line}, column #{column}")
279
+ reader_error(:unexpected_closing_delimiter, source_position, char:)
263
280
  end
264
281
 
265
282
  def unclosed_opening_delim(char, position)
266
- line, column = position
267
- Error.new("unclosed opening delimiter '#{char}' at line #{line}, column #{column}")
283
+ reader_error(:unclosed_delimiter, position, char:)
284
+ end
285
+
286
+ def reader_error(code, position, **args)
287
+ Error.new(Kapusta::Errors.format(code, **args), line: position[0], column: position[1])
268
288
  end
269
289
 
270
290
  def source_position
@@ -276,13 +296,15 @@ module Kapusta
276
296
  [line, column]
277
297
  end
278
298
 
279
- def parse_atom(token)
299
+ def parse_atom(token, position)
280
300
  return true if token == 'true'
281
301
  return false if token == 'false'
282
302
  return if token == 'nil'
283
303
  return Integer(token, 10) if token.match?(/\A-?\d+\z/)
284
304
  return Float(token) if token.match?(/\A-?\d+\.\d+\z/)
285
305
 
306
+ raise reader_error(:could_not_read_number, position, token:) if token.match?(/\A-?\d/)
307
+
286
308
  if token.start_with?(':') && token.length > 1
287
309
  Kapusta.kebab_to_snake(token[1..]).to_sym
288
310
  elsif token.length > 1 && token.end_with?('#') && !token[0..-2].include?('#')
@@ -294,7 +316,7 @@ module Kapusta
294
316
 
295
317
  def normalize_hash_pair(item, value)
296
318
  if item.is_a?(Sym) && item.name == ':'
297
- raise Error, 'bad shorthand' unless value.is_a?(Sym)
319
+ raise reader_error(:bad_shorthand, source_position) unless value.is_a?(Sym)
298
320
 
299
321
  key = Kapusta.kebab_to_snake(value.name).to_sym
300
322
  [key, value]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'kapusta/version'
4
4
  require_relative 'kapusta/error'
5
+ require_relative 'kapusta/errors'
5
6
  require_relative 'kapusta/support'
6
7
  require_relative 'kapusta/ast'
7
8
  require_relative 'kapusta/reader'
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'open3'
5
+ require 'rbconfig'
6
+
7
+ ERRORS_DIR = File.expand_path('../examples-errors', __dir__)
8
+ KAPUSTA_BIN = File.expand_path('../exe/kapusta', __dir__)
9
+ KAPFMT_BIN = File.expand_path('../exe/kapfmt', __dir__)
10
+
11
+ def run_error_example(name)
12
+ k_out, k_err, k_status = Open3.capture3(RbConfig.ruby, KAPUSTA_BIN, name, chdir: ERRORS_DIR)
13
+ f_out, f_err, f_status = Open3.capture3(RbConfig.ruby, KAPFMT_BIN, name, chdir: ERRORS_DIR)
14
+
15
+ raise "kapusta unexpectedly succeeded for #{name}" if k_status.success?
16
+ raise "kapfmt unexpectedly succeeded for #{name}" if f_status.success?
17
+ raise "kapusta wrote to stdout for #{name}: #{k_out.inspect}" unless k_out.empty?
18
+ raise "kapfmt wrote to stdout for #{name}: #{f_out.inspect}" unless f_out.empty?
19
+ raise "kapusta and kapfmt disagree for #{name}:\n kapusta: #{k_err} kapfmt: #{f_err}" unless k_err == f_err
20
+
21
+ k_err
22
+ end
23
+
24
+ RSpec.describe 'examples-errors' do
25
+ it 'accumulate-missing-iterator.kap' do
26
+ expect(run_error_example('accumulate-missing-iterator.kap'))
27
+ .to eq("accumulate-missing-iterator.kap:5:3: expected initial value and iterator binding table\n")
28
+ end
29
+
30
+ it 'call-empty-form.kap' do
31
+ expect(run_error_example('call-empty-form.kap'))
32
+ .to eq("call-empty-form.kap:7:8: expected a function, macro, or special to call\n")
33
+ end
34
+
35
+ it 'call-literal-number.kap' do
36
+ expect(run_error_example('call-literal-number.kap'))
37
+ .to eq("call-literal-number.kap:6:14: cannot call literal value 1\n")
38
+ end
39
+
40
+ it 'case-no-patterns.kap' do
41
+ expect(run_error_example('case-no-patterns.kap'))
42
+ .to eq("case-no-patterns.kap:3:5: expected at least one pattern/body pair\n")
43
+ end
44
+
45
+ it 'case-odd-pattern-body.kap' do
46
+ expect(run_error_example('case-odd-pattern-body.kap'))
47
+ .to eq("case-odd-pattern-body.kap:2:3: expected even number of pattern/body pairs\n")
48
+ end
49
+
50
+ it 'destructure-literal-number.kap' do
51
+ expect(run_error_example('destructure-literal-number.kap'))
52
+ .to eq("destructure-literal-number.kap:5:3: could not destructure literal\n")
53
+ end
54
+
55
+ it 'destructure-literal-table.kap' do
56
+ expect(run_error_example('destructure-literal-table.kap'))
57
+ .to eq("destructure-literal-table.kap:4:1: could not destructure literal\n")
58
+ end
59
+
60
+ it 'destructure-rest-as-table.kap' do
61
+ expect(run_error_example('destructure-rest-as-table.kap'))
62
+ .to eq("destructure-rest-as-table.kap:6:3: unable to bind table ...\n")
63
+ end
64
+
65
+ it 'dot-without-table.kap' do
66
+ expect(run_error_example('dot-without-table.kap'))
67
+ .to eq("dot-without-table.kap:5:15: expected table argument\n")
68
+ end
69
+
70
+ it 'each-not-binding-table.kap' do
71
+ expect(run_error_example('each-not-binding-table.kap'))
72
+ .to eq("each-not-binding-table.kap:6:3: expected binding table\n")
73
+ end
74
+
75
+ it 'faccumulate-missing-iterator.kap' do
76
+ expect(run_error_example('faccumulate-missing-iterator.kap'))
77
+ .to eq("faccumulate-missing-iterator.kap:7:3: expected initial value and iterator binding table\n")
78
+ end
79
+
80
+ it 'fcollect-missing-range.kap' do
81
+ expect(run_error_example('fcollect-missing-range.kap'))
82
+ .to eq("fcollect-missing-range.kap:6:3: expected range to include start and stop\n")
83
+ end
84
+
85
+ it 'fn-non-symbol-param.kap' do
86
+ expect(run_error_example('fn-non-symbol-param.kap'))
87
+ .to eq("fn-non-symbol-param.kap:4:1: destructure pattern this compiler cannot translate: [1 2]\n")
88
+ end
89
+
90
+ it 'fn-without-params.kap' do
91
+ expect(run_error_example('fn-without-params.kap'))
92
+ .to eq("fn-without-params.kap:4:11: expected parameters table\n")
93
+ end
94
+
95
+ it 'for-missing-stop.kap' do
96
+ expect(run_error_example('for-missing-stop.kap'))
97
+ .to eq("for-missing-stop.kap:6:3: expected range to include start and stop\n")
98
+ end
99
+
100
+ it 'global-non-symbol-name.kap' do
101
+ expect(run_error_example('global-non-symbol-name.kap'))
102
+ .to eq("global-non-symbol-name.kap:6:1: unable to bind integer 1\n")
103
+ end
104
+
105
+ it 'global-without-value.kap' do
106
+ expect(run_error_example('global-without-value.kap'))
107
+ .to eq("global-without-value.kap:6:1: expected name and value\n")
108
+ end
109
+
110
+ it 'icollect-missing-iterator.kap' do
111
+ expect(run_error_example('icollect-missing-iterator.kap'))
112
+ .to eq("icollect-missing-iterator.kap:6:3: expected iterator binding table\n")
113
+ end
114
+
115
+ it 'if-no-body.kap' do
116
+ expect(run_error_example('if-no-body.kap'))
117
+ .to eq("if-no-body.kap:8:5: expected condition and body\n")
118
+ end
119
+
120
+ it 'import-macros-missing-module.kap' do
121
+ expect(run_error_example('import-macros-missing-module.kap'))
122
+ .to eq("import-macros-missing-module.kap:4:1: import-macros is not yet supported\n")
123
+ end
124
+
125
+ it 'let-odd-bindings.kap' do
126
+ expect(run_error_example('let-odd-bindings.kap'))
127
+ .to eq("let-odd-bindings.kap:2:3: expected even number of name/value bindings\n")
128
+ end
129
+
130
+ it 'let-without-body-form.kap' do
131
+ expect(run_error_example('let-without-body-form.kap'))
132
+ .to eq("let-without-body-form.kap:4:5: expected body expression\n")
133
+ end
134
+
135
+ it 'local-with-extra-args.kap' do
136
+ expect(run_error_example('local-with-extra-args.kap'))
137
+ .to eq("local-with-extra-args.kap:6:3: local: expected name and value\n")
138
+ end
139
+
140
+ it 'local-without-value.kap' do
141
+ expect(run_error_example('local-without-value.kap'))
142
+ .to eq("local-without-value.kap:6:3: local: expected name and value\n")
143
+ end
144
+
145
+ it 'macro-unsafe-bind.kap' do
146
+ expect(run_error_example('macro-unsafe-bind.kap'))
147
+ .to eq("macro-unsafe-bind.kap:13:8: macro tried to bind unsafe without gensym\n")
148
+ end
149
+
150
+ it 'macro-vararg-with-operator.kap' do
151
+ expect(run_error_example('macro-vararg-with-operator.kap'))
152
+ .to eq("macro-vararg-with-operator.kap:5:3: tried to use vararg with operator\n")
153
+ end
154
+
155
+ it 'match-no-patterns.kap' do
156
+ expect(run_error_example('match-no-patterns.kap'))
157
+ .to eq("match-no-patterns.kap:3:5: expected at least one pattern/body pair\n")
158
+ end
159
+
160
+ it 'mismatched-brackets.kap' do
161
+ expect(run_error_example('mismatched-brackets.kap'))
162
+ .to eq("mismatched-brackets.kap:4:19: unexpected closing delimiter ')'\n")
163
+ end
164
+
165
+ it 'only-rest-param.kap' do
166
+ expect(run_error_example('only-rest-param.kap'))
167
+ .to eq("only-rest-param.kap:4:1: expected rest argument before last parameter\n")
168
+ end
169
+
170
+ it 'quote-runtime.kap' do
171
+ expect(run_error_example('quote-runtime.kap'))
172
+ .to eq("quote-runtime.kap:6:1: cannot emit form: `hello\n")
173
+ end
174
+
175
+ it 'rest-not-last.kap' do
176
+ expect(run_error_example('rest-not-last.kap'))
177
+ .to eq("rest-not-last.kap:6:3: expected rest argument before last parameter\n")
178
+ end
179
+
180
+ it 'set-immutable-local.kap' do
181
+ expect(run_error_example('set-immutable-local.kap'))
182
+ .to eq("set-immutable-local.kap:8:3: expected var counter\n")
183
+ end
184
+
185
+ it 'shadow-special-fn.kap' do
186
+ expect(run_error_example('shadow-special-fn.kap'))
187
+ .to eq("shadow-special-fn.kap:6:3: local fn was overshadowed by a special form or macro\n")
188
+ end
189
+
190
+ it 'shadow-special-if.kap' do
191
+ expect(run_error_example('shadow-special-if.kap'))
192
+ .to eq("shadow-special-if.kap:4:1: local if was overshadowed by a special form or macro\n")
193
+ end
194
+
195
+ it 'symbol-starting-with-digit.kap' do
196
+ expect(run_error_example('symbol-starting-with-digit.kap'))
197
+ .to eq("symbol-starting-with-digit.kap:6:10: could not read number \"5var\"\n")
198
+ end
199
+
200
+ it 'tset-missing-value.kap' do
201
+ expect(run_error_example('tset-missing-value.kap'))
202
+ .to eq("tset-missing-value.kap:5:5: tset: expected table, key, and value arguments\n")
203
+ end
204
+
205
+ it 'unbalanced-parens.kap' do
206
+ expect(run_error_example('unbalanced-parens.kap'))
207
+ .to eq("unbalanced-parens.kap:4:1: unclosed opening delimiter '('\n")
208
+ end
209
+
210
+ it 'unclosed-table.kap' do
211
+ expect(run_error_example('unclosed-table.kap'))
212
+ .to eq("unclosed-table.kap:4:21: unexpected closing delimiter ']'\n")
213
+ end
214
+
215
+ it 'unquote-outside-quote.kap' do
216
+ expect(run_error_example('unquote-outside-quote.kap'))
217
+ .to eq("unquote-outside-quote.kap:5:10: cannot emit form: ,x\n")
218
+ end
219
+
220
+ it 'var-without-value.kap' do
221
+ expect(run_error_example('var-without-value.kap'))
222
+ .to eq("var-without-value.kap:6:3: var: expected name and value\n")
223
+ end
224
+
225
+ it 'when-no-body.kap' do
226
+ expect(run_error_example('when-no-body.kap'))
227
+ .to eq("when-no-body.kap:4:3: when: expected body\n")
228
+ end
229
+ end
@@ -43,7 +43,7 @@ RSpec.describe Kapusta::Formatter do
43
43
  Dir.mktmpdir do |dir|
44
44
  path = File.join(dir, 'sample.kap')
45
45
  File.write(path, <<~KAP)
46
- (let [uri (URI.join (ivar base-uri) (.. "/posts/" id)) body (Net.HTTP.get uri) post (JSON.parse body {:symbolize-names true}) {: title : author} post] (values title author))
46
+ (fn fetch-post [id] (let [uri (URI.join (ivar base-uri) (.. "/posts/" id)) body (Net.HTTP.get uri) post (JSON.parse body {:symbolize-names true}) {: title : author} post] (values title author)))
47
47
  KAP
48
48
 
49
49
  previous_path = ENV.fetch('PATH', nil)
@@ -54,11 +54,12 @@ RSpec.describe Kapusta::Formatter do
54
54
  end
55
55
 
56
56
  expect(output).to eq(<<~KAP)
57
- (let [uri (URI.join (ivar base-uri) (.. "/posts/" id))
58
- body (Net.HTTP.get uri)
59
- post (JSON.parse body {:symbolize-names true})
60
- {: title : author} post]
61
- (values title author))
57
+ (fn fetch-post [id]
58
+ (let [uri (URI.join (ivar base-uri) (.. "/posts/" id))
59
+ body (Net.HTTP.get uri)
60
+ post (JSON.parse body {:symbolize-names true})
61
+ {: title : author} post]
62
+ (values title author)))
62
63
  KAP
63
64
  ensure
64
65
  ENV['PATH'] = previous_path
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -126,14 +126,15 @@ files:
126
126
  - lib/kapusta/compiler/normalizer.rb
127
127
  - lib/kapusta/env.rb
128
128
  - lib/kapusta/error.rb
129
+ - lib/kapusta/errors.rb
129
130
  - lib/kapusta/formatter.rb
130
131
  - lib/kapusta/reader.rb
131
132
  - lib/kapusta/support.rb
132
133
  - lib/kapusta/version.rb
133
134
  - spec/cli_spec.rb
135
+ - spec/examples_errors_spec.rb
134
136
  - spec/examples_spec.rb
135
137
  - spec/formatter_spec.rb
136
- - spec/reader_spec.rb
137
138
  - spec/spec_helper.rb
138
139
  homepage: https://github.com/evmorov/kapusta
139
140
  licenses:
data/spec/reader_spec.rb DELETED
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe Kapusta::Reader do
6
- describe 'error messages' do
7
- it 'reports unexpected closing delimiters with their source position' do
8
- expect { Kapusta.eval('(print 1))') }
9
- .to raise_error(Kapusta::Reader::Error,
10
- /unexpected closing delimiter '\)' at line 1, column 10/)
11
- end
12
-
13
- it 'reports unclosed opening delimiters with their source position' do
14
- cases = {
15
- '(print 1' => /unclosed opening delimiter '\(' at line 1, column 1/,
16
- '[1 2' => /unclosed opening delimiter '\[' at line 1, column 1/,
17
- '{:name "A"' => /unclosed opening delimiter '\{' at line 1, column 1/
18
- }
19
-
20
- cases.each do |source, message|
21
- expect { Kapusta.eval(source) }
22
- .to raise_error(Kapusta::Reader::Error, message)
23
- end
24
- end
25
- end
26
- end