jade-lang 0.11.3 → 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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/lib/jade/ast.rb +1 -1
  4. data/lib/jade/codegen/context.rb +18 -1
  5. data/lib/jade/codegen/function_call.rb +9 -2
  6. data/lib/jade/codegen.rb +7 -5
  7. data/lib/jade/diagnostics/renderer.rb +5 -1
  8. data/lib/jade/entry.rb +32 -10
  9. data/lib/jade/frontend/forward_declaration/error/ambiguous_type.rb +36 -0
  10. data/lib/jade/frontend/forward_declaration/error.rb +1 -0
  11. data/lib/jade/frontend/forward_declaration/helper.rb +7 -0
  12. data/lib/jade/frontend/forward_declaration/import_declaration.rb +10 -2
  13. data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +5 -0
  14. data/lib/jade/frontend/semantic_analysis/error/ambiguous_name.rb +36 -0
  15. data/lib/jade/frontend/semantic_analysis/error/top_level_statement.rb +28 -0
  16. data/lib/jade/frontend/semantic_analysis/error.rb +2 -0
  17. data/lib/jade/frontend/semantic_analysis/module_node.rb +21 -1
  18. data/lib/jade/frontend/semantic_analysis/variable_reference.rb +5 -0
  19. data/lib/jade/frontend/semantic_analysis.rb +4 -6
  20. data/lib/jade/frontend/type_checking/error/narrowed_method_variable.rb +36 -0
  21. data/lib/jade/frontend/type_checking/error/narrowed_signature.rb +53 -0
  22. data/lib/jade/frontend/type_checking/error.rb +2 -0
  23. data/lib/jade/frontend/type_checking/inference/assign.rb +5 -1
  24. data/lib/jade/frontend/type_checking/inference/constructor_reference.rb +1 -1
  25. data/lib/jade/frontend/type_checking/inference/function_declaration.rb +21 -0
  26. data/lib/jade/frontend/type_checking/inference/helpers.rb +11 -0
  27. data/lib/jade/frontend/type_checking/inference/implementation.rb +23 -2
  28. data/lib/jade/frontend/type_checking/inference/list.rb +16 -2
  29. data/lib/jade/frontend/type_checking/narrowing.rb +93 -0
  30. data/lib/jade/frontend/type_checking.rb +1 -5
  31. data/lib/jade/frontend.rb +0 -22
  32. data/lib/jade/parsing/error.rb +15 -0
  33. data/lib/jade/registry.rb +1 -1
  34. data/lib/jade/stdlib/compiled.rb +8 -1
  35. data/lib/jade/stdlib/intrinsics.rb +8 -1
  36. data/lib/jade/stdlib.rb +8 -1
  37. data/lib/jade/symbol/ambiguous.rb +5 -0
  38. data/lib/jade/symbol.rb +1 -0
  39. data/lib/jade/version.rb +1 -1
  40. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5529a34cf9b8a3d407d78642bca0281ec0a422dcded4f5875636e85f7018d7e4
4
- data.tar.gz: c8da773021523ebb4d93836d8c8dee0ba0dca6acf4115f32ff46443a8dfbf08e
3
+ metadata.gz: 2e6f1ff92a99cc704dd81615de9b97ffd0f7e650ca23adac6c7ede429eeb8ffe
4
+ data.tar.gz: ba35d16db8a3ff88f46ef726df1f19c27fc93a20366d0acd1faec55764690f9c
5
5
  SHA512:
6
- metadata.gz: 26bab9740a9f008e4611a52210ec002f314e1fec4169239cd1a4aaa37eb090829a6fd20f4742ab475c169e4dc856652deddf12eca95f862f413668accdeda724
7
- data.tar.gz: 243a05ffe69948767ccb7900b4e2cf443dccdb50d540f6cd16998863a6917e2742718769b742c0d0bf9f1e121aa4ee51cd85b70084ea7f26d669a1e49bf14d54
6
+ metadata.gz: 9f07bb37ec67df7e734a007eadbf0d4ac0b2309e893f0b29b139ed6f04984478551cab0c5ac7ed0c02a83ffb4cf82353490aed477ec1e51b26c0a5961c3fa799
7
+ data.tar.gz: 8d87c617b0784d10185cbaaa851d2f34c649ce80ede4b82cafdcde1b4bc569f3e2841e514476fc3c95a4fa6eddf67f09e921f0b838dcf6aa45d8d1bedb34ad3b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,94 @@ 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
+
7
95
  ## [0.11.3] - 2026-09-16
