kapusta 0.5.0 → 0.8.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -6
  3. data/bin/fennel-parity +11 -4
  4. data/examples/classify-wallet.kap +11 -0
  5. data/examples/import-helpers.kapm +9 -0
  6. data/examples/macros-import-helpers.kap +3 -0
  7. data/examples/macros-import-whole.kap +5 -0
  8. data/examples/macros-import.kap +6 -0
  9. data/examples/power-of-three.kap +12 -0
  10. data/examples/shared-macros.kapm +4 -0
  11. data/exe/kapusta-ls +14 -0
  12. data/kapusta.gemspec +2 -2
  13. data/lib/kapusta/compiler/emitter/bindings.rb +38 -4
  14. data/lib/kapusta/compiler/emitter/collections.rb +51 -59
  15. data/lib/kapusta/compiler/emitter/control_flow.rb +24 -2
  16. data/lib/kapusta/compiler/emitter/expressions.rb +0 -2
  17. data/lib/kapusta/compiler/emitter/interop.rb +2 -1
  18. data/lib/kapusta/compiler/emitter/patterns.rb +52 -4
  19. data/lib/kapusta/compiler/emitter/support.rb +1 -1
  20. data/lib/kapusta/compiler/emitter.rb +1 -1
  21. data/lib/kapusta/compiler/lua_compat.rb +149 -0
  22. data/lib/kapusta/compiler/macro_expander.rb +55 -141
  23. data/lib/kapusta/compiler/macro_gensym.rb +21 -0
  24. data/lib/kapusta/compiler/macro_importer.rb +81 -0
  25. data/lib/kapusta/compiler/macro_lowerer.rb +184 -0
  26. data/lib/kapusta/compiler/normalizer.rb +4 -19
  27. data/lib/kapusta/compiler.rb +4 -2
  28. data/lib/kapusta/errors.rb +9 -3
  29. data/lib/kapusta/formatter.rb +4 -0
  30. data/lib/kapusta/lsp/definition.rb +67 -0
  31. data/lib/kapusta/lsp/diagnostics.rb +42 -0
  32. data/lib/kapusta/lsp/formatting.rb +30 -0
  33. data/lib/kapusta/lsp/identifier.rb +28 -0
  34. data/lib/kapusta/lsp/rename.rb +417 -0
  35. data/lib/kapusta/lsp/scope_walker.rb +643 -0
  36. data/lib/kapusta/lsp/workspace_index.rb +225 -0
  37. data/lib/kapusta/lsp.rb +312 -0
  38. data/lib/kapusta/reader.rb +0 -2
  39. data/lib/kapusta/version.rb +1 -1
  40. data/spec/examples_errors_spec.rb +142 -1
  41. data/spec/examples_spec.rb +12 -0
  42. data/spec/lsp_spec.rb +603 -0
  43. metadata +23 -1
@@ -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,14 +24,18 @@ 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',
30
30
  global_non_symbol_name: 'unable to bind %{type} %{value}',
31
31
  icollect_no_iterator: 'expected iterator binding table',
32
32
  if_no_body: 'expected condition and body',
33
- import_macros_unsupported: 'import-macros is not yet supported',
33
+ import_macros_cycle: 'import-macros cycle detected for module %{module}',
34
+ import_macros_destructure_invalid: 'import-macros expects a hash literal as first argument',
35
+ import_macros_macro_not_found: 'import-macros: macro %{macro} not exported by module %{module}',
36
+ import_macros_module_invalid: 'import-macros expects a symbol or string module name',
37
+ import_macros_module_no_exports: 'import-macros: module %{module} has no export table',
38
+ import_macros_module_not_found: 'import-macros: module %{module} not found',
34
39
  invalid_class_name: 'invalid class name: %{name}',
35
40
  invalid_module_name: 'invalid module name: %{name}',
36
41
  let_no_body: 'expected body expression',
@@ -46,16 +51,17 @@ module Kapusta
46
51
  odd_forms_in_hash: 'odd number of forms in hash',
47
52
  rest_not_last: 'expected rest argument before last parameter',
48
53
  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
54
  tset_no_value: 'tset: expected table, key, and value arguments',
51
55
  unclosed_delimiter: "unclosed opening delimiter '%{char}'",
52
56
  undefined_symbol: 'undefined symbol: %{name}',
53
57
  unexpected_closing_delimiter: "unexpected closing delimiter '%{char}'",
54
58
  unexpected_eof: 'unexpected eof',
59
+ unexpected_vararg: 'unexpected vararg',
55
60
  unknown_special_form: 'unknown special form: %{name}',
56
61
  unquote_outside_quasiquote: 'unquote outside quasiquote',
57
62
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
58
63
  unterminated_string: 'unterminated string',
64
+ vararg_not_last: 'expected vararg as last parameter',
59
65
  vararg_with_operator: 'tried to use vararg with operator',
60
66
  when_no_body: '%{form}: expected body'
