jade-lang 0.10.1 → 0.11.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/lib/jade/ast.rb +15 -3
  4. data/lib/jade/codegen/function_call.rb +10 -10
  5. data/lib/jade/codegen/function_declaration.rb +2 -10
  6. data/lib/jade/codegen/helpers.rb +42 -0
  7. data/lib/jade/codegen/implementation.rb +33 -8
  8. data/lib/jade/extensions.rb +8 -3
  9. data/lib/jade/formatter/leaves.rb +12 -1
  10. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +16 -3
  11. data/lib/jade/frontend/type_checking/error/cross_module_requirement.rb +36 -0
  12. data/lib/jade/frontend/type_checking/error/implementation_function_constraint.rb +36 -0
  13. data/lib/jade/frontend/type_checking/error/undeclared_requirement.rb +35 -0
  14. data/lib/jade/frontend/type_checking/error.rb +3 -0
  15. data/lib/jade/frontend/type_checking/inference/function_call.rb +1 -0
  16. data/lib/jade/frontend/type_checking/inference/function_declaration.rb +0 -5
  17. data/lib/jade/frontend/type_checking/inference/helpers.rb +9 -0
  18. data/lib/jade/frontend/type_checking/inference/implementation.rb +93 -16
  19. data/lib/jade/frontend/type_checking/inference/qualified_access.rb +1 -0
  20. data/lib/jade/frontend/type_checking/inference/variable_reference.rb +1 -0
  21. data/lib/jade/frontend/type_checking/requirements.rb +115 -0
  22. data/lib/jade/frontend/type_checking/state.rb +10 -2
  23. data/lib/jade/frontend/type_checking.rb +9 -3
  24. data/lib/jade/parsing.rb +14 -1
  25. data/lib/jade/symbol/interface_function.rb +2 -1
  26. data/lib/jade/symbol.rb +2 -2
  27. data/lib/jade/type.rb +12 -3
  28. data/lib/jade/version.rb +1 -1
  29. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b0d9367dd2a235c282b3e7bbeabc2ee7874fa6899fdbdd746c66d546279cf30
4
- data.tar.gz: cf83815353b341a4834b5b17712148db973f96a3a914ac6aa2ae019a00ae84df
3
+ metadata.gz: c07abf955aeeae107bd0cbb0f2f9ba95aab3c9240946e96d69521c2ae406f56b
4
+ data.tar.gz: a6750f4f9734070babfde95fa657a37cec27acc43c705742b63b58691f4c225a
5
5
  SHA512:
6
- metadata.gz: 1551750ffbca65f37071206e6eccc559f2d7ae81b5ecf0545c089bc3b0f8d36ba3cc3d98c4c33ce3b2c572c8a650d494c40c9d926f210a1f449c478257ca084c
7
- data.tar.gz: 2ec0e124c2057bd34cf99728253b5dbd93765c77694a7f6ca0799be1c56eb1baa6d79e4cbc8268ad56a38fa811877defb5ccbd2f7538ce24e33958754ee90aa6
6
+ metadata.gz: c485c3028caa4b4ad1da0abd708c9c6c1b935ed26a242f19efbc5440f3e9a46e6236cf9e93912e870c941ba64096ae488499ce351c0dfd04a75084a6e8b9f6c1
7
+ data.tar.gz: 2176bf5dc17eacdbbc902a3243e8676c2232350846a45913181489ee74ea6ff9f576d9278b0f5bfaffb1d235c7d8337ce6ebd0d03ff141e23d3bc74b95f2f8dd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.11.1] - 2026-09-15
8
+
9
+ ### Added
10
+
11
+ - **A `:call` check sees what the call returns.** `Extensions::CallContext`
12
+ carried the argument types and not the result, so a check on a function
13
+ whose interesting type is the shape the caller asked for had nothing to
14
+ read. jade-sql needs it to compare a read's result against the table's
15
+ columns, the way it already compares a write's.
16
+
17
+ ## [0.11.0] - 2026-09-15
18
+
19
+ ### Added
20
+
21
+ - **An interface method may require an interface of another type.** A method
22
+ can mention a type variable the interface itself does not name, and say what
23
+ that variable has to satisfy:
24
+
25
+ interface Fetchable(x) with
26
+ projection : x -> Sel(a) with Selectable(a)
27
+ end
28
+
29
+ `a` is settled where `projection` is called, not where the implementation is
30
+ picked, so each call site resolves the requirement against its own types and
31
+ passes that dictionary alongside the one for `x`. Left undeclared, the
32
+ requirement is inferred from the implementations instead. Declared and
33
+ inferred are reconciled rather than one overriding the other: an
34
+ implementation may require what the method declares, and anything beyond it
35
+ is refused by name.
36
+
37
+ Two cases stay refused. A requirement *inferred* on an interface from
38
+ another module, because a declaration is what travels with the interface and
39
+ there is no foreign entry to amend — declare it and it works across modules.
40
+ And a requirement on a variable the method's signature never names, which
41
+ nothing could satisfy.
42
+
43
+ ### Fixed
44
+
45
+ - **A bare reference to a zero-arg interface method resolves through its
46
+ dictionary.** Dictionaries were only ever attached where a method was called
47
+ or passed as an argument, so a function body that was just `selector` — or
48
+ `Module.selector` — compiled to that name as a Ruby method and died on the
49
+ first call. It now reads the enclosing function's dictionary, or is a compile
50
+ error where none is in scope. Code that was silently broken this way may stop
51
+ compiling.
52
+
7
53
  ## [0.10.1] - 2026-09-11
