kapusta 0.5.0 → 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
@@ -89,6 +89,8 @@ module Kapusta
89
89
  when 'macros'
90
90
  register_macros_form(list.rest)
91
91
  return List.new([Sym.new('do')])
92
+ when 'import-macros'
93
+ raise macro_error(:import_macros_unsupported, list)
92
94
  end
93
95
 
94
96
  key = lookup_key(name)
@@ -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
@@ -57,25 +59,8 @@ module Kapusta
57
59
  List.new([Sym.new('set'), List.new([Sym.new('.'), items[1], items[2]]), items[3]]),
58
60
  list
59
61
  )
60
- when 'pcall'
61
- fn = items[1]
62
- args = items[2..]
63
- List.new([
64
- Sym.new('try'),
65
- List.new([Sym.new('values'), true, List.new([fn, *args])]),
66
- List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
67
- List.new([Sym.new('values'), false, Sym.new('e')])])
68
- ])
69
- when 'xpcall'
70
- fn = items[1]
71
- handler = items[2]
72
- args = items[3..]
73
- List.new([
74
- Sym.new('try'),
75
- List.new([Sym.new('values'), true, List.new([fn, *args])]),
76
- List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
77
- List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
78
- ])
62
+ when *LuaCompat::SPECIAL_FORMS
63
+ normalize_lua_compat_form(head.name, items)
79
64
  when '->', '->>', '-?>', '-?>>'
80
65
  inherit_position(normalize(thread(items[1..], head.name)), list)
81
66
  when 'doto'
@@ -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,7 +9,7 @@ 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
+ CORE_SPECIAL_FORMS = %w[
12
13
  fn lambda λ let local var global set if when unless case match
13
14
  while for each do values
14
15
  -> ->> -?> -?>> doto
@@ -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,6 +32,7 @@ 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)
@@ -14,6 +14,7 @@ module Kapusta
14
14
  cannot_emit_form: 'cannot emit form: %{form}',
15
15
  cannot_set_method_binding: 'cannot set method binding: %{name}',
16
16
  case_no_patterns: 'expected at least one pattern/body pair',
17
+ case_no_subject: 'missing subject',
17
18
  case_odd_patterns: 'expected even number of pattern/body pairs',
18
19
  case_unsupported: 'case/match clauses use patterns this compiler cannot translate',
19
20
  could_not_destructure_literal: 'could not destructure literal',
@@ -23,7 +24,6 @@ module Kapusta
23
24
  dot_no_args: 'expected table argument',
24
25
  each_no_binding: 'expected binding table',
25
26
  empty_call: 'expected a function, macro, or special to call',
26
- empty_token: 'empty token',
27
27
  expected_var: 'expected var %{name}',
28
28
  fn_no_params: 'expected parameters table',
29
29
  global_arity: 'expected name and value',
@@ -46,16 +46,17 @@ module Kapusta
46
46
  odd_forms_in_hash: 'odd number of forms in hash',
47
47
  rest_not_last: 'expected rest argument before last parameter',
48
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
49
  tset_no_value: 'tset: expected table, key, and value arguments',
51
50
  unclosed_delimiter: "unclosed opening delimiter '%{char}'",
52
51
  undefined_symbol: 'undefined symbol: %{name}',
53
52
  unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
54
53
  unexpected_eof: 'unexpected eof',
54
+ unexpected_vararg: 'unexpected vararg',
55
55
  unknown_special_form: 'unknown special form: %{name}',
56
56
  unquote_outside_quasiquote: 'unquote outside quasiquote',
57
57
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
58
58
  unterminated_string: 'unterminated string',
59
+ vararg_not_last: 'expected vararg as last parameter',
59
60
  vararg_with_operator: 'tried to use vararg with operator',
60
61
  when_no_body: '%{form}: expected body'
61
62
  }.freeze
