jade-lang 0.9.0 → 0.10.1

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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +442 -12
  3. data/README.md +1 -0
  4. data/docs/interop.md +18 -1
  5. data/docs/json.md +34 -3
  6. data/docs/stdlib.md +3 -1
  7. data/docs/syntax.md +96 -0
  8. data/lib/jade/api.rb +9 -0
  9. data/lib/jade/ast.rb +37 -1
  10. data/lib/jade/cli/eject.rb +132 -0
  11. data/lib/jade/cli/init.rb +86 -0
  12. data/lib/jade/cli.rb +4 -0
  13. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  14. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  15. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  16. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  17. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  18. data/lib/jade/codegen/boundary.rb +12 -2
  19. data/lib/jade/codegen/context.rb +17 -0
  20. data/lib/jade/codegen/error.rb +1 -0
  21. data/lib/jade/codegen/function_call.rb +38 -7
  22. data/lib/jade/codegen/function_declaration.rb +25 -13
  23. data/lib/jade/codegen/helpers.rb +27 -4
  24. data/lib/jade/codegen/implementation.rb +2 -2
  25. data/lib/jade/codegen/inline.rb +1 -1
  26. data/lib/jade/codegen/inlines.rb +6 -1
  27. data/lib/jade/codegen/names.rb +31 -0
  28. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  29. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  30. data/lib/jade/codegen.rb +76 -15
  31. data/lib/jade/compiler.rb +2 -0
  32. data/lib/jade/entry.rb +1 -1
  33. data/lib/jade/formatter/calls.rb +68 -11
  34. data/lib/jade/formatter/collections.rb +4 -4
  35. data/lib/jade/formatter/declarations.rb +13 -15
  36. data/lib/jade/formatter/helper.rb +20 -0
  37. data/lib/jade/formatter/infix_application.rb +19 -4
  38. data/lib/jade/formatter/lambda.rb +24 -12
  39. data/lib/jade/formatter/pattern.rb +3 -0
  40. data/lib/jade/formatter/type.rb +3 -5
  41. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  42. data/lib/jade/frontend/desugaring.rb +29 -9
  43. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  44. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  45. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  46. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  47. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  48. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  49. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  50. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  51. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  52. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  53. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  54. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  55. data/lib/jade/frontend/forward_declaration.rb +2 -0
  56. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  57. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  58. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  59. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  60. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  61. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  62. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  63. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  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 +3 -0
  69. data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
  70. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
  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.rb +18 -3
  84. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  85. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  86. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  87. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  88. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  89. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  90. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  91. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  92. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  93. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  94. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  95. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  96. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  97. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  98. data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
  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 +7 -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 +26 -1
  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/record_update.rb +8 -1
  110. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  111. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  112. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  113. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  114. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  115. data/lib/jade/frontend/type_checking.rb +3 -1
  116. data/lib/jade/frontend/usage_analysis.rb +5 -1
  117. data/lib/jade/interop/boundary.rb +64 -24
  118. data/lib/jade/interop/error.rb +35 -7
  119. data/lib/jade/interop/lowering.rb +8 -2
  120. data/lib/jade/lsp/converters.rb +83 -2
  121. data/lib/jade/lsp/handlers.rb +122 -25
  122. data/lib/jade/lsp/snippets.rb +12 -0
  123. data/lib/jade/lsp/state.rb +11 -4
  124. data/lib/jade/module_loader/build.rb +36 -0
  125. data/lib/jade/module_loader.rb +1 -0
  126. data/lib/jade/parsing/combinators.rb +78 -2
  127. data/lib/jade/parsing/error.rb +43 -6
  128. data/lib/jade/parsing/type.rb +6 -4
  129. data/lib/jade/parsing.rb +140 -26
  130. data/lib/jade/runtime.rb +32 -13
  131. data/lib/jade/source.rb +6 -2
  132. data/lib/jade/stdlib/basics.rb +22 -3
  133. data/lib/jade/stdlib/char.rb +2 -0
  134. data/lib/jade/stdlib/clock.rb +6 -2
  135. data/lib/jade/stdlib/decimal.rb +51 -14
  136. data/lib/jade/stdlib/decode.rb +11 -0
  137. data/lib/jade/stdlib/intrinsics.rb +59 -6
  138. data/lib/jade/stdlib/list.rb +37 -1
  139. data/lib/jade/stdlib/number.rb +47 -0
  140. data/lib/jade/stdlib/range.rb +211 -0
  141. data/lib/jade/stdlib.rb +11 -4
  142. data/lib/jade/symbol/alias.rb +11 -0
  143. data/lib/jade/symbol/parser.rb +1 -1
  144. data/lib/jade/symbol/stdlib_function.rb +1 -1
  145. data/lib/jade/symbol.rb +13 -1
  146. data/lib/jade/task.rb +0 -2
  147. data/lib/jade/type/anonymous_record.rb +11 -2
  148. data/lib/jade/type/application.rb +11 -2
  149. data/lib/jade/type/base.rb +26 -0
  150. data/lib/jade/type/function.rb +11 -3
  151. data/lib/jade/type.rb +36 -10
  152. data/lib/jade/version.rb +1 -1
  153. metadata +35 -2
