jade-lang 0.3.0 → 0.4.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 +91 -1
- data/README.md +12 -11
- data/lib/jade/codegen/function_call.rb +1 -1
- data/lib/jade/codegen/inlines.rb +13 -0
- data/lib/jade/codegen/{port_decoder.rb → port_codec.rb} +27 -15
- data/lib/jade/codegen.rb +7 -3
- data/lib/jade/debug.rb +59 -0
- data/lib/jade/diagnostics/renderer.rb +16 -5
- data/lib/jade/frontend/forward_declaration/interop_import_declaration.rb +13 -3
- data/lib/jade/frontend/pattern_analysis/matrix.rb +57 -23
- data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +33 -7
- data/lib/jade/frontend/semantic_analysis/error/constructor_not_found.rb +20 -5
- data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +25 -17
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +25 -34
- data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +51 -175
- data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +133 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/show.rb +186 -0
- data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
- data/lib/jade/frontend/type_checking/env.rb +3 -0
- data/lib/jade/frontend/type_checking/error/port_not_encodable.rb +38 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +64 -16
- data/lib/jade/interop/runtime.rb +10 -2
- data/lib/jade/module_loader.rb +27 -3
- data/lib/jade/parsing/error.rb +19 -0
- data/lib/jade/runtime.rb +1 -0
- data/lib/jade/stdlib/debug.rb +12 -0
- data/lib/jade/stdlib/decode.rb +22 -0
- data/lib/jade/stdlib/encode.rb +17 -0
- data/lib/jade/stdlib/intrinsics.rb +3 -0
- data/lib/jade/stdlib/show.rb +40 -0
- data/lib/jade/stdlib.rb +5 -2
- data/lib/jade/symbol/interop_function.rb +3 -1
- data/lib/jade/symbol.rb +2 -2
- data/lib/jade/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cb9527b610f268fc9ae7d784abcbf41018cf99f91b0bdaea462a655a8f12bfb
|
|
4
|
+
data.tar.gz: d888ed1e08771d41147879b5576c0b894f94795af1e4f849f6ca31044661366b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95f7b32d5542bb1d386d18a7e1e8749de7a7f5c6ecfffaacee351dcb0d4205d698269954fe4fc023f2fed3cc7fe9f32c0f6344f9415770916ea6237be3f16e8f
|
|
7
|
+
data.tar.gz: 897851f20e95afe86c92a7c4fe104299b46a4239644f06ea26c5580f5d47660834560e5c218336efc3d569f56e5754b8c68e45b1e68524166d347bdf5d178bb6
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,94 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0]
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Port arguments are encoded on the way out**, the mirror of the return value
|
|
14
|
+
being decoded on the way back. A port declared `Instant, Int -> Task(Instant,
|
|
15
|
+
Never)` used to hand Ruby a live `Jade::Clock::Instant` while demanding an ISO
|
|
16
|
+
string back; both sides are now the wire form. **Every argument type needs an
|
|
17
|
+
`Encodable` instance** — a port taking one without it is a compile error
|
|
18
|
+
(``Port `shift` cannot encode argument 1 (`Shape`): no Encodable instance``)
|
|
19
|
+
rather than a value leaking Jade's internals into Ruby. Declare the argument
|
|
20
|
+
as `Decode.Value` to opt out and pass it untouched. A port whose arguments are
|
|
21
|
+
primitives is unaffected; one taking a struct, a `Clock`/`Calendar`/`Decimal`
|
|
22
|
+
type, or a type variable now receives encoded data, so its Ruby side needs
|
|
23
|
+
updating.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `Show`, an interface rendering a value the way Jade writes it: `show(B(2))`
|
|
28
|
+
is `"B(2)"`, `show(Point(3, 4))` is `"Point { x: 3, y: 4 }"`, a list shows
|
|
29
|
+
through its elements. Instances ship for the primitives and derive for
|
|
30
|
+
unions, structs and records — including a variant that mixes a type
|
|
31
|
+
parameter with a concrete type. A function shows as `<function>` rather than
|
|
32
|
+
refusing. `Never` has an instance too, so an annotated `Result(a, Never)` —
|
|
33
|
+
what a port-free task returns — is showable; it raises rather than printing
|
|
34
|
+
a plausible string, since it has no values to render.
|
|
35
|
+
- `Debug.log(label, value)` prints `label: value` to stderr and returns the
|
|
36
|
+
value untouched, so it drops into a pipeline without changing it.
|
|
37
|
+
- `Encodable` and `Decodable` derive for tuples, `Dict` and `Set`, from their
|
|
38
|
+
elements' instances — the same way `List` and `Maybe` already did. The
|
|
39
|
+
combinators (`Encode.tuple`, `Decode.dict`, …) existed all along; nothing
|
|
40
|
+
resolved them, so a `(String, Int)` port argument or a `Task(Dict(k, v), e)`
|
|
41
|
+
arm was rejected for want of an instance. A tuple crosses as a positional
|
|
42
|
+
array, a dict as `[key, value]` pairs, a set as an array that drops
|
|
43
|
+
duplicates on the way back. Adds the two functions the set instances needed,
|
|
44
|
+
`Encode.set` and `Decode.set`.
|
|
45
|
+
- `Encodable` and `Decodable` instances for `Decode.Value`, both the identity,
|
|
46
|
+
plus the `Decode.value` and `Encode.value` functions behind them. `Value` is
|
|
47
|
+
the un-decoded value, and it opted out of conversion only as a whole arm or
|
|
48
|
+
argument: `List(Value)`, `Maybe(Value)`, and a struct with a `Value` field
|
|
49
|
+
had no instance to derive from and were rejected at the boundary.
|
|
50
|
+
- A parse error inside a `uses`, `interface`, or `implements` block where the
|
|
51
|
+
entries are separated by newlines instead of commas says so, instead of only
|
|
52
|
+
reporting the next entry as an unexpected token.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- `Eq` derives for a union whose variants carry concrete types. `B(1) == B(1)`
|
|
57
|
+
over `type Box = B(Int) | Empty` took the compiler down with a
|
|
58
|
+
`NoMatchingPatternError`; `Just(1) == Just(1)` only worked because `Maybe`'s
|
|
59
|
+
payload is a type parameter. Variants mixing a parameter with a concrete
|
|
60
|
+
type derive too.
|
|
61
|
+
- Diagnostic source excerpts are sliced by byte offset. Spans and line starts
|
|
62
|
+
are the lexer's byte offsets, but the excerpt was cut and measured in
|
|
63
|
+
characters, so one multi-byte glyph anywhere earlier in the file shifted every
|
|
64
|
+
later excerpt and its caret — `def notify_channel(…)` printed as
|
|
65
|
+
`f notify_channel(…)`.
|
|
66
|
+
- Exhaustiveness checking no longer collapses when the scrutinee is a tuple.
|
|
67
|
+
`case (a, b)` over two `Maybe`s reported `Missing cases: (_, _)` with all four
|
|
68
|
+
arms present, and the `else` that silenced it hid genuinely missing cases from
|
|
69
|
+
then on. Two bugs: a tuple's element types were replaced with fresh type
|
|
70
|
+
variables, and specializing on a constructor dropped the types of every column
|
|
71
|
+
after it. A constructor the first column never mentions is now answered from
|
|
72
|
+
the rows that would have covered it, which is also what keeps a recursive type
|
|
73
|
+
from expanding forever — the recursion guard that used to do that job is gone,
|
|
74
|
+
and with it the false "exhaustive" verdicts it caused on any second column.
|
|
75
|
+
- The missing-constructor diagnostic tells you which `exposing` list is missing
|
|
76
|
+
the `(..)`. When the defining module already exposes `Route(..)` and it's the
|
|
77
|
+
importer's `import Routes exposing (Route)` that brought in the type alone, it
|
|
78
|
+
no longer sends you to the wrong file. It also fires for a variant whose name
|
|
79
|
+
differs from its type's (`Home` of `Route`), which previously got no hint.
|
|
80
|
+
|
|
81
|
+
## [0.3.1]
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- A module whose dependency failed to compile is no longer type checked. It
|
|
86
|
+
reached for bindings the broken module never produced and raised through the
|
|
87
|
+
compiler, naming the importer rather than the module at fault, and the
|
|
88
|
+
original error was never reported. Dependents now say which dependency
|
|
89
|
+
failed, and modules broken independently of each other are all still
|
|
90
|
+
reported.
|
|
91
|
+
- The load-path line at the top of every compiled entry module appends the
|
|
92
|
+
application's `lib` instead of prepending it. At position 0 it shadowed, for
|
|
93
|
+
the whole process, any gem sharing a name with a directory under `lib` —
|
|
94
|
+
`sidekiq/`, `sentry/`, `warden/`. The line exists so `uses App::Thing` can
|
|
95
|
+
resolve `lib/app/thing.rb`; it never needed to outrank gems.
|
|
96
|
+
|
|
9
97
|
## [0.3.0]
|
|
10
98
|
|
|
11
99
|
### Changed
|
|
@@ -99,7 +187,9 @@ Initial release.
|
|
|
99
187
|
- `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
|
|
100
188
|
- Language server and headless query interface for editor/agent tooling.
|
|
101
189
|
|
|
102
|
-
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.
|
|
190
|
+
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.4.0...HEAD
|
|
191
|
+
[0.4.0]: https://github.com/agustinrhcp/jade/compare/v0.3.1...v0.4.0
|
|
192
|
+
[0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
|
|
103
193
|
[0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
|
|
104
194
|
[0.2.0]: https://github.com/agustinrhcp/jade/compare/v0.1.1...v0.2.0
|
|
105
195
|
[0.1.1]: https://github.com/agustinrhcp/jade/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
|
@@ -148,7 +148,7 @@ at compile time, no annotation needed. You can define your own, with
|
|
|
148
148
|
implementations dispatched by type:
|
|
149
149
|
|
|
150
150
|
```jade
|
|
151
|
-
module
|
|
151
|
+
module Labels exposing (person_label)
|
|
152
152
|
|
|
153
153
|
struct Person = {
|
|
154
154
|
name: String,
|
|
@@ -156,18 +156,18 @@ struct Person = {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
interface
|
|
160
|
-
|
|
159
|
+
interface Labelled(a) with
|
|
160
|
+
label : a -> String
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
implements
|
|
165
|
-
|
|
164
|
+
implements Labelled(Person) with
|
|
165
|
+
label: (p) -> { p.name ++ " (" ++ String.from_int(p.age) ++ ")" }
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
def
|
|
170
|
-
|
|
169
|
+
def person_label(p: Person) -> String
|
|
170
|
+
label(p)
|
|
171
171
|
end
|
|
172
172
|
```
|
|
173
173
|
|
|
@@ -357,8 +357,9 @@ pre-commit hook.
|
|
|
357
357
|
## Standard library
|
|
358
358
|
|
|
359
359
|
`Basics`, `String`, `Char`, `List`, `Dict`, `Set`, `Tuple`, `Maybe`, `Result`,
|
|
360
|
-
`Task`, `Decode`, `Encode`, `Bytes`, `Calendar`, `Clock`, `Decimal
|
|
361
|
-
compile inline rather than through a runtime
|
|
360
|
+
`Task`, `Decode`, `Encode`, `Bytes`, `Calendar`, `Clock`, `Decimal`, `Show`,
|
|
361
|
+
`Debug`. Stdlib operations compile inline rather than through a runtime
|
|
362
|
+
dispatch layer.
|
|
362
363
|
|
|
363
364
|
## Docs
|
|
364
365
|
|
|
@@ -374,8 +375,8 @@ compile inline rather than through a runtime dispatch layer.
|
|
|
374
375
|
|
|
375
376
|
Early and experimental — being tried out on small projects.
|
|
376
377
|
|
|
377
|
-
**In progress:** `Comparable`
|
|
378
|
-
|
|
378
|
+
**In progress:** `Comparable` derivation for user types, partial record types
|
|
379
|
+
in signatures, a stable REPL.
|
|
379
380
|
|
|
380
381
|
**Not great for:** throwaway scripts, libraries you ship to other Ruby
|
|
381
382
|
projects (they'd inherit the dependency), and performance-critical hot paths
|
|
@@ -180,7 +180,7 @@ module Jade
|
|
|
180
180
|
in Symbol::InteropFunction
|
|
181
181
|
registry
|
|
182
182
|
.lookup(callee.symbol.to_ref)
|
|
183
|
-
.then {
|
|
183
|
+
.then { PortCodec.task_call(it, registry, dictionaries) }
|
|
184
184
|
|
|
185
185
|
in Symbol::StdlibFunction => symbol if symbol.constraints.any?
|
|
186
186
|
dictionaries
|
data/lib/jade/codegen/inlines.rb
CHANGED
|
@@ -16,6 +16,15 @@ module Jade
|
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
18
|
INLINES = {
|
|
19
|
+
'Debug.log' => ->(l, v) { "Jade::Debug.log(#{l}, #{v})" },
|
|
20
|
+
'Show.int_show' => ->(n) { "#{n}.to_s" },
|
|
21
|
+
'Show.float_show' => ->(f) { "#{f}.to_s" },
|
|
22
|
+
'Show.bool_show' => ->(b) { "#{b}.to_s" },
|
|
23
|
+
'Show.str_show' => ->(s) { "#{s}.inspect" },
|
|
24
|
+
'Show.char_show' => ->(c) { "#{c}.inspect" },
|
|
25
|
+
# Never is uninhabited, so reaching this is a compiler bug, not a
|
|
26
|
+
# rendering problem — say so rather than print a plausible string.
|
|
27
|
+
'Show.never_show' => ->(_) { 'fail("Show.never_show: Never has no values")' },
|
|
19
28
|
'Basics.identity' => ->(a) { a },
|
|
20
29
|
'Basics.always' => ->(x) { "->(_) { #{x} }" },
|
|
21
30
|
'Basics.int_add' => ->(a, b) { "(#{a} + #{b})" },
|
|
@@ -191,6 +200,7 @@ module Jade
|
|
|
191
200
|
Decode.optional_field
|
|
192
201
|
Decode.required
|
|
193
202
|
Decode.sequence
|
|
203
|
+
Decode.set
|
|
194
204
|
Decode.string
|
|
195
205
|
Decode.string_enum
|
|
196
206
|
Decode.succeed
|
|
@@ -198,6 +208,7 @@ module Jade
|
|
|
198
208
|
Decode.tuple3
|
|
199
209
|
Decode.tuple4
|
|
200
210
|
Decode.type_
|
|
211
|
+
Decode.value
|
|
201
212
|
Decode.variant
|
|
202
213
|
Encode.bool
|
|
203
214
|
Encode.dict
|
|
@@ -209,10 +220,12 @@ module Jade
|
|
|
209
220
|
Encode.null
|
|
210
221
|
Encode.nullable
|
|
211
222
|
Encode.object
|
|
223
|
+
Encode.set
|
|
212
224
|
Encode.string
|
|
213
225
|
Encode.tuple
|
|
214
226
|
Encode.tuple3
|
|
215
227
|
Encode.tuple4
|
|
228
|
+
Encode.value
|
|
216
229
|
Encode.variant
|
|
217
230
|
Task.and_then
|
|
218
231
|
Task.fail
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module Codegen
|
|
3
|
-
module
|
|
3
|
+
module PortCodec
|
|
4
4
|
extend self
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
PASS_DECODER = "Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]"
|
|
7
|
+
PASS_ENCODER = "->(v) { v }"
|
|
7
8
|
|
|
8
9
|
# `dictionaries` is the call site's constraint-attachment list; only
|
|
9
|
-
# used when an arm is a Dict marker.
|
|
10
|
+
# used when an arm or a parameter is a Dict marker.
|
|
10
11
|
def task_call(interop_fn, registry, dictionaries = [])
|
|
11
12
|
[
|
|
12
13
|
interop_fn.interop_module_name,
|
|
13
14
|
":#{interop_fn.name}",
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
codec(interop_fn.decoders.fetch(:ok), 'decoder', interop_fn, registry, dictionaries),
|
|
16
|
+
codec(interop_fn.decoders.fetch(:err), 'decoder', interop_fn, registry, dictionaries),
|
|
17
|
+
encoders(interop_fn, registry, dictionaries),
|
|
16
18
|
]
|
|
17
19
|
.join(', ')
|
|
18
20
|
.then { "Jade::Runtime.task_call(#{it})" }
|
|
@@ -20,32 +22,42 @@ module Jade
|
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def encoders(interop_fn, registry, dictionaries)
|
|
26
|
+
interop_fn
|
|
27
|
+
.encoders
|
|
28
|
+
.map { codec(it, 'encoder', interop_fn, registry, dictionaries) }
|
|
29
|
+
.join(', ')
|
|
30
|
+
.then { "[#{it}]" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Per-arm, per-argument codec. PortResolution stamps one of:
|
|
34
|
+
# - :pass — Decode.Value or Never, nothing to convert
|
|
25
35
|
# - Symbol::Implementation — concrete OR partial impl (with marker
|
|
26
36
|
# deps for free vars). We push a synthetic dict_env mapping the
|
|
27
37
|
# port's vars to the call-site dictionaries so generate_impl_dispatch
|
|
28
38
|
# can resolve markers uniformly.
|
|
29
|
-
# - Symbol::InteropFunction::Dict — bare-var
|
|
39
|
+
# - Symbol::InteropFunction::Dict — bare-var position; the codec comes
|
|
30
40
|
# from the caller's threaded dictionary at the given constraint index
|
|
31
|
-
def
|
|
32
|
-
case
|
|
41
|
+
def codec(resolution, kind, interop_fn, registry, dictionaries)
|
|
42
|
+
case resolution
|
|
33
43
|
in Symbol::InteropFunction::PASS
|
|
34
|
-
|
|
44
|
+
kind == 'decoder' ? PASS_DECODER : PASS_ENCODER
|
|
35
45
|
|
|
36
46
|
in Symbol::Implementation => impl
|
|
37
|
-
|
|
47
|
+
emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
|
|
38
48
|
|
|
39
49
|
in Symbol::InteropFunction::Dict(constraint_index:)
|
|
40
|
-
|
|
50
|
+
FunctionCall
|
|
51
|
+
.dispatch_value(dictionaries.fetch(constraint_index), registry)
|
|
52
|
+
.then { "#{it}[#{kind.inspect}]" }
|
|
41
53
|
end
|
|
42
54
|
end
|
|
43
55
|
|
|
44
|
-
def
|
|
56
|
+
def emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
|
|
45
57
|
synthetic_env = build_synthetic_dict_env(impl, interop_fn, dictionaries, registry)
|
|
46
58
|
|
|
47
59
|
Codegen.with_dict_env(synthetic_env) {
|
|
48
|
-
FunctionCall.generate_impl_dispatch(impl, registry).fetch(
|
|
60
|
+
FunctionCall.generate_impl_dispatch(impl, registry).fetch(kind)
|
|
49
61
|
}
|
|
50
62
|
end
|
|
51
63
|
|
data/lib/jade/codegen.rb
CHANGED
|
@@ -19,7 +19,7 @@ require 'jade/codegen/transforms/fold_shape'
|
|
|
19
19
|
require 'jade/codegen/function_declaration'
|
|
20
20
|
require 'jade/codegen/function_call'
|
|
21
21
|
require 'jade/codegen/implementation'
|
|
22
|
-
require 'jade/codegen/
|
|
22
|
+
require 'jade/codegen/port_codec'
|
|
23
23
|
|
|
24
24
|
module Jade
|
|
25
25
|
module Codegen
|
|
@@ -159,7 +159,7 @@ module Jade
|
|
|
159
159
|
in Symbol::InteropFunction => sym
|
|
160
160
|
registry
|
|
161
161
|
.lookup(sym.to_ref)
|
|
162
|
-
.then {
|
|
162
|
+
.then { PortCodec.task_call(it, registry) }
|
|
163
163
|
|
|
164
164
|
in Symbol::StdlibFunction(codegen:)
|
|
165
165
|
codegen
|
|
@@ -393,8 +393,12 @@ module Jade
|
|
|
393
393
|
node.is_a?(AST::ImportDeclaration) || node.is_a?(AST::InteropImportDeclaration)
|
|
394
394
|
end
|
|
395
395
|
|
|
396
|
+
# Appended, not prepended: this exists so `uses App::Thing` can find
|
|
397
|
+
# `lib/app/thing.rb`, and an application's lib directory routinely holds
|
|
398
|
+
# directories named after the gems it depends on. At position 0 those
|
|
399
|
+
# shadow the real gems for the whole process.
|
|
396
400
|
def load_path
|
|
397
|
-
'$LOAD_PATH.
|
|
401
|
+
'$LOAD_PATH.push(File.expand_path("lib")).uniq!'
|
|
398
402
|
end
|
|
399
403
|
end
|
|
400
404
|
end
|
data/lib/jade/debug.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Debug
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
MAX_DEPTH = 12
|
|
6
|
+
|
|
7
|
+
def log(label, value)
|
|
8
|
+
warn("#{label}: #{render(value)}")
|
|
9
|
+
value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(value, depth = 0)
|
|
13
|
+
return '…' if depth > MAX_DEPTH
|
|
14
|
+
|
|
15
|
+
case value
|
|
16
|
+
when ::String then value.inspect
|
|
17
|
+
when ::Symbol then value.to_s
|
|
18
|
+
when nil then 'Nothing'
|
|
19
|
+
when true, false, ::Integer, ::Float then value.to_s
|
|
20
|
+
when ::Array then render_list(value, depth)
|
|
21
|
+
when ::Hash then render_record(value, depth)
|
|
22
|
+
when ::Data then render_variant(value, depth)
|
|
23
|
+
when ::Proc then '<function>'
|
|
24
|
+
else value.inspect
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def render_list(items, depth)
|
|
31
|
+
'[' + items.map { render(it, depth + 1) }.join(', ') + ']'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render_record(fields, depth)
|
|
35
|
+
return '{}' if fields.empty?
|
|
36
|
+
|
|
37
|
+
'{ ' + fields.map { |k, v| "#{k}: #{render(v, depth + 1)}" }.join(', ') + ' }'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_variant(value, depth)
|
|
41
|
+
name = constructor_name(value)
|
|
42
|
+
args = value.deconstruct
|
|
43
|
+
|
|
44
|
+
return '(' + args.map { render(it, depth + 1) }.join(', ') + ')' if name.match?(/\ATuple\d+\z/)
|
|
45
|
+
return name if args.empty?
|
|
46
|
+
return "#{name}(#{args.map { render(it, depth + 1) }.join(', ')})" if positional?(value)
|
|
47
|
+
|
|
48
|
+
"#{name} " + render_record(value.to_h, depth)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def positional?(value)
|
|
52
|
+
value.members.each_with_index.all? { |m, i| m.to_s == "_#{i + 1}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def constructor_name(value)
|
|
56
|
+
value.class.name.to_s.split('::').last.then { it.empty? ? 'Anonymous' : it }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -49,13 +49,13 @@ module Jade::Diagnostics
|
|
|
49
49
|
def span_block(label, severity:)
|
|
50
50
|
loc = location_of(label.source, label.span.begin)
|
|
51
51
|
end_offset = [label.span.end - 1, 0].max
|
|
52
|
-
end_loc = location_of(label.source, [end_offset, label.source.text.
|
|
52
|
+
end_loc = location_of(label.source, [end_offset, label.source.text.bytesize - 1].min)
|
|
53
53
|
multiline = end_loc.line > loc.line
|
|
54
54
|
gutter_w = (multiline ? end_loc.line : loc.line).to_s.length
|
|
55
55
|
blank = " " * gutter_w
|
|
56
56
|
col_offset = loc.col - 1
|
|
57
57
|
first_line = extract_line(label.source, loc.line).chomp
|
|
58
|
-
span_len = [multiline ? first_line.length - col_offset : label
|
|
58
|
+
span_len = [multiline ? first_line.length - col_offset : span_width(label), 1].max
|
|
59
59
|
caret = severity == :secondary ? "-" : "^"
|
|
60
60
|
underline = " " * col_offset + caret * span_len
|
|
61
61
|
ann_text = label.message ? " #{label.message}" : ""
|
|
@@ -91,15 +91,26 @@ module Jade::Diagnostics
|
|
|
91
91
|
|
|
92
92
|
Location = Data.define(:line, :col)
|
|
93
93
|
|
|
94
|
+
# Spans and line starts are byte offsets — the lexer's — while everything
|
|
95
|
+
# drawn here is counted in characters: slice the text by byte offset, then
|
|
96
|
+
# measure the result.
|
|
94
97
|
def location_of(source, offset)
|
|
95
98
|
(source.line_starts.rindex { _1 <= offset } || 0)
|
|
96
|
-
.then { Location[it + 1,
|
|
99
|
+
.then { Location[it + 1, column_of(source, source.line_starts[it], offset)] }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def column_of(source, line_start, offset)
|
|
103
|
+
source.text.byteslice(line_start, offset - line_start).length + 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def span_width(label)
|
|
107
|
+
label.source.text.byteslice(label.span.begin, label.span.size).length
|
|
97
108
|
end
|
|
98
109
|
|
|
99
110
|
def extract_line(source, line_number)
|
|
100
111
|
s = source.line_starts[line_number - 1]
|
|
101
|
-
e = source.line_starts[line_number] || source.text.
|
|
102
|
-
source.text
|
|
112
|
+
e = source.line_starts[line_number] || source.text.bytesize
|
|
113
|
+
source.text.byteslice(s, e - s)
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
def color(severity)
|
|
@@ -47,8 +47,8 @@ module Jade
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def fn_type_to_interop(interop_mod_name, function_node, symbol, entry, registry)
|
|
50
|
-
lifted_errors =
|
|
51
|
-
.validate(
|
|
50
|
+
lifted_errors = [symbol.return_type, *symbol.params]
|
|
51
|
+
.flat_map { Interop::Lowering.validate(it, registry, entry) }
|
|
52
52
|
.map { Error::TypeNotLowerable.new(entry, function_node.range, message: it.message) }
|
|
53
53
|
|
|
54
54
|
Symbol
|
|
@@ -57,7 +57,8 @@ module Jade
|
|
|
57
57
|
symbol.params,
|
|
58
58
|
symbol.return_type,
|
|
59
59
|
interop_mod_name.name,
|
|
60
|
-
constraints: implicit_decodable_constraints(symbol.return_type)
|
|
60
|
+
constraints: implicit_decodable_constraints(symbol.return_type) +
|
|
61
|
+
implicit_encodable_constraints(symbol.params),
|
|
61
62
|
)
|
|
62
63
|
.then { [it, lifted_errors] }
|
|
63
64
|
end
|
|
@@ -78,6 +79,15 @@ module Jade
|
|
|
78
79
|
end
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
# Arguments travel the other way, so a free variable in a parameter
|
|
83
|
+
# needs the caller's Encodable instance rather than its Decodable one.
|
|
84
|
+
def implicit_encodable_constraints(params)
|
|
85
|
+
params
|
|
86
|
+
.flat_map { collect_var_names(it) }
|
|
87
|
+
.uniq
|
|
88
|
+
.map { ['Encode.Encodable', it] }
|
|
89
|
+
end
|
|
90
|
+
|
|
81
91
|
def collect_var_names(type_sym)
|
|
82
92
|
case type_sym
|
|
83
93
|
in Symbol::Variable(name:)
|
|
@@ -39,7 +39,7 @@ module Jade
|
|
|
39
39
|
with(rows: rows + other.rows)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def missing_patterns(env
|
|
42
|
+
def missing_patterns(env)
|
|
43
43
|
if types.empty?
|
|
44
44
|
return rows.empty? ? Matrix[[[]], types] : Matrix.empty
|
|
45
45
|
end
|
|
@@ -49,37 +49,72 @@ module Jade
|
|
|
49
49
|
return Matrix.empty if never?(type)
|
|
50
50
|
return Matrix.wildcard(types) if rows.empty?
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
matrix = default
|
|
54
|
-
.missing_patterns(env, seen_recursive_types)
|
|
52
|
+
heads = head_constructors
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if infinite?(type) || type_var?(type)
|
|
55
|
+
unmatched_column(env)
|
|
58
56
|
|
|
59
57
|
elsif expandable?(type, env)
|
|
60
|
-
expand(env).missing_patterns(env
|
|
58
|
+
expand(env).missing_patterns(env)
|
|
59
|
+
|
|
60
|
+
elsif heads.empty?
|
|
61
|
+
unmatched_column(env)
|
|
61
62
|
|
|
62
63
|
else
|
|
63
|
-
new_seen = seen_recursive_types | ::Set[type]
|
|
64
64
|
constructors_of(type, env)
|
|
65
65
|
.reduce(Matrix.empty.with(types:)) do |acc, constructor|
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
missing
|
|
70
|
-
.map do |row|
|
|
71
|
-
new_args = row.take(constructor.args.size)
|
|
72
|
-
tail = row.drop(constructor.args.size)
|
|
73
|
-
|
|
74
|
-
[Constructor[constructor.name, new_args]] + tail
|
|
75
|
-
end
|
|
76
|
-
.then { acc.concat(it) }
|
|
66
|
+
witnesses(constructor, env, heads.include?(constructor.name))
|
|
67
|
+
.then { acc.concat(it) }
|
|
77
68
|
end
|
|
78
69
|
end
|
|
79
70
|
end
|
|
80
71
|
|
|
81
72
|
protected
|
|
82
73
|
|
|
74
|
+
# Nothing in this column narrows anything — a type with no constructors
|
|
75
|
+
# to enumerate, or one every row left open. Whatever is missing lives in
|
|
76
|
+
# the columns after it.
|
|
77
|
+
def unmatched_column(env)
|
|
78
|
+
default
|
|
79
|
+
.missing_patterns(env)
|
|
80
|
+
.map { [Wildcard[]] + it }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A constructor the first column matches is explored by specializing on
|
|
84
|
+
# it. One it never matches can only be missing as a whole, so its
|
|
85
|
+
# witnesses come from the rows that would have covered it — the ones
|
|
86
|
+
# headed by a wildcard. Recursing there instead of into the constructor
|
|
87
|
+
# is what keeps a recursive type from expanding forever.
|
|
88
|
+
def witnesses(constructor, env, matched)
|
|
89
|
+
return Matrix.empty if constructor.args.any? { never?(it) }
|
|
90
|
+
|
|
91
|
+
arity = constructor.args.size
|
|
92
|
+
|
|
93
|
+
if matched
|
|
94
|
+
specialize(constructor)
|
|
95
|
+
.missing_patterns(env)
|
|
96
|
+
.map { [Constructor[constructor.name, it.take(arity)]] + it.drop(arity) }
|
|
97
|
+
|
|
98
|
+
else
|
|
99
|
+
default
|
|
100
|
+
.missing_patterns(env)
|
|
101
|
+
.map { [Constructor[constructor.name, Array.new(arity) { Wildcard[] }]] + it }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def head_constructors
|
|
106
|
+
rows.filter_map { head_constructor(it.first) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def head_constructor(pattern)
|
|
110
|
+
case pattern
|
|
111
|
+
in Constructor(constructor: name) then name
|
|
112
|
+
in Literal(value: true) then 'Basics.True'
|
|
113
|
+
in Literal(value: false) then 'Basics.False'
|
|
114
|
+
in Literal | Record | Wildcard then nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
83
118
|
def expandable?(type, env)
|
|
84
119
|
case type
|
|
85
120
|
in Type::AnonymousRecord
|
|
@@ -125,9 +160,8 @@ module Jade
|
|
|
125
160
|
TypeChecking::ConstructorDef['Basics.True', 'Basics.Bool', []],
|
|
126
161
|
TypeChecking::ConstructorDef['Basics.False', 'Basics.Bool', []],
|
|
127
162
|
]
|
|
128
|
-
in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple
|
|
129
|
-
|
|
130
|
-
[TypeChecking::Definition.constructor(name, name, Array.new(n) { env.fresh })]
|
|
163
|
+
in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple[2-4]$/ => name), args:)
|
|
164
|
+
[TypeChecking::Definition.constructor(name, name, args)]
|
|
131
165
|
|
|
132
166
|
in Type::Application(constructor: Type::Constructor(name: 'List.List'), args: [elem_type])
|
|
133
167
|
[
|
|
@@ -181,7 +215,7 @@ module Jade
|
|
|
181
215
|
end
|
|
182
216
|
end
|
|
183
217
|
.then { with(rows: it) }
|
|
184
|
-
.then { it.with(types: constructor.args) }
|
|
218
|
+
.then { it.with(types: constructor.args + types.drop(1)) }
|
|
185
219
|
end
|
|
186
220
|
|
|
187
221
|
def default
|
|
@@ -17,7 +17,7 @@ module Jade
|
|
|
17
17
|
entry.name,
|
|
18
18
|
node.range,
|
|
19
19
|
name:,
|
|
20
|
-
|
|
20
|
+
hint: constructor_hint(name, entry, registry),
|
|
21
21
|
candidates: scope.bindings.keys.select { it.match?(/\A[A-Z]/) },
|
|
22
22
|
))
|
|
23
23
|
.then do
|
|
@@ -49,14 +49,40 @@ module Jade
|
|
|
49
49
|
&.then { klass.new(entry.name, span, arity: it) }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# An imported module owning a constructor by this name is the whole
|
|
53
|
+
# story: either it keeps its constructors private, or it exposes them
|
|
54
|
+
# and the import here asked for the type alone.
|
|
55
|
+
def constructor_hint(name, entry, registry)
|
|
56
|
+
entry
|
|
57
|
+
.imports
|
|
58
|
+
.filter_map { hint_for(it.module_name, name, registry) }
|
|
59
|
+
.first
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def hint_for(module_name, name, registry)
|
|
63
|
+
target = registry.get(module_name)
|
|
64
|
+
return nil if target.nil? || Stdlib.is_stdlib?(target)
|
|
65
|
+
|
|
66
|
+
type_name = owning_type(target, name)
|
|
67
|
+
return nil unless type_name && target.exposed_type(type_name)
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
exposes_constructor?(target, type_name, name)
|
|
70
|
+
.then { it ? :import : :expose }
|
|
71
|
+
.then { Error::ConstructorNotFound::Hint[module_name, type_name, it] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def owning_type(target, name)
|
|
75
|
+
target
|
|
76
|
+
.defined_types
|
|
77
|
+
.find { |_, sym| sym.constructor_refs.any? { it.name == name } }
|
|
78
|
+
&.first
|
|
79
|
+
end
|
|
58
80
|
|
|
59
|
-
|
|
81
|
+
def exposes_constructor?(target, type_name, name)
|
|
82
|
+
target
|
|
83
|
+
.exposed_type_variants(type_name)
|
|
84
|
+
.to_a
|
|
85
|
+
.any? { it.name == name }
|
|
60
86
|
end
|
|
61
87
|
end
|
|
62
88
|
end
|