61
67
  }.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,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rename'
4
+
5
+ module Kapusta
6
+ class LSP
7
+ module Definition
8
+ module_function
9
+
10
+ def find(uri, text, line_zero, character, workspace_index:)
11
+ target = Rename.locate(text, line_zero, character)
12
+ return unless target
13
+
14
+ case target.kind
15
+ when :local, :toplevel_fn, :constant
16
+ location_for_binding(uri, target.binding) if target.binding
17
+ when :macro
18
+ locations_for_macro(uri, target.binding, workspace_index)
19
+ when :free_toplevel
20
+ locations_for_toplevel(target.name, workspace_index)
21
+ when :free_constant
22
+ locations_for_constant(target.segment_prefix, workspace_index)
23
+ end
24
+ end
25
+
26
+ def locations_for_macro(uri, binding, workspace_index)
27
+ return unless binding
28
+
29
+ case binding.kind
30
+ when :macro
31
+ location_for_binding(uri, binding)
32
+ when :macro_import
33
+ def_uri, def_binding = workspace_index.find_macro_definition(
34
+ uri, binding.import_module, binding.import_key
35
+ )
36
+ location_for_binding(def_uri, def_binding) if def_uri && def_binding
37
+ end
38
+ end
39
+
40
+ def location_for_binding(uri, binding)
41
+ { uri:, range: binding_range(binding) }
42
+ end
43
+
44
+ def locations_for_toplevel(name, workspace_index)
45
+ defs = workspace_index.toplevel_fn_definitions(name)
46
+ return if defs.empty?
47
+
48
+ defs.map { |uri, b| { uri:, range: binding_range(b) } }
49
+ end
50
+
51
+ def locations_for_constant(prefix, workspace_index)
52
+ defs = workspace_index.constant_definitions_with_prefix(prefix)
53
+ return if defs.empty?
54
+
55
+ defs.map { |uri, b| { uri:, range: binding_range(b) } }
56
+ end
57
+
58
+ def binding_range(binding)
59
+ line = binding.line - 1
60
+ {
61
+ start: { line:, character: binding.column - 1 },
62
+ end: { line:, character: binding.end_column - 1 }
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ class LSP
5
+ module Diagnostics
6
+ SEVERITY_ERROR = 1
7
+
8
+ module_function
9
+
10
+ def collect(text, path)
11
+ Kapusta.compile(text, path: path || '(buffer)')
12
+ []
13
+ rescue Kapusta::Error => e
14
+ [diagnostic_from(e, text)]
15
+ end
16
+
17
+ def diagnostic_from(error, text)
18
+ line = [(error.line || 1) - 1, 0].max
19
+ column = [(error.column || 1) - 1, 0].max
20
+
21
+ {
22
+ range: {
23
+ start: { line:, character: column },
24
+ end: { line:, character: column + token_length(text, line, column) }
25
+ },
26
+ severity: SEVERITY_ERROR,
27
+ source: 'kapusta-ls',
28
+ message: error.reason
29
+ }
30
+ end
31
+
32
+ def token_length(text, line, column)
33
+ source_line = text.lines[line]
34
+ return 1 unless source_line
35
+
36
+ tail = source_line[column..] || ''
37
+ match = tail.match(/\A[^\s()\[\]{}";`,]+/)
38
+ match && match[0].length.positive? ? match[0].length : 1
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../formatter'
4
+
5
+ module Kapusta
6
+ class LSP
7
+ module Formatting
8
+ module_function
9
+
10
+ def text_edits(text, path)
11
+ formatted = Kapusta::Formatter.format(text, path:)
12
+ return [] if formatted == text
13
+
14
+ [{ range: full_range(text), newText: formatted }]
15
+ rescue Kapusta::Error
16
+ []
17
+ end
18
+
19
+ def full_range(text)
20
+ lines = text.split("\n", -1)
21
+ end_line = [lines.length - 1, 0].max
22
+ end_character = lines.last ? lines.last.length : 0
23
+ {
24
+ start: { line: 0, character: 0 },
25
+ end: { line: end_line, character: end_character }
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../compiler'
4
+
5
+ module Kapusta
6
+ class LSP
7
+ module Identifier
8
+ DELIM_CHARS = '()[]{}";`,'
9
+
10
+ module_function
11
+
12
+ def valid_local?(name)
13
+ return false if name.nil? || name.empty?
14
+ return false if name.match?(/\s/)
15
+ return false if name.match?(/[#{Regexp.escape(DELIM_CHARS)}]/o)
16
+ return false if name.match?(/\A-?\d/)
17
+ return false if name.include?('.')
18
+ return false if Kapusta::Compiler::SPECIAL_FORMS.include?(name)
19
+
20
+ true
21
+ end
22
+
23
+ def valid_constant_segment?(segment)
24
+ !segment.nil? && segment.match?(/\A[A-Z]\w*\z/)
25
+ end
26
+ end
27
+ end
28
+ end