@@ -0,0 +1,50 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Inference
5
+ # `/` asks for a `NonZero`, which nothing but `non_zero` can build.
6
+ # A literal is the exception: the compiler can see whether it is
7
+ # zero, so `cents / 100` needs no ceremony and `n / 0` is caught
8
+ # where it is written.
9
+ module Division
10
+ extend self
11
+
12
+ OPERATOR = '(/)'.freeze
13
+
14
+ # Lifted even when the literal is zero: the divide-by-zero error is
15
+ # the one worth reading, and a type mismatch under it is noise.
16
+ def lift(node, acc)
17
+ return acc unless divisor(node)
18
+
19
+ acc.with(types: [*acc.types[0..-2], non_zero(acc.types.last)])
20
+ end
21
+
22
+ def by_zero(node, entry_name)
23
+ return [] unless divisor(node)&.zero?
24
+
25
+ [Error::DivisionByZero.new(entry_name, node.args.last.range)]
26
+ end
27
+
28
+ private
29
+
30
+ def divisor(node)
31
+ return nil unless node.args.size == 2 && operator?(node.callee)
32
+
33
+ case node.args.last
34
+ in AST::Literal(value: ::Integer | ::Float => value) then value
35
+ else nil
36
+ end
37
+ end
38
+
39
+ def operator?(callee)
40
+ callee in AST::VariableReference(name: OPERATOR)
41
+ end
42
+
43
+ def non_zero(type)
44
+ Type.constructor('Basics.NonZero').apply([type])
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -28,10 +28,12 @@ module Jade
28
28
  .then { |(new_state, result)| [new_state, acc.add(result)] }
29
29
  end
30
30
 
31
+ args_state = args_state.add_errors(Division.by_zero(node, args_state.env.entry_name))
32
+
31
33
  after_callee_state, result_type = unify_callee(
32
34
  args_state,
33
35
  callee_result,
34
- args_acc,
36
+ Division.lift(node, args_acc),
35
37
  node,
36
38
  state,
37
39
  )
@@ -170,9 +172,32 @@ module Jade
170
172
  expected: e.expected,
171
173
  actual: e.actual,
172
174
  infix: node.infix,
175
+ lifted_placeholder: lifted_placeholder?(node),
176
+ callee_name: source_name(node.callee),
177
+ arg_names: node.args.map { source_name(it) },
173
178
  )
174
179
  end
175
180
  end
181
+
182
+ # Desugaring lifted the `_` into a lambda parameter, so a hole
183
+ # reads as an argument referring to one by the time we get here.
184
+ def lifted_placeholder?(node)
185
+ node.args.any? { it in AST::VariableReference(name: /\A__p\d+__\z/) }
186
+ end
187
+
188
+ # What the author called this, where they called it something.
189
+ def source_name(node)
190
+ case node
191
+ in AST::ConstructorReference | AST::VariableReference
192
+ node.name
193
+
194
+ in AST::QualifiedAccess(path:)
195
+ path.join('.')
196
+
197
+ else
198
+ nil
199
+ end
200
+ end
176
201
  end
177
202
  end
178
203
  end
@@ -56,7 +56,7 @@ module Jade
56
56
  else
57
57
  concrete_t = body_state.env.substitution.apply(t)
58
58
  PatternAnalysis::Exhaustiveness
59
- .assert([p], p.range, acc.env, registry, concrete_t)
59
+ .assert([p], p.range, acc.env, concrete_t)
60
60
  .then { acc.add_errors(it) }
