jade-lang 0.10.0 → 0.11.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/README.md +1 -0
  4. data/docs/json.md +34 -3
  5. data/lib/jade/ast.rb +15 -3
  6. data/lib/jade/cli/init.rb +86 -0
  7. data/lib/jade/cli.rb +2 -0
  8. data/lib/jade/codegen/function_call.rb +25 -10
  9. data/lib/jade/codegen/function_declaration.rb +2 -10
  10. data/lib/jade/codegen/helpers.rb +42 -0
  11. data/lib/jade/codegen/implementation.rb +33 -8
  12. data/lib/jade/formatter/leaves.rb +12 -1
  13. data/lib/jade/formatter/type.rb +3 -5
  14. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +16 -3
  15. data/lib/jade/frontend/type_checking/error/cross_module_requirement.rb +36 -0
  16. data/lib/jade/frontend/type_checking/error/implementation_function_constraint.rb +36 -0
  17. data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
  18. data/lib/jade/frontend/type_checking/error/undeclared_requirement.rb +35 -0
  19. data/lib/jade/frontend/type_checking/error.rb +4 -0
  20. data/lib/jade/frontend/type_checking/inference/function_declaration.rb +0 -5
  21. data/lib/jade/frontend/type_checking/inference/helpers.rb +9 -0
  22. data/lib/jade/frontend/type_checking/inference/implementation.rb +93 -16
  23. data/lib/jade/frontend/type_checking/inference/qualified_access.rb +1 -0
  24. data/lib/jade/frontend/type_checking/inference/record_update.rb +8 -1
  25. data/lib/jade/frontend/type_checking/inference/variable_reference.rb +1 -0
  26. data/lib/jade/frontend/type_checking/requirements.rb +115 -0
  27. data/lib/jade/frontend/type_checking/state.rb +10 -2
  28. data/lib/jade/frontend/type_checking.rb +9 -3
  29. data/lib/jade/parsing/type.rb +1 -1
  30. data/lib/jade/parsing.rb +14 -1
  31. data/lib/jade/stdlib/decode.rb +11 -0
  32. data/lib/jade/symbol/interface_function.rb +2 -1
  33. data/lib/jade/symbol/stdlib_function.rb +1 -1
  34. data/lib/jade/symbol.rb +2 -2
  35. data/lib/jade/type/function.rb +12 -13
  36. data/lib/jade/type.rb +12 -3
  37. data/lib/jade/version.rb +1 -1
  38. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba543580eec5d50cae2249c010a03f7a56a96f276705b3b87df7f25f06ff8eeb
4
- data.tar.gz: 15923caf94302e7326aef6cc8a0e581c349b7b46d54ae4898e6bd8d8529657cf
3
+ metadata.gz: b73668e7e5ab3a62df195d2f978ffcba2ef69e3a2e578eb084ab56dccfe0770f
4
+ data.tar.gz: 6d7420d31cf750127e51ad9191ac068231e92b041779ef4927d0e6feea26fd2f
5
5
  SHA512:
6
- metadata.gz: 3906343d3094e2b9142aebc9324722c584db45dfb0951d6546d917794559a8cb9249d9ff4243bb380dc383932e78b93609455743a9e349ec3b60086450693846
7
- data.tar.gz: 841c318dc43aa3e99df702eba6ffda7621b5a8c0f97e9302f2d75ef2149a36bcc19a1287d26a3cef5091c1361508d333a964f9eb70094e30a5791e8c4f8394a9
6
+ metadata.gz: 0036c6f868f892ff83938bfbf5eda69531d4a4fa11aa1abd27978efafddc936bb64320acd6a181caa0b2f55cccb70957c9ba80aad833584857a4b51dc1083ed1
7
+ data.tar.gz: 95940e59148590400e15d252ed8bbb4a7a6b67cd3febbc481a8b176fbb373afd2e20069e3a91541af52aafbf510baa6d1c9fb3f5cd2277b76adaff07015a75e8
data/CHANGELOG.md CHANGED
@@ -4,6 +4,84 @@ 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.0] - 2026-09-15
8
+
9
+ ### Added
10
+
11
+ - **An interface method may require an interface of another type.** A method
12
+ can mention a type variable the interface itself does not name, and say what
13
+ that variable has to satisfy:
14
+
15
+ interface Fetchable(x) with
16
+ projection : x -> Sel(a) with Selectable(a)
17
+ end
18
+
19
+ `a` is settled where `projection` is called, not where the implementation is
20
+ picked, so each call site resolves the requirement against its own types and
21
+ passes that dictionary alongside the one for `x`. Left undeclared, the
22
+ requirement is inferred from the implementations instead. Declared and
23
+ inferred are reconciled rather than one overriding the other: an
24
+ implementation may require what the method declares, and anything beyond it
25
+ is refused by name.
26
+
27
+ Two cases stay refused. A requirement *inferred* on an interface from
28
+ another module, because a declaration is what travels with the interface and
29
+ there is no foreign entry to amend — declare it and it works across modules.
30
+ And a requirement on a variable the method's signature never names, which
31
+ nothing could satisfy.
32
+
33
+ ### Fixed
34
+
35
+ - **A bare reference to a zero-arg interface method resolves through its
36
+ dictionary.** Dictionaries were only ever attached where a method was called
37
+ or passed as an argument, so a function body that was just `selector` — or
38
+ `Module.selector` — compiled to that name as a Ruby method and died on the
39
+ first call. It now reads the enclosing function's dictionary, or is a compile
40
+ error where none is in scope. Code that was silently broken this way may stop
41
+ compiling.
42
+
43
+ ## [0.10.1] - 2026-09-11
44
+
45
+ ### Added
46
+
47
+ - **`Decode.decoder`, the derived decoder as a value.** `Decode.from_json` has
48
+ always picked a decoder off the return type, but there was no way to name
49
+ that decoder and hand it to a combinator — every project ended up with a
50
+ `uuid_decoder` and a `date_decoder` per module, reimplementing an instance
51
+ that already existed. `Decode.field("on", Decode.decoder)` now resolves the
52
+ `Decodable` instance from the position the decoder stands in, including
53
+ derived ones, and a type without an instance is a compile error naming the
54
+ type.
55
+
56
+ - **`jade init`.** Writes `jade.json`, which is how every tool that runs
57
+ outside the app finds your sources, creates the source directory, and adds
58
+ `.jade/` to a `.gitignore` that has one. It refuses to overwrite an existing
59
+ manifest rather than discarding a source root and extension list that cannot
60
+ be recovered from the directory. Until now the first thing a new project met
61
+ was the error telling it to write that file by hand.
62
+
63
+ ### Fixed
64
+
65
+ - **A function type printed as `(a) -> b`, which means something else.**
66
+ `Type::Function#to_s` parenthesised its parameters unconditionally, so
67
+ `jade q api` printed `List.fold : (List(a), b, (b, a) -> b) -> b`, a
68
+ signature whose outer parentheses read as a tuple argument. It prints the
69
+ bare form now, `List(a), b, (b, a -> b) -> b`, which is what the formatter
70
+ writes and what you can paste back into a file.
71
+
72
+ Parentheses around a comma list are a tuple everywhere, and a parameter
73
+ list is bare. A nested function type keeps parentheses of its own, told
74
+ apart from a tuple by the arrow inside them. `(Int) -> Int` also parses
75
+ now, which it did not: one element is not a tuple.
76
+
77
+ - **A record update that does not fit crashed the compiler.** The unification
78
+ that closes an update was the one call with no error block, and
79
+ `State#unify` calls the block unconditionally when unification fails, so a
80
+ mismatch reached `nil.call` and took the compiler out with a `NoMethodError`
81
+ naming a line in `state.rb`. Changing a field's type is the usual way in;
82
+ anything the surrounding code cannot accept arrives the same way. It now
83
+ reads `This update produces Box(Int), but String was expected`.
84
+
7
85
  ## [0.10.0] - 2026-09-02
8
86
 
9
87
  ### Breaking
data/README.md CHANGED
@@ -348,6 +348,7 @@ for us so far.
348
348
  A single `jade` binary fronts the toolchain:
349
349
 
350
350
  ```
351
+ jade init # write jade.json and the source directory
351
352
  jade check [file...] # type-check; exits 1 on errors, generates nothing
352
353
  jade fmt [-i|-c] [file] # format .jd source (stdin or file)
353
354
  jade lsp # language server over stdio (hover, defn, refs, diagnostics)
data/docs/json.md CHANGED
@@ -61,9 +61,23 @@ DecodeJson::Internal.user('{"name":"Ada"}')
61
61
  # => Err(MissingField("age"))
62
62
  ```
63
63
 
64
- The struct decoder is `Decode.succeed(User(_, _))` piped through one
65
- `Decode.required` per field — the `_` placeholders are the constructor's holes,
66
- filled left to right as each field decodes.
64
+ The struct decoder is `Decode.succeed(User(_, _))` piped through one step per
65
+ field — the `_` placeholders are the constructor's holes, filled left to right
66
+ as each field decodes. `Decode.required` fails on a missing key;
67
+ `Decode.optional` takes what a missing key stands for instead, which is a
68
+ `Maybe` when that is the field's type:
69
+
70
+ ```jade
71
+ Decode.succeed(Note(_, _, _))
72
+ |> Decode.required("body", Decode.nullable(Decode.string))
73
+ |> Decode.optional("kind", Decode.string, "note")
74
+ |> Decode.optional("archived_on", Decode.map(Decode.decoder, Just), Nothing)
75
+ ```
76
+
77
+ A nullable field is a required one: the key has to be there, and `null` is a
78
+ value the decoder admits. Pick the step at the field — once a step has run the
79
+ pipeline holds the constructor's remaining arguments, so nothing downstream
80
+ reaches back into one field.
67
81
 
