jade-lang 0.12.0 → 0.12.2
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 +39 -0
- data/lib/jade/codegen/context.rb +14 -0
- data/lib/jade/codegen/function_call.rb +2 -1
- data/lib/jade/codegen/inlines.rb +2 -0
- data/lib/jade/codegen.rb +23 -5
- data/lib/jade/frontend/type_checking/constraints/deriving/show.rb +24 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +2 -3
- data/lib/jade/frontend/type_checking/inference/case_of.rb +1 -6
- data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -4
- data/lib/jade/frontend/type_checking/pattern_checks.rb +30 -0
- data/lib/jade/frontend/type_checking/state.rb +9 -2
- data/lib/jade/frontend/type_checking.rb +2 -1
- data/lib/jade/stdlib/dict.rb +12 -0
- data/lib/jade/stdlib/set.rb +12 -0
- data/lib/jade/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd36bbcd9dc0af93f281ebf187acafe5eb8b19d9cb868375a4261a2c8346e3c8
|
|
4
|
+
data.tar.gz: 631ba0267189d7d59bf014fe44fb07bec65d657bc0f87ba809bee8bfd577c503
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7343d1d44fc2eab19f07360d5d87369c3707e005b51493a58911c07ef52e6c6832e270a54741e325d0ccbdbd65d7ea5f7b6b744fa21bea6bcc68c7d14d235534
|
|
7
|
+
data.tar.gz: 31c9091262b1472f3e51a5797bf1b6ab13359693b774f2912b62d3a53e185124260a6c493cff2d1a5cf7f712b87367ce3fcd346da1577e568d349519441331ae
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.2] - 2026-09-24
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A hoisted dictionary is built on first use, not while the module loads.**
|
|
14
|
+
One can hold the result of calling a module function, and that function's
|
|
15
|
+
own body can name another dictionary — emitted as constants in the order
|
|
16
|
+
they were written, the second was still undefined while the first was being
|
|
17
|
+
built, and requiring the module raised `uninitialized constant DICT_6`.
|
|
18
|
+
Whether it happened depended on the order the dictionaries were discovered
|
|
19
|
+
in, so adding an unrelated function could break a module that compiled
|
|
20
|
+
yesterday. They are memoised singleton methods now, cleared on each load so
|
|
21
|
+
a reloaded module does not keep dictionaries closing over classes the
|
|
22
|
+
reload replaced.
|
|
23
|
+
|
|
24
|
+
## [0.12.1] - 2026-09-21
|
|
25
|
+
|
|
26
|
+
Both of these make code compile that should have compiled, so they ride a
|
|
27
|
+
patch rather than waiting for a minor.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **A pattern in a lambda is checked against the type the caller gives it.**
|
|
32
|
+
Coverage was decided where the pattern was written, which for a lambda is
|
|
33
|
+
before the call that types its parameter — so the subject was still a
|
|
34
|
+
variable and anything but a binding or a wildcard was reported as
|
|
35
|
+
non-exhaustive. `(a, b) = p` inside a `List.map` is the common shape;
|
|
36
|
+
`Tuple.first` / `Tuple.second` or a one-armed `case` were the workarounds.
|
|
37
|
+
Coverage and redundancy now run once inference has finished.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`Show` renders a `Dict` and a `Set`.** Both already had `Eq`, so they
|
|
42
|
+
compared but could not be printed, and anything that shows a value on
|
|
43
|
+
failure — a test assertion, a debug line — stopped compiling at
|
|
44
|
+
`Show cannot be derived for Dict(String, Int)`. Keys, values and elements
|
|
45
|
+
render through their own instances, so a `String` key keeps its quotes and a
|
|
46
|
+
nested list prints as a list.
|
|
47
|
+
|
|
9
48
|
## [0.12.0] - 2026-09-17
|
|
10
49
|
|
|
11
50
|
### Breaking
|
data/lib/jade/codegen/context.rb
CHANGED
|
@@ -56,6 +56,20 @@ module Jade
|
|
|
56
56
|
@dict_consts = prev
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# The module the dictionary methods are defined on, so a reference from
|
|
60
|
+
# inside `Internal` still finds them.
|
|
61
|
+
def dict_owner
|
|
62
|
+
@dict_owner
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_dict_owner(owner)
|
|
66
|
+
prev = @dict_owner
|
|
67
|
+
@dict_owner = owner
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
@dict_owner = prev
|
|
71
|
+
end
|
|
72
|
+
|
|
59
73
|
# When set, references with this name emit as `self` (and field accesses
|
|
60
74
|
# on them as bare method calls). Used to rewrite operator-impl lambda
|
|
61
75
|
# bodies — `(a, b) -> { a.amount == b.amount }` becomes
|
|
@@ -140,7 +140,8 @@ module Jade
|
|
|
140
140
|
table = Codegen.dict_consts or return source
|
|
141
141
|
return source if Codegen.dict_env.values.any? { source.include?(it) }
|
|
142
142
|
|
|
143
|
-
table[source] ||= "
|
|
143
|
+
table[source] ||= "dict_#{table.size}"
|
|
144
|
+
"::#{Codegen.dict_owner}.#{table[source]}"
|
|
144
145
|
end
|
|
145
146
|
|
|
146
147
|
# Polymorphic fn referenced as a value (not called). Wraps the fn with
|
data/lib/jade/codegen/inlines.rb
CHANGED
data/lib/jade/codegen.rb
CHANGED
|
@@ -109,10 +109,12 @@ module Jade
|
|
|
109
109
|
outer, inner, wrappers =
|
|
110
110
|
with_substitution(registry.get(name).env.substitution) do
|
|
111
111
|
with_dict_consts(dict_consts) do
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
with_dict_owner(to_qualified(name)) do
|
|
113
|
+
with_boundary_cache(boundary_cache) do
|
|
114
|
+
with_dispatched_methods(collect_dispatched_methods(body, registry)) do
|
|
115
|
+
with_hoisted_records do
|
|
116
|
+
partition_module_body(body.expressions, registry, name.count('.'))
|
|
117
|
+
end
|
|
116
118
|
end
|
|
117
119
|
end
|
|
118
120
|
end
|
|
@@ -365,9 +367,25 @@ module Jade
|
|
|
365
367
|
# weighed against its specialized form, say), and a discarded one can
|
|
366
368
|
# still have claimed a constant. Keep the ones that survived, and the
|
|
367
369
|
# ones those refer to.
|
|
370
|
+
# Memoised methods rather than constants: a dictionary can hold the
|
|
371
|
+
# result of calling a module function, and that function's own body can
|
|
372
|
+
# name another dictionary. Constants are evaluated in the order they are
|
|
373
|
+
# written, so that dependency could reach forward to one not defined
|
|
374
|
+
# yet; a method body runs when it is first called, by which time every
|
|
375
|
+
# definition exists.
|
|
368
376
|
def referenced_dicts(table, body)
|
|
369
377
|
reachable_dicts(table, body.join(Pretty.newline))
|
|
370
|
-
.map { |source, const|
|
|
378
|
+
.map { |source, const| dict_method(const, source) }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# The nil assignment runs on every load, so a reloaded module rebuilds
|
|
382
|
+
# its dictionaries rather than keeping ones that close over the classes
|
|
383
|
+
# the reload just replaced.
|
|
384
|
+
def dict_method(const, source)
|
|
385
|
+
[
|
|
386
|
+
"@#{const} = nil",
|
|
387
|
+
Pretty.block("def self.#{const}", "@#{const} ||= #{source}.freeze"),
|
|
388
|
+
].join(Pretty.newline(2))
|
|
371
389
|
end
|
|
372
390
|
|
|
373
391
|
# A kept dictionary can name another one, so widen the search text
|
|
@@ -24,6 +24,8 @@ module Jade
|
|
|
24
24
|
# reason. Contained because derivation is private — see
|
|
25
25
|
# vault jade/plans/jade-test-runner.md for the four layers involved.
|
|
26
26
|
LIST = 'List.List'
|
|
27
|
+
DICT = 'Dict.Dict'
|
|
28
|
+
SET = 'Set.Set'
|
|
27
29
|
|
|
28
30
|
def special_case(constraint, lookup)
|
|
29
31
|
case constraint.type
|
|
@@ -33,6 +35,12 @@ module Jade
|
|
|
33
35
|
in Type::Application(constructor: Type::Constructor(name: LIST), args: [inner])
|
|
34
36
|
list_show(constraint, inner, lookup)
|
|
35
37
|
|
|
38
|
+
in Type::Application(constructor: Type::Constructor(name: DICT), args: [key, value])
|
|
39
|
+
container_show(constraint, [key, value], 'Dict.show_with', lookup)
|
|
40
|
+
|
|
41
|
+
in Type::Application(constructor: Type::Constructor(name: SET), args: [inner])
|
|
42
|
+
container_show(constraint, [inner], 'Set.show_with', lookup)
|
|
43
|
+
|
|
36
44
|
else
|
|
37
45
|
nil
|
|
38
46
|
end
|
|
@@ -57,6 +65,22 @@ module Jade
|
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# Threads each element's own `show` into the stdlib function that
|
|
69
|
+
# renders the container.
|
|
70
|
+
def container_show(constraint, inners, fn, lookup)
|
|
71
|
+
inners
|
|
72
|
+
.map { lookup.call(Type.constraint(INTERFACE, it, constraint.origin)) }
|
|
73
|
+
.then { Results.sequence(it) }
|
|
74
|
+
.and_then do |deps|
|
|
75
|
+
args = deps.each_index.map { [:impl_arg, it, 'show'] } + [[:var, 'value']]
|
|
76
|
+
|
|
77
|
+
Symbol::DerivedFunction
|
|
78
|
+
.new(params: ['value'], body: [:call, [:stdlib_fn, fn], args])
|
|
79
|
+
.then { Ok[implementation(constraint, { 'show' => it }, deps:)] }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
60
84
|
def constant(text)
|
|
61
85
|
Symbol::DerivedFunction.new(params: ['value'], body: text)
|
|
62
86
|
end
|
|
@@ -33,9 +33,8 @@ module Jade
|
|
|
33
33
|
[pattern_state, expr_result.constraints]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.then { final_state.add_errors(it) }
|
|
36
|
+
final_state
|
|
37
|
+
.defer_patterns([pattern], pattern.range, expr_result.type)
|
|
39
38
|
.unify_result(
|
|
40
39
|
expr_result.with(constraints: residual_cs),
|
|
41
40
|
expected.type,
|
|
@@ -86,12 +86,7 @@ module Jade
|
|
|
86
86
|
patterns = branches.map(&:pattern)
|
|
87
87
|
type = result.apply(state.env.substitution).type
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
PatternAnalysis::Exhaustiveness.assert(patterns, node.range, state.env, type),
|
|
91
|
-
PatternAnalysis::Redundancy.assert(patterns, state.env, type),
|
|
92
|
-
]
|
|
93
|
-
.flatten
|
|
94
|
-
.then { state.add_errors(it) }
|
|
89
|
+
state.defer_patterns(patterns, node.range, type)
|
|
95
90
|
end
|
|
96
91
|
|
|
97
92
|
def mistyped_patterns?(node, state)
|
|
@@ -54,10 +54,7 @@ module Jade
|
|
|
54
54
|
in AST::Pattern::Binding | AST::Pattern::Wildcard
|
|
55
55
|
acc
|
|
56
56
|
else
|
|
57
|
-
|
|
58
|
-
PatternAnalysis::Exhaustiveness
|
|
59
|
-
.assert([p], p.range, acc.env, concrete_t)
|
|
60
|
-
.then { acc.add_errors(it) }
|
|
57
|
+
acc.defer_patterns([p], p.range, t)
|
|
61
58
|
end
|
|
62
59
|
end
|
|
63
60
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
# The coverage and redundancy of every `case` in a definition, decided
|
|
5
|
+
# against the substitution inference ended with rather than the one in
|
|
6
|
+
# scope where the patterns were written.
|
|
7
|
+
module PatternChecks
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def run(state)
|
|
11
|
+
state
|
|
12
|
+
.pattern_checks
|
|
13
|
+
.flat_map { |(patterns, range, type)| errors_for(patterns, range, type, state.env) }
|
|
14
|
+
.then { state.add_errors(it) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def errors_for(patterns, range, type, env)
|
|
20
|
+
resolved = env.substitution.apply(type)
|
|
21
|
+
|
|
22
|
+
[
|
|
23
|
+
PatternAnalysis::Exhaustiveness.assert(patterns, range, env, resolved),
|
|
24
|
+
PatternAnalysis::Redundancy.assert(patterns, env, resolved),
|
|
25
|
+
].flatten
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module Frontend
|
|
3
3
|
module TypeChecking
|
|
4
|
-
State = Data.define(:env, :errors, :skip_constraints, :impl_requirements) do
|
|
4
|
+
State = Data.define(:env, :errors, :skip_constraints, :impl_requirements, :pattern_checks) do
|
|
5
5
|
def self.init(env, skip_constraints: false)
|
|
6
|
-
new(env, [], skip_constraints, {})
|
|
6
|
+
new(env, [], skip_constraints, {}, [])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Coverage is decided once inference is done: a pattern in a lambda is
|
|
10
|
+
# checked before the call that gives the lambda's parameter its type,
|
|
11
|
+
# so the subject can still be a variable here.
|
|
12
|
+
def defer_patterns(patterns, range, type)
|
|
13
|
+
with(pattern_checks: pattern_checks + [[patterns, range, type]])
|
|
7
14
|
end
|
|
8
15
|
|
|
9
16
|
def require_impl(key, constraints)
|
|
@@ -8,6 +8,7 @@ require 'jade/frontend/type_checking/expected'
|
|
|
8
8
|
require 'jade/frontend/type_checking/inference'
|
|
9
9
|
require 'jade/frontend/type_checking/loader'
|
|
10
10
|
require 'jade/frontend/type_checking/narrowing'
|
|
11
|
+
require 'jade/frontend/type_checking/pattern_checks'
|
|
11
12
|
require 'jade/frontend/type_checking/port_resolution'
|
|
12
13
|
require 'jade/frontend/type_checking/requirements'
|
|
13
14
|
require 'jade/frontend/type_checking/result'
|
|
@@ -28,7 +29,7 @@ module Jade
|
|
|
28
29
|
.load(entry, registry)
|
|
29
30
|
.then { collect_constraints(entry, registry, it) }
|
|
30
31
|
.then { check_node(entry.ast, registry, State.init(it), Expected.infer(it.fresh)) }
|
|
31
|
-
.then { |state, _| Requirements.reconcile(entry, registry, state) }
|
|
32
|
+
.then { |state, _| Requirements.reconcile(entry, registry, PatternChecks.run(state)) }
|
|
32
33
|
.then { |amended, state| complete(amended, state, registry) }
|
|
33
34
|
.and_then { PortResolution.resolve(it, registry) }
|
|
34
35
|
end
|
data/lib/jade/stdlib/dict.rb
CHANGED
|
@@ -69,6 +69,18 @@ module Jade
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
function(
|
|
73
|
+
:show_with,
|
|
74
|
+
{ show_key: 'k -> String', show_value: 'v -> String', dict: 'Dict(k, v)' },
|
|
75
|
+
'String',
|
|
76
|
+
private: true,
|
|
77
|
+
) do |show_key, show_value, dict|
|
|
78
|
+
dict.hash
|
|
79
|
+
.map { |k, v| "#{show_key.call(k)}: #{show_value.call(v)}" }
|
|
80
|
+
.join(', ')
|
|
81
|
+
.then { "Dict(#{it})" }
|
|
82
|
+
end
|
|
83
|
+
|
|
72
84
|
function(:keys, { dict: 'Dict(k, v)' }, 'List(k)')
|
|
73
85
|
function(:values, { dict: 'Dict(k, v)' }, 'List(v)')
|
|
74
86
|
function(:to_list, { dict: 'Dict(k, v)' }, 'List(Tuple2(k, v))')
|
data/lib/jade/stdlib/set.rb
CHANGED
|
@@ -24,6 +24,18 @@ module Jade
|
|
|
24
24
|
constraints: [['Basics.Eq', 'a']],
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
function(
|
|
28
|
+
:show_with,
|
|
29
|
+
{ show_value: 'a -> String', set: 'Set(a)' },
|
|
30
|
+
'String',
|
|
31
|
+
private: true,
|
|
32
|
+
) do |show_value, set|
|
|
33
|
+
set.hash.keys
|
|
34
|
+
.map { show_value.call(it) }
|
|
35
|
+
.join(', ')
|
|
36
|
+
.then { "Set(#{it})" }
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
function(:"empty?", { set: 'Set(a)' }, 'Bool')
|
|
28
40
|
function(:size, { set: 'Set(a)' }, 'Int')
|
|
29
41
|
|
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.12.
|
|
4
|
+
version: 0.12.2
|
|
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-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -315,6 +315,7 @@ files:
|
|
|
315
315
|
- lib/jade/frontend/type_checking/instantiation.rb
|
|
316
316
|
- lib/jade/frontend/type_checking/loader.rb
|
|
317
317
|
- lib/jade/frontend/type_checking/narrowing.rb
|
|
318
|
+
- lib/jade/frontend/type_checking/pattern_checks.rb
|
|
318
319
|
- lib/jade/frontend/type_checking/placeholder.rb
|
|
319
320
|
- lib/jade/frontend/type_checking/port_resolution.rb
|
|
320
321
|
- lib/jade/frontend/type_checking/requirements.rb
|