61
61
  end
62
62
  end
@@ -11,6 +11,32 @@ module Jade
11
11
  .then { |s, r| [s.with(env: s.env.pin_type(pattern.id, r.type)), r] }
12
12
  end
13
13
 
14
+ def range_type_error(state, pattern)
15
+ ->(error) do
16
+ Error::RangePatternType.new(
17
+ state.env.entry_name, pattern.range,
18
+ expected: error.expected, actual: error.actual,
19
+ )
20
+ end
21
+ end
22
+
23
+ def empty_range(pattern, state, result)
24
+ pattern => AST::Pattern::Range(from:, to:)
25
+
26
+ return [] unless state.env.substitution.apply(result.type) in Type::Application(
27
+ constructor: Type::Constructor(name: 'Basics.Int'),
28
+ )
29
+
30
+ return [] unless from && to && from.value > to.value
31
+
32
+ [Error::EmptyRangePattern.new(
33
+ state.env.entry_name,
34
+ pattern.range,
35
+ from: from.value,
36
+ to: to.value,
37
+ )]
38
+ end
39
+
14
40
  def infer_(pattern, registry, state, expected)
15
41
  case pattern
16
42
  in AST::Pattern::Record(fields:, symbol:)
@@ -34,6 +60,15 @@ module Jade
34
60
  .unify_result(it, expected.type, &type_error(state, pattern))
35
61
  end
36
62
 
63
+ in AST::Pattern::Range
64
+ state
65
+ .unify_result(
66
+ Result.init(Type.int),
67
+ expected.type,
68
+ &range_type_error(state, pattern)
69
+ )
70
+ .then { |(st, r)| [st.add_errors(empty_range(pattern, st, r)), r] }
71
+
37
72
  in AST::Pattern::Literal(literal:)
38
73
  new_state, literal_result = check(literal, registry, state, expected)
39
74
  new_state.unify_result(literal_result, expected.type, &type_error(state, pattern))
@@ -28,7 +28,14 @@ module Jade
28
28
  )
29
29
  end
30
30
 
31
- after_state.unify_result(result, expected.type)
31
+ after_state.unify_result(result, expected.type) do
32
+ Error::RecordUpdateTypeMismatch.new(
33
+ state.env.entry_name,
34
+ node.range,
35
+ expected: it.expected,
36
+ actual: it.actual,
37
+ )
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -0,0 +1,18 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Inference
5
+ module TypeAliasDeclaration
6
+ extend Helpers
7
+ extend self
8
+
9
+ def infer(node, _, state, _)
10
+ node => AST::TypeAliasDeclaration
11
+
12
+ [state, Result.init(Type.unit)]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -3,6 +3,7 @@ require 'jade/frontend/type_checking/inference/helpers'
3
3
  require 'jade/frontend/type_checking/inference/body'
4
4
  require 'jade/frontend/type_checking/inference/case_of'
5
5
  require 'jade/frontend/type_checking/inference/constructor_reference'
6
+ require 'jade/frontend/type_checking/inference/division'
6
7
  require 'jade/frontend/type_checking/inference/function_call'
7
8
  require 'jade/frontend/type_checking/inference/function_declaration'
8
9
  require 'jade/frontend/type_checking/inference/implementation'
@@ -21,6 +22,7 @@ require 'jade/frontend/type_checking/inference/record_field'
21
22
  require 'jade/frontend/type_checking/inference/record_literal'
22
23
  require 'jade/frontend/type_checking/inference/record_update'
23
24
  require 'jade/frontend/type_checking/inference/struct_declaration'
25
+ require 'jade/frontend/type_checking/inference/type_alias_declaration'
24
26
  require 'jade/frontend/type_checking/inference/interface_declaration'
25
27
  require 'jade/frontend/type_checking/inference/type_declaration'
26
28
  require 'jade/frontend/type_checking/inference/assign'
@@ -35,12 +35,22 @@ module Jade
35
35
  entry
36
36
  .types
37
37
  .reduce(env) do |e, (_, sym)|
38
+ # Aliases are transparent — they get expanded into their
39
+ # body whenever a TypeRef resolves to one. They never need
40
+ # to be looked up as definitions during unification.
41
+ next e if alias_symbol?(sym, registry)
42
+
38
43
  Definition