8
54
 
9
55
  ### Added
data/lib/jade/ast.rb CHANGED
@@ -77,7 +77,8 @@ module Jade
77
77
  define(:ImplementationFunction, :name, :fn)
78
78
 
79
79
  define(:InterfaceDeclaration, :name, :type_param, :functions)
80
- define(:InterfaceFunctionDecl, :name, :type)
80
+ define(:InterfaceFunctionDecl, :name, :type, :constraints)
81
+ define(:InterfaceConstraint, :interface, :type_param)
81
82
 
82
83
  module Pattern
83
84
  extend self
@@ -751,7 +752,7 @@ module Jade
751
752
  end
752
753
 
753
754
  def interface_function_decl
754
- ->((name, type)) do
755
+ ->((name, type, constraints)) do
755
756
  canonical_name =
756
757
  case name.type
757
758
  in :identifier then name.value
@@ -761,7 +762,18 @@ module Jade
761
762
  InterfaceFunctionDecl[
762
763
  canonical_name,
763
764
  type,
764
- name.range.begin...type.range.end,
765
+ constraints,
766
+ name.range.begin...(constraints.last&.range&.end || type.range.end),
767
+ ]
768
+ end
769
+ end
770
+
771
+ def interface_constraint
772
+ ->((name, type_param)) do
773
+ InterfaceConstraint[
774
+ name.value,
775
+ type_param,
776
+ name.range.begin...type_param.range.end,
765
777
  ]
766
778
  end
767
779
  end
@@ -246,15 +246,6 @@ module Jade
246
246
  end
247
247
  end
248
248
 
249
- # When a user fn has var-typed constraints, two definitions are emitted:
250
- # `name` (Ruby-boundary wrapper, no dicts) and `__name__impl__` (takes
251
- # dicts). Jade-internal calls target the latter.
252
- def fn_target_name(fn_sym, registry)
253
- return fn_sym.name if dict_constraints(fn_sym, registry).empty?
254
-
255
- fn_impl_synthetic_name(fn_sym.name)
256
- end
257
-
258
249
  # Returns the list of dict args to pass after regular args. Only
259
250
  # Symbol::Function callees take dict params; other branches dispatch
260
251
  # via `dictionaries` directly inside generate_callee. dictionaries are
@@ -267,6 +258,7 @@ module Jade
267
258
  else callee.symbol
268
259
  end
269
260
 
261
+ return interface_dict_args(symbol, dictionaries, registry) if symbol.is_a?(Symbol::InterfaceFunction)
270
262
  return "" unless symbol.is_a?(Symbol::Function)
271
263
 
272
264
  fn_constraints(symbol, registry)
@@ -275,6 +267,14 @@ module Jade
275
267
  .join(', ')
276
268
  end
277
269
 
270
+ def interface_dict_args(_symbol, dictionaries, registry)
271
+ dictionaries
272
+ .drop(1)
273
+ .compact
274
+ .filter_map { dispatch_value(it, registry) }
275
+ .join(', ')
276
+ end
277
+
278
278
  # Ruby-block intrinsics (Dict's `Eq k`, etc.) ignore dispatches — the
279
279
  # `in String` body of `generate_impl_fn` drops them.
280
280
  def dispatch_dict(entry, registry)
@@ -356,7 +356,7 @@ module Jade
356
356
  "#{internal(fn.module_name)}.#{fn.name}"
357
357
 
358
358
  in Symbol::Function => fn
359
- "#{internal(fn.module_name)}.method(:#{fn.name})"
359
+ "#{internal(fn.module_name)}.method(:#{fn_target_name(fn, registry)})"
360
360
  end
361
361
  end
362
362
 
@@ -34,10 +34,9 @@ module Jade
34
34
 
35
35
  var_cs = dict_constraints(symbol, registry)
36
36
  param_names = params.map { generate_node(it, registry) }
37
- dict_params = var_cs.each_index.map { dict_synthetic_name(it) }
38
37
 
39
- body_code = build_dict_env(var_cs)
40
- .then { Codegen.with_dict_env(it) { emit_body(body, symbol, param_names, registry) } }
38
+ dict_params, body_code =
39
+ with_dict_params(var_cs) { emit_body(body, symbol, param_names, registry) }
41
40
 
42
41
  target = var_cs.empty? ? name : fn_impl_synthetic_name(name)
43
42
  sig = (param_names + dict_params).join(', ')
@@ -200,13 +199,6 @@ module Jade
200
199
  .then { it.substitution.apply(it.bindings[symbol.qualified_name].type) }
201
200
  end
202
201
 
