jade-lang 0.11.2 → 0.12.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 +112 -0
- data/lib/jade/ast.rb +1 -1
- data/lib/jade/codegen/context.rb +18 -1
- data/lib/jade/codegen/emitter.rb +55 -13
- data/lib/jade/codegen/function_call.rb +10 -3
- data/lib/jade/codegen.rb +7 -5
- data/lib/jade/diagnostics/renderer.rb +5 -1
- data/lib/jade/entry.rb +32 -10
- data/lib/jade/frontend/forward_declaration/error/ambiguous_type.rb +36 -0
- data/lib/jade/frontend/forward_declaration/error.rb +1 -0
- data/lib/jade/frontend/forward_declaration/helper.rb +7 -0
- data/lib/jade/frontend/forward_declaration/import_declaration.rb +10 -2
- data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +5 -0
- data/lib/jade/frontend/semantic_analysis/error/ambiguous_name.rb +36 -0
- data/lib/jade/frontend/semantic_analysis/error/top_level_statement.rb +28 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +2 -0
- data/lib/jade/frontend/semantic_analysis/module_node.rb +21 -1
- data/lib/jade/frontend/semantic_analysis/variable_reference.rb +5 -0
- data/lib/jade/frontend/semantic_analysis.rb +4 -6
- data/lib/jade/frontend/type_checking/error/narrowed_method_variable.rb +36 -0
- data/lib/jade/frontend/type_checking/error/narrowed_signature.rb +53 -0
- data/lib/jade/frontend/type_checking/error.rb +2 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +5 -1
- data/lib/jade/frontend/type_checking/inference/constructor_reference.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/function_declaration.rb +21 -0
- data/lib/jade/frontend/type_checking/inference/helpers.rb +11 -0
- data/lib/jade/frontend/type_checking/inference/implementation.rb +23 -2
- data/lib/jade/frontend/type_checking/inference/list.rb +16 -2
- data/lib/jade/frontend/type_checking/narrowing.rb +93 -0
- data/lib/jade/frontend/type_checking.rb +1 -5
- data/lib/jade/frontend.rb +0 -22
- data/lib/jade/parsing/error.rb +15 -0
- data/lib/jade/registry.rb +1 -1
- data/lib/jade/stdlib/compiled.rb +8 -1
- data/lib/jade/stdlib/intrinsics.rb +8 -1
- data/lib/jade/stdlib.rb +8 -1
- data/lib/jade/symbol/ambiguous.rb +5 -0
- data/lib/jade/symbol.rb +1 -0
- data/lib/jade/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e6f1ff92a99cc704dd81615de9b97ffd0f7e650ca23adac6c7ede429eeb8ffe
|
|
4
|
+
data.tar.gz: ba35d16db8a3ff88f46ef726df1f19c27fc93a20366d0acd1faec55764690f9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f07bb37ec67df7e734a007eadbf0d4ac0b2309e893f0b29b139ed6f04984478551cab0c5ac7ed0c02a83ffb4cf82353490aed477ec1e51b26c0a5961c3fa799
|
|
7
|
+
data.tar.gz: 8d87c617b0784d10185cbaaa851d2f34c649ce80ede4b82cafdcde1b4bc569f3e2841e514476fc3c95a4fa6eddf67f09e921f0b838dcf6aa45d8d1bedb34ad3b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,118 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.12.0] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
Each of these refuses a program that compiled before. All four were
|
|
14
|
+
accepting something unsound, so the refusal is the fix — but a build that
|
|
15
|
+
passed on 0.11.3 may not pass on this one.
|
|
16
|
+
|
|
17
|
+
- **A signature wider than its body was accepted.** `def anything -> a`
|
|
18
|
+
returning `Nothing`, or `def wrap -> Maybe(a)` returning `Just(1)`, type
|
|
19
|
+
checked: the body quietly fixed the declared variable, and every caller then
|
|
20
|
+
saw the narrower type. A declared type variable now has to stay a variable
|
|
21
|
+
through the body. Fixing it to a concrete type, or making two declared
|
|
22
|
+
variables the same, is an error that says which.
|
|
23
|
+
|
|
24
|
+
- **A statement at the top level of a module is an error.** `x = 2` between
|
|
25
|
+
two `def`s type-checked, and a function reading `x` compiled clean, then
|
|
26
|
+
raised `NameError` at runtime: the binding became a local of the Ruby
|
|
27
|
+
`module` body, out of every method's reach. A module's top level holds
|
|
28
|
+
declarations only; a value belongs in a zero-argument `def`.
|
|
29
|
+
|
|
30
|
+
- **A name imported from two modules picked one without a word.**
|
|
31
|
+
`import Alpha exposing (name)` beside `import Beta exposing (name)`
|
|
32
|
+
compiled, and `name` meant whichever import came last. Using it unqualified
|
|
33
|
+
is now an error naming both modules; qualified uses, and a module that never
|
|
34
|
+
uses it, still compile. The same goes for constructors and for a type named
|
|
35
|
+
in a signature. An explicit import still beats one of the stdlib's default
|
|
36
|
+
imports, now by rule rather than by the order imports were processed in.
|
|
37
|
+
|
|
38
|
+
- **An implementation could fix a variable the method leaves to the call
|
|
39
|
+
site.** 0.11.0 let an interface method quantify a variable the interface
|
|
40
|
+
itself does not name, settled where the method is called. Nothing then
|
|
41
|
+
checked that the implementation was actually that general:
|
|
42
|
+
|
|
43
|
+
interface Runnable(c) with
|
|
44
|
+
to_select : Q(c) -> Sel(a) with Selectable(a)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
implements Runnable(Sel(a)) with
|
|
48
|
+
to_select: (q) -> { q.result } -- Q(Sel(a)) -> Sel(a), not -> Sel(any)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Unification bound the method's `a` to the implementation's, and the binding
|
|
52
|
+
reached no call site, so `Q(Sel(Row)) -> Sel(Other)` compiled and returned a
|
|
53
|
+
row of the wrong type. It is refused now, by name, where it is written.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **A file that ends mid-declaration crashed `jade check`.** The parse error
|
|
58
|
+
for input that runs out carries no span, since there is no token to point
|
|
59
|
+
at, and the renderer called `.begin` on it. The diagnostic now points at the
|
|
60
|
+
last character written, where the missing part belongs, and a label with no
|
|
61
|
+
span renders as its message alone.
|
|
62
|
+
|
|
63
|
+
- **A body its signature rejects crashed the compiler.** A list, a bare
|
|
64
|
+
constructor, or a binding in last position, checked against a type it could
|
|
65
|
+
not be, reached `nil.call` in `State#unify` — the crash 0.10.1 fixed for
|
|
66
|
+
record updates, at the call sites that still had no error block.
|
|
67
|
+
`def limit -> Int` returning `[1]` now reads
|
|
68
|
+
`Expected Int but got List(Int)`.
|
|
69
|
+
|
|
70
|
+
- **A dictionary marker kept the var it was attached with.** A constraint
|
|
71
|
+
records its marker while inference is still running, so it holds whatever
|
|
72
|
+
type variable it had at that moment. Unification may bind that variable to
|
|
73
|
+
another one afterwards, and the enclosing function's dict parameter is keyed
|
|
74
|
+
on the second — so the lookup missed and codegen raised `no dict in scope`,
|
|
75
|
+
naming itself as the bug.
|
|
76
|
+
|
|
77
|
+
It took two constraints on one function to see it: with one, the marker's
|
|
78
|
+
var and the parameter's var were usually the same. jade-sql wanted a
|
|
79
|
+
`pluck` — one column of the caller's type out of every row — which needs
|
|
80
|
+
`Decodable(a)` threaded alongside the error widening, and neither constraint
|
|
81
|
+
alone reproduced it.
|
|
82
|
+
|
|
83
|
+
Codegen now carries the entry's substitution while it emits a body, and
|
|
84
|
+
resolves a marker's var through it before giving up.
|
|
85
|
+
|
|
86
|
+
- **`import M exposing (..)` crashed the compiler.** It asked the imported
|
|
87
|
+
module for `exposed`, which does not exist, so the only way to bring a
|
|
88
|
+
module's whole surface in unqualified was a `NoMethodError`. It now
|
|
89
|
+
imports everything the module exposes, constructors included.
|
|
90
|
+
|
|
91
|
+
- **A string literal's range started one byte in.** `Literal` took its range
|
|
92
|
+
from the content token, so a diagnostic on `"ab"` underlined `ab"`, and
|
|
93
|
+
anything editing source by node range landed inside the quotes.
|
|
94
|
+
|
|
95
|
+
## [0.11.3] - 2026-09-16
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **A derived body can call a Jade function.** A deriver's body is an
|
|
100
|
+
s-expression over a fixed vocabulary, and none of it reached an ordinary
|
|
101
|
+
function: `:stdlib_fn` finds intrinsics, `:struct_constructor` finds
|
|
102
|
+
constructors, and `:call` needs a callee one of those produced. A deriver
|
|
103
|
+
that wanted to reuse a function had to rebuild it, and the copy drifts from
|
|
104
|
+
the original in silence.
|
|
105
|
+
|
|
106
|
+
[:call, [:fn, "Sql.Query.selected", [0]], [[:var, "q"]]]
|
|
107
|
+
|
|
108
|
+
The name resolves through the registry, which is what knows whether the
|
|
109
|
+
target is constrained — those are compiled twice, as `name` wrapping the Ruby
|
|
110
|
+
boundary and `__name__impl__` taking the dictionaries, and the boundary
|
|
111
|
+
wrapper is the wrong one to call from generated code.
|
|
112
|
+
|
|
113
|
+
Dictionaries come from the deriver's own `impl_arg`, one index per
|
|
114
|
+
constraint, in the order the callee declares them. A count that does not
|
|
115
|
+
match what the callee takes is refused where it is written, rather than
|
|
116
|
+
becoming Ruby that loads and misbehaves later. `[:impl_dict, i]` yields a
|
|
117
|
+
whole dictionary, where `[:impl_arg, i, fn]` yields one function out of one.
|
|
118
|
+
|
|
7
119
|
## [0.11.2] - 2026-09-15
|
|
8
120
|
|
|
9
121
|
### Fixed
|
data/lib/jade/ast.rb
CHANGED
data/lib/jade/codegen/context.rb
CHANGED
|
@@ -22,6 +22,23 @@ module Jade
|
|
|
22
22
|
@dict_env = prev
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# The substitution of the module being emitted. A dictionary marker is
|
|
26
|
+
# attached mid-inference, so it holds whatever var the constraint had
|
|
27
|
+
# then; unification may bind that var to another, and the dict env is
|
|
28
|
+
# keyed on the second. Module-scoped because every kind of body resolves
|
|
29
|
+
# markers, not just function declarations.
|
|
30
|
+
def substitution
|
|
31
|
+
@substitution
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_substitution(sub)
|
|
35
|
+
prev = @substitution
|
|
36
|
+
@substitution = sub
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
@substitution = prev
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
# Dictionary source => constant name, filled while a module emits.
|
|
26
43
|
# A dictionary built entirely from concrete implementations depends
|
|
27
44
|
# on nothing at the call site, so it is built once at load instead
|
|
@@ -58,7 +75,7 @@ module Jade
|
|
|
58
75
|
@self_var_name = prev
|
|
59
76
|
end
|
|
60
77
|
|
|
61
|
-
# False outside a Module so bare expressions
|
|
78
|
+
# False outside a Module so bare expressions get the runtime
|
|
62
79
|
# fallback — no constants exist to reference.
|
|
63
80
|
def hoist_records?
|
|
64
81
|
@hoist_records
|
data/lib/jade/codegen/emitter.rb
CHANGED
|
@@ -6,19 +6,19 @@ module Jade
|
|
|
6
6
|
extend self
|
|
7
7
|
extend Helpers
|
|
8
8
|
|
|
9
|
-
def emit(ir)
|
|
9
|
+
def emit(ir, registry = nil)
|
|
10
10
|
case ir
|
|
11
11
|
in [:var, expr]
|
|
12
12
|
expr
|
|
13
13
|
|
|
14
14
|
in [:!, expr]
|
|
15
|
-
"!(#{emit(expr)})"
|
|
15
|
+
"!(#{emit(expr, registry)})"
|
|
16
16
|
|
|
17
17
|
in [:and, expr1, expr2]
|
|
18
|
-
"#{emit(expr1)} && #{emit(expr2)}"
|
|
18
|
+
"#{emit(expr1, registry)} && #{emit(expr2, registry)}"
|
|
19
19
|
|
|
20
20
|
in [:==, expr1, expr2]
|
|
21
|
-
"#{emit(expr1)} == #{emit(expr2)}"
|
|
21
|
+
"#{emit(expr1, registry)} == #{emit(expr2, registry)}"
|
|
22
22
|
|
|
23
23
|
in Integer | TrueClass | FalseClass | Float
|
|
24
24
|
ir.to_s
|
|
@@ -29,24 +29,33 @@ module Jade
|
|
|
29
29
|
in [:case, subject, branches]
|
|
30
30
|
branches
|
|
31
31
|
.map { [:case_branch, *it] }
|
|
32
|
-
.map { emit(it) }
|
|
32
|
+
.map { emit(it, registry) }
|
|
33
33
|
.join("\n")
|
|
34
|
-
.then { "case #{emit(subject)}\n#{it}\nend" }
|
|
34
|
+
.then { "case #{emit(subject, registry)}\n#{it}\nend" }
|
|
35
35
|
|
|
36
36
|
in [:case_branch, pattern, body]
|
|
37
37
|
body
|
|
38
|
-
.map { emit(it) }.join('; ')
|
|
39
|
-
.then { "in #{emit(pattern)} then #{it}" }
|
|
38
|
+
.map { emit(it, registry) }.join('; ')
|
|
39
|
+
.then { "in #{emit(pattern, registry)} then #{it}" }
|
|
40
40
|
|
|
41
41
|
in [:call, callee, args]
|
|
42
42
|
args
|
|
43
|
-
.map { emit(it) }
|
|
43
|
+
.map { emit(it, registry) }
|
|
44
44
|
.join(', ')
|
|
45
|
-
.then { "#{emit(callee)}.call(#{it})"}
|
|
45
|
+
.then { "#{emit(callee, registry)}.call(#{it})"}
|
|
46
46
|
|
|
47
47
|
in [:impl_arg, index, fn]
|
|
48
48
|
"impl_arg[#{index}]['#{fn}']"
|
|
49
49
|
|
|
50
|
+
in [:impl_dict, index]
|
|
51
|
+
"impl_arg[#{index}]"
|
|
52
|
+
|
|
53
|
+
in [:fn, qualified_name]
|
|
54
|
+
fn_reference(qualified_name, [], registry)
|
|
55
|
+
|
|
56
|
+
in [:fn, qualified_name, dict_indices]
|
|
57
|
+
fn_reference(qualified_name, dict_indices, registry)
|
|
58
|
+
|
|
50
59
|
in [:stdlib_fn, name]
|
|
51
60
|
"Jade::Runtime.intr(#{name.inspect})"
|
|
52
61
|
|
|
@@ -61,18 +70,18 @@ module Jade
|
|
|
61
70
|
|
|
62
71
|
in [:list, exprs]
|
|
63
72
|
exprs
|
|
64
|
-
.map { emit(it) }
|
|
73
|
+
.map { emit(it, registry) }
|
|
65
74
|
.join(', ')
|
|
66
75
|
.then { "[#{it}]" }
|
|
67
76
|
|
|
68
77
|
in [:hash, pairs]
|
|
69
78
|
pairs
|
|
70
|
-
.map { |key, value| "#{key.inspect} => #{emit(value)}" }
|
|
79
|
+
.map { |key, value| "#{key.inspect} => #{emit(value, registry)}" }
|
|
71
80
|
.join(', ')
|
|
72
81
|
.then { it.empty? ? '{}' : "{ #{it} }" }
|
|
73
82
|
|
|
74
83
|
in [:access, expr, key]
|
|
75
|
-
"#{emit(expr)}.#{key}"
|
|
84
|
+
"#{emit(expr, registry)}.#{key}"
|
|
76
85
|
|
|
77
86
|
# patterns
|
|
78
87
|
|
|
@@ -88,6 +97,39 @@ module Jade
|
|
|
88
97
|
end
|
|
89
98
|
end
|
|
90
99
|
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def fn_reference(qualified_name, dict_indices, registry)
|
|
103
|
+
registry or fail("[:fn, #{qualified_name.inspect}] needs a registry")
|
|
104
|
+
|
|
105
|
+
*module_parts, name = qualified_name.split('.')
|
|
106
|
+
|
|
107
|
+
Symbol
|
|
108
|
+
.value_ref(module_parts.join('.'), name)
|
|
109
|
+
.then { registry.lookup(it) }
|
|
110
|
+
.then { checked_reference(it, qualified_name, dict_indices, registry) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def checked_reference(symbol, qualified_name, dict_indices, registry)
|
|
114
|
+
wanted = dict_constraints(symbol, registry).size
|
|
115
|
+
|
|
116
|
+
unless wanted == dict_indices.size
|
|
117
|
+
fail(
|
|
118
|
+
"#{qualified_name} takes #{wanted} #{wanted == 1 ? 'dictionary' : 'dictionaries'}, " \
|
|
119
|
+
"#{dict_indices.size} given — a derived body passes one impl_arg " \
|
|
120
|
+
'index per constraint, in the order the callee declares them'
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
target = "#{internal(symbol.module_name)}.#{fn_target_name(symbol, registry)}"
|
|
125
|
+
return "#{internal(symbol.module_name)}.method(:#{fn_target_name(symbol, registry)})" if wanted.zero?
|
|
126
|
+
|
|
127
|
+
dict_indices
|
|
128
|
+
.map { "impl_arg[#{it}]" }
|
|
129
|
+
.join(', ')
|
|
130
|
+
.then { "->(*args) { #{target}(*args, #{it}) }" }
|
|
131
|
+
end
|
|
132
|
+
|
|
91
133
|
end
|
|
92
134
|
end
|
|
93
135
|
end
|
|
@@ -113,14 +113,21 @@ module Jade
|
|
|
113
113
|
|
|
114
114
|
def dispatch_value(entry, registry)
|
|
115
115
|
case entry
|
|
116
|
-
in Type::Constraint(interface:, type: Type::Var(id:))
|
|
117
|
-
Codegen.dict_env[[interface, id]]
|
|
116
|
+
in Type::Constraint(interface:, type: Type::Var(id:) => var)
|
|
117
|
+
Codegen.dict_env[[interface, id]] || dict_for_bound_var(interface, var)
|
|
118
118
|
|
|
119
119
|
in Symbol::Implementation
|
|
120
120
|
hoisted(Pretty.hash(generate_impl_dispatch(entry, registry)))
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
+
def dict_for_bound_var(interface, var)
|
|
125
|
+
Codegen
|
|
126
|
+
.substitution
|
|
127
|
+
.apply(var)
|
|
128
|
+
.then { it.is_a?(Type::Var) ? Codegen.dict_env[[interface, it.id]] : nil }
|
|
129
|
+
end
|
|
130
|
+
|
|
124
131
|
# A dictionary built only from concrete implementations is the same
|
|
125
132
|
# object on every call, so it is worth building once. One that names
|
|
126
133
|
# a dict parameter is not: that slot is the caller's, and differs per
|
|
@@ -393,7 +400,7 @@ module Jade
|
|
|
393
400
|
end
|
|
394
401
|
|
|
395
402
|
def emit(ir, registry)
|
|
396
|
-
Emitter.emit(ir)
|
|
403
|
+
Emitter.emit(ir, registry)
|
|
397
404
|
end
|
|
398
405
|
end
|
|
399
406
|
end
|
data/lib/jade/codegen.rb
CHANGED
|
@@ -107,11 +107,13 @@ module Jade
|
|
|
107
107
|
dict_consts = {}
|
|
108
108
|
|
|
109
109
|
outer, inner, wrappers =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
with_substitution(registry.get(name).env.substitution) do
|
|
111
|
+
with_dict_consts(dict_consts) do
|
|
112
|
+
with_boundary_cache(boundary_cache) do
|
|
113
|
+
with_dispatched_methods(collect_dispatched_methods(body, registry)) do
|
|
114
|
+
with_hoisted_records do
|
|
115
|
+
partition_module_body(body.expressions, registry, name.count('.'))
|
|
116
|
+
end
|
|
115
117
|
end
|
|
116
118
|
end
|
|
117
119
|
end
|
|
@@ -20,7 +20,7 @@ module Jade::Diagnostics
|
|
|
20
20
|
def render(diagnostic)
|
|
21
21
|
[
|
|
22
22
|
header(diagnostic),
|
|
23
|
-
diagnostic.primary&.then {
|
|
23
|
+
diagnostic.primary&.then { primary_block(it, diagnostic.severity) },
|
|
24
24
|
*diagnostic
|
|
25
25
|
.secondary
|
|
26
26
|
.map { span_block(_1, severity: :secondary) },
|
|
@@ -41,6 +41,10 @@ module Jade::Diagnostics
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
+
def primary_block(label, severity)
|
|
45
|
+
span_block(label, severity:) if label.source && label.span
|
|
46
|
+
end
|
|
47
|
+
|
|
44
48
|
def header(diagnostic)
|
|
45
49
|
"#{bold}#{color(diagnostic.severity)}#{diagnostic.severity}:#{reset} " \
|
|
46
50
|
"#{bold}#{diagnostic.message}#{reset}"
|
data/lib/jade/entry.rb
CHANGED
|
@@ -64,19 +64,19 @@ module Jade
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def imported_values
|
|
67
|
-
|
|
68
|
-
.flat_map(&:unqualified_symbols)
|
|
69
|
-
.select { it.is_a?(Symbol::ValueRef) }
|
|
70
|
-
.map { [it.name, it] }
|
|
71
|
-
.to_h
|
|
67
|
+
unqualified(Symbol::ValueRef)
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def imported_types
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
unqualified(Symbol::TypeRef)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ambiguous_values
|
|
75
|
+
ambiguous(Symbol::ValueRef)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ambiguous_types
|
|
79
|
+
ambiguous(Symbol::TypeRef)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def values
|
|
@@ -128,6 +128,28 @@ module Jade
|
|
|
128
128
|
|
|
129
129
|
private
|
|
130
130
|
|
|
131
|
+
def unqualified(kind)
|
|
132
|
+
imports
|
|
133
|
+
.partition(&:implicit)
|
|
134
|
+
.map { |group| refs(group, kind).to_h { [it.name, it] } }
|
|
135
|
+
.then { |(implicit, explicit)| implicit.merge(explicit) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ambiguous(kind)
|
|
139
|
+
imports
|
|
140
|
+
.reject(&:implicit)
|
|
141
|
+
.then { refs(it, kind) }
|
|
142
|
+
.uniq(&:qualified_name)
|
|
143
|
+
.group_by(&:name)
|
|
144
|
+
.select { |_, candidates| candidates.size > 1 }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def refs(imports, kind)
|
|
148
|
+
imports
|
|
149
|
+
.flat_map(&:unqualified_symbols)
|
|
150
|
+
.select { it.is_a?(kind) }
|
|
151
|
+
end
|
|
152
|
+
|
|
131
153
|
def interface_snapshot
|
|
132
154
|
value_names = exposes.filter_map { it.is_a?(Symbol::ValueRef) ? it.name : nil }.sort
|
|
133
155
|
type_names = exposes.filter_map { it.is_a?(Symbol::TypeRef) ? it.name : nil }.sort
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module ForwardDeclaration
|
|
4
|
+
module Error
|
|
5
|
+
class AmbiguousType < Jade::Error
|
|
6
|
+
def initialize(entry, span, name:, modules:)
|
|
7
|
+
@name = name
|
|
8
|
+
@modules = modules
|
|
9
|
+
super(entry:, span:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def message
|
|
13
|
+
"Type `#{@name}` is imported from both #{sentence(@modules)}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def label
|
|
17
|
+
'ambiguous'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def notes
|
|
21
|
+
[Jade::Diagnostics::Annotation[
|
|
22
|
+
:help,
|
|
23
|
+
"qualify it, as in `#{@modules.first}.#{@name}`, or drop it from all but one import",
|
|
24
|
+
]]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def sentence(names)
|
|
30
|
+
"#{names[0...-1].join(', ')} and #{names.last}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,6 +2,7 @@ require 'jade/error'
|
|
|
2
2
|
|
|
3
3
|
require 'jade/frontend/forward_declaration/error/bad_import'
|
|
4
4
|
require 'jade/frontend/forward_declaration/error/alias_expansion'
|
|
5
|
+
require 'jade/frontend/forward_declaration/error/ambiguous_type'
|
|
5
6
|
require 'jade/frontend/forward_declaration/error/duplicate_constructor_name'
|
|
6
7
|
require 'jade/frontend/forward_declaration/error/duplicate_type_name'
|
|
7
8
|
require 'jade/frontend/forward_declaration/error/exposed_type_not_found'
|
|
@@ -123,6 +123,13 @@ module Jade
|
|
|
123
123
|
private
|
|
124
124
|
|
|
125
125
|
def require_type(entry, name, span)
|
|
126
|
+
if !entry.defined_types.key?(name) && (candidates = entry.ambiguous_types[name])
|
|
127
|
+
return Err[Error::AmbiguousType.new(
|
|
128
|
+
entry.name, span, name:,
|
|
129
|
+
modules: candidates.map(&:module_name),
|
|
130
|
+
)]
|
|
131
|
+
end
|
|
132
|
+
|
|
126
133
|
entry.lookup_type(name)
|
|
127
134
|
&.then { Ok[it] } ||
|
|
128
135
|
Err[Error::TypeNotFound.new(
|
|
@@ -19,7 +19,15 @@ module Jade
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
exposing_to_symbol(exposing, entry, importing_module)
|
|
22
|
-
.map
|
|
22
|
+
.map do
|
|
23
|
+
ImportEntry.new(
|
|
24
|
+
module_name:,
|
|
25
|
+
alias: as&.as || module_name,
|
|
26
|
+
unqualified_symbols: it,
|
|
27
|
+
qualified_symbols: importing_module.exposes,
|
|
28
|
+
implicit: false,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
23
31
|
.on_err { return Result[entry, it] } => Ok(import_entry)
|
|
24
32
|
|
|
25
33
|
Result[entry.import(import_entry), []]
|
|
@@ -34,7 +42,7 @@ module Jade
|
|
|
34
42
|
def exposing_to_symbol(exposing, current_entry, importing_module)
|
|
35
43
|
case exposing
|
|
36
44
|
in AST::ExposeNone then Ok[[]]
|
|
37
|
-
in AST::ExposeAll then Ok[importing_module.
|
|
45
|
+
in AST::ExposeAll then Ok[importing_module.exposes.to_a]
|
|
38
46
|
in AST::ExposeList(items:) then handle_exposing_list(items, current_entry, importing_module)
|
|
39
47
|
end
|
|
40
48
|
end
|
|
@@ -26,6 +26,11 @@ module Jade
|
|
|
26
26
|
.add_errors([it])
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
in Symbol::Ambiguous(modules:)
|
|
30
|
+
Result
|
|
31
|
+
.init(node, scope)
|
|
32
|
+
.add_errors([Error::AmbiguousName.new(entry.name, node.range, name:, modules:)])
|
|
33
|
+
|
|
29
34
|
in symbol
|
|
30
35
|
Result.init(node.with(symbol: symbol.to_ref), scope)
|
|
31
36
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module SemanticAnalysis
|
|
4
|
+
module Error
|
|
5
|
+
class AmbiguousName < Jade::Error
|
|
6
|
+
def initialize(entry, span, name:, modules:)
|
|
7
|
+
@name = name
|
|
8
|
+
@modules = modules
|
|
9
|
+
super(entry:, span:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def message
|
|
13
|
+
"`#{@name}` is imported from both #{sentence(@modules)}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def label
|
|
17
|
+
'ambiguous'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def notes
|
|
21
|
+
[Jade::Diagnostics::Annotation[
|
|
22
|
+
:help,
|
|
23
|
+
"qualify it, as in `#{@modules.first}.#{@name}`, or drop it from all but one import",
|
|
24
|
+
]]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def sentence(names)
|
|
30
|
+
"#{names[0...-1].join(', ')} and #{names.last}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module SemanticAnalysis
|
|
4
|
+
module Error
|
|
5
|
+
class TopLevelStatement < Jade::Error
|
|
6
|
+
def initialize(entry, span)
|
|
7
|
+
super(entry:, span:)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def message
|
|
11
|
+
"Only declarations can appear at the top level of a module"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def label
|
|
15
|
+
'not a declaration'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def notes
|
|
19
|
+
[Jade::Diagnostics::Annotation[
|
|
20
|
+
:help,
|
|
21
|
+
'a value belongs in a zero-argument `def`, like `def limit -> Int`',
|
|
22
|
+
]]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'jade/error'
|
|
2
2
|
|
|
3
3
|
require 'jade/frontend/semantic_analysis/error/no_base_case'
|
|
4
|
+
require 'jade/frontend/semantic_analysis/error/ambiguous_name'
|
|
4
5
|
require 'jade/frontend/semantic_analysis/error/circular_extends'
|
|
5
6
|
require 'jade/frontend/semantic_analysis/error/constant_not_callable'
|
|
6
7
|
require 'jade/frontend/semantic_analysis/error/constructor_not_found'
|
|
@@ -25,6 +26,7 @@ require 'jade/frontend/semantic_analysis/error/predicate_must_return_bool'
|
|
|
25
26
|
require 'jade/frontend/semantic_analysis/error/predicate_name_not_allowed'
|
|
26
27
|
require 'jade/frontend/semantic_analysis/error/recursive_type_alias'
|
|
27
28
|
require 'jade/frontend/semantic_analysis/error/shadowing_error'
|
|
29
|
+
require 'jade/frontend/semantic_analysis/error/top_level_statement'
|
|
28
30
|
require 'jade/frontend/semantic_analysis/error/type_args_mismatch'
|
|
29
31
|
require 'jade/frontend/semantic_analysis/error/type_param_required'
|
|
30
32
|
require 'jade/frontend/semantic_analysis/error/unbound_type_variable'
|
|
@@ -5,6 +5,17 @@ module Jade
|
|
|
5
5
|
extend self
|
|
6
6
|
extend Helper
|
|
7
7
|
|
|
8
|
+
DECLARATIONS = [
|
|
9
|
+
AST::FunctionDeclaration,
|
|
10
|
+
AST::TypeDeclaration,
|
|
11
|
+
AST::TypeAliasDeclaration,
|
|
12
|
+
AST::StructDeclaration,
|
|
13
|
+
AST::InterfaceDeclaration,
|
|
14
|
+
AST::Implementation,
|
|
15
|
+
AST::ImportDeclaration,
|
|
16
|
+
AST::InteropImportDeclaration,
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
8
19
|
def analyze(node, registry, scope, entry)
|
|
9
20
|
node => AST::Module(body:, exposing:)
|
|
10
21
|
|
|
@@ -19,7 +30,16 @@ module Jade
|
|
|
19
30
|
.combine(node, scope:,
|
|
20
31
|
body: analyze_node(body, registry, scope, entry),
|
|
21
32
|
)
|
|
22
|
-
.add_errors(exposing_errors)
|
|
33
|
+
.add_errors(exposing_errors + statement_errors(body, entry))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def statement_errors(body, entry)
|
|
39
|
+
body
|
|
40
|
+
.expressions
|
|
41
|
+
.reject { |node| DECLARATIONS.any? { node.is_a?(it) } }
|
|
42
|
+
.map { Error::TopLevelStatement.new(entry.name, it.range) }
|
|
23
43
|
end
|
|
24
44
|
end
|
|
25
45
|
end
|
|
@@ -17,6 +17,11 @@ module Jade
|
|
|
17
17
|
candidates: scope.bindings.keys,
|
|
18
18
|
)])
|
|
19
19
|
|
|
20
|
+
in Symbol::Ambiguous(modules:)
|
|
21
|
+
Result
|
|
22
|
+
.init(node.with(symbol: Symbol.var(name, node.range)), scope)
|
|
23
|
+
.add_errors([Error::AmbiguousName.new(entry.name, node.range, name:, modules:)])
|
|
24
|
+
|
|
20
25
|
in symbol
|
|
21
26
|
Result.init(node.with(symbol:), scope)
|
|
22
27
|
end
|
|
@@ -53,11 +53,6 @@ module Jade
|
|
|
53
53
|
.then { it.errors.any? ? Err[it.errors] : Ok[entry.with(ast: it.node)] }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def analyze_repl(ast, registry, scope, entry)
|
|
57
|
-
analyze_node(ast, registry, scope, entry)
|
|
58
|
-
.then { it.errors.any? ? Err[it.errors] : Ok[[it.node, it.scope]] }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
56
|
def analyze_entry(entry, registry)
|
|
62
57
|
analyze(entry, registry)
|
|
63
58
|
end
|
|
@@ -66,7 +61,10 @@ module Jade
|
|
|
66
61
|
|
|
67
62
|
def initialize_scope(entry)
|
|
68
63
|
entry
|
|
69
|
-
.
|
|
64
|
+
.ambiguous_values
|
|
65
|
+
.reject { |name, _| entry.defined_values.key?(name) }
|
|
66
|
+
.to_h { |name, refs| [name, Symbol::Ambiguous[name, refs.map(&:module_name)]] }
|
|
67
|
+
.then { entry.values.merge(it) }
|
|
70
68
|
.reduce(Scope.new) { |acc, (unq_name, sym)| acc.bind(unq_name, sym) }
|
|
71
69
|
end
|
|
72
70
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
module Error
|
|
5
|
+
class NarrowedMethodVariable < Jade::Error
|
|
6
|
+
def initialize(entry, span, interface:, fn_name:, var_name:, bound_to:)
|
|
7
|
+
super(entry:, span:)
|
|
8
|
+
@interface = interface
|
|
9
|
+
@fn_name = fn_name
|
|
10
|
+
@var_name = var_name
|
|
11
|
+
@bound_to = bound_to
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def message
|
|
15
|
+
"Implementation of #{@interface}.#{@fn_name} fixes `#{@var_name}` to " \
|
|
16
|
+
"#{@bound_to}, which #{@fn_name} leaves to the call site"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def label
|
|
20
|
+
"fixes `#{@var_name}`"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def notes
|
|
24
|
+
[
|
|
25
|
+
Jade::Diagnostics::Annotation[
|
|
26
|
+
:help,
|
|
27
|
+
"the implementation has to work for every `#{@var_name}` the " \
|
|
28
|
+
'method is written for, not one of them',
|
|
29
|
+
],
|
|
30
|
+
]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
module Error
|
|
5
|
+
class NarrowedSignature < Jade::Error
|
|
6
|
+
def initialize(entry, span, function_name:, var:, found:)
|
|
7
|
+
@function_name = function_name
|
|
8
|
+
@var = var
|
|
9
|
+
@found = found
|
|
10
|
+
super(entry:, span:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def message
|
|
14
|
+
case @found
|
|
15
|
+
in Type::Var
|
|
16
|
+
"`#{@function_name}` says `#{@var}` and `#{@found}` can be different " \
|
|
17
|
+
'types, but its body needs them to be the same'
|
|
18
|
+
else
|
|
19
|
+
"`#{@function_name}` says it works for any `#{@var}`, but its body only " \
|
|
20
|
+
"works when `#{@var}` is #{shown}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def label
|
|
25
|
+
'narrower than its signature'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def notes
|
|
29
|
+
[Jade::Diagnostics::Annotation[
|
|
30
|
+
:help,
|
|
31
|
+
'write the type the body needs in the signature, or make the body ' \
|
|
32
|
+
'work for any type',
|
|
33
|
+
]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def shown
|
|
39
|
+
kept, renamed = @found
|
|
40
|
+
.unbound_vars
|
|
41
|
+
.uniq(&:id)
|
|
42
|
+
.partition { it.name && it.name != @var }
|
|
43
|
+
|
|
44
|
+
renamed
|
|
45
|
+
.zip(('a'..'z').to_a - [@var, *kept.map(&:name)])
|
|
46
|
+
.to_h { |(var, letter)| [var.id, Type.var(var.id, letter)] }
|
|
47
|
+
.then { Substitution[it].apply(@found).to_s }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -7,6 +7,7 @@ require 'jade/frontend/type_checking/error/case_of_branches_type_mismatch'
|
|
|
7
7
|
require 'jade/frontend/type_checking/error/derivation_failed'
|
|
8
8
|
require 'jade/frontend/type_checking/error/recursive_derivation'
|
|
9
9
|
require 'jade/frontend/type_checking/error/function_body_type_mismatch'
|
|
10
|
+
require 'jade/frontend/type_checking/error/narrowed_signature'
|
|
10
11
|
require 'jade/frontend/type_checking/error/function_call_type_mismatch'
|
|
11
12
|
require 'jade/frontend/type_checking/error/if_branch_type_mismatch'
|
|
12
13
|
require 'jade/frontend/type_checking/error/if_branches_type_mismatch'
|
|
@@ -24,5 +25,6 @@ require 'jade/frontend/type_checking/error/unresolved_constraint'
|
|
|
24
25
|
require 'jade/frontend/type_checking/error/implementation_type_mismatch'
|
|
25
26
|
require 'jade/frontend/type_checking/error/implementation_function_constraint'
|
|
26
27
|
require 'jade/frontend/type_checking/error/cross_module_requirement'
|
|
28
|
+
require 'jade/frontend/type_checking/error/narrowed_method_variable'
|
|
27
29
|
require 'jade/frontend/type_checking/error/undeclared_requirement'
|
|
28
30
|
require 'jade/frontend/type_checking/error/missing_implementation'
|
|
@@ -36,7 +36,11 @@ module Jade
|
|
|
36
36
|
PatternAnalysis::Exhaustiveness
|
|
37
37
|
.assert([pattern], pattern.range, final_state.env, expr_result.type)
|
|
38
38
|
.then { final_state.add_errors(it) }
|
|
39
|
-
.unify_result(
|
|
39
|
+
.unify_result(
|
|
40
|
+
expr_result.with(constraints: residual_cs),
|
|
41
|
+
expected.type,
|
|
42
|
+
&type_mismatch(state, node)
|
|
43
|
+
)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
private
|
|
@@ -41,6 +41,7 @@ module Jade
|
|
|
41
41
|
function_name: node.name,
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
|
+
.then { |st| report_narrowing(st, node, symbol, registry, fn_type) }
|
|
44
45
|
.then do |st|
|
|
45
46
|
next st if st.env.bindings[symbol.qualified_name].is_a?(Scheme) && !st.skip_constraints
|
|
46
47
|
|
|
@@ -58,6 +59,26 @@ module Jade
|
|
|
58
59
|
end
|
|
59
60
|
.then { [it, Result.init(Type.unit)] }
|
|
60
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def report_narrowing(state, node, symbol, registry, fn_type)
|
|
66
|
+
Type
|
|
67
|
+
.from_symbol(registry.lookup(symbol), registry, state.env.var_gen)
|
|
68
|
+
.first
|
|
69
|
+
.then { Narrowing.against_declaration(it, state.env.substitution.apply(fn_type)) }
|
|
70
|
+
.then { it ? state.add_errors([narrowed(it, state, node)]) : state }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def narrowed((var, found), state, node)
|
|
74
|
+
Error::NarrowedSignature.new(
|
|
75
|
+
state.env.entry_name,
|
|
76
|
+
node.range,
|
|
77
|
+
function_name: node.name,
|
|
78
|
+
var: var.name,
|
|
79
|
+
found:,
|
|
80
|
+
)
|
|
81
|
+
end
|
|
61
82
|
end
|
|
62
83
|
end
|
|
63
84
|
end
|
|
@@ -36,6 +36,17 @@ module Jade
|
|
|
36
36
|
|
|
37
37
|
result
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
def type_mismatch(state, node)
|
|
41
|
+
->(error) do
|
|
42
|
+
Error::TypeMismatch.new(
|
|
43
|
+
state.env.entry_name,
|
|
44
|
+
node.range,
|
|
45
|
+
expected: error.expected,
|
|
46
|
+
actual: error.actual,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
@@ -58,7 +58,7 @@ module Jade
|
|
|
58
58
|
Expected.check(expected_type),
|
|
59
59
|
interface_qname,
|
|
60
60
|
concrete_type,
|
|
61
|
-
iface_fn_type,
|
|
61
|
+
[iface_fn_type, t_var],
|
|
62
62
|
)
|
|
63
63
|
end
|
|
64
64
|
.then { [it, Result.init(Type.unit)] }
|
|
@@ -151,11 +151,19 @@ module Jade
|
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
def record_requirements(state, constraints, head_type,
|
|
154
|
+
def record_requirements(state, constraints, head_type, signature, interface_qname, impl_fn, fn_name)
|
|
155
|
+
signature => [sig_type, t_var]
|
|
155
156
|
substitution = state.env.substitution
|
|
156
157
|
head_vars = substitution.apply(head_type).unbound_vars.map(&:id).to_set
|
|
157
158
|
names = signature_names(sig_type, substitution)
|
|
158
159
|
|
|
160
|
+
narrowed = Narrowing.against_method(sig_type, t_var, head_vars, substitution)
|
|
161
|
+
unless narrowed.empty?
|
|
162
|
+
return state.add_errors(
|
|
163
|
+
narrowed.map { narrowing(state, impl_fn, interface_qname, fn_name, it) },
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
159
167
|
free = constraints
|
|
160
168
|
.map { substitution.apply(it) }
|
|
161
169
|
.select { it.type.is_a?(Type::Var) && !head_vars.include?(it.type.id) }
|
|
@@ -171,6 +179,19 @@ module Jade
|
|
|
171
179
|
)
|
|
172
180
|
end
|
|
173
181
|
|
|
182
|
+
def narrowing(state, impl_fn, interface_qname, fn_name, narrowed)
|
|
183
|
+
narrowed => [var_name, bound_to]
|
|
184
|
+
|
|
185
|
+
Error::NarrowedMethodVariable.new(
|
|
186
|
+
state.env.entry_name,
|
|
187
|
+
impl_fn.range,
|
|
188
|
+
interface: interface_qname,
|
|
189
|
+
fn_name:,
|
|
190
|
+
var_name:,
|
|
191
|
+
bound_to: bound_to.to_s,
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
174
195
|
def signature_names(sig_type, substitution)
|
|
175
196
|
sig_type
|
|
176
197
|
.unbound_vars
|
|
@@ -12,7 +12,14 @@ module Jade
|
|
|
12
12
|
if items.empty?
|
|
13
13
|
Type.list.apply([state.fresh])
|
|
14
14
|
.then { Result.init(it) }
|
|
15
|
-
.then
|
|
15
|
+
.then do
|
|
16
|
+
return state.unify_result(
|
|
17
|
+
it,
|
|
18
|
+
expected.type,
|
|
19
|
+
expected.rigid_vars,
|
|
20
|
+
&type_mismatch(state, node)
|
|
21
|
+
)
|
|
22
|
+
end
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
head, *rest = items
|
|
@@ -29,7 +36,14 @@ module Jade
|
|
|
29
36
|
|
|
30
37
|
Result
|
|
31
38
|
.init(Type.list.apply([items_result.type]), items_result.constraints)
|
|
32
|
-
.then
|
|
39
|
+
.then do
|
|
40
|
+
items_state.unify_result(
|
|
41
|
+
it,
|
|
42
|
+
expected.type,
|
|
43
|
+
expected.rigid_vars,
|
|
44
|
+
&type_mismatch(items_state, node)
|
|
45
|
+
)
|
|
46
|
+
end
|
|
33
47
|
end
|
|
34
48
|
|
|
35
49
|
private
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
# Unification is symmetric, so pinning a variable a declaration
|
|
5
|
+
# quantifies simply binds it, and a call site instantiating that
|
|
6
|
+
# declaration afresh never sees that it did. Two places can do it: a
|
|
7
|
+
# function's own body, and an implementation against the method it
|
|
8
|
+
# implements.
|
|
9
|
+
module Narrowing
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
# Against a fresh copy of the declaration, since a narrowing collected
|
|
13
|
+
# in an earlier round is already part of the generalised type and the
|
|
14
|
+
# substitution no longer shows it.
|
|
15
|
+
def against_declaration(declared, actual)
|
|
16
|
+
pairs(declared, actual).then do |found|
|
|
17
|
+
found.find { |(_, type)| !type.is_a?(Type::Var) } || merged(found)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# An implementation is the other place a quantified variable can be
|
|
23
|
+
# pinned: the method promises a variable each call site settles, and
|
|
24
|
+
# unification will happily bind it to the implementation's own.
|
|
25
|
+
#
|
|
26
|
+
# The interface's own parameter is exempt — binding that to the
|
|
27
|
+
# implemented type is what an implementation is.
|
|
28
|
+
def against_method(sig_type, t_var, head_vars, substitution)
|
|
29
|
+
sig_type
|
|
30
|
+
.unbound_vars
|
|
31
|
+
.reject { it.id == t_var.id }
|
|
32
|
+
.filter_map { pinned(it, substitution.apply(it), head_vars) }
|
|
33
|
+
.uniq(&:first)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Pinned to one of the implemented type's own variables is the common
|
|
39
|
+
# case, and naming it reads as nonsense when the two are spelled alike.
|
|
40
|
+
def pinned(var, applied, head_vars)
|
|
41
|
+
case applied
|
|
42
|
+
in Type::Var(id:) if !head_vars.include?(id) then nil
|
|
43
|
+
in Type::Var then [var.name, 'the type this implements']
|
|
44
|
+
else [var.name, "`#{applied}`"]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merged(found)
|
|
49
|
+
found
|
|
50
|
+
.uniq { |(var, _)| var.id }
|
|
51
|
+
.group_by { |(_, type)| type.id }
|
|
52
|
+
.values
|
|
53
|
+
.find { it.size > 1 }
|
|
54
|
+
&.then { |((var, _), (other, _))| [var, other] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pairs(declared, actual)
|
|
58
|
+
case declared
|
|
59
|
+
in Type::Var
|
|
60
|
+
[[declared, actual]]
|
|
61
|
+
|
|
62
|
+
in Type::Application(constructor:, args:)
|
|
63
|
+
pairs(constructor, actual.constructor) + zipped(args, actual.args)
|
|
64
|
+
|
|
65
|
+
in Type::Function(args:, return_type:)
|
|
66
|
+
zipped(args, actual.args) + pairs(return_type, actual.return_type)
|
|
67
|
+
|
|
68
|
+
in Type::AnonymousRecord(fields:, row_var:)
|
|
69
|
+
fields.flat_map { |name, type| pairs(type, actual.fields.fetch(name)) } +
|
|
70
|
+
row_pairs(row_var, fields, actual)
|
|
71
|
+
|
|
72
|
+
else
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def zipped(declared, actual)
|
|
78
|
+
declared
|
|
79
|
+
.zip(actual)
|
|
80
|
+
.flat_map { |(d, a)| pairs(d, a) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def row_pairs(row_var, fields, actual)
|
|
84
|
+
return [] unless row_var
|
|
85
|
+
|
|
86
|
+
(actual.fields.keys - fields.keys).empty? && actual.row_var ?
|
|
87
|
+
pairs(row_var, actual.row_var) :
|
|
88
|
+
[[row_var, actual]]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -7,6 +7,7 @@ require 'jade/frontend/type_checking/error'
|
|
|
7
7
|
require 'jade/frontend/type_checking/expected'
|
|
8
8
|
require 'jade/frontend/type_checking/inference'
|
|
9
9
|
require 'jade/frontend/type_checking/loader'
|
|
10
|
+
require 'jade/frontend/type_checking/narrowing'
|
|
10
11
|
require 'jade/frontend/type_checking/port_resolution'
|
|
11
12
|
require 'jade/frontend/type_checking/requirements'
|
|
12
13
|
require 'jade/frontend/type_checking/result'
|
|
@@ -126,11 +127,6 @@ module Jade
|
|
|
126
127
|
.is_a?(Symbol::InteropFunction)
|
|
127
128
|
end
|
|
128
129
|
|
|
129
|
-
def check_repl(node, registry, env = Env.new)
|
|
130
|
-
check_node(node, registry, env, Expected.infer(env.fresh))
|
|
131
|
-
.to_result
|
|
132
|
-
end
|
|
133
|
-
|
|
134
130
|
def check_node(node, registry, state, expected_type)
|
|
135
131
|
case node
|
|
136
132
|
in AST::Body then Inference::Body
|
data/lib/jade/frontend.rb
CHANGED
|
@@ -46,28 +46,6 @@ module Jade
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def run_repl(ast, registry, current_entry, scope, env, var_gen)
|
|
50
|
-
registry ||= registry_with_basics
|
|
51
|
-
current_entry ||= entry_with_basics('JadeRepl')
|
|
52
|
-
scope ||= SemanticAnalysis::Scope.new
|
|
53
|
-
env ||= TypeChecking::Env.new
|
|
54
|
-
var_gen ||= TypeChecking::VarGen.new
|
|
55
|
-
|
|
56
|
-
ForwardDeclaration
|
|
57
|
-
.declare(ast, registry, current_entry)
|
|
58
|
-
.then { |entry| FixityFixer.fix(ast).then { [it, entry] } }
|
|
59
|
-
.then do |fixed_ast, updated_entry|
|
|
60
|
-
updated_registry = registry.update_module(updated_entry)
|
|
61
|
-
SemanticAnalysis
|
|
62
|
-
.analyze_repl(fixed_ast, updated_registry, scope, updated_entry)
|
|
63
|
-
.and_then do |(enhanced_ast, new_scope)|
|
|
64
|
-
enhanced_ast = Desugaring.desugar_resolved(enhanced_ast, updated_registry)
|
|
65
|
-
TypeChecking.check_repl(enhanced_ast, updated_registry, env, var_gen)
|
|
66
|
-
.map { |type, new_env| [enhanced_ast, type, updated_registry, updated_entry, new_scope, new_env] }
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
49
|
def run_up_to_semantic_analysis(ast)
|
|
72
50
|
registry, current_entry = entry_with_basics(ast)
|
|
73
51
|
|
data/lib/jade/parsing/error.rb
CHANGED
|
@@ -52,6 +52,21 @@ module Jade
|
|
|
52
52
|
def message
|
|
53
53
|
"#{context_prefix}Unexpected end of input, expected #{expected}"
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
def to_diagnostic(registry = nil, source: nil)
|
|
57
|
+
super.then do |diagnostic|
|
|
58
|
+
diagnostic.primary => { source: labelled, span: }
|
|
59
|
+
next diagnostic unless labelled && span.nil?
|
|
60
|
+
|
|
61
|
+
diagnostic.with(primary: diagnostic.primary.with(span: last_character(labelled.text)))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def last_character(text)
|
|
68
|
+
text.b.rstrip.bytesize.then { it.zero? ? nil : (it - 1)...it }
|
|
69
|
+
end
|
|
55
70
|
end
|
|
56
71
|
|
|
57
72
|
class UnexpectedTokenError < Error
|
data/lib/jade/registry.rb
CHANGED
data/lib/jade/stdlib/compiled.rb
CHANGED
|
@@ -39,7 +39,14 @@ module Jade
|
|
|
39
39
|
def resolve_imports(entry)
|
|
40
40
|
imports
|
|
41
41
|
.reduce(entry) do |acc, stdlib|
|
|
42
|
-
ImportEntry
|
|
42
|
+
ImportEntry
|
|
43
|
+
.new(
|
|
44
|
+
module_name: stdlib.entry.name,
|
|
45
|
+
alias: stdlib.entry.name,
|
|
46
|
+
unqualified_symbols: stdlib.default_imports,
|
|
47
|
+
qualified_symbols: stdlib.entry.exposes,
|
|
48
|
+
implicit: true,
|
|
49
|
+
)
|
|
43
50
|
.then { acc.import(it) }
|
|
44
51
|
end
|
|
45
52
|
end
|
|
@@ -285,7 +285,14 @@ module Jade
|
|
|
285
285
|
# TODO: This is the same code from stdlib that auto imports stuff.
|
|
286
286
|
imports
|
|
287
287
|
.reduce(entry) do |acc, stdlib|
|
|
288
|
-
ImportEntry
|
|
288
|
+
ImportEntry
|
|
289
|
+
.new(
|
|
290
|
+
module_name: stdlib.entry.name,
|
|
291
|
+
alias: stdlib.entry.name,
|
|
292
|
+
unqualified_symbols: stdlib.default_imports,
|
|
293
|
+
qualified_symbols: stdlib.entry.exposes,
|
|
294
|
+
implicit: true,
|
|
295
|
+
)
|
|
289
296
|
.then { acc.import(it) }
|
|
290
297
|
end
|
|
291
298
|
end
|
data/lib/jade/stdlib.rb
CHANGED
|
@@ -107,7 +107,14 @@ module Jade
|
|
|
107
107
|
def add_imports(entry)
|
|
108
108
|
(STDLIBS - EXTENSIONS)
|
|
109
109
|
.reduce(entry) do |acc, stdlib|
|
|
110
|
-
ImportEntry
|
|
110
|
+
ImportEntry
|
|
111
|
+
.new(
|
|
112
|
+
module_name: stdlib.entry.name,
|
|
113
|
+
alias: stdlib.entry.name,
|
|
114
|
+
unqualified_symbols: stdlib.default_imports,
|
|
115
|
+
qualified_symbols: stdlib.entry.exposes,
|
|
116
|
+
implicit: true,
|
|
117
|
+
)
|
|
111
118
|
.then { acc.import(it) }
|
|
112
119
|
end
|
|
113
120
|
end
|
data/lib/jade/symbol.rb
CHANGED
data/lib/jade/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Cornu
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -126,6 +126,7 @@ files:
|
|
|
126
126
|
- lib/jade/frontend/forward_declaration/body.rb
|
|
127
127
|
- lib/jade/frontend/forward_declaration/error.rb
|
|
128
128
|
- lib/jade/frontend/forward_declaration/error/alias_expansion.rb
|
|
129
|
+
- lib/jade/frontend/forward_declaration/error/ambiguous_type.rb
|
|
129
130
|
- lib/jade/frontend/forward_declaration/error/bad_import.rb
|
|
130
131
|
- lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb
|
|
131
132
|
- lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb
|
|
@@ -164,6 +165,7 @@ files:
|
|
|
164
165
|
- lib/jade/frontend/semantic_analysis/char_literal.rb
|
|
165
166
|
- lib/jade/frontend/semantic_analysis/constructor_reference.rb
|
|
166
167
|
- lib/jade/frontend/semantic_analysis/error.rb
|
|
168
|
+
- lib/jade/frontend/semantic_analysis/error/ambiguous_name.rb
|
|
167
169
|
- lib/jade/frontend/semantic_analysis/error/circular_extends.rb
|
|
168
170
|
- lib/jade/frontend/semantic_analysis/error/constant_not_callable.rb
|
|
169
171
|
- lib/jade/frontend/semantic_analysis/error/constructor_not_found.rb
|
|
@@ -189,6 +191,7 @@ files:
|
|
|
189
191
|
- lib/jade/frontend/semantic_analysis/error/predicate_name_not_allowed.rb
|
|
190
192
|
- lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb
|
|
191
193
|
- lib/jade/frontend/semantic_analysis/error/shadowing_error.rb
|
|
194
|
+
- lib/jade/frontend/semantic_analysis/error/top_level_statement.rb
|
|
192
195
|
- lib/jade/frontend/semantic_analysis/error/type_args_mismatch.rb
|
|
193
196
|
- lib/jade/frontend/semantic_analysis/error/type_param_required.rb
|
|
194
197
|
- lib/jade/frontend/semantic_analysis/error/unbound_type_variable.rb
|
|
@@ -264,6 +267,8 @@ files:
|
|
|
264
267
|
- lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb
|
|
265
268
|
- lib/jade/frontend/type_checking/error/missing_implementation.rb
|
|
266
269
|
- lib/jade/frontend/type_checking/error/missing_patterns.rb
|
|
270
|
+
- lib/jade/frontend/type_checking/error/narrowed_method_variable.rb
|
|
271
|
+
- lib/jade/frontend/type_checking/error/narrowed_signature.rb
|
|
267
272
|
- lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb
|
|
268
273
|
- lib/jade/frontend/type_checking/error/port_not_decodable.rb
|
|
269
274
|
- lib/jade/frontend/type_checking/error/port_not_encodable.rb
|
|
@@ -309,6 +314,7 @@ files:
|
|
|
309
314
|
- lib/jade/frontend/type_checking/inference/variable_reference.rb
|
|
310
315
|
- lib/jade/frontend/type_checking/instantiation.rb
|
|
311
316
|
- lib/jade/frontend/type_checking/loader.rb
|
|
317
|
+
- lib/jade/frontend/type_checking/narrowing.rb
|
|
312
318
|
- lib/jade/frontend/type_checking/placeholder.rb
|
|
313
319
|
- lib/jade/frontend/type_checking/port_resolution.rb
|
|
314
320
|
- lib/jade/frontend/type_checking/requirements.rb
|
|
@@ -381,6 +387,7 @@ files:
|
|
|
381
387
|
- lib/jade/stdlib/tuple.rb
|
|
382
388
|
- lib/jade/symbol.rb
|
|
383
389
|
- lib/jade/symbol/alias.rb
|
|
390
|
+
- lib/jade/symbol/ambiguous.rb
|
|
384
391
|
- lib/jade/symbol/anonymous_record.rb
|
|
385
392
|
- lib/jade/symbol/base.rb
|
|
386
393
|
- lib/jade/symbol/constructor.rb
|