68
82
  ## Encoding
69
83
 
@@ -146,6 +160,23 @@ end
146
160
  Reach for the explicit combinators above when the JSON shape doesn't match the
147
161
  struct one-to-one — renamed keys, nested lookups, optional fields.
148
162
 
163
+ Those combinators still take derived decoders for the leaves. `Decode.decoder`
164
+ is the instance for whatever type is expected of it, so a hand-built shape can
165
+ be filled with types that already know how to read themselves:
166
+
167
+ ```jade
168
+ def decoder -> Decoder(Movement)
169
+ Decode.succeed(Movement(_, _, _))
170
+ |> Decode.and_map(Decode.field("from_id", Decode.decoder))
171
+ |> Decode.and_map(Decode.field("to_id", Decode.decoder))
172
+ |> Decode.and_map(Decode.field("occurred_on", Decode.decoder))
173
+ end
174
+ ```
175
+
176
+ Two of those fields are `Uuid` and one is a `Date`; each resolves from the
177
+ position it stands in. A type with no instance is a compile error naming the
178
+ type, not a decoder that fails at runtime.
179
+
149
180
  Derivation reaches through the structural types to their elements, so anything
150
181
  built out of encodable parts is itself encodable:
151
182
 
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
@@ -0,0 +1,86 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+
4
+ require 'jade'
5
+
6
+ module Jade
7
+ module CLI
8
+ # Writes the manifest every tool outside the app reads. Without one,
9
+ # the CLI and the language server have to guess where the sources are,
10
+ # which is why `Project::NotFound` says to write this file by hand.
11
+ module Init
12
+ module_function
13
+
14
+ IGNORED = '.jade/'.freeze
15
+
16
+ def run(argv)
17
+ usage if argv.any? { it == '-h' || it == '--help' }
18
+
19
+ source_root(argv)
20
+ .then { [it, File.expand_path(Project::MANIFEST)] }
21
+ .then { |root, manifest| write(root, manifest) }
22
+ end
23
+
24
+ def write(root, manifest)
25
+ refuse(manifest) if File.exist?(manifest)
26
+
27
+ File.write(manifest, "#{JSON.pretty_generate(config(root))}\n")
28
+ FileUtils.mkdir_p(root)
29
+
30
+ report(root, manifest)
31
+ end
32
+
33
+ # Only what the defaults do not already say. A manifest naming every
34
+ # setting reads as though each were a decision.
35
+ def config(root)
36
+ { 'source_roots' => [root], 'extensions' => [] }
37
+ end
38
+
39
+ def source_root(argv)
40
+ argv
41
+ .each_cons(2)
42
+ .find { |flag, _| flag == '--source-root' }
43
+ &.last || Project::DEFAULTS[:source_roots].first
44
+ end
45
+
46
+ def refuse(manifest)
47
+ warn "jade: #{File.basename(manifest)} already exists in #{File.dirname(manifest)}"
48
+ exit 1
49
+ end
50
+
51
+ def report(root, manifest)
52
+ puts <<~TXT
53
+ Wrote #{File.basename(manifest)} and #{root}/.
54
+
55
+ Put a module in #{root}/, then:
56
+
57
+ jade check type-check it
58
+ jade fmt #{root}/x.jd#{' ' * [0, 7 - root.length].max} format it
59
+
60
+ #{gitignore_note}
61
+ TXT
62
+ end
63
+
64
+ # Build and cache output, which nobody wants in a diff.
65
+ def gitignore_note
66
+ path = File.expand_path('.gitignore')
67
+ return "Add #{IGNORED} to .gitignore." unless File.exist?(path)
68
+ return "#{IGNORED} is already ignored." if File.read(path).match?(/^\.jade\b/)
69
+
70
+ File.write(path, "#{File.read(path).chomp}\n#{IGNORED}\n")
71
+ "Added #{IGNORED} to .gitignore."
72
+ end
73
+
74
+ def usage
75
+ warn <<~USAGE
76
+ Usage: jade init [--source-root DIR]
77
+
78
+ Writes jade.json, which is how every tool outside the app finds
79
+ your sources, and creates the source directory. Refuses to
80
+ overwrite an existing manifest.
81
+ USAGE
82
+ exit 1
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/jade/cli.rb CHANGED
@@ -6,6 +6,7 @@ module Jade
6
6
  'check' => 'Check',
7
7
  'eject' => 'Eject',