39
44
  .from_symbol(sym, registry)
40
45
  .then { e.define(sym.qualified_name, it) }
41
46
  end
42
47
  end
43
48
 
49
+ def alias_symbol?(sym, registry)
50
+ sym.is_a?(Symbol::Alias) ||
51
+ (sym.is_a?(Symbol::TypeRef) && registry.lookup(sym).is_a?(Symbol::Alias))
52
+ end
53
+
44
54
  def load_imports(env, entry, registry)
45
55
  entry
46
56
  .imports
@@ -65,6 +75,7 @@ module Jade
65
75
 
66
76
  in Symbol::TypeRef
67
77
  return env if env.lookup_def(sym.qualified_name)
78
+ return env if registry.lookup(sym).is_a?(Symbol::Alias)
68
79
 
69
80
  Definition
70
81
  .from_symbol(sym, registry)
@@ -36,9 +36,12 @@ module Jade
36
36
 
37
37
  private
38
38
 
39
+ # Every port reaching here returns a Task: `NonTaskPort` runs in
40
+ # semantic analysis and stops compilation before this, which is what
41
+ # lets `resolve_port` destructure the return type without checking.
39
42
  def resolve_value(sym, entry, registry)
40
43
  case sym
41
- in Symbol::InteropFunction(return_type: Symbol::TypeApplication)
44
+ in Symbol::InteropFunction
42
45
  resolve_port(sym, entry, registry)
43
46
 
44
47
  else
@@ -47,7 +50,7 @@ module Jade
47
50
  end
48
51
 
49
52
  def resolve_port(interop_fn, entry, registry)
50
- interop_fn.return_type => Symbol::TypeApplication(args: [ok_sym, err_sym])
53
+ ok_sym, err_sym = arm_symbols(interop_fn.return_type)
51
54
 
52
55
  # Single Type.from_symbol over the whole port so a var appearing in
53
56
  # more than one position gets the same Type::Var id. PortCodec relies
@@ -66,6 +69,19 @@ module Jade
66
69
  ]
67
70
  end
68
71
 
72
+ # The arms are only wanted for their spans, and an alias keeps its
73
+ # arms in another declaration, so there is often none to point at and
74
+ # the port's own return type is the best there is.
75
+ def arm_symbols(return_type)
76
+ case return_type
77
+ in Symbol::TypeApplication(constructor: Symbol::TypeRef['Task', 'Task'], args: [ok, err])
78
+ [ok, err]
79
+
80
+ else
81
+ [nil, nil]
82
+ end
83
+ end
84
+
69
85
  # Without the constraints the shared VarGen would still hand every
70
86
  # position its own var; `from_symbol` only ties them together through
71
87
  # the map it threads across params and return.
@@ -83,7 +99,7 @@ module Jade
83
99
  end
84
100
 
85
101
  def resolve_param(type_sym, type, interop_fn, index, entry, registry)
86
- return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
102
+ return [Symbol::InteropFunction::PASS, []] if pass_through?(type)
87
103
 
88
104
  case type
89
105
  in Type::Var(name:)
@@ -99,7 +115,7 @@ module Jade
99
115
  end
100
116
 
101
117
  def resolve_arm(type_sym, type, interop_fn, arm, entry, registry)
102
- return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
118
+ return [Symbol::InteropFunction::PASS, []] if pass_through?(type)
103
119
 
104
120
  case type
105
121
  in Type::Var(name:)
@@ -156,10 +172,12 @@ module Jade
156
172
  end
157
173
  end
158
174
 
159
- def pass_through?(type_sym)
160
- case type_sym
161
- in Symbol::TypeApplication(constructor: Symbol::TypeRef(module_name:, name:))
162
- [module_name, name] in ['Basics', 'Never'] | ['Decode', 'Value']
175
+ # Asked of the expanded type, so an alias over `Never` or
176
+ # `Decode.Value` passes through the same as the type it names.
177
+ def pass_through?(type)
178
+ case type
179
+ in Type::Application(constructor: Type::Constructor(name: 'Basics.Never' | 'Decode.Value'))
180
+ true
163
181
 
164
182
  else
165
183
  false
@@ -47,7 +47,7 @@ module Jade
47
47
  in Type::Application(args:)
48
48
  case apply(type.constructor)
