jade-lang 0.8.0 → 0.10.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +439 -2
  3. data/docs/interop.md +18 -1
  4. data/docs/stdlib.md +3 -1
  5. data/docs/syntax.md +96 -0
  6. data/lib/jade/api.rb +9 -0
  7. data/lib/jade/ast.rb +37 -1
  8. data/lib/jade/cli/eject.rb +132 -0
  9. data/lib/jade/cli.rb +2 -0
  10. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  11. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  12. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  13. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  14. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  15. data/lib/jade/codegen/boundary.rb +12 -2
  16. data/lib/jade/codegen/context.rb +17 -0
  17. data/lib/jade/codegen/error.rb +1 -0
  18. data/lib/jade/codegen/function_call.rb +23 -7
  19. data/lib/jade/codegen/function_declaration.rb +25 -13
  20. data/lib/jade/codegen/helpers.rb +27 -4
  21. data/lib/jade/codegen/implementation.rb +2 -2
  22. data/lib/jade/codegen/inline.rb +1 -1
  23. data/lib/jade/codegen/inlines.rb +6 -1
  24. data/lib/jade/codegen/names.rb +31 -0
  25. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  26. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  27. data/lib/jade/codegen.rb +76 -15
  28. data/lib/jade/compiler.rb +2 -0
  29. data/lib/jade/entry.rb +1 -1
  30. data/lib/jade/error.rb +5 -0
  31. data/lib/jade/extensions.rb +70 -0
  32. data/lib/jade/formatter/calls.rb +68 -11
  33. data/lib/jade/formatter/collections.rb +4 -4
  34. data/lib/jade/formatter/declarations.rb +13 -15
  35. data/lib/jade/formatter/helper.rb +20 -0
  36. data/lib/jade/formatter/infix_application.rb +19 -4
  37. data/lib/jade/formatter/lambda.rb +24 -12
  38. data/lib/jade/formatter/pattern.rb +3 -0
  39. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  40. data/lib/jade/frontend/desugaring.rb +29 -9
  41. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  42. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  43. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  44. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  45. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  46. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  47. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  48. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  49. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  50. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  51. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  52. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  53. data/lib/jade/frontend/forward_declaration.rb +2 -0
  54. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  55. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  56. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  57. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  58. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  59. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  60. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  61. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  62. data/lib/jade/frontend/semantic_analysis/body.rb +29 -1
  63. data/lib/jade/frontend/semantic_analysis/error/duplicate_implementation.rb +39 -0
  64. data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
  65. data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
  66. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
  67. data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
  68. data/lib/jade/frontend/semantic_analysis/error.rb +4 -0
  69. data/lib/jade/frontend/semantic_analysis/helper.rb +41 -0
  70. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -10
  71. data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
  72. data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
  73. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
  74. data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
  75. data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
  76. data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
  77. data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
  78. data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
  79. data/lib/jade/frontend/semantic_analysis.rb +5 -0
  80. data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
  81. data/lib/jade/frontend/type_checking/cascade.rb +90 -0
  82. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
  83. data/lib/jade/frontend/type_checking/constraints/deriving.rb +5 -4
  84. data/lib/jade/frontend/type_checking/constraints.rb +18 -3
  85. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  86. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  87. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  88. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  89. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  90. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  91. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  92. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  93. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  94. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  95. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  96. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  97. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  98. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  99. data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
  100. data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
  101. data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
  102. data/lib/jade/frontend/type_checking/error.rb +6 -0
  103. data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
  104. data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
  105. data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
  106. data/lib/jade/frontend/type_checking/inference/function_call.rb +47 -2
  107. data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
  108. data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
  109. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  110. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  111. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  112. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  113. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  114. data/lib/jade/frontend/type_checking.rb +3 -1
  115. data/lib/jade/frontend/usage_analysis.rb +5 -1
  116. data/lib/jade/interop/boundary.rb +64 -24
  117. data/lib/jade/interop/error.rb +35 -7
  118. data/lib/jade/interop/lowering.rb +8 -2
  119. data/lib/jade/lsp/converters.rb +83 -2
  120. data/lib/jade/lsp/handlers.rb +122 -25
  121. data/lib/jade/lsp/snippets.rb +12 -0
  122. data/lib/jade/lsp/state.rb +11 -4
  123. data/lib/jade/module_loader/build.rb +36 -0
  124. data/lib/jade/module_loader.rb +1 -0
  125. data/lib/jade/parsing/combinators.rb +78 -2
  126. data/lib/jade/parsing/error.rb +43 -6
  127. data/lib/jade/parsing/type.rb +5 -3
  128. data/lib/jade/parsing.rb +140 -26
  129. data/lib/jade/runtime.rb +32 -13
  130. data/lib/jade/source.rb +6 -2
  131. data/lib/jade/stdlib/basics.rb +22 -3
  132. data/lib/jade/stdlib/char.rb +2 -0
  133. data/lib/jade/stdlib/clock.rb +6 -2
  134. data/lib/jade/stdlib/decimal.rb +51 -14
  135. data/lib/jade/stdlib/intrinsics.rb +59 -6
  136. data/lib/jade/stdlib/list.rb +37 -1
  137. data/lib/jade/stdlib/number.rb +47 -0
  138. data/lib/jade/stdlib/range.rb +211 -0
  139. data/lib/jade/stdlib.rb +11 -4
  140. data/lib/jade/symbol/alias.rb +11 -0
  141. data/lib/jade/symbol/parser.rb +1 -1
  142. data/lib/jade/symbol.rb +13 -1
  143. data/lib/jade/task.rb +74 -56
  144. data/lib/jade/type/anonymous_record.rb +11 -2
  145. data/lib/jade/type/application.rb +11 -2
  146. data/lib/jade/type/base.rb +26 -0
  147. data/lib/jade/type/function.rb +11 -2
  148. data/lib/jade/type.rb +36 -10
  149. data/lib/jade/version.rb +1 -1
  150. data/lib/jade.rb +1 -0
  151. metadata +35 -3
  152. data/lib/jade/frontend/type_checking/constraints/deriving/assignable.rb +0 -170
