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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +1 -0
- data/docs/json.md +34 -3
- data/lib/jade/ast.rb +15 -3
- data/lib/jade/cli/init.rb +86 -0
- data/lib/jade/cli.rb +2 -0
- data/lib/jade/codegen/function_call.rb +25 -10
- data/lib/jade/codegen/function_declaration.rb +2 -10
- data/lib/jade/codegen/helpers.rb +42 -0
- data/lib/jade/codegen/implementation.rb +33 -8
- data/lib/jade/formatter/leaves.rb +12 -1
- data/lib/jade/formatter/type.rb +3 -5
- data/lib/jade/frontend/forward_declaration/interface_declaration.rb +16 -3
- data/lib/jade/frontend/type_checking/error/cross_module_requirement.rb +36 -0
- data/lib/jade/frontend/type_checking/error/implementation_function_constraint.rb +36 -0
- data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
- data/lib/jade/frontend/type_checking/error/undeclared_requirement.rb +35 -0
- data/lib/jade/frontend/type_checking/error.rb +4 -0
- data/lib/jade/frontend/type_checking/inference/function_declaration.rb +0 -5
- data/lib/jade/frontend/type_checking/inference/helpers.rb +9 -0
- data/lib/jade/frontend/type_checking/inference/implementation.rb +93 -16
- data/lib/jade/frontend/type_checking/inference/qualified_access.rb +1 -0
- data/lib/jade/frontend/type_checking/inference/record_update.rb +8 -1
- data/lib/jade/frontend/type_checking/inference/variable_reference.rb +1 -0
- data/lib/jade/frontend/type_checking/requirements.rb +115 -0
- data/lib/jade/frontend/type_checking/state.rb +10 -2
- data/lib/jade/frontend/type_checking.rb +9 -3
- data/lib/jade/parsing/type.rb +1 -1
- data/lib/jade/parsing.rb +14 -1
- data/lib/jade/stdlib/decode.rb +11 -0
- data/lib/jade/symbol/interface_function.rb +2 -1
- data/lib/jade/symbol/stdlib_function.rb +1 -1
- data/lib/jade/symbol.rb +2 -2
- data/lib/jade/type/function.rb +12 -13
- data/lib/jade/type.rb +12 -3
- data/lib/jade/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b73668e7e5ab3a62df195d2f978ffcba2ef69e3a2e578eb084ab56dccfe0770f
|
|
4
|
+
data.tar.gz: 6d7420d31cf750127e51ad9191ac068231e92b041779ef4927d0e6feea26fd2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
40
|
-
|
|
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
|
data/lib/jade/codegen/helpers.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
data/lib/jade/formatter/type.rb
CHANGED
|
@@ -26,11 +26,9 @@ module Jade
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
in AST::TypeFunction(params:, return_type:)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
.
|
|
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
|