49
49
  in Type::PartialApplication(constructor:, args: tail_args)
50
- Type::Application[constructor, args.map { apply(it) } + tail_args]
50
+ Type::Application[constructor, args.map { apply(it) } + tail_args, type.display]
51
51
 
52
52
  in constructor
53
53
  type.with(constructor:, args: args.map { apply(it) })
@@ -60,7 +60,7 @@ module Jade
60
60
 
61
61
  case apply(row_var)
62
62
  in Type::AnonymousRecord(fields: extra_fields, row_var: new_row_var)
63
- Type.anonymous_record(applied_fields.merge(extra_fields), new_row_var)
63
+ Type.anonymous_record(applied_fields.merge(extra_fields), new_row_var, type.display)
64
64
 
65
65
  in Type::Var => applied_row_var
66
66
  type.with(fields: applied_fields, row_var: applied_row_var)
@@ -1,3 +1,4 @@
1
+ require 'jade/frontend/type_checking/cascade'
1
2
  require 'jade/frontend/type_checking/canonicalize'
2
3
  require 'jade/frontend/type_checking/constraints'
3
4
  require 'jade/frontend/type_checking/definition'
@@ -97,7 +98,7 @@ module Jade
97
98
  # with those constraints — rather than being dropped silently.
98
99
 
99
100
  state
100
- .with(errors: state.errors + errors)
101
+ .with(errors: Cascade.prune(state.errors + errors))
101
102
  .to_result
102
103
  end
103
104
 
@@ -143,6 +144,7 @@ module Jade
143
144
  in AST::QualifiedAccess then Inference::QualifiedAccess
144
145
  in AST::RecordAccess then Inference::RecordAccess
145
146
  in AST::StructDeclaration then Inference::StructDeclaration
147
+ in AST::TypeAliasDeclaration then Inference::TypeAliasDeclaration
146
148
  in AST::InterfaceDeclaration then Inference::InterfaceDeclaration
147
149
  in AST::RecordField then Inference::RecordField
148
150
  in AST::RecordLiteral then Inference::RecordLiteral
@@ -97,7 +97,8 @@ module Jade
97
97
  in AST::Pattern::Record(fields:)
98
98
  fields.flat_map { walk(it.pattern, :as_value, entry, owner) }
99
99
 
100
- in AST::Pattern::Literal | AST::Pattern::Binding | AST::Pattern::Wildcard
100
+ in AST::Pattern::Literal | AST::Pattern::Binding | AST::Pattern::Wildcard |
101
+ AST::Pattern::Range
101
102
  []
102
103
 
103
104
  in AST::Grouping(expression:)
@@ -138,6 +139,9 @@ module Jade
138
139
  in AST::StructDeclaration(record_type:, symbol:)
139
140
  walk_type(record_type, entry, ReferenceIndex.key_for(symbol))
140
141
 
142
+ in AST::TypeAliasDeclaration(body_type:, symbol:)
143
+ walk_type(body_type, entry, ReferenceIndex.key_for(symbol))
144
+
141
145
  in AST::InterfaceDeclaration(functions:, symbol:)
142
146
  ReferenceIndex.key_for(symbol).then do |declared|
143
147
  functions.flat_map { walk_type(it.type, entry, declared) }
@@ -11,6 +11,50 @@ module Jade
11
11
  # Result wrap/unwrap that user-level Decode.from_value uses is dead
12
12
  # weight at the boundary because failure always raises anyway. Skipping
13
13
  # it removes one allocation per arg per Ruby→Jade call.