8
96
 
9
97
  ### Added
data/lib/jade/ast.rb CHANGED
@@ -99,7 +99,7 @@ module Jade
99
99
  ->(tokens) do
100
100
  tokens => [open, token, close]
101
101
 
102
- Literal[token.value, token.range.begin...close.range.end]
102
+ Literal[token.value, open.range.begin...close.range.end]
103
103
  end
104
104
  end
105
105
 
@@ -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 (REPL) get the runtime
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
@@ -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
data/lib/jade/codegen.rb CHANGED
@@ -107,11 +107,13 @@ module Jade
107
107
  dict_consts = {}
108
108
 
109
109
  outer, inner, wrappers =
110
- with_dict_consts(dict_consts) do
111
- with_boundary_cache(boundary_cache) do
112
- with_dispatched_methods(collect_dispatched_methods(body, registry)) do
113
- with_hoisted_records do
114
- partition_module_body(body.expressions, registry, name.count('.'))
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 { span_block(it, severity: diagnostic.severity) if it.source },
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
- imports
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
- imports
76
- .flat_map(&:unqualified_symbols)
77
- .select { it.is_a?(Symbol::TypeRef) }
78
- .map { [it.name, it] }
79
- .to_h
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 { ImportEntry[module_name, as&.as || module_name, it, importing_module.exposes] }
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.exposed]
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
- .values
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(expr_result.with(constraints: residual_cs), expected.type)
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
@@ -12,7 +12,7 @@ module Jade
12
12
  state
13
13
  .env
14
14
  .lookup(symbol.qualified_name)
15
- .then { state.unify_result(it, expected.type) }
15
+ .then { state.unify_result(it, expected.type, &type_mismatch(state, node)) }
16
16
  end
17
17
  end
18
18
  end
@@ -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, sig_type, interface_qname, impl_fn, fn_name)
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 { return state.unify_result(it, expected.type, expected.rigid_vars) }
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 { items_state.unify_result(it, expected.type, expected.rigid_vars) }
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
 
@@ -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
@@ -75,5 +75,5 @@ module Jade
75
75
  end
76
76
  end
77
77
 
78
- ImportEntry = Data.define(:module_name, :alias, :unqualified_symbols, :qualified_symbols)
78
+ ImportEntry = Data.define(:module_name, :alias, :unqualified_symbols, :qualified_symbols, :implicit)
79
79
  end
@@ -39,7 +39,14 @@ module Jade
39
39
  def resolve_imports(entry)
40
40
  imports
41
41
  .reduce(entry) do |acc, stdlib|
42
- ImportEntry[stdlib.entry.name, stdlib.entry.name, stdlib.default_imports, stdlib.entry.exposes]
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[stdlib.entry.name, stdlib.entry.name, stdlib.default_imports, stdlib.entry.exposes]
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[stdlib.entry.name, stdlib.entry.name, stdlib.default_imports, stdlib.entry.exposes]
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
@@ -0,0 +1,5 @@
1
+ module Jade
2
+ module Symbol
3
+ Ambiguous = Data.define(:name, :modules)
4
+ end
5
+ end
data/lib/jade/symbol.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'jade/symbol/base'
2
2
 
3
+ require 'jade/symbol/ambiguous'
3
4
  require 'jade/symbol/anonymous_record'
4
5
  require 'jade/symbol/constructor'
5
6
  require 'jade/symbol/derived_function'
data/lib/jade/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.11.3'
2
+ VERSION = '0.12.0'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jade-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.3
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-16 00:00:00.000000000 Z
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