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
@@ -83,7 +83,7 @@ module Jade
83
83
  impl_sym
84
84
  .functions[fn_name]
85
85
  .then { it.is_a?(Symbol::ValueRef) ? it : nil }
86
- &.then { "::#{to_qualified(it.module_name)}::Internal.#{it.name}" }
86
+ &.then { "#{internal(it.module_name)}.#{it.name}" }
87
87
  &.then { Pretty.block("def #{ruby_method}(other)", "#{it}(self, other)") }
88
88
  end
89
89
  end
@@ -143,7 +143,7 @@ module Jade
143
143
  fn_map = symbol.functions.filter_map { |fn_name, ref|
144
144
  next unless ref.is_a?(Symbol::ValueRef)
145
145
 
146
- [fn_name, "->(*args) { ::#{to_qualified(ref.module_name)}::Internal.#{ref.name}(*args) }"]
146
+ [fn_name, "->(*args) { #{internal(ref.module_name)}.#{ref.name}(*args) }"]
147
147
  }.to_h
148
148
 
149
149
  return "" if fn_map.empty?
@@ -80,7 +80,7 @@ module Jade
80
80
 
81
81
  def lambda_param_name(pattern)
82
82
  case pattern
83
- in AST::Pattern::Binding(name:) then name
83
+ in AST::Pattern::Binding(name:) then Names.local(name)
84
84
  in AST::Pattern::Wildcard then '_'
85
85
  end
86
86
  end
@@ -5,7 +5,9 @@ module Jade
5
5
 
6
6
  # Ruby's native ordering on these types matches LT/EQ/GT semantics, so
7
7
  # the derived comparison ops can collapse to plain operators.
8
- RUBY_NATIVE_COMPARES = %w[Basics.int_compare Basics.float_compare String.str_compare].to_set.freeze
8
+ RUBY_NATIVE_COMPARES = %w[
9
+ Basics.int_compare Basics.float_compare String.str_compare Char.char_compare
10
+ ].to_set.freeze
9
11
  RUBY_NATIVE_EQS = %w[Basics.int_eq Basics.float_eq Basics.bool_eq String.str_eq].to_set.freeze
10
12
 