14
+ # Names the argument a failure came from. Costs nothing until one
15
+ # does: Ruby only pays for a rescue that fires.
16
+ def arg(where)
17
+ yield
18
+ rescue DecodeError => e
19
+ raise e.under(where)
20
+ end
21
+ alias at arg
22
+
23
+ # Finding the element costs a second pass, which only a failure pays
24
+ # for: the map stays a map.
25
+ def elements(array, where = nil)
26
+ array.map { yield(it) }
27
+ rescue DecodeError => e
28
+ raise e.under("#{where}[#{failing_index(array) { yield(it) }}]")
29
+ end
30
+
31
+ def failing_index(array)
32
+ array.each_with_index do |element, index|
33
+ yield(element)
34
+ rescue DecodeError
35
+ return index
36
+ end
37
+ end
38
+
39
+ # An absent key reads as nil to the field decoder, which then blames
40
+ # the type it wanted. The path says which key that was, so a struct
41
+ # can check whether it was ever there.
42
+ def blame(error, hash, label)
43
+ key = error.where.to_s[/\A\.([^.\[]+)/, 1]
44
+ return error if key.nil? || hash.key?(key)
45
+
46
+ symbol_keys!(label, hash)
47
+ error.as(Jade::Decode::MissingField[key])
48
+ end
49
+
50
+ # Mixed keys are a real shape mismatch; the per-field errors say more
51
+ # than a guess about intent would.
52
+ def symbol_keys!(label, hash)
53
+ return if hash.empty? || hash.keys.any?(::String)
54
+
55
+ hash.keys.grep(::Symbol).then { raise SymbolKeys.new(label, it) unless it.empty? }
56
+ end
57
+
14
58
  def decode_or_raise(decoder, value)
15
59
  Jade::Decode::Runner.run!(decoder, value) do |error|
16
60
  raise Jade::Interop::DecodeError.new(error, value)
@@ -21,54 +65,50 @@ module Jade
21
65
  # argument types in place of the generic `decode_or_raise` path —
22
66
  # avoids constructing a Decoder descriptor and walking the
23
67
  # interpreter for primitives.
24
- def integer(label, v)
25
- ::Integer === v ? v : type_error!(label, v)
68
+ def integer(label, v, where = nil)
69
+ ::Integer === v ? v : type_error!(label, v, where)
26
70
  end
27
71
 
28
- def string(label, v)
29
- ::String === v ? v.dup : type_error!(label, v)
72
+ def string(label, v, where = nil)
73
+ ::String === v ? v.dup : type_error!(label, v, where)
30
74
  end
31
75
 
32
- def bool(label, v)
33
- v == true || v == false ? v : type_error!(label, v)
76
+ def bool(label, v, where = nil)
77
+ v == true || v == false ? v : type_error!(label, v, where)
34
78
  end
35
79
 
36
- def float(label, v)
37
- ::Numeric === v ? v.to_f : type_error!(label, v)
80
+ def float(label, v, where = nil)
81
+ ::Numeric === v ? v.to_f : type_error!(label, v, where)
38
82
  end
39
83
 
40
- def list_of(klass, label, v)
41
- v.is_a?(::Array) && v.all? { klass === _1 } ? v : type_error!(label, v)
84
+ # Copied, because the caller keeps its array and Jade values do not
85
+ # change. The copy is under 1% of the check that precedes it.
86
+ def list_of(klass, label, v, where = nil)
87
+ v.is_a?(::Array) && v.all? { klass === _1 } ? v.dup : type_error!(label, v, where)
42
88
  end
43
89
 
44
90
  # Validates that v is an Array but doesn't check element types — used
45
91
  # when the per-element decoder isn't a simple `is_a?` (e.g. nested
46
92
  # structs). The caller maps a decoder over the result.
47
- def array(label, v)
48
- v.is_a?(::Array) ? v : type_error!(label, v)
93
+ def array(label, v, where = nil)
94
+ v.is_a?(::Array) ? v : type_error!(label, v, where)
49
95
  end
50
96
 
97
+ # Symbol keys are not checked here: every field would then be
98
+ # missing, and the rescue around the fields catches that for free.
51
99
  def hash(label, v)
52
100
  case v
53
- when ::Hash then string_keyed!(label, v)
101
+ when ::Hash then v
54
102
  when ::Data then v.to_h.transform_keys(&:to_s)
55
- else type_error!(label, v)
103
+ else type_error!(label, v, nil)
56
104
  end
57
105
  end
58
106
 
59
- # Mixed keys are a real shape mismatch; the per-field errors say more
60
- # than a guess about intent would.
61
- def string_keyed!(label, v)
62
- return v if v.empty? || v.keys.any?(::String)
63
-
64
- v.keys.grep(::Symbol)
65
- .then { it.empty? ? v : raise(Jade::Interop::SymbolKeys.new(label, it)) }
66
- end
67
-
68
- def type_error!(label, v)
107
+ def type_error!(label, v, where)
69
108
  raise Jade::Interop::DecodeError.new(
70
109
  Jade::Decode::WrongType[label, Jade::Decode.type_name(v)],
71
110
  v,
111
+ where:,
72
112
  )
73
113
  end
74
114
  end
@@ -54,36 +54,48 @@ module Jade
54
54
  end
55
55
 
56
56
  class DecodeError < Error
57
- attr_reader :decode_error, :value, :source
57
+ attr_reader :decode_error, :value, :source, :where
58
58
 
59
59
  SUBJECTS = {
60
60
  argument: 'Ruby passed a value that failed to decode',
61
61
  port_return: 'Port returned a value that failed to decode',
62
62
  }.freeze
63
63
 
64
- def initialize(decode_error, value, source: :argument)
64
+ # Ruby has no null.
65
+ TYPE_NAMES = { 'null' => 'nil' }.freeze
66
+
67
+ def initialize(decode_error, value, source: :argument, where: nil)
65
68
  @decode_error = decode_error
66
69
  @value = value
67
70
  @source = source
71
+ @where = where
68
72
  super(format(decode_error, value))
69
73
  end
70
74
 
75
+ # Frames unwind innermost first, so each one puts itself in front of
76
+ # the path the frames below it built.
77
+ def under(segment)
78
+ self.class.new(decode_error, value, source:, where: "#{segment}#{where}")
79
+ end
80
+
81
+ def as(error)
82
+ self.class.new(error, value, source:, where:)
83
+ end
84
+
71
85
  private
72
86
 
73
87
  def format(error, value)
74
88
  path, leaf = unwind(error)
75
- location = path.empty? ? "value" : path.join
76
- subject = SUBJECTS.fetch(@source)
77
89
 
78
90
  case leaf
79
91
  in Jade::Decode::WrongType[expected, got]
80
- "#{subject} at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
92
+ "#{subject(path)}: expected #{expected}, got #{type_name(got)}#{seen(value)}"
81
93
 
82
94
  in Jade::Decode::MissingField[key]
83
- "#{subject} at #{location}: missing field `#{key}` (#{value.inspect})"
95
+ "#{subject(path)}: missing field `#{key}`#{seen(value)}"
84
96
 
85
97
  in Jade::Decode::Custom[msg]
86
- "#{subject} at #{location}: #{msg} (#{value.inspect})"
98
+ "#{subject(path)}: #{msg}#{seen(value)}"
87
99
 
88
100
  in Jade::Decode::Multiple[errors]
89
101
  errors
@@ -92,6 +104,22 @@ module Jade
92
104
  end
93
105
  end
94
106
 
107
+ # `Shop.price(item).cents` when the caller said where it was, and the
108
+ # old sentence when it did not.
109
+ def subject(path)
110
+ return "#{SUBJECTS.fetch(@source)} at #{path.empty? ? 'value' : path.join}" unless @where
111
+
112
+ "#{@where}#{path.join}"
113
+ end
114
+
115
+ def type_name(got)
116
+ TYPE_NAMES.fetch(got.to_s, got)
117
+ end
118
+
119
+ def seen(value)
120
+ value.nil? ? '' : " (#{value.inspect})"
121
+ end
122
+
95
123
  def unwind(error, path = [])
96
124
  case error
97
125
  in Jade::Decode::AtField[key, inner] then unwind(inner, path + [".#{key}"])
@@ -21,8 +21,9 @@ module Jade
21
21
  in Symbol::FunctionType
22
22
  [FunctionError.new('inline function type')]
23
23
 
24
- in Symbol::TypeApplication(args:)
25
- args.flat_map { validate(it, registry, entry) }
24
+ in Symbol::TypeApplication(constructor:, args:)
25
+ validate(constructor, registry, entry) +
26
+ args.flat_map { validate(it, registry, entry) }
26
27
 
27
28
  in Symbol::RecordType(fields:)
28
29
  fields.values.flat_map { validate(it, registry, entry) }
@@ -34,6 +35,11 @@ module Jade
34
35
  lookup_type(symbol, registry, entry)
35
36
  &.then { validate(it, registry, entry) } || []
36
37
 
38
+ # An alias is its body here as everywhere else, so a function type
39
+ # behind one is still a function type that cannot be lowered.
40
+ in Symbol::Alias(body:)
41
+ body ? validate(body, registry, entry) : []
42
+
37
43
  else
38
44
  []
39
45
  end