203
- def build_dict_env(var_cs)
204
- var_cs
205
- .each_with_index
206
- .reduce({}) do |env, (c, i)|
207
- env.merge([c.interface, c.type.id] => dict_synthetic_name(i))
208
- end
209
- end
210
202
  end
211
203
  end
212
204
  end
@@ -74,6 +74,48 @@ module Jade
74
74
  .then { "__#{it}__impl__" }
75
75
  end
76
76
 
77
+ def fn_target_name(fn_sym, registry)
78
+ return fn_sym.name if dict_constraints(fn_sym, registry).empty?
79
+
80
+ fn_impl_synthetic_name(fn_sym.name)
81
+ end
82
+
83
+ def with_dict_params(constraints)
84
+ constraints
85
+ .each_with_index
86
+ .to_h { |c, i| [[c.interface, c.type.id], dict_synthetic_name(i)] }
87
+ .then { |env| Codegen.with_dict_env(env) { yield } }
88
+ .then { |body| [constraints.each_index.map { dict_synthetic_name(it) }, body] }
89
+ end
90
+
91
+ def body_markers(node)
92
+ return [] unless node.is_a?(AST::Node)
93
+
94
+ node
95
+ .deconstruct_keys(nil)
96
+ .each_value
97
+ .flat_map { marker_children(it) }
98
+ .flat_map { body_markers(it) }
99
+ .then { own_markers(node) + it }
100
+ end
101
+
102
+ def own_markers(node)
103
+ return [] unless node.respond_to?(:dictionaries)
104
+
105
+ node
106
+ .dictionaries
107
+ .select { it.is_a?(Type::Constraint) && it.type.is_a?(Type::Var) }
108
+ end
109
+
110
+ def marker_children(value)
111
+ case value
112
+ in Array then value.flat_map { marker_children(it) }
113
+ in Hash then value.each_value.flat_map { marker_children(it) }
114
+ in AST::Node then [value]
115
+ else []
116
+ end
117
+ end
118
+
77
119
  def fn_constraints(fn_symbol, registry)
78
120
  env = registry.get(fn_symbol.module_name).env
79
121
 
@@ -106,7 +106,7 @@ module Jade
106
106
  end
107
107
 
108
108
  def generate_defs(node, registry)
109
- node => AST::Implementation(interface:, applied_type:, functions:)
109
+ node => AST::Implementation(interface:, applied_type:, functions:, symbol:)
110
110
 
111
111
  type_name =
112
112
  case applied_type.constructor
@@ -115,7 +115,7 @@ module Jade
115
115
  end
116
116
 
117
117
  functions
118
- .filter_map { generate_function(it, registry, interface, type_name) }
118
+ .filter_map { generate_function(it, registry, interface, type_name, symbol) }
119
119
  .join(Pretty.newline(2))
120
120
  end
121
121
 
@@ -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) { #{internal(ref.module_name)}.#{ref.name}(*args) }"]
146
+ [fn_name, "->(*args) { #{internal(ref.module_name)}.#{registered_target(ref, registry)}(*args) }"]
147
147
  }.to_h
148
148
 
149
149
  return "" if fn_map.empty?
@@ -155,16 +155,41 @@ module Jade
155
155
  .join(Pretty.newline)
156
156
  end
157
157
 
158
- def generate_function(impl_fn, registry, interface, type_name)
158
+ def requirement_markers(body, impl_sym, fn_name, registry)
159
+ required = registry
160
+ .lookup(impl_sym.interface)
161
+ .functions
162
+ .find { it.name == fn_name }
163
+ .constraints
164
+
165
+ return [] if required.empty?
166
+
167
+ body_markers(body)
168
+ .uniq { [it.interface, it.type.id] }
169
+ .sort_by { |c| required.index { |iface, _| iface == c.interface } || required.size }
170
+ end
171
+
172
+ def registered_target(ref, registry)
173
+ case registry.lookup(ref)
174
+ in Symbol::Function => fn then FunctionCall.fn_target_name(fn, registry)
175
+ else ref.name
176
+ end
177
+ end
178
+
179
+ def generate_function(impl_fn, registry, interface, type_name, impl_sym)
159
180
  impl_fn => AST::ImplementationFunction(name: fn_name, fn:)
160
181
 
161
182
  case fn
162
183
  in AST::Lambda(params:, body:)
163
- synth = impl_synthetic_name(interface, type_name, fn_name)
164
- param_str = params.map { generate_node(it, registry) }.join(', ')
165
- sig = param_str.empty? ? '' : "(#{param_str})"
184
+ synth = impl_synthetic_name(interface, type_name, fn_name)
185
+
186
+ dicts, body_code = requirement_markers(body, impl_sym, fn_name, registry)
187
+ .then { with_dict_params(it) { generate_node(body, registry) } }
166
188
 
167
- Pretty.block("def #{synth}#{sig}", generate_node(body, registry))
189
+ (params.map { generate_node(it, registry) } + dicts)
190
+ .join(', ')
191
+ .then { it.empty? ? '' : "(#{it})" }
192
+ .then { Pretty.block("def #{synth}#{it}", body_code) }
168
193
 
169
194
  # Bare VariableReference, and the auto-invoke FunctionCall the
170
195
  # desugar pass synthesises for zero-arg fn refs, both dispatch via