@@ -33,6 +33,7 @@ module Jade
33
33
  enum_member: 22,
34
34
  struct: 23,
35
35
  interface: 11,
36
+ type_parameter: 26,
36
37
  }.freeze
37
38
 
38
39
  HOVERABLE_SYMBOLS = [
@@ -44,6 +45,7 @@ module Jade
44
45
  Jade::Symbol::Variant,
45
46
  Jade::Symbol::Union,
46
47
  Jade::Symbol::Struct,
48
+ Jade::Symbol::Alias,
47
49
  ].freeze
48
50
 
49
51
  # LSP CompletionItemKind values we use.
@@ -66,8 +68,8 @@ module Jade
66
68
  nil
67
69
  end
68
70
 
69
- def completion_items
70
- Snippets::ALL.map do |snippet|
71
+ def completion_items(module_name = nil)
72
+ Snippets.for_module(module_name).map do |snippet|
71
73
  {
72
74
  label: snippet.label,
73
75
  kind: COMPLETION_KIND_SNIPPET,
@@ -104,6 +106,21 @@ module Jade
104
106
  end
105
107
  end
106
108
 
109
+ # Enter and `end` are the two moments an editor can indent from what
110
+ # is already written, without the parse it will not get mid-typing.
111
+ def on_type_edits(text, line, character, typed)
112
+ text
113
+ .lines
114
+ .map(&:chomp)
115
+ .then do |lines|
116
+ case typed
117
+ in "\n" then indent_edit(lines, line, indent_after(lines, line))
118
+ in "d" then closing_edit(lines, line, character)
119
+ else []
120
+ end
121
+ end
122
+ end
123
+
107
124
  def whole_document_edit(source, new_text)
108
125
  last_line = source.line_starts.size - 1
109
126
  {
@@ -162,6 +179,9 @@ module Jade
162
179
  in AST::StructDeclaration(name:, range:)
163
180
  document_symbol(name, :struct, source, range)
164
181
 
182
+ in AST::TypeAliasDeclaration(name:, range:)
183
+ document_symbol(name, :type_parameter, source, range)
184
+
165
185
  in AST::InterfaceDeclaration(name:, range:, functions:)
166
186
  document_symbol(
167
187
  name, :interface, source, range,
@@ -374,8 +394,12 @@ module Jade
374
394
 
375
395
  def hover_body(symbol, registry)
376
396
  case symbol
397
+ in Jade::Symbol::Alias
398
+ render_alias(symbol, registry)
399
+
377
400
  in Jade::Symbol::Union | Jade::Symbol::Struct
378
401
  render_type(symbol, registry)
402
+
379
403
  else
380
404
  render_value(symbol, registry)
381
405
  end
@@ -404,6 +428,21 @@ module Jade
404
428
  )
405
429
  end
406
430
 
431
+ # Expansion erases the name everywhere else, so this is the only place
432
+ # left that says what an alias stands for.
433
+ def render_alias(symbol, registry)
434
+ Jade::Type
435
+ .from_symbol(symbol.body, registry, Frontend::TypeChecking::VarGen.new)
436
+ .first
437
+ .then { "type alias #{symbol.name}#{alias_params(symbol)} = #{it}" }
438
+ end
439
+
440
+ def alias_params(symbol)
441
+ symbol.type_params.empty? ?
442
+ '' :
443
+ "(#{symbol.type_params.map(&:name).join(', ')})"
444
+ end
445
+
407
446
  def render_type(symbol, registry)
408
447
  impls_for(symbol, registry)
409
448
  .then { it.empty? ? '' : "\n\nimplements #{it.join(', ')}" }
@@ -471,6 +510,7 @@ module Jade
471
510
  in AST::VariableReference | AST::ConstructorReference |
472
511
  AST::QualifiedAccess | AST::FunctionDeclaration |
473
512
  AST::TypeDeclaration | AST::StructDeclaration |
513
+ AST::TypeAliasDeclaration |
474
514
  AST::InterfaceDeclaration | AST::VariantDeclaration
475
515
  node.symbol.then { resolve_ref(it, registry) }
476
516
 
@@ -518,6 +558,47 @@ module Jade
518
558
  document_symbol(fn.name, :function, source, fn.range)
519
559
  end
520
560
 
561
+ INDENT = 2
562
+
563
+ # A block opens on a header, on a keyword that ends one, or on an
564
+ # unclosed bracket. `in` is a branch head: it sits with its `case`.
565
+ OPENS = /(\A(def|case|implements|interface|uses|if)\b)|((\bthen|\bwith|\belse|[({\[])\z)/
566
+
567
+ def indent_after(lines, line)
568
+ previous(lines, line)
569
+ .then { it ? indent_of(it) + (OPENS.match?(it.strip) ? INDENT : 0) : 0 }
570
+ end
571
+
572
+ # `end` closes the block its opener began, so it lands where the
573
+ # opener sits: one level out from the body above it.
574
+ def closing_edit(lines, line, character)
575
+ return [] unless lines[line].to_s[0...character].strip == 'end'
576
+
577
+ previous(lines, line)
578
+ .then { it ? [indent_of(it) - INDENT, 0].max : 0 }
579
+ .then { indent_edit(lines, line, it) }
580
+ end
581
+
582
+ def previous(lines, line)
583
+ lines[0...line].reverse.find { !it.strip.empty? }
584
+ end
585
+
586
+ def indent_edit(lines, line, width)
587
+ return [] if indent_of(lines[line].to_s) == width
588
+
589
+ [{
590
+ range: {
591
+ start: { line:, character: 0 },
592
+ end: { line:, character: indent_of(lines[line].to_s) },
593
+ },
594
+ newText: ' ' * width,
595
+ }]
596
+ end
597
+
598
+ def indent_of(line)
599
+ line[/\A */].length
600
+ end
601
+
521
602
  def diagnostic_message(diagnostic)
522
603
  [
523
604
  diagnostic.message,
@@ -24,6 +24,7 @@ module Jade
24
24
  when 'textDocument/rename' then on_rename(state, message)
25
25
  when 'textDocument/inlayHint' then on_inlay_hint(state, message)
26
26
  when 'textDocument/formatting' then on_formatting(state, message)
27
+ when 'textDocument/onTypeFormatting' then on_type_formatting(state, message)
27
28
  else on_unknown(state, message)
28
29
  end
29
30
  end
@@ -39,9 +40,13 @@ module Jade
39
40
 
40
41
  def on_initialize(state, message)
41
42
  params = (message['params'] || {})
42
- root = params
43
+ # The editor's root is the project root, not the source root the
44
+ # compiler names modules from. Reading the manifest also loads the
45
+ # extension gems a project imports.
46
+ project = params
43
47
  .then { it['rootUri'] || it.dig('workspaceFolders', 0, 'uri') }
44
48
  .then { it ? it.sub(%r{\Afile://}, '') : Dir.pwd }
49
+ .then { [Project.find(it), it] }
45
50
 
46
51
  {
47
52
  capabilities: {
@@ -55,10 +60,14 @@ module Jade
55
60
  renameProvider: { prepareProvider: true },
56
61
  inlayHintProvider: true,
57
62
  documentFormattingProvider: true,
63
+ documentOnTypeFormattingProvider: {
64
+ firstTriggerCharacter: "\n",
65
+ moreTriggerCharacter: ['d'],
66
+ },
58
67
  },
59
68
  serverInfo: { name: 'jade-lsp', version: '0.1.0' },
60
69
  }
61
- .then { [state.with_root(root), [respond(message['id'], it)]] }
70
+ .then { [state.with_project(*project), [respond(message['id'], it)]] }
62
71
  end
63
72
 
64
73
  def negotiate_encoding(params)
@@ -70,7 +79,7 @@ module Jade
70
79
  def on_did_open(state, params)
71
80
  params['textDocument']
72
81
  .then { state.put_buffer(it['uri'], it['text']) }
73
- .then { recompile_and_publish(it) }
82
+ .then { recompile_and_publish(it, scope: :project) }
74
83
  end
75
84
 
76
85
  def on_did_change(state, params)
@@ -83,7 +92,7 @@ module Jade
83
92
 
84
93
  def on_did_close(state, params)
85
94
  params['textDocument']['uri']
86
- .then { recompile_and_publish(state.close(it), extra_uris: [it]) }
95
+ .then { recompile_and_publish(state.close(it), extra_uris: [it], scope: :project) }
87
96
  end
88
97
 
89
98
  def on_document_symbol(state, message)
@@ -116,7 +125,18 @@ module Jade
116
125
  end
117
126
 
118
127
  def on_completion(state, message)
119
- [state, [respond(message['id'], Converters.completion_items)]]
128
+ message
129
+ .dig('params', 'textDocument', 'uri')
130
+ .then { module_name_for(it, state.source_root) }
131
+ .then { [state, [respond(message['id'], Converters.completion_items(it))]] }
132
+ end
133
+
134
+ def module_name_for(uri, source_root)
135
+ return nil unless uri && source_root
136
+
137
+ Converters
138
+ .relative_path(uri, source_root)
139
+ .then { Source.module_name_for(it) }
120
140
  end
121
141
 
122
142
  def on_prepare_rename(state, message)
@@ -148,6 +168,23 @@ module Jade
148
168
  [state, [respond(message['id'], edits)]]
149
169
  end
150
170
 
171
+ def on_type_formatting(state, message)
172
+ params = message['params']
173
+ state
174
+ .buffers[params.dig('textDocument', 'uri')]
175
+ .then { it ? on_type_edits(it, params) : [] }
176
+ .then { [state, [respond(message['id'], it)]] }
177
+ end
178
+
179
+ def on_type_edits(text, params)
180
+ Converters.on_type_edits(
181
+ text,
182
+ params.dig('position', 'line'),
183
+ params.dig('position', 'character'),
184
+ params['ch'],
185
+ )
186
+ end
187
+
151
188
  def formatting_edits_for(state, uri)
152
189
  text = state.buffers[uri]
153
190
  return nil unless text
@@ -261,43 +298,103 @@ module Jade
261
298
  # extra_uris always receive a publishDiagnostics, but compile output
262
299
  # wins: if a real diagnostic came back for the URI, we send that, not
263
300
  # an empty clear.
264
- def recompile_and_publish(state, extra_uris: [])
265
- if state.buffers.empty? || state.source_root.nil?
266
- return [state, extra_uris.map { publish_for(it, []) }]
267
- end
301
+ def recompile_and_publish(state, extra_uris: [], scope: :buffers)
302
+ return [state, extra_uris.map { publish_for(it, []) }] if state.source_root.nil?
268
303
 
269
304
  overlays = state.buffers
270
305
  .to_h { |uri, text| [Converters.relative_path(uri, state.source_root), text] }
271
306
 
272
- diagnostics_by_uri, registry = compile_each(state.source_root, overlays)
307
+ diagnostics_by_uri, registry =
308
+ compile_each(state, overlays, entries(state, overlays, scope))
273
309
 
274
- uris = (diagnostics_by_uri.keys + state.buffers.keys + extra_uris).uniq
310
+ uris = (diagnostics_by_uri.keys + state.buffers.keys + state.published + extra_uris).uniq
275
311
  messages = uris.map { publish_for(it, diagnostics_by_uri[it] || []) }
276
312
 
277
- next_state = registry ? state.set_registry(registry) : state
313
+ next_state = (registry ? state.set_registry(registry) : state)
314
+ .with_published(diagnostics_by_uri.keys)
278
315
  [next_state, messages]
279
316
  end
280
317
 
281
- # Compile from each open buffer so unrelated modules still produce
282
- # diagnostics. The same module reached twice yields identical
283
- # diagnostics (deterministic over overlays), so Hash#merge is safe.
284
- def compile_each(source_root, overlays)
285
- overlays.keys.reduce([{}, nil]) do |(diag_acc, reg_acc), entry|
286
- registry, diagnostics = compile(source_root, entry, overlays)
287
- [diag_acc.merge(diagnostics), registry || reg_acc]
288
- end
318
+ # Typing recompiles what is open; opening and closing sweep.
319
+ def entries(state, overlays, scope)
320
+ return overlays.keys if scope == :buffers
321
+
322
+ Dir
323
+ .glob('**/*.jd', base: state.source_root)
324
+ .sort
325
+ .then { imported_last(it, state.registry) }
289
326
  end
290
327
 
291
- def compile(source_root, entry_path, overlays)
328
+ # Compiling a module compiles what it imports, so the modules nothing
329
+ # imports cover the project in one pass. A file the last compile never
330
+ # saw goes first, since it may be the new root.
331
+ def imported_last(paths, registry)
332
+ return paths unless registry
333
+
334
+ [imported(registry), module_names(registry)]
335
+ .then { |names, by_path| paths.partition { !names.include?(by_path[it]) } }
336
+ .flatten
337
+ end
338
+
339
+ def imported(registry)
340
+ registry.dependency_graph.nodes.values.flatten.to_set
341
+ end
342
+
343
+ def module_names(registry)
344
+ registry
345
+ .modules
346
+ .each_value
347
+ .filter_map { [it.source.uri, it.name] if it.source }
348
+ .to_h
349
+ end
350
+
351
+ def compile_each(state, overlays, entries)
352
+ entries
353
+ .reduce([{}, nil, []]) { |acc, entry| compile_unless_seen(acc, state, overlays, entry) }
354
+ .first(2)
355
+ end
356
+
357
+ def compile_unless_seen((diagnostics, registry, seen), state, overlays, entry)
358
+ return [diagnostics, registry, seen] if seen.include?(entry)
359
+
360
+ compile(state, entry, overlays)
361
+ .then { |(compiled, found)| [diagnostics.merge(found), compiled || registry, seen + compiled_paths(compiled)] }
362
+ end
363
+
364
+ def compiled_paths(registry)
365
+ return [] unless registry
366
+
367
+ registry
368
+ .modules
369
+ .each_value
370
+ .reject { Stdlib.is_stdlib?(it) }
371
+ .filter_map { it.source&.uri }
372
+ end
373
+
374
+ def compile(state, entry_path, overlays)
292
375
  ModuleLoader
293
- .load(source_root, entry_path, tolerant: true, overlays:)
294
- .then { [it, collect_diagnostics(it, source_root)] }
376
+ .load(state.source_root, entry_path, cache_dir: state.cache_dir, tolerant: true, overlays:)
377
+ .then { [it, collect_diagnostics(it, state.source_root)] }
295
378
  rescue Jade::CompilationError => e
296
- [nil, diagnostics_by_uri(e.diagnostics, source_root)]
379
+ [nil, diagnostics_by_uri(e.diagnostics, state.source_root)]
297
380
  rescue StandardError => e
298
381
  $stderr.puts "[jade-lsp] compile crash: #{e.class}: #{e.message}"
299
382
  $stderr.puts e.backtrace.first(20).join("\n")
300
- [nil, {}]
383
+ [nil, crash_diagnostic(state.source_root, entry_path, overlays, e)]
384
+ end
385
+
386
+ # Publishing nothing renders a crashed compile as a clean file.
387
+ def crash_diagnostic(source_root, entry_path, overlays, error)
388
+ Jade::Source
389
+ .new(uri: entry_path, text: overlays[entry_path] || '')
390
+ .then { Jade::Diagnostics::Label[it, 0...0, nil] }
391
+ .then do
392
+ Jade::Diagnostics::Diagnostic.error(
393
+ "jade-lsp crashed compiling this file: #{error.class}: #{error.message}",
394
+ primary: it,
395
+ )
396
+ end
397
+ .then { { Converters.lsp_uri(entry_path, source_root) => [it] } }
301
398
  end
302
399
 
303
400
  def collect_diagnostics(registry, source_root)
@@ -105,6 +105,18 @@ module Jade
105
105
 
106
106
  module_function
107
107
 
108
+ # The compiler requires the name to match the path, so a placeholder
109
+ # here offers a decision with one right answer.
110
+ def for_module(module_name)
111
+ return ALL unless module_name
112
+
113
+ ALL.map do
114
+ next it unless it.label == 'module'
115
+
116
+ it.with(body: "module #{module_name} exposing (${0})")
117
+ end
118
+ end
119
+
108
120
  # Every form, as plain Jade — what `jade q syntax` serves.
109
121
  def catalog
110
122
  ALL.map { { form: it.label, detail: it.detail, source: it.source } }
@@ -1,12 +1,14 @@
1
1
  module Jade
2
2
  module LSP
3
- State = Data.define(:source_root, :buffers, :registry) do
3
+ State = Data.define(:source_root, :cache_dir, :buffers, :registry, :published) do
4
4
  def self.empty
5
- new(source_root: nil, buffers: {}, registry: nil)
5
+ new(source_root: nil, cache_dir: nil, buffers: {}, registry: nil, published: [])
6
6
  end
7
7
 
8
- def with_root(root)
9
- with(source_root: root)
8
+ # No manifest means no cache directory to share with the CLI, and
9
+ # every compile starts from nothing.
10
+ def with_project(project, root)
11
+ with(source_root: project&.source_root || root, cache_dir: project&.cache_path)
10
12
  end
11
13
 
12
14
  def put_buffer(uri, text)
@@ -20,6 +22,11 @@ module Jade
20
22
  def set_registry(reg)
21
23
  with(registry: reg)
22
24
  end
25
+
26
+ # Cleared next round, whether or not the file is still open.
27
+ def with_published(uris)
28
+ with(published: uris)
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -0,0 +1,36 @@
1
+ require 'fileutils'
2
+
3
+ require 'jade/module_loader/cache'
4
+
5
+ module Jade
6
+ module ModuleLoader
7
+ # The emitted Ruby, as opposed to `Cache`, which holds compiled
8
+ # modules. Generated code calls the runtime by name, so a build left
9
+ # by another version of the compiler can call a helper this one no
10
+ # longer has. Sources are untouched in that case, so mtimes say
11
+ # nothing: keep the fingerprint next to the build and drop the build
12
+ # when it does not match.
13
+ module Build
14
+ extend self
15
+
16
+ FINGERPRINT = '.fingerprint'
17
+
18
+ def discard_foreign(root)
19
+ return unless File.exist?(root)
20
+ return if fingerprint(root) == Cache.compiler_fingerprint
21
+
22
+ FileUtils.rm_rf(root)
23
+ end
24
+
25
+ def stamp(root)
26
+ File.write(File.join(root, FINGERPRINT), Cache.compiler_fingerprint)
27
+ end
28
+
29
+ private
30
+
31
+ def fingerprint(root)
32
+ File.join(root, FINGERPRINT).then { File.read(it) if File.exist?(it) }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -6,6 +6,7 @@ require 'jade/ast'
6
6
  require 'jade/registry'
7
7
  require 'jade/frontend'
8
8
  require 'jade/codegen'
9
+ require 'jade/module_loader/build'
9
10
  require 'jade/module_loader/cache'
10
11
  require 'jade/module_loader/dependency_resolver'
11
12
  require 'jade/module_loader/dependency_graph'
@@ -166,6 +166,26 @@ module Jade
166
166
  end
167
167
  end
168
168
 
169
+ # A word that only reads as a keyword in one position, so it stays
170
+ # usable as an ordinary name everywhere else.
171
+ def contextual(word)
172
+ (@contextuals ||= {})[word] ||= P.new do |state|
173
+ type(:identifier).call(state).and_then do |(token, next_state)|
174
+ next Ok[[token, next_state]] if token.value == word
175
+
176
+ Err[[
177
+ Parsing::UnexpectedTokenError.new(
178
+ entry: state.entry,
179
+ span: token.range,
180
+ actual: token,
181
+ expected: word,
182
+ ),
183
+ state,
184
+ ]]
185
+ end
186
+ end
187
+ end
188
+
169
189
  def lazy(&block)
170
190
  P.new do |input|
171
191
  block.call.call(input)
@@ -216,6 +236,13 @@ module Jade
216
236
  with(diagnostics: diagnostics.add(diagnostic))
217
237
  end
218
238
 
239
+ # Is the token here on the same line as the one before it?
240
+ def continues_line?
241
+ return true if eof? || position.zero?
242
+
243
+ same_line?(tokens[position - 1].range.begin, current.range.begin)
244
+ end
245
+
219
246
  def skip_until(sync_types)
220
247
  (position...tokens.length)
221
248
  .find { sync_types.include?(tokens[it].type) }
@@ -228,6 +255,12 @@ module Jade
228
255
  @fn = block
229
256
  end
230
257
 
258
+ def stalled?(error, state)
259
+ return state.eof? if error.span.nil?
260
+
261
+ error.span.begin == state.current&.range&.begin
262
+ end
263
+
231
264
  def call(tokens)
232
265
  @fn.call(tokens)
233
266
  end
@@ -282,8 +315,51 @@ module Jade
282
315
  map_error(&:commit)
283
316
  end
284
317
 
285
- def context(name)
286
- map_error { |err| err.with_context(name) }
318
+ # A stalled alternation reports whichever branch it tried last, so
319
+ # say what the position wanted instead. One that got somewhere is
320
+ # already reporting the useful error.
321
+ def expecting(what)
322
+ P.new do |state|
323
+ call(state).map_error do |(err, err_state)|
324
+ [!err.committed? && stalled?(err, state) ? err.copy(expected: what) : err, err_state]
325
+ end
326
+ end
327
+ end
328
+
329
+ # A `(` opening a new line starts a statement, it does not continue
330
+ # the expression above it: `(x, y) = p` is a binding, not a call on
331
+ # whatever the line before ended with.
332
+ def on_same_line
333
+ P.new do |state|
334
+ next Err[[
335
+ Parsing::UnexpectedTokenError.new(
336
+ entry: state.entry,
337
+ span: state.current.range,
338
+ actual: state.current,
339
+ expected: 'the same line',
340
+ ),
341
+ state,
342
+ ]] unless state.continues_line?
343
+
344
+ call(state)
345
+ end
346
+ end
347
+
348
+ def parsing(name)
349
+ map_error { |err| err.copy(context: [name, *err.context]) }
350
+ end
351
+
352
+ # Opens an empty body's span out to the construct it belongs to.
353
+ # The innermost one gets there first; past that the span is wider
354
+ # than the marker `body` left, so an enclosing construct skips it.
355
+ def spanning_empty_body
356
+ P.new do |state|
357
+ call(state).map_error do |(err, err_state)|
358
+ next [err, err_state] unless err.is_a?(MissingBodyError) && err.span.size == 1
359
+
360
+ [err.copy(span: state.current.range.begin...err.actual.range.end), err_state]
361
+ end
362
+ end
287
363
  end
288
364
  end
289
365
  end
@@ -21,14 +21,15 @@ module Jade
21
21
  self
22
22
  end
23
23
 
24
- def with_context(name)
24
+ def copy(**changes)
25
25
  self.class.new(
26
- entry: @entry,
27
- span: @span,
28
- actual: @actual,
29
- expected: @expected,
26
+ entry: @entry,
27
+ span: @span,
28
+ actual: @actual,
29
+ expected: @expected,
30
30
  committed: @committed,
31
- context: [name, *@context],
31
+ context: @context,
32
+ **changes,
32
33
  )
33
34
  end
34
35
 
@@ -152,6 +153,42 @@ module Jade
152
153
  end
153
154
  end
154
155
 
156
+ # `in 0..limit`. Rejecting it is right — coverage is decided before
157
+ # `limit` exists — but the reader needs to be told that, not sent to
158
+ # look at their `then`.
159
+ class NonLiteralRangeBoundError < Error
160
+ def message
161
+ "#{context_prefix}A range pattern's bounds have to be literals, and " \
162
+ "#{@actual.value.inspect} is a value. Which integers a branch " \
163
+ "covers is settled when the code is compiled, so a bound the " \
164
+ "compiler cannot see cannot be checked."
165
+ end
166
+
167
+ def label
168
+ 'range bound is not a literal'
169
+ end
170
+
171
+ def notes
172
+ [
173
+ Jade::Diagnostics::Annotation[
174
+ :help,
175
+ 'match `_` and compare in the body, or use `if` — either way the ' \
176
+ 'branch is no longer something exhaustiveness can account for',
177
+ ],
178
+ ]
179
+ end
180
+ end
181
+
182
+ class MissingBodyError < UnexpectedTokenError
183
+ def hint
184
+ '(a body has to produce a value)'
185
+ end
186
+
187
+ def label
188
+ 'missing body'
189
+ end
190
+ end
191
+
155
192
  class InvalidOperatorError < Error
156
193
  HINTS = {
157
194
  '/=' => 'Use `!=` for inequality.',
@@ -5,7 +5,9 @@ module Jade
5
5
  module Type
6
6
  extend Combinators::Dsl
7
7
 
8
- parser(:type_expression) { type_function | type_atom | type_record }
8
+ parser(:type_expression) {
9
+ (type_function | type_atom | type_record).expecting('a type')
10
+ }
9
11
 
10
12
  parser(:type_atom) {
11
13
  type_application | type_var | type_tuple | grouped(lazy { type_function })
@@ -65,8 +67,8 @@ module Jade
65
67
  no_args = [Combinators::CommaList.empty, nil]
66
68
 
67
69
  (
68
- (type_name >> optional(type_application_args, default: no_args)) |
69
- (type_var >> type_application_args)
70
+ (type_name >> optional(type_application_args.on_same_line, default: no_args)) |
71
+ (type_var >> type_application_args.on_same_line)
70
72
  ).map(&AST.type_application)
71
73
  }
72
74