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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e6f1ff92a99cc704dd81615de9b97ffd0f7e650ca23adac6c7ede429eeb8ffe
4
- data.tar.gz: ba35d16db8a3ff88f46ef726df1f19c27fc93a20366d0acd1faec55764690f9c
3
+ metadata.gz: fd36bbcd9dc0af93f281ebf187acafe5eb8b19d9cb868375a4261a2c8346e3c8
4
+ data.tar.gz: 631ba0267189d7d59bf014fe44fb07bec65d657bc0f87ba809bee8bfd577c503
5
5
  SHA512:
6
- metadata.gz: 9f07bb37ec67df7e734a007eadbf0d4ac0b2309e893f0b29b139ed6f04984478551cab0c5ac7ed0c02a83ffb4cf82353490aed477ec1e51b26c0a5961c3fa799
7
- data.tar.gz: 8d87c617b0784d10185cbaaa851d2f34c649ce80ede4b82cafdcde1b4bc569f3e2841e514476fc3c95a4fa6eddf67f09e921f0b838dcf6aa45d8d1bedb34ad3b
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
@@ -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] ||= "DICT_#{table.size}"
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
@@ -178,6 +178,8 @@ module Jade
178
178
  # Bodies that don't fit a single Ruby expression.
179
179
  NO_INLINE = %w[
180
180
  Number.checked
181
+ Dict.show_with
182
+ Set.show_with
181
183
  List.sort_with
182
184
  List.sort_by_with
183
185
  List.filter_map
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
- 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('.'))
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| "#{const} = #{source}.freeze" }
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
- PatternAnalysis::Exhaustiveness
37
- .assert([pattern], pattern.range, final_state.env, expr_result.type)
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
- concrete_t = body_state.env.substitution.apply(t)
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
@@ -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))')
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.12.0'
2
+ VERSION = '0.12.2'
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.12.0
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-17 00:00:00.000000000 Z
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