@@ -45,13 +45,18 @@ module Jade
45
45
 
46
46
  # The node travels with the types because a literal's value — a SQL string,
47
47
  # a constant predicate — is not in them.
48
- CallContext = Data.define(:name, :arg_types, :node, :registry, :entry_name, :span)
48
+ # `return_type` is what the call produces, which a check needs when the
49
+ # thing being checked is the shape the caller asked for rather than
50
+ # something it passed in. Still a variable when nothing has pinned it.
51
+ CallContext = Data.define(
52
+ :name, :arg_types, :return_type, :node, :registry, :entry_name, :span
53
+ )
49
54
 
50
- def check_call(name, arg_types, node, registry, entry_name, span)
55
+ def check_call(name, arg_types, return_type, node, registry, entry_name, span)
51
56
  call_checks[name].then do |watching|
52
57
  next [] if watching.nil?
53
58
 
54
- CallContext[name, arg_types, node, registry, entry_name, span]
59
+ CallContext[name, arg_types, return_type, node, registry, entry_name, span]
55
60
  .then { |ctx| watching.flat_map { it.check(ctx) } }
56
61
  end
57
62
  end
@@ -113,7 +113,18 @@ module Jade
113
113
  extend Helper
114
114
 
115
115
  def format(node, indent:, source:)
116
- "#{node.name} : #{format_type(node.type)}".then(&and_indent(indent))
116
+ "#{node.name} : #{format_type(node.type)}#{constraints_clause(node)}"
117
+ .then(&and_indent(indent))
118
+ end
119
+
120
+ def constraints_clause(node)
121
+ return '' if node.constraints.empty?
122
+
123
+ node
124
+ .constraints
125
+ .map { "#{it.interface}(#{it.type_param.name})" }
126
+ .join(', ')
127
+ .then { " with #{it}" }
117
128
  end
118
129
  end
119
130
  end
@@ -50,10 +50,14 @@ module Jade
50
50
  private
51
51
 
52
52
  def build_interface_function(entry, interface_ref, fn_decl)
53
- fn_decl => AST::InterfaceFunctionDecl(name:, type:, range:)
53
+ fn_decl => AST::InterfaceFunctionDecl(name:, type:, constraints:, range:)
54
54
 
55
55
  figure_out_type(entry, type)
56
- .map do |type_symbol|
56
+ .and_then do |type_symbol|
57
+ declared_constraints(entry, constraints)
58
+ .map { [type_symbol, it] }
59
+ end
60
+ .map do |(type_symbol, declared)|
57
61
  params, return_type =
58
62
  case type_symbol
59
63
  in Symbol::FunctionType(params:, return_type:)
@@ -63,9 +67,18 @@ module Jade
63
67
  end
64
68
 
65
69
  Symbol
66
- .interface_function(name, interface_ref, params, return_type, range)
70
+ .interface_function(name, interface_ref, params, return_type, range, declared)
67
71
  end
68
72
  end
73
+
74
+ def declared_constraints(entry, constraints)
75
+ constraints
76
+ .map do |constraint|
77
+ require_type(entry, constraint.interface, constraint.range)
78
+ .map { [it.to_ref.qualified_name, constraint.type_param.name] }
79
+ end
80
+ .then { Results.sequence(it) }
81
+ end
69
82
  end
70
83
  end
71
84
  end
@@ -0,0 +1,36 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Error
5
+ class CrossModuleRequirement < Jade::Error
6
+ def initialize(entry, span, interface:, fn_name:, constraint:)
7
+ super(entry:, span:)
8
+ @interface = interface
9
+ @fn_name = fn_name
10
+ @constraint = constraint
11
+ end
12
+
13
+ def message
14
+ "Implementation of #{@interface}.#{@fn_name} requires #{@constraint}, " \
15
+ "but #{@interface} is declared in another module. A requirement " \
16
+ 'can only be added to an interface this module declares'
17
+ end
18
+
19
+ def label
20
+ "requires #{@constraint}"
21
+ end
22
+
23
+ def notes
24
+ [
25
+ Jade::Diagnostics::Annotation[
26
+ :help,
27
+ 'give the function a concrete type for that parameter, or move ' \
28
+ 'the implementation into the module that declares the interface',
29
+ ],
30
+ ]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Error
5
+ class ImplementationFunctionConstraint < Jade::Error
6
+ def initialize(entry, span, interface:, fn_name:, constraint:)
7
+ super(entry:, span:)
8
+ @interface = interface
9
+ @fn_name = fn_name
10
+ @constraint = constraint
11
+ end
12
+
13
+ def message
14
+ "Implementation of #{@interface}.#{@fn_name} requires #{@constraint}, " \
15
+ 'whose type is not the one being implemented. An implementation ' \
16
+ 'can only require interfaces of the type it implements'
17
+ end
18
+
19
+ def label
20
+ "requires #{@constraint}"
21
+ end
22
+
23
+ def notes
24
+ [
25
+ Jade::Diagnostics::Annotation[
26
+ :help,
27
+ 'give the function a concrete type for that parameter, or move ' \
28
+ 'the requirement onto the type being implemented',
29
+ ],
30
+ ]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Error
5
+ class UndeclaredRequirement < Jade::Error
6
+ def initialize(entry, span, interface:, fn_name:, constraint:, declared:)
7
+ super(entry:, span:)
8
+ @interface = interface
9
+ @fn_name = fn_name
10
+ @constraint = constraint
11
+ @declared = declared
12
+ end
13
+
14
+ def message
15
+ "Implementation of #{@interface}.#{@fn_name} requires #{@constraint}, " \
16
+ "which #{@interface} does not declare. It declares #{@declared}"
17
+ end
18
+
19
+ def label
20
+ "requires #{@constraint}"
21
+ end
22
+
23
+ def notes
24
+ [
25
+ Jade::Diagnostics::Annotation[
26
+ :help,
27
+ "add it to the method: `#{@fn_name} : ... with #{@constraint}`",
28
+ ],
29
+ ]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -22,4 +22,7 @@ require 'jade/frontend/type_checking/error/record_access_type_mismatch'
22
22
  require 'jade/frontend/type_checking/error/record_update_type_mismatch'