@@ -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 = []
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require_relative '../kapusta'
6
+ require_relative 'formatter'
7
+
8
+ module Kapusta
9
+ class LSP
10
+ NOT_INITIALIZED = -32_002
11
+ METHOD_NOT_FOUND = -32_601
12
+ SEVERITY_ERROR = 1
13
+ FULL_SYNC = 1
14
+
15
+ def self.start(input: $stdin, output: $stdout, log: $stderr)
16
+ new(input:, output:, log:).run
17
+ end
18
+
19
+ def initialize(input:, output:, log:)
20
+ @input = input.binmode
21
+ @output = output.binmode
22
+ @log = log
23
+ @sources = {}
24
+ @initialized = false
25
+ @shutdown = false
26
+ end
27
+
28
+ def run
29
+ until (message = read_message).nil?
30
+ handle(message)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def read_message
37
+ headers = read_headers
38
+ return if headers.nil?
39
+
40
+ raw_length = headers['Content-Length']
41
+ return if raw_length.nil?
42
+
43
+ length = Integer(raw_length, 10, exception: false)
44
+ return if length.nil? || length.negative?
45
+
46
+ body = @input.read(length)
47
+ return if body.nil?
48
+
49
+ JSON.parse(body.force_encoding(Encoding::UTF_8))
50
+ rescue JSON::ParserError => e
51
+ log("parse error: #{e.message}")
52
+ nil
53
+ end
54
+
55
+ def read_headers
56
+ headers = {}
57
+ loop do
58
+ line = @input.gets
59
+ return if line.nil?
60
+ break if line.chomp.empty?
61
+
62
+ name, value = line.chomp.split(': ', 2)
63
+ headers[name] = value if name && value
64
+ end
65
+ headers
66
+ end
67
+
68
+ def write_message(payload)
69
+ body = JSON.generate(payload)
70
+ @output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
71
+ @output.flush
72
+ end
73
+
74
+ def handle(message)
75
+ method = message['method']
76
+ id = message['id']
77
+ params = message['params'] || {}
78
+
79
+ return handle_pre_init(method, id, params) unless @initialized || method == 'initialize' || method == 'exit'
80
+
81
+ dispatch(method, id, params)
82
+ rescue StandardError => e
83
+ log("#{e.class}: #{e.message}")
84
+ log(e.backtrace.first(5).join("\n"))
85
+ reply_error(id, METHOD_NOT_FOUND, e.message)
86
+ end
87
+
88
+ def handle_pre_init(method, id, _params)
89
+ return if id.nil?
90
+
91
+ reply_error(id, NOT_INITIALIZED, "received #{method} before initialize")
92
+ end
93
+
94
+ def dispatch(method, id, params)
95
+ case method
96
+ when 'initialize'
97
+ @initialized = true
98
+ reply(id, initialize_result)
99
+ when 'initialized' then nil
100
+ when 'shutdown'
101
+ @shutdown = true
102
+ reply(id, nil)
103
+ when 'exit' then exit(@shutdown ? 0 : 1)
104
+ when 'textDocument/didOpen' then on_did_open(params)
105
+ when 'textDocument/didChange' then on_did_change(params)
106
+ when 'textDocument/didSave' then on_did_save(params)
107
+ when 'textDocument/didClose' then on_did_close(params)
108
+ when 'textDocument/formatting' then reply(id, formatting(params))
109
+ else
110
+ reply_error(id, METHOD_NOT_FOUND, "method not found: #{method}")
111
+ end
112
+ end
113
+
114
+ def reply(id, result)
115
+ return if id.nil?
116
+
117
+ write_message(jsonrpc: '2.0', id:, result:)
118
+ end
119
+
120
+ def reply_error(id, code, message)
121
+ return if id.nil?
122
+
123
+ write_message(jsonrpc: '2.0', id:, error: { code:, message: })
124
+ end
125
+
126
+ def notify(method, params)
127
+ write_message(jsonrpc: '2.0', method:, params:)
128
+ end
129
+
130
+ def initialize_result
131
+ {
132
+ capabilities: {
133
+ textDocumentSync: { openClose: true, change: FULL_SYNC, save: { includeText: false } },
134
+ documentFormattingProvider: true
135
+ },
136
+ serverInfo: { name: 'kapusta-ls', version: Kapusta::VERSION }
137
+ }
138
+ end
139
+
140
+ def on_did_open(params)
141
+ doc = params['textDocument'] || {}
142
+ uri = doc['uri']
143
+ return unless uri
144
+
145
+ version = doc['version']
146
+ text = doc['text'] || ''
147
+ store(uri, text, version)
148
+ publish_diagnostics(uri, text, version)
149
+ end
150
+
151
+ def on_did_change(params)
152
+ uri = params.dig('textDocument', 'uri')
153
+ version = params.dig('textDocument', 'version')
154
+ changes = params['contentChanges'] || []
155
+ return if uri.nil? || changes.empty?
156
+
157
+ text = changes.last['text']
158
+ store(uri, text, version)
159
+ publish_diagnostics(uri, text, version)
160
+ end
161
+
162
+ def on_did_save(params)
163
+ uri = params.dig('textDocument', 'uri')
164
+ entry = @sources[uri]
165
+ return unless entry
166
+
167
+ publish_diagnostics(uri, entry[:text], entry[:version])
168
+ end
169
+
170
+ def on_did_close(params)
171
+ uri = params.dig('textDocument', 'uri')
172
+ return unless uri
173
+
174
+ @sources.delete(uri)
175
+ notify('textDocument/publishDiagnostics', { uri:, diagnostics: [] })
176
+ end
177
+
178
+ def formatting(params)
179
+ uri = params.dig('textDocument', 'uri')
180
+ entry = @sources[uri]
181
+ return [] unless entry
182
+
183
+ formatted = Kapusta::Formatter.format(entry[:text], path: uri_to_path(uri))
184
+ return [] if formatted == entry[:text]
185
+
186
+ [{ range: full_range(entry[:text]), newText: formatted }]
187
+ rescue Kapusta::Error
188
+ []
189
+ end
190
+
191
+ def store(uri, text, version)
192
+ @sources[uri] = { text:, version: }
193
+ end
194
+
195
+ def publish_diagnostics(uri, text, version)
196
+ diagnostics = collect_diagnostics(text, uri_to_path(uri))
197
+ params = { uri:, diagnostics: }
198
+ params[:version] = version unless version.nil?
199
+ notify('textDocument/publishDiagnostics', params)
200
+ end
201
+
202
+ def collect_diagnostics(text, path)
203
+ Kapusta.compile(text, path: path || '(buffer)')
204
+ []
205
+ rescue Kapusta::Error => e
206
+ [diagnostic_from(e, text)]
207
+ end
208
+
209
+ def diagnostic_from(error, text)
210
+ line = [(error.line || 1) - 1, 0].max
211
+ column = [(error.column || 1) - 1, 0].max
212
+
213
+ {
214
+ range: {
215
+ start: { line:, character: column },
216
+ end: { line:, character: column + token_length(text, line, column) }
217
+ },
218
+ severity: SEVERITY_ERROR,
219
+ source: 'kapusta-ls',
220
+ message: error.reason
221
+ }
222
+ end
223
+
224
+ def token_length(text, line, column)
225
+ source_line = text.lines[line]
226
+ return 1 unless source_line
227
+
228
+ tail = source_line[column..] || ''
229
+ match = tail.match(/\A[^\s()\[\]{}";`,]+/)
230
+ match && match[0].length.positive? ? match[0].length : 1
231
+ end
232
+
233
+ def full_range(text)
234
+ lines = text.split("\n", -1)
235
+ end_line = [lines.length - 1, 0].max
236
+ end_character = lines.last ? lines.last.length : 0
237
+ {
238
+ start: { line: 0, character: 0 },
239
+ end: { line: end_line, character: end_character }
240
+ }
241
+ end
242
+
243
+ def uri_to_path(uri)
244
+ return unless uri
245
+
246
+ parsed = URI.parse(uri)
247
+ return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
248
+
249
+ uri
250
+ rescue URI::InvalidURIError
251
+ uri
252
+ end
253
+
254
+ def log(message)
255
+ @log.puts "kapusta-ls: #{message}"
256
+ end
257
+ end
258
+ end
@@ -270,8 +270,6 @@ module Kapusta
270
270
  start = @pos
271
271
  advance until delim?(peek)
272
272
  token = @src[start...@pos]
273
- raise reader_error(:empty_token, position) if token.empty?
274
-
275
273
  parse_atom(token, position)
276
274
  end
277
275
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.5.0'
4
+ VERSION = '0.7.0'
5
5
  end