11
13
  DERIVED_COMPARISONS = {
@@ -32,6 +34,7 @@ module Jade
32
34
  'Basics.int_mul' => ->(a, b) { "(#{a} * #{b})" },
33
35
  'Basics.int_div' => ->(a, b) { "(#{a} / #{b})" },
34
36
  'Basics.mod' => ->(a, b) { "(#{a} % #{b})" },
37
+ 'Number.unwrap' => ->(n) { n },
35
38
  'Basics.float_add' => ->(a, b) { "(#{a} + #{b})" },
36
39
  'Basics.float_sub' => ->(a, b) { "(#{a} - #{b})" },
37
40
  'Basics.float_mul' => ->(a, b) { "(#{a} * #{b})" },
@@ -113,6 +116,7 @@ module Jade
113
116
  'Char.upper?' => ->(c) { "#{c}.match?(/[A-Z]/)" },
114
117
  'Char.lower?' => ->(c) { "#{c}.match?(/[a-z]/)" },
115
118
  'Char.char_eq' => ->(a, b) { "(#{a} == #{b})" },
119
+ 'Char.char_compare' => ->(a, b) { "#{a}.compare(#{b})" },
116
120
 
117
121
  'Tuple.pair' => ->(a, b) { "Jade::Tuple::Tuple2[#{a}, #{b}]" },
118
122
  'Tuple.first' => ->(t) { "#{t}._1" },
@@ -173,6 +177,7 @@ module Jade
173
177
 
174
178
  # Bodies that don't fit a single Ruby expression.
175
179
  NO_INLINE = %w[
180
+ Number.checked
176
181
  List.sort_with
177
182
  List.sort_by_with
178
183
  List.filter_map
@@ -0,0 +1,31 @@
1
+ module Jade
2
+ module Codegen
3
+ # Ruby rejects its keywords as local variables and parameters, but accepts
4
+ # them as method names and Data members — so only bindings are rewritten
5
+ # here, and nothing a Ruby caller sees changes.
6
+ module Names
7
+ extend self
8
+
9
+ RESERVED = ::Set[
10
+ 'BEGIN', 'END', '__ENCODING__', '__FILE__', '__LINE__',
11
+ 'alias', 'and', 'begin', 'break', 'case', 'class', 'def',
12
+ 'defined?', 'do', 'else', 'elsif', 'end', 'ensure', 'false',
13
+ 'for', 'if', 'in', 'module', 'next', 'nil', 'not', 'or', 'redo',
14
+ 'rescue', 'retry', 'return', 'self', 'super', 'then', 'true',
15
+ 'undef', 'unless', 'until', 'when', 'while', 'yield',
16
+ ].freeze
17
+
18
+ # Injective: a name that is a reserved word under trailing underscores
19
+ # gains one more, so `begin` and `begin_` stay distinct.
20
+ def local(name)
21
+ reserved_stem?(name) ? "#{name}_" : name
22
+ end
23
+
24
+ private
25
+
26
+ def reserved_stem?(name)
27
+ RESERVED.include?(name.sub(/_+\z/, ''))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -8,7 +8,7 @@ module Jade
8
8
  def generate(node, registry)
9
9
  node => AST::Pattern::Constructor(symbol:, patterns:)
10
10
  constructor = registry.lookup(symbol)
11
- qualified = to_qualified(constructor.qualified_name)
11
+ qualified = rooted(constructor.qualified_name)
12
12
 
13
13
  if keyed_variant?(constructor)
14
14
  generate_keyed(qualified, patterns.first, registry)
@@ -39,7 +39,7 @@ module Jade
39
39
  def generate_keyed(qualified, pattern, registry)
40
40
  case pattern
41
41
  in AST::Pattern::Binding(name:)
42
- "#{qualified} => #{name}"
42
+ "#{qualified} => #{Names.local(name)}"
43
43
 
44
44
  in AST::Pattern::Wildcard
45
45
  qualified
@@ -69,7 +69,7 @@ module Jade
69
69
  .then { generate_node(it, registry) }
70
70
 
71
71
  "#{list_arg}.reverse.reduce(#{base_code}) " \
72
- "{ |#{ACC_NAME}, #{head_name}| #{combine_code} }"
72
+ "{ |#{ACC_NAME}, #{Names.local(head_name)}| #{combine_code} }"
73
73
  end
74
74
 
75
75
  private
data/lib/jade/codegen.rb CHANGED
@@ -3,6 +3,7 @@ require 'jade/codegen/context'
3
3
  require 'jade/codegen/helpers'
4
4
  require 'jade/codegen/pretty'
5
5
  require 'jade/codegen/method_names'
6
+ require 'jade/codegen/names'
6
7
  require 'jade/codegen/inlines'
7
8
  require 'jade/codegen/inline'
8
9
  require 'jade/codegen/boundary'
@@ -83,10 +84,13 @@ module Jade
83
84
  def generate(node, registry, depth: 0)
84
85
  case node
85
86
  in AST::Module(name:, body:)
87
+ reopened = reopened_type(name, registry)
88
+
86
89
  preamble = [
87
90
  "require 'jade/runtime'",
88
91
  *Stdlib.requires(name),
89
92
  *namespace_setup_lines(name),
93
+ *reopening_require(reopened, name),
90
94
  ].reject(&:empty?).join(Pretty.newline)
91
95
 
92
96
  shape_consts = collect_record_shapes(body)
@@ -100,11 +104,15 @@ module Jade
100
104
  boundary_helpers = Boundary::Specialized.collect_helpers(body, registry)
101
105
  .then { Boundary::Specialized.emit_helpers(it, registry) }
102
106
 
107
+ dict_consts = {}
108
+
103
109
  outer, inner, wrappers =
104
- with_boundary_cache(boundary_cache) do
105
- with_dispatched_methods(collect_dispatched_methods(body, registry)) do
106
- with_hoisted_records do
107
- partition_module_body(body.expressions, registry, name.count('.'))
110
+ with_dict_consts(dict_consts) do
111
+ with_boundary_cache(boundary_cache) do
112
+ with_dispatched_methods(collect_dispatched_methods(body, registry)) do
113
+ with_hoisted_records do
114
+ partition_module_body(body.expressions, registry, name.count('.'))
115
+ end
108
116
  end
109
117
  end
110
118
  end
@@ -114,17 +122,18 @@ module Jade
114
122
  .then { Pretty.block("module Internal", it) }
115
123
 
116
124
  [
117
- "extend self",
125
+ (reopened ? "" : "extend self"),
118
126
  *shape_consts,
119
127
  *outer,
120
128
  inner_module,
129
+ *referenced_dicts(dict_consts, [*outer, *inner, *wrappers]),
121
130
  *boundary_consts,
122
131
  *boundary_helpers,
123
132
  *wrappers,
124
133
  ]
125
134
  .reject(&:empty?)
126
135
  .join(Pretty.newline(2))
127
- .then { Pretty.block("module #{to_qualified(name)}", it) }
136
+ .then { Pretty.block("#{reopened ? 'class' : 'module'} #{to_qualified(name)}", it) }
128
137
  .then { [preamble, it].reject(&:empty?).join(Pretty.newline(2)) }
129
138
 
130
139
  in AST::ImportDeclaration(module_name:)
@@ -138,7 +147,11 @@ module Jade
138
147
  end
139
148
 
140
149
  in AST::InteropImportDeclaration(module: mod)
141
- Pretty.block("begin", "require '#{mod.name.gsub('::', '/').downcase}'\nrescue LoadError")
150
+ Pretty.block(
151
+ "begin",
152
+ "require '#{mod.name.gsub('::', '/').downcase}'",
153
+ "rescue LoadError\nend",
154
+ )
142
155
 
143
156
  in AST::Implementation
144
157
  Implementation.generate(node, registry)
@@ -166,17 +179,17 @@ module Jade
166
179
  codegen
167
180
 
168
181
  in Symbol::Function => fn
169
- "#{to_qualified(fn.module_name)}::Internal.method(:#{fn.name})"
182
+ "#{internal(fn.module_name)}.method(:#{fn.name})"
170
183
 
171
184
  else
172
- name
185
+ Names.local(name)
173
186
  end
174
187
  end
175
188
 
176
189
  in AST::Assign(pattern:, expression:)
177
190
  case pattern
178
191
  in AST::Pattern::Binding(name:)
179
- "#{name} = #{generate(expression, registry)}"
192
+ "#{Names.local(name)} = #{generate(expression, registry)}"
180
193
  in AST::Pattern::Wildcard
181
194
  generate(expression, registry)
182
195
  else
@@ -193,7 +206,7 @@ module Jade
193
206
  FunctionDeclaration.generate(node, registry)
194
207
 
195
208
  in AST::FunctionDeclarationParam(name:)
196
- name
209
+ Names.local(name)
197
210
 
198
211
  in AST::FunctionCall
199
212
  FunctionCall.generate(node, registry)
@@ -209,6 +222,9 @@ module Jade
209
222
  in AST::StructDeclaration(name:, record_type:, symbol:)
210
223
  data_define_with_methods(name, record_type.fields.keys, "::#{to_qualified(symbol.qualified_name)}")
211
224
 
225
+ in AST::TypeAliasDeclaration
226
+ ""
227
+
212
228
  in AST::InterfaceDeclaration
213
229
  ""
214
230
 
@@ -221,7 +237,7 @@ module Jade
221
237
  codegen
222
238
 
223
239
  in Symbol::Function => fn
224
- "#{to_qualified(fn.module_name)}::Internal.method(:#{fn.name})"
240
+ "#{internal(fn.module_name)}.method(:#{fn.name})"
225
241
 
226
242
  in Symbol::Constructor => sym
227
243
  ConstructorReference.from_symbol(sym)
@@ -254,11 +270,14 @@ module Jade
254
270
  in AST::Pattern::Literal(literal:)
255
271
  generate(literal, registry)
256
272
 
273
+ in AST::Pattern::Range(from:, to:)
274
+ "#{from&.value}..#{to&.value}"
275
+
257
276
  in AST::Pattern::Wildcard
258
277
  "_"
259
278
 
260
279
  in AST::Pattern::Binding(name:)
261
- name
280
+ Names.local(name)
262
281
 
263
282
  in AST::Pattern::Record(fields:)
264
283
  generate_many(fields, registry)
@@ -270,7 +289,7 @@ module Jade
270
289
  in AST::Pattern::List(patterns:, rest:)
271
290
  rest_part =
272
291
  case rest
273
- in AST::Pattern::Binding(name:) then ["*#{name}"]
292
+ in AST::Pattern::Binding(name:) then ["*#{Names.local(name)}"]
274
293
  in AST::Pattern::Wildcard then ["*"]
275
294
  in nil then []
276
295
  end
@@ -325,7 +344,7 @@ module Jade
325
344
 
326
345
  def param_name(pattern, index = 0)
327
346
  case pattern
328
- in AST::Pattern::Binding(name:) then name
347
+ in AST::Pattern::Binding(name:) then Names.local(name)
329
348
  in AST::Pattern::Wildcard then '_'
330
349
  else
331
350
  param_synthetic_name(index)
@@ -340,6 +359,48 @@ module Jade
340
359
  "#{'../' * current_depth}#{import_path}"
341
360
  end
342
361
 
362
+ # Emission generates some expressions only to discard them (a decoder
363
+ # weighed against its specialized form, say), and a discarded one can
364
+ # still have claimed a constant. Keep the ones that survived, and the
365
+ # ones those refer to.
366
+ def referenced_dicts(table, body)
367
+ reachable_dicts(table, body.join(Pretty.newline))
368
+ .map { |source, const| "#{const} = #{source}.freeze" }
369
+ end
370
+
371
+ # A kept dictionary can name another one, so widen the search text
372
+ # with what was kept until nothing new turns up.
373
+ def reachable_dicts(table, code, kept = {})
374
+ table
375
+ .select { |_, const| code.match?(/\b#{const}\b/) }
376
+ .then do |found|
377
+ next found if found.size == kept.size
378
+
379
+ reachable_dicts(table, [code, *found.keys].join(Pretty.newline), found)
380
+ end
381
+ end
382
+
383
+ # A module whose name is already a type in its parent shares one Ruby
384
+ # constant with it, so it reopens that class rather than defining a
385
+ # module beside it and clobbering whichever loads second.
386
+ def reopened_type(name, registry)
387
+ *parent, last = name.split('.')
388
+ return nil if parent.empty?
389
+
390
+ registry
391
+ .get(parent.join('.'))
392
+ &.then { it.defined_types.key?(last) ? it : nil }
393
+ end
394
+
395
+ # Before the class body, not inside it: an import writes its require
396
+ # within the body, by which point `class` has already made a plain
397
+ # class for the constant and the type will clobber it on load.
398
+ def reopening_require(entry, name)
399
+ return [] if entry.nil? || Stdlib.is_stdlib?(entry)
400
+
401
+ ["require_relative '#{relative_require(entry.path, name.count('.'))}'"]
402
+ end
403
+
343
404
  def namespace_setup_lines(name)
344
405
  parts = name.split('.')
345
406
  (0...parts.length - 1)
data/lib/jade/compiler.rb CHANGED
@@ -7,6 +7,7 @@ module Jade
7
7
  end
8
8
 
9
9
  def require(path)
10
+ ModuleLoader::Build.discard_foreign(build_root)
10
11
  target = File.expand_path("#{build_root}/#{path}.rb", config.project_root)
11
12
 
12
13
  if needs_rebuild?(target)
@@ -14,6 +15,7 @@ module Jade
14
15
  .load(config.source_root, path + '.jd', cache_dir: cache_root)
15
16
  .tap { render_diagnostics(it) }
16
17
  .then { ModuleLoader.emit(it, path: build_root) }
18
+ .then { ModuleLoader::Build.stamp(build_root) }
17
19
  end
18
20
 
19
21
  Kernel.require(File.realpath(target))
data/lib/jade/entry.rb CHANGED
@@ -89,7 +89,7 @@ module Jade
89
89
 
90
90
  def define(symbol)
91
91
  case symbol
92
- in Symbol::Union | Symbol::Struct | Symbol::Interface
92
+ in Symbol::Union | Symbol::Struct | Symbol::Alias | Symbol::Interface
93
93
  add_defined_type(symbol)
94
94
 
95
95
  in Symbol::Implementation
data/lib/jade/error.rb CHANGED
@@ -21,6 +21,10 @@ module Jade
21
21
  []
22
22
  end
23
23
 
24
+ def secondary
25
+ []
26
+ end
27
+
24
28
  def queried_name
25
29
  nil
26
30
  end
@@ -39,6 +43,7 @@ module Jade
39
43
  Jade::Diagnostics::Diagnostic.error(
40
44
  message,
41
45
  primary: Jade::Diagnostics::Label[source, span, label],
46
+ secondary: secondary.map { |sp, text| Jade::Diagnostics::Label[source, sp, text] },
42
47
  annotations: notes + did_you_mean_notes,
43
48
  )
44
49
  end
@@ -0,0 +1,70 @@
1
+ module Jade
2
+ # Where an extension gem hooks into compilation. Only gems named in ALLOWED
3
+ # may register, so jade knows who extends it and nothing about what they do.
4
+ module Extensions
5
+ extend self
6
+
7
+ ALLOWED = %w[jade-sql].freeze
8
+ PHASES = %i[call].freeze
9
+
10
+ class NotAllowed < StandardError
11
+ def initialize(gem_name)
12
+ super(
13
+ "#{gem_name} may not extend the compiler; " \
14
+ "allowed: #{ALLOWED.join(', ')}"
15
+ )
16
+ end
17
+ end
18
+
19
+ class UnknownPhase < StandardError
20
+ def initialize(phase)
21
+ super("no such check phase #{phase.inspect}; known: #{PHASES.join(', ')}")
22
+ end
23
+ end
24
+
25
+ def register_deriver(gem_name, deriver)
26
+ allow!(gem_name)
27
+ @derivers = derivers | [deriver]
28
+ end
29
+
30
+ # A `:call` check answers `watches -> [qualified name]` and
31
+ # `check(context) -> [error]`. Watching by name keeps every other call in
32
+ # the program off the check's path entirely.
33
+ def register_check(gem_name, phase, check)
34
+ allow!(gem_name)
35
+ fail UnknownPhase.new(phase) unless PHASES.include?(phase)
36
+
37
+ @call_checks = check
38
+ .watches
39
+ .reduce(call_checks) { |acc, name| acc.merge(name => acc.fetch(name, []) | [check]) }
40
+ end
41
+
42
+ def derivers = @derivers ||= []
43
+
44
+ def call_checks = @call_checks ||= {}
45
+
46
+ # The node travels with the types because a literal's value — a SQL string,
47
+ # a constant predicate — is not in them.
48
+ CallContext = Data.define(:name, :arg_types, :node, :registry, :entry_name, :span)
49
+
50
+ def check_call(name, arg_types, node, registry, entry_name, span)
51
+ call_checks[name].then do |watching|
52
+ next [] if watching.nil?
53
+
54
+ CallContext[name, arg_types, node, registry, entry_name, span]
55
+ .then { |ctx| watching.flat_map { it.check(ctx) } }
56
+ end
57
+ end
58
+
59
+ def reset!
60
+ @derivers = []
61
+ @call_checks = {}
62
+ end
63
+
64
+ private
65
+
66
+ def allow!(gem_name)
67
+ fail NotAllowed.new(gem_name) unless ALLOWED.include?(gem_name)
68
+ end
69
+ end
70
+ end
@@ -12,7 +12,10 @@ module Jade
12
12
 
13
13
  callee_str = format_node(callee, source:)
14
14
  args_strs = args.map { format_node(it, source:) }
15
- Calls.render(callee_str, args_strs, trailing_comma, indent)
15
+
16
+ Calls::Call[callee_str, args_strs, indent].then do |call|
17
+ Calls.render(call, trailing_comma, opened_last: Calls.render_open(args.last, source))
18
+ end
16
19
  end
17
20
  end
18
21
 
@@ -25,7 +28,10 @@ module Jade
25
28
 
26
29
  callee_str = format_node(callee, source:)
27
30
  field_strs = fields.map { "#{it.key}: #{format_node(it.value, source:)}" }
28
- Calls.render(callee_str, field_strs, trailing_comma, indent)
31
+
32
+ Calls::Call[callee_str, field_strs, indent].then do |call|
33
+ Calls.render(call, trailing_comma, opened_last: Calls.render_open_field(fields.last, source))
34
+ end
29
35
  end
30
36
  end
31
37
 
@@ -33,19 +39,70 @@ module Jade
33
39
  extend self
34
40
  extend Helper
35
41
 
36
- def render(callee_str, item_strs, trailing_comma, indent)
37
- inline = "#{callee_str}(#{item_strs.join(', ')})"
42
+ # A trailing block-shaped argument reads as the call's body, the way a
43
+ # Ruby block does. Only these three grow a body worth hugging.
44
+ HUGGABLE = [AST::Lambda, AST::List, AST::RecordLiteral].freeze
45
+
46
+ # A rendered call, before the decision of how to lay it out: the callee,
47
+ # its already-formatted arguments, and the column they sit at.
48
+ Call = Data.define(:callee, :items, :indent)
49
+
50
+ def huggable?(node)
51
+ HUGGABLE.any? { node.is_a?(it) }
52
+ end
53
+
54
+ def render_open(node, source)
55
+ return nil unless huggable?(node)
56
+
57
+ dispatch_for(node).format(node, indent: 0, source:, open: true)
58
+ end
38
59
 
39
- if trailing_comma || too_long?(inline, indent)
40
- inner = item_strs
41
- .map { "#{it.then(&and_indent(indent + 1))}," }
42
- .join("\n")
60
+ def render_open_field(field, source)
61
+ render_open(field&.value, source)&.then { "#{field.key}: #{it}" }
62
+ end
43
63
 
44
- "#{callee_str.then(&and_indent(indent))}(\n#{inner}\n#{INDENT * indent})"
45
- else
46
- inline.then(&and_indent(indent))
64
+ def render(call, trailing_comma, opened_last: nil)
65
+ case [trailing_comma, inline(call), opened_last]
66
+ in [false, String => fits, _] then fits
67
+ in [false, nil, String => last] then hug_last(call, last) || explode(call)
68
+ else explode(call)
47
69
  end
48
70
  end
71
+
72
+ private
73
+
74
+ def inline(call)
75
+ "#{call.callee}(#{call.items.join(', ')})"
76
+ .then { too_long?(it, call.indent) ? nil : it.then(&and_indent(call.indent)) }
77
+ end
78
+
79
+ # An earlier multi-line argument would leave the head straddling lines.
80
+ def hug_last(call, last)
81
+ case [call.items[..-2], last.split("\n")]
82
+ in [[*head], [_]] then nil
83
+ in [[*head], _] if head.any? { it.include?("\n") } then nil
84
+ in [[*head], [opener, *rest]]
85
+ "#{call.callee}(#{(head + [opener]).join(', ')}"
86
+ .then { too_long?(it, call.indent) ? nil : hug_lines(it, rest, call.indent) }
87
+ end
88
+ end
89
+
90
+ def hug_lines(opening, rest, indent)
91
+ [opening, *rest[..-2], "#{rest.last})"]
92
+ .join("\n")
93
+ .then(&and_indent(indent))
94
+ end
95
+
96
+ def explode(call)
97
+ call.items
98
+ .map { "#{it.then(&and_indent(call.indent + 1))}," }
99
+ .join("\n")
100
+ .then { "#{indent_callee(call)}(\n#{it}\n#{INDENT * call.indent})" }
101
+ end
102
+
103
+ def indent_callee(call)
104
+ call.callee.then(&and_indent(call.indent))
105
+ end
49
106
  end
50
107
  end
51
108
  end
@@ -22,12 +22,12 @@ module Jade
22
22
  extend self
23
23
  extend Helper
24
24
 
25
- def format(node, indent:, source:)
25
+ def format(node, indent:, source:, open: false)
26
26
  node => AST::List(items:, trailing_comma:)
27
27
 
28
28
  format_delimited(
29
29
  items.map { format_node(it, source:) },
30
- '[', ']', trailing_comma, indent,
30
+ '[', ']', trailing_comma || open, indent,
31
31
  )
32
32
  end
33
33
  end
@@ -36,13 +36,13 @@ module Jade
36
36
  extend self
37
37
  extend Helper
38
38
 
39
- def format(node, indent:, source:)
39
+ def format(node, indent:, source:, open: false)
40
40
  node => AST::RecordLiteral(fields:, trailing_comma:)
41
41
 
42
42
  field_strs = fields.map { "#{it.key}: #{format_node(it.value, source:)}" }
43
43
  inline = "{ #{field_strs.join(', ')} }"
44
44
 
45
- if trailing_comma || too_long?(inline, indent)
45
+ if trailing_comma || open || too_long?(inline, indent)
46
46
  inner = field_strs
47
47
  .map { "#{it.then(&and_indent(indent + 1))}," }
48
48
  .join("\n")
@@ -68,24 +68,22 @@ module Jade
68
68
  "(#{type_params.map(&:name).join(', ')})"
69
69
  header = "struct #{name}#{params_str} ="
70
70
 
71
- record_type => AST::TypeRecord(fields:, row_var:)
72
-
73
- if fields.size > 1
74
- format_multiline(header, fields, row_var, indent)
75
- else
76
- "#{header} #{format_type(record_type)}".then(&and_indent(indent))
77
- end
71
+ format_record_declaration(header, record_type, indent)
78
72
  end
73
+ end
79
74
 
80
- def format_multiline(header, fields, row_var, indent)
81
- open_brace = row_var ? "{ #{row_var.name} |" : "{"
82
- fields_str = fields
83
- .map { |k, v| "#{k}: #{format_type(v)}".then(&and_indent(indent + 1)) }
84
- .join(",\n")
75
+ module TypeAliasDeclaration
76
+ extend self
77
+ extend Helper
78
+
79
+ def format(node, indent:, source:)
80
+ node => AST::TypeAliasDeclaration(name:, type_params:, body_type:)
81
+
82
+ params_str = type_params.empty? ?
83
+ "" :
84
+ "(#{type_params.map(&:name).join(', ')})"
85
85
 
86
- and_indent(indent)
87
- .call("#{header} #{open_brace}")
88
- .then { "#{it}\n#{fields_str}\n#{INDENT * indent}}" }
86
+ format_record_declaration("type alias #{name}#{params_str} =", body_type, indent)
89
87
  end
90
88
  end
91
89
 
@@ -30,6 +30,7 @@ module Jade
30
30
  in AST::TypeDeclaration then TypeDeclaration
31
31
  in AST::VariantDeclaration then VariantDeclaration
32
32
  in AST::StructDeclaration then StructDeclaration
33
+ in AST::TypeAliasDeclaration then TypeAliasDeclaration
33
34
  in AST::ImportDeclaration then ImportDeclaration
34
35
  in AST::InteropImportDeclaration then InteropImportDeclaration
35
36
  in AST::InteropFunction then InteropFunction
@@ -113,6 +114,25 @@ module Jade
113
114
  Pattern.format(node, source:)
114
115
  end
115
116
 
117
+ # A record body past one field goes multi-line, so a `type alias` over a
118
+ # record breaks the same way as the `struct` beside it.
119
+ def format_record_declaration(header, body, indent)
120
+ case body
121
+ in AST::TypeRecord(fields:, row_var:) if fields.size > 1
122
+ open_brace = row_var ? "{ #{row_var.name} |" : "{"
123
+ fields_str = fields
124
+ .map { |k, v| "#{k}: #{format_type(v)}".then(&and_indent(indent + 1)) }
125
+ .join(",\n")
126
+
127
+ and_indent(indent)
128
+ .call("#{header} #{open_brace}")
129
+ .then { "#{it}\n#{fields_str}\n#{INDENT * indent}}" }
130
+
131
+ else
132
+ "#{header} #{format_type(body)}".then(&and_indent(indent))
133
+ end
134
+ end
135
+
116
136
  def format_type(node)
117
137
  Type.format(node)
118
138
  end