23
23
  require 'jade/frontend/type_checking/error/unresolved_constraint'
24
24
  require 'jade/frontend/type_checking/error/implementation_type_mismatch'
25
+ require 'jade/frontend/type_checking/error/implementation_function_constraint'
26
+ require 'jade/frontend/type_checking/error/cross_module_requirement'
27
+ require 'jade/frontend/type_checking/error/undeclared_requirement'
25
28
  require 'jade/frontend/type_checking/error/missing_implementation'
@@ -107,6 +107,7 @@ module Jade
107
107
  column_errors = Extensions.check_call(
108
108
  callee_name(callee),
109
109
  args_acc.types.map { st.env.substitution.apply(it) },
110
+ st.env.substitution.apply(result_type),
110
111
  node,
111
112
  registry,
112
113
  st.env.entry_name,
@@ -48,11 +48,6 @@ module Jade
48
48
  .map { st.env.substitution.apply(it) }
49
49
  .uniq { it.type.is_a?(Type::Var) ? [it.interface, it.type] : it }
50
50
 
51
- # TODO: for impl function declarations, unresolved constraints here
52
- # (e.g. Eq(a) when the body calls == on a field of type a) should
53
- # be stored as impl-level constraints, not function-level ones.
54
- # The impl finalization pass (see TypeChecking.finalize) should then
55
- # promote them into deps when the impl is instantiated for a concrete type.
56
51
  st.bind(
57
52
  symbol.qualified_name,
58
53
  Placeholder[
@@ -27,6 +27,15 @@ module Jade
27
27
  def type_from_symbol(symbol, registry, var_gen)
28
28
  Type.from_symbol(symbol, registry, var_gen)
29
29
  end
30
+
31
+ def attach_markers(result)
32
+ result
33
+ .constraints
34
+ .select { it.type.is_a?(Type::Var) && it.index != :unindex }
35
+ .each { Constraints.attach_dictionary(it, it) }
36
+
37
+ result
38
+ end
30
39
  end
31
40
  end
32
41
  end
@@ -57,6 +57,8 @@ module Jade
57
57
  st_after_bind,
58
58
  Expected.check(expected_type),
59
59
  interface_qname,
60
+ concrete_type,
61
+ iface_fn_type,
60
62
  )
61
63
  end
62
64
  .then { [it, Result.init(Type.unit)] }
@@ -64,7 +66,7 @@ module Jade
64
66
 
65
67
  private
66
68
 
67
- def infer_fn(impl_fn, registry, state, expected, interface_qname)
69
+ def infer_fn(impl_fn, registry, state, expected, interface_qname, head_type, sig_type)
68
70
  impl_fn => AST::ImplementationFunction(name:, fn:)
69
71
 
70
72
  case fn
@@ -83,29 +85,63 @@ module Jade
83
85
  state,
84
86
  Expected.infer(state.fresh),
85
87
  )
86
- body_state.unify(
87
- body_result.type,
88
- expected.type,
89
- &mismatch_error(state.env.entry_name, impl_fn, interface_qname, name)
90
- )
88
+ body_state
89
+ .unify(
90
+ body_result.type,
91
+ expected.type,
92
+ &mismatch_error(state.env.entry_name, impl_fn, interface_qname, name)
93
+ )
94
+ .then do |unified|
95
+ record_requirements(
96
+ unified,
97
+ body_result.constraints,
98
+ head_type,
99
+ sig_type,
100
+ interface_qname,
101
+ impl_fn,
102
+ name,
103
+ )
104
+ end
91
105
  end
92
106
 
93
107
  in AST::Lambda
94
- check(fn, registry, state, expected).first
108
+ lambda_state, lambda_result = check(fn, registry, state, expected)
109
+
110
+ record_requirements(
111
+ lambda_state,
112
+ lambda_result.constraints,
113
+ head_type,
114
+ sig_type,
115
+ interface_qname,
116
+ impl_fn,
117
+ name,
118
+ )
95
119
 
96
120
  in AST::VariableReference
97
121
  ref_state, ref_result = check(fn, registry, state, expected)
98
122
 
99
- ref_state.unify(
100
- ref_result.type,
101
- expected.type,
102
- &mismatch_error(
103
- state.env.entry_name,
104
- impl_fn,
105
- interface_qname,
106
- name
123
+ ref_state
124
+ .unify(
125
+ ref_result.type,
126
+ expected.type,
127
+ &mismatch_error(
128
+ state.env.entry_name,
129
+ impl_fn,
130
+ interface_qname,
131
+ name
132
+ )
107
133
  )
108
- )
134
+ .then do |unified|
135
+ record_requirements(
136
+ unified,
137
+ ref_result.constraints,
138
+ head_type,
139
+ sig_type,
140
+ interface_qname,
141
+ impl_fn,
142
+ name,
143
+ )
144
+ end
109
145
 
110
146
  # `decoder: zero_arg_fn` becomes `FunctionCall(zero_arg_fn, [])`
111
147
  # after ZeroArgRewrite. Type the call; its result must match the
@@ -115,6 +151,47 @@ module Jade
115
151
  end
116
152
  end
117
153
 
154
+ def record_requirements(state, constraints, head_type, sig_type, interface_qname, impl_fn, fn_name)
155
+ substitution = state.env.substitution
156
+ head_vars = substitution.apply(head_type).unbound_vars.map(&:id).to_set
157
+ names = signature_names(sig_type, substitution)
158
+
159
+ free = constraints
160
+ .map { substitution.apply(it) }
161
+ .select { it.type.is_a?(Type::Var) && !head_vars.include?(it.type.id) }
162
+ .uniq { [it.interface, it.type.id] }
163
+
164
+ free
165
+ .reject { names.key?(it.type.id) }
166
+ .map { unsatisfiable(state, impl_fn, interface_qname, fn_name, it) }
167
+ .then { state.add_errors(it) }
168
+ .require_impl(
169
+ [interface_qname, fn_name],
170
+ free.filter_map { |c| names[c.type.id]&.then { |n| [c.interface, n] } },
171
+ )
172
+ end
173
+
174
+ def signature_names(sig_type, substitution)
175
+ sig_type
176
+ .unbound_vars
177
+ .filter_map do |var|
178
+ substitution
179
+ .apply(var)
180
+ .then { it.is_a?(Type::Var) ? [it.id, var.name] : nil }
181
+ end
182
+ .to_h
183
+ end
184
+
185
+ def unsatisfiable(state, impl_fn, interface_qname, fn_name, constraint)
186
+ Error::ImplementationFunctionConstraint.new(
187
+ state.env.entry_name,
188
+ impl_fn.range,
189
+ interface: interface_qname,
190
+ fn_name:,
191
+ constraint: constraint.to_s,
192
+ )
193
+ end
194
+
118
195
  def constructor_var_in?(type, var_id)
119
196
  case type
120
197
  in Type::Application(constructor: Type::Var(id:)) if id == var_id
@@ -13,6 +13,7 @@ module Jade
13
13
  .env
14
14
  .lookup(symbol.qualified_name)
15
15
  .then { it.attach_origin(node) }
16
+ .then { state.skip_constraints ? it : attach_markers(it) }
16
17
  .then { state.unify_result(it, expected.type) }
17
18
  end
18
19
  end
@@ -17,6 +17,7 @@ module Jade
17
17
  end
18
18
  .then { state.env.lookup(it) }
19
19
  .then { it.attach_origin(node) }
20
+ .then { state.skip_constraints ? it : attach_markers(it) }
20
21
  .then { [state, it] }
21
22
  end
22
23
  end
@@ -0,0 +1,115 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Requirements
5
+ extend self
6
+
7
+ def reconcile(entry, registry, state)
8
+ pending = state
9
+ .impl_requirements
10
+ .to_h { |key, pairs| [key, pairs - declared_for(key, registry)] }
11
+ .reject { |_, pairs| pairs.empty? }
12
+
13
+ errors(entry, registry, pending)
14
+ .then { return [entry, state.add_errors(it)] if it.any? }
15
+
16
+ amended = amend_all(entry, pending)
17
+ return [entry, state] if amended.equal?(entry)
18
+
19
+ rebind(state.env, amended, registry)
20
+ .then { recheck(entry, registry, it) }
21
+ .then { |rechecked| [amended, rechecked] }
22
+ end
23
+
24
+ private
25
+
26
+ def recheck(entry, registry, env)
27
+ TypeChecking
28
+ .check_node(entry.ast, registry, State.init(env), Expected.infer(env.fresh))
29
+ .first
30
+ end
31
+
32
+ def rebind(env, entry, registry)
33
+ entry
34
+ .defined_values
35
+ .each_value
36
+ .select { it.is_a?(Symbol::InterfaceFunction) && it.constraints.any? }
37
+ .reduce(env) do |acc, sym|
38
+ Type
39
+ .from_symbol(sym, registry, acc.var_gen)
40
+ .then { Inference::Helpers.generalize(acc, *it) }
41
+ .then { acc.bind(sym.qualified_name, it) }
42
+ end
43
+ end
44
+
45
+ def declared_for((interface, fn_name), registry)
46
+ Symbol
47
+ .type_ref_from_qualified_name(interface)
48
+ .then { registry.lookup(it) }
49
+ .then { it.is_a?(Symbol::Interface) ? it.functions : [] }
50
+ .find { it.name == fn_name }
51
+ &.constraints || []
52
+ end
53
+
54
+ def errors(entry, registry, pending)
55
+ pending.flat_map do |(interface, fn_name), constraints|
56
+ declared = declared_for([interface, fn_name], registry)
57
+ next [] if declared.empty? && local?(entry, interface)
58
+
59
+ constraints.map do |iface, var|
60
+ error_for(entry, interface, fn_name, "#{iface} #{var}", declared)
61
+ end
62
+ end
63
+ end
64
+
65
+ def local?(entry, interface)
66
+ entry.defined_types.key?(interface.split('.').last)
67
+ end
68
+
69
+ def error_for(entry, interface, fn_name, constraint, declared)
70
+ return cross_module_error(entry, interface, fn_name, constraint) if declared.empty?
71
+
72
+ Error::UndeclaredRequirement.new(
73
+ entry.name,
74
+ nil,
75
+ interface:,
76
+ fn_name:,
77
+ constraint:,
78
+ declared: declared.map { |iface, var| "#{iface} #{var}" }.join(', '),
79
+ )
80
+ end
81
+
82
+ def cross_module_error(entry, interface, fn_name, constraint)
83
+ Error::CrossModuleRequirement.new(
84
+ entry.name,
85
+ nil,
86
+ interface:,
87
+ fn_name:,
88
+ constraint:,
89
+ )
90
+ end
91
+
92
+ def amend_all(entry, pending)
93
+ pending.reduce(entry) do |acc, ((interface, fn_name), constraints)|
94
+ amend_method(acc, interface, fn_name, constraints)
95
+ end
96
+ end
97
+
98
+ def amend_method(entry, interface_qname, fn_name, constraints)
99
+ symbol = entry.defined_types[interface_qname.split('.').last]
100
+ return entry unless symbol.is_a?(Symbol::Interface)
101
+
102
+ symbol
103
+ .functions
104
+ .map { it.name == fn_name ? with_constraints(it, constraints) : it }
105
+ .then { symbol.with(functions: it) }
106
+ .then { |iface| iface.functions.reduce(entry.define(iface)) { |acc, fn| acc.define(fn) } }
107
+ end
108
+
109
+ def with_constraints(fn, constraints)
110
+ fn.with(constraints: (fn.constraints + constraints).uniq)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -1,9 +1,17 @@
1
1
  module Jade
2
2
  module Frontend
3
3
  module TypeChecking
4
- State = Data.define(:env, :errors, :skip_constraints) do
4
+ State = Data.define(:env, :errors, :skip_constraints, :impl_requirements) do
5
5
  def self.init(env, skip_constraints: false)
6
- new(env, [], skip_constraints)
6
+ new(env, [], skip_constraints, {})
7
+ end
8
+
9
+ def require_impl(key, constraints)
10
+ return self if constraints.empty?
11
+
12
+ impl_requirements
13
+ .merge(key => constraints) { |_, old, new| (old + new).uniq }
14
+ .then { with(impl_requirements: it) }
7
15
  end
8
16
 
9
17
  def unify_result(result, right, rigid_vars = [], &block)
@@ -8,6 +8,7 @@ require 'jade/frontend/type_checking/expected'
8
8
  require 'jade/frontend/type_checking/inference'
9
9
  require 'jade/frontend/type_checking/loader'
10
10
  require 'jade/frontend/type_checking/port_resolution'
11
+ require 'jade/frontend/type_checking/requirements'
11
12
  require 'jade/frontend/type_checking/result'
12
13
  require 'jade/frontend/type_checking/state'
13
14
  require 'jade/frontend/type_checking/substitution'
@@ -26,11 +27,16 @@ module Jade
26
27
  .load(entry, registry)
27
28
  .then { collect_constraints(entry, registry, it) }
28
29
  .then { check_node(entry.ast, registry, State.init(it), Expected.infer(it.fresh)) }
29
- .then { finalize(*it, registry) }
30
+ .then { |state, _| Requirements.reconcile(entry, registry, state) }
31
+ .then { |amended, state| complete(amended, state, registry) }
32
+ .and_then { PortResolution.resolve(it, registry) }
33
+ end
34
+
35
+ def complete(entry, state, registry)
36
+ finalize(state, registry)
30
37
  .map { Canonicalize.run(entry.ast, it, registry) }
31
38
  .map { it.canonicalize_node_types }
32
39
  .map { entry.with(env: it) }
33
- .and_then { PortResolution.resolve(it, registry) }
34
40
  end
35
41
 
36
42
  # A collecting pass walks declarations in source order, so a call to a
@@ -76,7 +82,7 @@ module Jade
76
82
  .then { it + 1 }
77
83
  end
78
84
 
79
- def finalize(state, result, registry)
85
+ def finalize(state, registry)
80
86
  state.env => { bindings:, entry_name: }
81
87
 
82
88
  errors = bindings
data/lib/jade/parsing.rb CHANGED
@@ -753,10 +753,23 @@ module Jade
753
753
  }
754
754
 
755
755
  parser(:interface_function_decl) {
756
+ constraint =
757
+ (
758
+ constant >>
759
+ type(:lparen).skip >>
760
+ type_param >>
761
+ type(:rparen).skip
762
+ ).map(&AST.interface_constraint)
763
+
764
+ constraints =
765
+ type(:with).skip >>
766
+ at_least_one(constraint, separated_by: type(:comma).skip)
767
+
756
768
  (
757
769
  (type(:identifier) | (type(:lparen).skip >> operator >> type(:rparen).skip)) >>
758
770
  type(:colon).skip >>
759
- type_expression
771
+ type_expression >>
772
+ optional(constraints, default: []).map { [it] }
760
773
  ).map(&AST.interface_function_decl)
761
774
  }
762
775
 
@@ -6,7 +6,8 @@ module Jade
6
6
  :interface,
7
7
  :params,
8
8
  :return_type,
9
- :decl_span
9
+ :decl_span,
10
+ :constraints
10
11
  ) do