8
8
  'fmt' => 'Fmt',
9
+ 'init' => 'Init',
9
10
  'lsp' => 'Lsp',
10
11
  'q' => 'Q',
11
12
  }.freeze
@@ -40,6 +41,7 @@ module Jade
40
41
  check Type-check the project (or the given files).
41
42
  eject Write the project as Ruby that runs without the gem.
42
43
  fmt Format .jd source (stdin or file).
44
+ init Write jade.json and the source directory.
43
45
  lsp Run the language server (stdio JSON-RPC).
44
46
  q Headless query interface (hover/symbols/defn/refs/api).
45
47
 
@@ -16,6 +16,9 @@ module Jade
16
16
  Inline.try_for(callee, args, dictionaries, registry)
17
17
  .then { return it if it }
18
18
 
19
+ constrained_constant(callee, dictionaries, registry)
20
+ .then { return it if it }
21
+
19
22
  return constructor_call(callee, args, registry) if constructor_callee?(callee, registry)
20
23
 
21
24
  [generate_many(args, registry), generate_dict_args(callee, dictionaries, registry)]
@@ -24,6 +27,18 @@ module Jade
24
27
  .then { "#{generate_callee(callee, args, registry, dictionaries)}#{invocation_op(callee, registry)}(#{it})" }
25
28
  end
26
29
 
30
+ # A constrained constant — `Decode.decoder` — has no parameters, so its
31
+ # dictionary slot holds the value itself rather than something to call.
32
+ # generate_callee already produced that value; invoking it would be
33
+ # calling a Decoder.
34
+ def constrained_constant(callee, dictionaries, registry)
35
+ symbol = resolve_callee_symbol(callee, registry)
36
+ return nil unless symbol.is_a?(Symbol::StdlibFunction)
37
+ return nil unless symbol.params.empty? && symbol.constraints.any?
38
+
39
+ generate_callee(callee, [], registry, dictionaries)
40
+ end
41
+
27
42
  def constructor_call(callee, args, registry)
28
43
  resolve_callee_symbol(callee, registry)
29
44
  .then { "::#{to_qualified(it.qualified_name)}" }
@@ -231,15 +246,6 @@ module Jade
231
246
  end
232
247
  end
233
248
 
234
- # When a user fn has var-typed constraints, two definitions are emitted:
235
- # `name` (Ruby-boundary wrapper, no dicts) and `__name__impl__` (takes
236
- # dicts). Jade-internal calls target the latter.
237
- def fn_target_name(fn_sym, registry)
238
- return fn_sym.name if dict_constraints(fn_sym, registry).empty?
239
-
240
- fn_impl_synthetic_name(fn_sym.name)
241
- end
242
-
243
249
  # Returns the list of dict args to pass after regular args. Only
244
250
  # Symbol::Function callees take dict params; other branches dispatch
245
251
  # via `dictionaries` directly inside generate_callee. dictionaries are
@@ -252,6 +258,7 @@ module Jade
252
258
  else callee.symbol
253
259
  end
254
260
 
261
+ return interface_dict_args(symbol, dictionaries, registry) if symbol.is_a?(Symbol::InterfaceFunction)
255
262
  return "" unless symbol.is_a?(Symbol::Function)
256
263
 
257
264
  fn_constraints(symbol, registry)
@@ -260,6 +267,14 @@ module Jade
260
267
  .join(', ')
261
268
  end
262
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
+
263
278
  # Ruby-block intrinsics (Dict's `Eq k`, etc.) ignore dispatches — the
264
279
  # `in String` body of `generate_impl_fn` drops them.
265
280
  def dispatch_dict(entry, registry)
@@ -341,7 +356,7 @@ module Jade
341
356
  "#{internal(fn.module_name)}.#{fn.name}"
342
357
 
343
358
  in Symbol::Function => fn
344
- "#{internal(fn.module_name)}.method(:#{fn.name})"
359
+ "#{internal(fn.module_name)}.method(:#{fn_target_name(fn, registry)})"
345
360
  end
346
361
  end
347
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
@@ -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
@@ -26,11 +26,9 @@ module Jade
26
26
  end
27
27
 
28
28
  in AST::TypeFunction(params:, return_type:)
29
- params_str = params.empty? ?
30
- "()" :
31
- params.map { format_atom(it) }.join(', ')
32
-
33
- "#{params_str} -> #{format_atom(return_type)}"
29
+ params
30
+ .then { it.empty? ? '()' : it.map { |p| format_atom(p) }.join(', ') }
31
+ .then { "#{it} -> #{format_atom(return_type)}" }
34
32
 
35
33
  in AST::TypeRecord(fields:, row_var:)
36
34
  fields_str = fields.map { |k, v| "#{k}: #{format(v)}" }.join(", ")
@@ -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