11
12
  include Base
12
13
 
data/lib/jade/symbol.rb CHANGED
@@ -134,8 +134,8 @@ module Jade
134
134
  Interface[nil, name, type_var, functions, default, span]
135
135
  end
136
136
 
137
- def interface_function(name, inteface, params, return_type, span)
138
- InterfaceFunction[nil, name, inteface, params, return_type, span]
137
+ def interface_function(name, inteface, params, return_type, span, constraints = [])
138
+ InterfaceFunction[nil, name, inteface, params, return_type, span, constraints]
139
139
  end
140
140
 
141
141
  def implementation(
data/lib/jade/type.rb CHANGED
@@ -178,9 +178,18 @@ module Jade
178
178
  constraint = Type
179
179
  .constraint(symbol.interface.qualified_name, param_type, nil)
180
180
 
181
- from_symbol_r(symbol.return_type, registry, var_gen, local_map)
182
- .then { |(t, c, _)| [args.empty? ? t : Type.function(args, t), c + arg_cs + [constraint]] }
183
- .then { it + [var_map] }
181
+ ret_type, ret_cs, full_map =
182
+ from_symbol_r(symbol.return_type, registry, var_gen, local_map)
183
+
184
+ extra_cs = symbol.constraints.map { |iface, var_name|
185
+ Type.constraint(iface, full_map.fetch(var_name), nil)
186
+ }
187
+
188
+ [
189
+ args.empty? ? ret_type : Type.function(args, ret_type),
190
+ ret_cs + arg_cs + [constraint] + extra_cs,
191
+ var_map,
192
+ ]
184
193
 
185
194
  in Symbol::Constructor if symbol.args.empty?
186
195
  from_symbol_r(symbol.parent, registry, var_gen, var_map)
data/lib/jade/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.10.1'
2
+ VERSION = '0.11.1'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jade-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agustin Cornu
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-11 00:00:00.000000000 Z
10
+ date: 2026-09-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -249,6 +249,7 @@ files:
249
249
  - lib/jade/frontend/type_checking/env.rb
250
250
  - lib/jade/frontend/type_checking/error.rb
251
251
  - lib/jade/frontend/type_checking/error/case_of_branches_type_mismatch.rb
252
+ - lib/jade/frontend/type_checking/error/cross_module_requirement.rb
252
253
  - lib/jade/frontend/type_checking/error/derivation_failed.rb
253
254
  - lib/jade/frontend/type_checking/error/display.rb
254
255
  - lib/jade/frontend/type_checking/error/division_by_zero.rb
@@ -258,6 +259,7 @@ files:
258
259
  - lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb
259
260
  - lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb
260
261
  - lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb
262
+ - lib/jade/frontend/type_checking/error/implementation_function_constraint.rb
261
263
  - lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb
262
264
  - lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb
263
265
  - lib/jade/frontend/type_checking/error/missing_implementation.rb
@@ -270,6 +272,7 @@ files:
270
272
  - lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb
271
273
  - lib/jade/frontend/type_checking/error/recursive_derivation.rb
272
274
  - lib/jade/frontend/type_checking/error/type_mismatch.rb
275
+ - lib/jade/frontend/type_checking/error/undeclared_requirement.rb
273
276
  - lib/jade/frontend/type_checking/error/unreachable_branch.rb
274
277
  - lib/jade/frontend/type_checking/error/unresolved_constraint.rb
275
278
  - lib/jade/frontend/type_checking/expected.rb
@@ -308,6 +311,7 @@ files:
308
311
  - lib/jade/frontend/type_checking/loader.rb
309
312
  - lib/jade/frontend/type_checking/placeholder.rb
310
313
  - lib/jade/frontend/type_checking/port_resolution.rb
314
+ - lib/jade/frontend/type_checking/requirements.rb
311
315
  - lib/jade/frontend/type_checking/result.rb
312
316
  - lib/jade/frontend/type_checking/scheme.rb
313
317
  - lib/jade/frontend/type_checking/state.rb