jade-lang 0.9.0 → 0.10.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +400 -12
  3. data/docs/interop.md +18 -1
  4. data/docs/stdlib.md +3 -1
  5. data/docs/syntax.md +96 -0
  6. data/lib/jade/api.rb +9 -0
  7. data/lib/jade/ast.rb +37 -1
  8. data/lib/jade/cli/eject.rb +132 -0
  9. data/lib/jade/cli.rb +2 -0
  10. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  11. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  12. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  13. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  14. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  15. data/lib/jade/codegen/boundary.rb +12 -2
  16. data/lib/jade/codegen/context.rb +17 -0
  17. data/lib/jade/codegen/error.rb +1 -0
  18. data/lib/jade/codegen/function_call.rb +23 -7
  19. data/lib/jade/codegen/function_declaration.rb +25 -13
  20. data/lib/jade/codegen/helpers.rb +27 -4
  21. data/lib/jade/codegen/implementation.rb +2 -2
  22. data/lib/jade/codegen/inline.rb +1 -1
  23. data/lib/jade/codegen/inlines.rb +6 -1
  24. data/lib/jade/codegen/names.rb +31 -0
  25. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  26. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  27. data/lib/jade/codegen.rb +76 -15
  28. data/lib/jade/compiler.rb +2 -0
  29. data/lib/jade/entry.rb +1 -1
  30. data/lib/jade/formatter/calls.rb +68 -11
  31. data/lib/jade/formatter/collections.rb +4 -4
  32. data/lib/jade/formatter/declarations.rb +13 -15
  33. data/lib/jade/formatter/helper.rb +20 -0
  34. data/lib/jade/formatter/infix_application.rb +19 -4
  35. data/lib/jade/formatter/lambda.rb +24 -12
  36. data/lib/jade/formatter/pattern.rb +3 -0
  37. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  38. data/lib/jade/frontend/desugaring.rb +29 -9
  39. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  40. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  41. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  42. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  43. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  44. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  45. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  46. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  47. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  48. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  49. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  50. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  51. data/lib/jade/frontend/forward_declaration.rb +2 -0
  52. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  53. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  54. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  55. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  56. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  57. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  58. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  59. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  60. data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
  61. data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
  62. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
  63. data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
  64. data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
  65. data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
  66. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
  67. data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
  68. data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
  69. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
  70. data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
  71. data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
  72. data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
  73. data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
  74. data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
  75. data/lib/jade/frontend/semantic_analysis.rb +5 -0
  76. data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
  77. data/lib/jade/frontend/type_checking/cascade.rb +90 -0
  78. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
  79. data/lib/jade/frontend/type_checking/constraints.rb +18 -3
  80. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  81. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  82. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  83. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  84. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  85. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  86. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  87. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  88. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  89. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  90. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  91. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  92. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  93. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  94. data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
  95. data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
  96. data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
  97. data/lib/jade/frontend/type_checking/error.rb +6 -0
  98. data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
  99. data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
  100. data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
  101. data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
  102. data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
  103. data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
  104. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  105. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  106. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  107. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  108. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  109. data/lib/jade/frontend/type_checking.rb +3 -1
  110. data/lib/jade/frontend/usage_analysis.rb +5 -1
  111. data/lib/jade/interop/boundary.rb +64 -24
  112. data/lib/jade/interop/error.rb +35 -7
  113. data/lib/jade/interop/lowering.rb +8 -2
  114. data/lib/jade/lsp/converters.rb +83 -2
  115. data/lib/jade/lsp/handlers.rb +122 -25
  116. data/lib/jade/lsp/snippets.rb +12 -0
  117. data/lib/jade/lsp/state.rb +11 -4
  118. data/lib/jade/module_loader/build.rb +36 -0
  119. data/lib/jade/module_loader.rb +1 -0
  120. data/lib/jade/parsing/combinators.rb +78 -2
  121. data/lib/jade/parsing/error.rb +43 -6
  122. data/lib/jade/parsing/type.rb +5 -3
  123. data/lib/jade/parsing.rb +140 -26
  124. data/lib/jade/runtime.rb +32 -13
  125. data/lib/jade/source.rb +6 -2
  126. data/lib/jade/stdlib/basics.rb +22 -3
  127. data/lib/jade/stdlib/char.rb +2 -0
  128. data/lib/jade/stdlib/clock.rb +6 -2
  129. data/lib/jade/stdlib/decimal.rb +51 -14
  130. data/lib/jade/stdlib/intrinsics.rb +59 -6
  131. data/lib/jade/stdlib/list.rb +37 -1
  132. data/lib/jade/stdlib/number.rb +47 -0
  133. data/lib/jade/stdlib/range.rb +211 -0
  134. data/lib/jade/stdlib.rb +11 -4
  135. data/lib/jade/symbol/alias.rb +11 -0
  136. data/lib/jade/symbol/parser.rb +1 -1
  137. data/lib/jade/symbol.rb +13 -1
  138. data/lib/jade/task.rb +0 -2
  139. data/lib/jade/type/anonymous_record.rb +11 -2
  140. data/lib/jade/type/application.rb +11 -2
  141. data/lib/jade/type/base.rb +26 -0
  142. data/lib/jade/type/function.rb +11 -2
  143. data/lib/jade/type.rb +36 -10
  144. data/lib/jade/version.rb +1 -1
  145. metadata +33 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b817b13aad1b314b6a8bf578f8a604246d19091712a77375376515732b6dcbc
4
- data.tar.gz: 7be6b4091ac7c57c6befe21fb0ffbea302b9e33bd2940479b7c4227dd37cd1b6
3
+ metadata.gz: ba543580eec5d50cae2249c010a03f7a56a96f276705b3b87df7f25f06ff8eeb
4
+ data.tar.gz: 15923caf94302e7326aef6cc8a0e581c349b7b46d54ae4898e6bd8d8529657cf
5
5
  SHA512:
6
- metadata.gz: 9b758da80e8b9033dd6b5ce840188f7d69a8d482cf496a3731c4a4f7b5bcb90ddd0119e5b5eb6e9b1763bddffca4f3df49b6cba7d6a26d9304538efde7a8aa49
7
- data.tar.gz: 22abc8374f37d7f9ff3c03f5f2a35861322f234227c3d0932e80c56e17bb49de5ce6ac67dc7c1f0b1896ce2b9a5c3562a2d8197f697f74fdd5d18e5db64094f6
6
+ metadata.gz: 3906343d3094e2b9142aebc9324722c584db45dfb0951d6546d917794559a8cb9249d9ff4243bb380dc383932e78b93609455743a9e349ec3b60086450693846
7
+ data.tar.gz: 841c318dc43aa3e99df702eba6ffda7621b5a8c0f97e9302f2d75ef2149a36bcc19a1287d26a3cef5091c1361508d333a964f9eb70094e30a5791e8c4f8394a9
data/CHANGELOG.md CHANGED
@@ -4,27 +4,396 @@ 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]
7
+ ## [0.10.0] - 2026-09-02
8
+
9
+ ### Breaking
10
+
11
+ - **Division cannot divide by zero.** `a / b` used to raise Ruby's
12
+ `ZeroDivisionError` straight through Jade, so `Int -> Int -> Int` was not as
13
+ total as it read. `/` now takes a `NonZero`, which only `non_zero(n)`
14
+ produces, and it hands back a `Maybe` so the caller decides what an absent
15
+ answer means. A literal divisor is already known: `cents / 100` is unchanged,
16
+ and `n / 0` is a compile error rather than an expression that type checks.
17
+ `NonZero` has no constructor and is erased at runtime, so division costs what
18
+ it costs in Ruby.
19
+
20
+ ```jade
21
+ def each(total: Int, people: Int) -> Maybe(Int)
22
+ Maybe.map(non_zero(people), (d) -> { total / d })
23
+ end
24
+ ```
25
+
26
+ Implementing `Numeric` for your own type means `(/): a -> NonZero(a) -> a`,
27
+ with `Number.unwrap` to unwrap the divisor. `Decimal.div` takes a `NonZero`
28
+ for the same reason.
8
29
 
9
30
  ### Added
10
31
 
11
- - **`Jade::Extensions`, where a gem hooks into compilation.** Two kinds, both
12
- read-only: a *deriver* builds an implementation for an interface it owns,
13
- and a *check* reads a call site and returns errors. Checks register against
14
- a named phase `:call` today and receive the call's AST node alongside
15
- its resolved argument types, because types alone cannot see a raw SQL
16
- string's placeholders or a constant predicate. Only gems named in
17
- `Extensions::ALLOWED` may register, so the compiler knows who extends it and
18
- nothing about what they do.
32
+ - **An anonymous record crosses to Ruby.** `def origin -> { x: Int, y: Int }`
33
+ returned `NotExposed`: `Encodable` derived for a `struct` but not for the
34
+ same shape written inline, so a function returning one was unreachable from
35
+ Ruby though every field was encodable. It now derives structurally, the way
36
+ `Decodable` already did, and a record with a field that has no instance
37
+ still refuses.
38
+
39
+ - **`type alias Name = T`.** A structural name for a type that already exists:
40
+ `type alias UserId = Int`, `type alias Point = (Float, Float)`,
41
+ `type alias User = { name: String, age: Int }`. The alias and its body are
42
+ the same type, so a record literal satisfies a record alias directly and no
43
+ constructor is introduced. Aliases take parameters
44
+ (`type alias Pair(a) = (a, a)`). An alias without parameters is expanded
45
+ during forward declaration and a parameterised one when its arguments are
46
+ known, which is what makes `UserId` and `Int` interchangeable everywhere.
47
+
48
+ An alias has no identity of its own, so it carries no implementations:
49
+ `implements Show(UserId)` is rejected rather than silently attaching to
50
+ `Int`, where it would collide with every other alias over `Int`. What an
51
+ alias *inherits* needs no declaring — `Encode.encode` on a `UserId` is
52
+ `Encode.encode` on an `Int`. A recursive alias is rejected too. Reach for
53
+ `struct` when you want a distinct type, or a single-variant `type` for a
54
+ newtype around an inner shape.
55
+
56
+ `jade fmt` breaks a record alias past one field the way it breaks the
57
+ `struct` beside it, and `alias` is a contextual keyword — read as one only
58
+ directly after `type` — so a record field or argument may still be called
59
+ `alias`.
60
+
61
+ Expansion reaches the edges too: an alias over a `Task` is a port return
62
+ type and cannot hide a nested one, a function type behind an alias is still
63
+ refused for interop, an alias cannot stand between a type and itself and
64
+ hide that there is no way to build one, `implements` on an *imported* alias
65
+ is reported rather than reaching codegen, and `exposing (UserId(..))` says
66
+ an alias has no constructors instead of accepting it silently.
67
+
68
+ The name survives expansion for the sake of reading: a mismatch against a
69
+ `UserId` says `should be UserId (= Int)` rather than `should be Int`, and
70
+ keeps the short name in a nested position (`List(UserId)`). `jade api`
71
+ reports an alias as an alias rather than as a function, and hovering one in
72
+ an editor gives its declaration — on the declaration itself and on every use
73
+ in a signature.
74
+
75
+ - **`jade eject`.** Writes the project as Ruby that runs without the gem: every
76
+ compiled module, the runtime they call, and requires pointing at each other
77
+ rather than at a load path. What it vendors is whatever a booted runtime
78
+ loads, asked at eject time rather than kept as a list, so it cannot drift.
79
+ The exit was a promise before this; now it is a command with a spec that
80
+ runs the ejected tree in a process that has no jade on its load path.
81
+
82
+ - **A guard on what the runtime loads.** Compiled code requires
83
+ `jade/runtime` and nothing else, so whatever that pulls in ships in every
84
+ production image and has to be vendored by an eject. A spec now pins the
85
+ set to seven files and fails if the parser, the AST, the frontend, the
86
+ formatter, the LSP or the CLI appear. Booting the intrinsics still does
87
+ pull the compiler in, and that example is pending rather than passing.
88
+
89
+ - **The LSP indents while you type.** It advertised `documentFormattingProvider`
90
+ and nothing else, so an editor without the tree-sitter grammar left the cursor
91
+ at column 0 after every Enter. `documentOnTypeFormatting` now indents the line
92
+ Enter opened, and puts `end` back where its opener sits, from the text above
93
+ the cursor rather than a parse, which a half-written file will not give.
94
+
95
+ - **`_` works in a tuple or record literal.** `5 |> (_, "five")` and
96
+ `{ w: _, h: 2 }` make a function of the hole, the way `f(_, y)` already did.
97
+ A variant carrying a record gets it too, since that is what a keyed call
98
+ lowers to: `Rect(w: _, h: 4)` was the one shape with no way to express it
99
+ short of writing the lambda out.
100
+
101
+ - **`(x, y) = p` destructures a tuple.** A tuple's arity is fixed by its type,
102
+ so the binding is irrefutable exactly as `T(x, y) = t` already was; the
103
+ workaround was a three-line `case` for a destructure that cannot fail. The
104
+ pattern was accepted all along, but a `(` opening a line was read as a call
105
+ on whatever the line above ended with, so the binding never got the chance.
106
+
107
+ - **`List.sum` and `List.product`.** Both fold over `Numeric`, which gained
108
+ `from_int : Int -> a` so the seed can come from the interface rather than the
109
+ list: `sum([])` is `0`, not "nothing", so a `Maybe` return would have modelled
110
+ a non-problem as a failure. `Int` implements it with `identity`, `Float` with
111
+ `to_float`, `Decimal` by scaling. A type of your own that implements `Numeric`
112
+ needs the new member.
113
+
114
+ - **A call short of its arguments says so.** `Rec(id: 1, status: Issued)`,
115
+ where `Issued` carries an `Int`, reported `expected (Int, Status) -> Rec but
116
+ found (Int, (Int) -> Status) -> Rec` and left the reader to notice which slot
117
+ had become a function. It now adds ``help: `Issued` needs 1 argument``, and a
118
+ call with the wrong number of arguments outright reads ``help: `add` takes 2
119
+ arguments, 1 given``.
120
+
121
+ - **A call with a hole and one argument too many says so.** `xs |> f(_, y)`
122
+ reads as though the `_` marks where the piped value goes, but `|>` has
123
+ already put it in front. The message now names the shape and the fix
124
+ instead of leaving a unification failure to be decoded.
125
+
126
+ - **The `module` snippet fills in the name.** A module's name is fixed by its
127
+ path, so the placeholder was offering a decision with one right answer;
128
+ completion now derives it from the document URI, turning `sql/mutation.jd`
129
+ into `module Sql.Mutation exposing (...)`.
19
130
 
20
131
  ### Changed
21
132
 
22
- - **The `Sql.Assignable` deriver moves to jade-sql.** The deriver list named
23
- jade-sql's interface outright, with a comment apologising for it; it now
24
- holds only the built-ins and whatever an allowed gem registers.
133
+ - **A crossing does less on the way in.** An argument with a specialized
134
+ decoder now carries its own name, in a string constant it was passing
135
+ anyway, so the wrapper no longer opens a block per argument just to label a
136
+ failure. And a hash is no longer scanned for symbol keys before its fields
137
+ are read: symbol keys make every field missing at once, which the rescue
138
+ around the fields already notices, so the message is unchanged and the check
139
+ is gone from the success path. A struct argument goes from 772ns to 724ns, a
140
+ scalar argument from 158ns to 127ns, and decoding a list of 1500 structs
141
+ from 940us to 820us.
142
+
143
+ - **A dictionary built from concrete implementations is built once.** Calling
144
+ a function with an interface constraint at a known type emitted the whole
145
+ dictionary as a literal at the point of use, so `List.sum(xs)` on a
146
+ `List(Int)` allocated a 5-entry Hash, an Array and two lambdas on every
147
+ call, 522ns before any adding happened. Inside a fold the literal landed in
148
+ the block and was rebuilt per element. Dictionaries that name no dict
149
+ parameter now become frozen module constants, shared by every call site
150
+ that wants the same one, and referencing an interface method as a value no
151
+ longer builds a Hash to index it once. A fold over 5000 values goes from
152
+ 7.3x the equivalent Ruby to 3.3x, a per-row `List.sum` from 2.8x to 2.0x.
153
+
154
+ - **A variant with no fields is one object, not a new one per construction.**
155
+ `compare` allocated a fresh `GT` on every call, which cost more than the
156
+ comparison: 214ns against Ruby's 23ns for `<=>`. Nullary variants carry no
157
+ state, so nothing can tell two instances apart, and every construction now
158
+ hands back the same object. `compare` drops to 87ns, and a function
159
+ returning a three-arm enum over 5000 values goes from 5.7x the equivalent
160
+ Ruby to 2.7x. Applies to `Maybe.Nothing`, `Result` arms, `Order` and every
161
+ user enum.
162
+
163
+ - **A rejected value says where in the value the problem is.** A field that
164
+ did not decode reads `Shop.total(items)[0].cents: expected Int, got String
165
+ ("lots")` rather than blaming the argument as a whole, and a field the hash
166
+ never had reads `missing field \`cents\`` rather than `expected Int, got
167
+ nil`. Paths cost nothing on the way in: the segments are constants in the
168
+ generated code, and the index of a bad element is found only when one is.
169
+
170
+ - **A build left by another version of the compiler is rebuilt.** Generated
171
+ Ruby calls the runtime by name, so a build made by a different version can
172
+ call a helper this one no longer has. `.jade/build` now carries the
173
+ fingerprint that `.jade/cache` already used, and is dropped when it does not
174
+ match. No source changes in that case, so mtimes could not see it.
175
+
176
+ - **A value Ruby cannot pass says which call and which argument rejected it.**
177
+ `Ruby passed a value that failed to decode at value: expected Int, got null
178
+ (nil)` is now `Shop.price(item): expected Int, got nil`. The caller may not
179
+ know Jade, so the message names the function, the argument it came in as, and
180
+ types in Ruby's vocabulary rather than the wire's.
181
+
182
+ - **`jade fmt` hugs a trailing block argument.** A call whose last argument is
183
+ a lambda, list or record literal keeps its head on one line and lets that
184
+ argument grow a body, the way a Ruby block reads:
185
+
186
+ ```jade
187
+ describe("Math", [
188
+ it("adds", -> { Expect.equal(1 + 1, 2) }),
189
+ ])
190
+ ```
191
+
192
+ It previously broke every argument onto its own line, burying the call the
193
+ body belonged to. The head still breaks when it cannot fit, or when an
194
+ earlier argument is the one that grew.
195
+
196
+ Two shapes that left output hanging go with it: a lambda whose inline body
197
+ broke anyway (`-> { Decode.map(` … `}) }`) now uses the block form, and a
198
+ `|>` or `++` operand that breaks across lines keeps its continuation under
199
+ the operator that introduced it rather than at column zero. **Formatted
200
+ output differs from 0.4.0** — a format check in CI reports diffs on the
201
+ first run after upgrading. Over 65 real modules, 25 reformat; formatting is
202
+ idempotent on all of them.
203
+
204
+ - **A `(` that opens a line starts a statement rather than continuing the one
205
+ above.** `foo` on one line and `(1, 2)` on the next is now two statements, not
206
+ a call. The same rule applies to a type's arguments, so a return type no
207
+ longer reaches across a newline to swallow the body's first line.
208
+
209
+ ### Removed
210
+
211
+ - The error saying a record literal has no placeholder, which is no longer
212
+ true. `Name(field: _)` on a *positional* variant still reports: there are no
213
+ field names there to refer to.
25
214
 
26
215
  ### Fixed
27
216
 
217
+ - **Generated Ruby no longer makes Ruby complain.** An interop import emitted
218
+ its `rescue LoadError` indented as though it were part of the `begin` body,
219
+ which Ruby warns about under `-w`, and the runtime had a circular require
220
+ between `jade/task` and `jade/tasks` that neither file needed at load time.
221
+ The ejected examples are now loaded in a fresh process under `-w` and the
222
+ suite fails if Ruby says anything at all, which is the check that would have
223
+ caught a module and a type sharing a constant on its own.
224
+
225
+ - **A type that contains itself no longer exhausts the stack at compile
226
+ time.** `type Tree = Leaf | Node(Tree, Tree)` compared with `==` raised
227
+ `SystemStackError` from the compiler, naming a Ruby frame rather than
228
+ anything you wrote: deriving an instance asks for its components'
229
+ instances, and `Tree`'s component is `Tree`. So did a struct that reaches
230
+ itself, `struct Pepe = { pepe: Maybe(Pepe) }`, on `==` or at the Ruby
231
+ boundary.
232
+
233
+ Equality now works on both: a derived `Eq` is structural, and Ruby's `==`
234
+ computes exactly that, dispatching to whatever each component defines — so a
235
+ component with a hand-written `implements Eq` is still asked. `Encodable`
236
+ and `Decodable` say `cannot be derived for Pepe because it contains itself`
237
+ and point at writing the implementation by hand, which was always the answer
238
+ and used to arrive as a crash.
239
+
240
+ - **A recursive type with nothing to stop it is reported where it is written.**
241
+ `struct Pepe = { pepe: Pepe }` declared fine and read fine; you found out at
242
+ the use site, in a message about the argument you passed rather than about
243
+ the declaration that can never be satisfied. It now says so at the
244
+ declaration, along with the way out:
245
+
246
+ ```
247
+ error: `Pepe` can never be built: every `Pepe` needs a `Pepe` inside it first
248
+ --> src/pepe.jd:3:1
249
+ |
250
+ 3 | struct Pepe = { pepe: Pepe }
251
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ nothing here stops the recursion
252
+ |
253
+ = note: a type may refer to itself; `Pepe` just has no case that stops,
254
+ so there is no smallest `Pepe` to start from
255
+ = help: wrap the field in something that has an empty case:
256
+ `Maybe(Pepe)` can be `Nothing`, `List(Pepe)` can be empty
257
+ ```
258
+
259
+ A type referring to itself is the point of a union, so what is checked is
260
+ whether there is a way out: `Maybe` has `Nothing`, `List` has `[]`, `Result`
261
+ has its `Err` arm, a union has any variant that does not lead back. All of
262
+ those still compile, `struct Pepe = { pepe: Maybe(Pepe) }` included, and so
263
+ does a field holding a function that returns the type, since the function
264
+ itself can be written. `type T = A(T)`, two declarations that need each
265
+ other, and a struct reaching itself through a tuple do not.
266
+
267
+ - **A module may be named after a type in its parent.** `module Sql.Expr`
268
+ beside `type Expr` in `Sql` compiled without complaint and then emitted two
269
+ definitions of one Ruby constant, so whichever file loaded second silently
270
+ took the name and calls into the other failed with a `NoMethodError` and a
271
+ Ruby warning. The module now reopens the type's class rather than defining a
272
+ module beside it, which is what Ruby wants and what Elixir does with the
273
+ same pair. Constructing, pattern matching and the module's own functions all
274
+ work together.
275
+
276
+ - **A list from Ruby is copied, not borrowed.** A `List(Int)` argument came
277
+ through as the caller's own Array, so pushing to it after the call changed a
278
+ value Jade had already taken. Lists of structs were copied already, by the
279
+ decoder that builds them; only the scalar fast path handed the object
280
+ straight through. The copy costs 0.3us for 1500 elements against the 46us
281
+ the element check already spends, so under 1% of a crossing that was already
282
+ paying to look at every element.
283
+
284
+ - **A Jade name that is a Ruby keyword compiles.** `begin`, `next`, `class`,
285
+ `self`, `return` and 20-odd others are ordinary Jade identifiers, and
286
+ emitting them verbatim produced Ruby that would not parse — a local or
287
+ parameter called `next` was a syntax error in the generated file, not a
288
+ message from the compiler. Bindings now carry a trailing underscore
289
+ (`next_`), and a name that already ends in one gains another so `next` and
290
+ `next_` stay distinct.
291
+
292
+ Only bindings are rewritten. Ruby accepts its keywords as method names and
293
+ as `Data` members, so a function called `next` is still `Mod.next` from Ruby
294
+ and a field called `class` is still `class` on the wire.
295
+
296
+ - **`examples/records.jd` compiles.** `update_email` built an anonymous record
297
+ where a `Person` was wanted, and nothing compiled the file: the examples
298
+ suite skipped it. It now uses a record update, and the suite covers it.
299
+
300
+ - **A running app no longer loads the compiler.** `Runtime.boot!` read the
301
+ stdlib files for their implementations and got their declarations too, so
302
+ every production image carried the parser, the AST and the type checker:
303
+ 301 files, 22k LOC, 171 ms. Those files are now read in a mode that
304
+ registers the intrinsics and skips the declarations, which is 53 files and
305
+ 23 ms, and the compiler reads them again in full when it arrives. It also
306
+ fixes a latent break: `require 'jade/runtime'` followed by `boot!`, with no
307
+ `require 'jade'` first, used to raise.
308
+
309
+ - **A mismatch spanning a mismatch already reported is dropped.** Cascade
310
+ suppression caught the case where both errors mention the same inference
311
+ variable; where the outer one had resolved to concrete types it named no
312
+ variable, and the same fault was still reported twice, at the call and at the
313
+ body around it.
314
+
315
+ - **A module whose only declaration is half-written no longer crashes the
316
+ tolerant parser.** Recovery drops a declaration it cannot parse, and the
317
+ module node took its span from the last one, so a file with nothing left
318
+ raised `undefined method 'range' for nil` inside the parser. In the editor
319
+ that surfaced as a crash notice on a file you were part-way through typing,
320
+ which is the state it exists to cope with.
321
+
322
+ - **The LSP reports errors in files you do not have open.** It compiled from
323
+ each open buffer, so a module nobody had opened was only ever reached as
324
+ someone else's import, and a broken one that nothing imports was reported
325
+ nowhere. Opening or closing a file now compiles every module under the source
326
+ root, starting from the modules nothing imports so the pass covers the project
327
+ once, and diagnostics published for a file are cleared when it is fixed even
328
+ if it was never open. Typing still recompiles only what is open, and the LSP
329
+ now reads and writes the project's compile cache the way the CLI does.
330
+
331
+ - **`List.range` returns `List(Int)`.** Its signature said `List(a)`, with the
332
+ `a` free and bound by nothing, so `List.range(1, 3)` type-checked as a list
333
+ of anything the caller happened to want.
334
+
335
+ - **Parse errors no longer name `lbrace` where a `{` could never go.** An
336
+ alternation reported whichever branch it tried last, and a record literal is
337
+ last in several chains, so `module Pepe exposing`, `def f -> Int end`, and
338
+ `(x, y) = p` all came back as "expected lbrace". A failure on the token the
339
+ alternation started from now says what the position wanted (an expression, a
340
+ type, a declaration), while a failure further in, which got somewhere, is
341
+ still the one reported. `exposing` also commits on its keyword, so a header
342
+ stopped after it asks for `(` rather than blaming the body.
343
+
344
+ - **An empty body says so.** `def f -> Int end` reported `expected lbrace` and
345
+ underlined the `end`, the one token there that was certainly right. It now
346
+ reads `expected an expression (a body has to produce a value)` and underlines
347
+ the construct the body belongs to. Every function passes through that state
348
+ while it is being typed.
349
+
350
+ - **`Name(field: _)` on a variant explains which variant you have.** The
351
+ message stated the rule; it now says whether the arguments have no names at
352
+ all (offering the `|>` form, which needs no placeholder) or carry a record
353
+ (offering the lambda). A positional variant no longer also reports every key as an
354
+ unknown field.
355
+
356
+ - **The LSP reads `jade.json`.** `on_initialize` took the editor's root as the
357
+ source root, so in a project with `"source_roots": ["lib"]` every module was
358
+ named from the project root: the editor demanded `Lib.Test` while the
359
+ compiler compiled `Test`. Reading the manifest also loads the extension gems
360
+ whose modules a project imports.
361
+
362
+ - **A crashed compile publishes a diagnostic instead of silence.** Zero
363
+ diagnostics is indistinguishable from a clean file, so an LSP bug looked
364
+ exactly like working code.
365
+
366
+ - **One mistake, one message.** `t |> Plan(_, 150)` reported four errors: the
367
+ call that failed to unify, the expression around it, and the body that
368
+ returned it, each wearing the type the one below left behind. Mismatches on a
369
+ single span are now read as one failure: the most specific of them survives,
370
+ since a subclass exists for no other reason. A later error naming an
371
+ inference variable an earlier one already named is downstream of it, so it
372
+ goes too. Errors that share nothing still both appear.
373
+
374
+ - **Inference ids no longer leak into messages.** `t8894` is a counter: it
375
+ means nothing to a reader, it changes between compiles, and two occurrences
376
+ of one variable look unrelated unless you notice the digits match. Within a
377
+ message they render as `a`, `b`, `c`, the letters the signature syntax
378
+ already uses. The ids stay internal.
379
+
380
+ - **A module named after its principal type loads.** `module Plan` holding
381
+ `type Plan` emitted `Plan::Building` and `Plan::Internal`, which Ruby resolves
382
+ lexically: inside `module Plan` the constant `Plan` is the *type*, so those
383
+ became `Plan::Plan::Building` and died at load with `uninitialized constant`,
384
+ after compiling clean. Every reference to generated code is now rooted;
385
+ constructor calls already were, patterns and `Internal` calls were not.
386
+ Naming a module after its type (`Plan`, `Member`, `Money`) is ordinary, so
387
+ this would have kept happening.
388
+
389
+ - **The same type declared twice is an error.** Two `type AgeTiers` blocks in
390
+ one module compiled, emitted both, and left the *Ruby interpreter* to mention
391
+ it with `warning: already initialized constant`, naming the generated file
392
+ rather than the `.jd`. The clash check only fired between kinds (a `struct` against a
393
+ `type`); it now fires within a kind too, and moved to the pass that can still
394
+ see both declarations, so the error lands on the second and points at the
395
+ first.
396
+
28
397
  - **A long chain of `Task.and_then` no longer exhausts the Ruby stack.** Each
29
398
  `and_then` ran the next task from inside the previous one's `run`, so depth
30
399
  cost a stack frame and anything built by recursion, a batch loop or a retry,
@@ -43,6 +412,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
43
412
  only arise within one module: the orphan rule and cycle detection between
44
413
  them rule out the cross-module case.
45
414
 
415
+ ## [0.9.0] - 2026-08-21
416
+
417
+ ### Added
418
+
419
+ - **`Jade::Extensions`, where a gem hooks into compilation.** Two kinds, both
420
+ read-only: a *deriver* builds an implementation for an interface it owns,
421
+ and a *check* reads a call site and returns errors. Checks register against
422
+ a named phase — `:call` today — and receive the call's AST node alongside
423
+ its resolved argument types, because types alone cannot see a raw SQL
424
+ string's placeholders or a constant predicate. Only gems named in
425
+ `Extensions::ALLOWED` may register, so the compiler knows who extends it and
426
+ nothing about what they do.
427
+
428
+ ### Changed
429
+
430
+ - **The `Sql.Assignable` deriver moves to jade-sql.** The deriver list named
431
+ jade-sql's interface outright, with a comment apologising for it; it now
432
+ holds only the built-ins and whatever an allowed gem registers.
433
+
46
434
  ## [0.8.0]
47
435
 
48
436
  ### Added
data/docs/interop.md CHANGED
@@ -166,6 +166,21 @@ Users.fetch(1)
166
166
  The Jade caller never sees a malformed `User` — the bug is caught at the entry
167
167
  point, and the error arm (here `String`) stays meaningful for real failures.
168
168
 
169
+ ## Reading a rejection
170
+
171
+ A value that does not cross is reported at the path where it went wrong,
172
+ starting from the call the Ruby caller made:
173
+
174
+ ```
175
+ Shop.price(item).cents: expected Int, got String ("lots")
176
+ Shop.total(items)[0].cents: missing field `cents`
177
+ ```
178
+
179
+ `missing field` means the key was absent, as opposed to present and holding
180
+ something of the wrong type. Only the failing call pays for any of this: the
181
+ path segments are constants in the generated code, and the index of a bad
182
+ element is searched for only once an element has failed.
183
+
169
184
  ## What the compiled boundary looks like
170
185
 
171
186
  For a function with a primitive argument:
@@ -197,7 +212,9 @@ module Sample
197
212
  end
198
213
 
199
214
  def self.absolute(n)
200
- Internal.absolute(Jade::Interop::Boundary.integer("Int", n))
215
+ Internal.absolute(
216
+ Jade::Interop::Boundary.arg("Sample.absolute(n)") { Jade::Interop::Boundary.integer("Int", n) },
217
+ )
201
218
  end
202
219
  end
203
220
  ```
data/docs/stdlib.md CHANGED
@@ -47,6 +47,7 @@ readable Ruby. This is a map of the territory; `jade q api` is the atlas.
47
47
  |--------|--------------|
48
48
  | `Basics` | The built-in interfaces — `Eq`, `Comparable`, `Appendable`, `Mappable`, `Chainable` — plus the `Ordering` type (`LT` / `EQ` / `GT`) and `Never`. `++` works on `String`, `List`, and `Bytes` via `Appendable`. |
49
49
  | `Maybe` | Optional values without `nil`: `Just(a)` / `Nothing`, with `map`, `and_then`, `with_default`. |
50
+ | `Number` | `non_zero(n)`, the only way to hold the `NonZero` that `/` asks for. A literal divisor other than zero is one already, so `cents / 100` needs nothing; a divisor that is a value goes through `non_zero` and comes back a `Maybe`. `unwrap` gives back the plain number, for anyone implementing `Numeric` themselves. |
50
51
  | `Result` | Errors as values, no exceptions: `Ok(a)` / `Err(e)`, with `map`, `and_then`, `map_error`, `on_error`, `sequence`. |
51
52
  | `List` | Immutable lists: `map`, `filter`, `fold`, `zip`, `sort`, `length`, `range`, `head`, `tail`, `take`, `drop`, … |
52
53
  | `String` | Text: `length`, `reverse`, `split`, `trim`, `to_int`, `contains?`, `uncons`, `cons`, `from_char`, `map`. |
@@ -63,7 +64,8 @@ readable Ruby. This is a map of the territory; `jade q api` is the atlas.
63
64
  | `Clock` | Timestamps and monotonic timing: `Instant`, `now`. Sub-second precision; the bridge to wall-clock time. |
64
65
  | `Show` | Renders a value the way Jade writes it: `show(Just(7))` is `"Just(7)"`, `show(Point(3, 4))` is `"Point { x: 3, y: 4 }"`. Instances for the primitives; derives for unions, structs, records and lists. A function shows as `<function>`, and `Never` raises — it has no values. |
65
66
  | `Debug` | `log(label, value)` prints `label: value` to stderr and returns the value untouched, so it drops into a pipeline. Unconstrained, unlike `Show`. |
66
- | `Decimal` | Exact base-10 decimals (`coefficient * 10 ^ exponent`) — money and rates without `Float` rounding. Opaque; build with `of` / `scaled` / `parse`. Arithmetic via `Numeric` (`+` `-` `*` `/`), plus `div` (scaled, half-up), `round`, `to_i`, `to_float`. JSON-encodes to a `<mantissa>e<exponent>` string. |
67
+ | `Decimal` | Exact base-10 decimals (`coefficient * 10 ^ exponent`) — money and rates without `Float` rounding. Opaque; build with `of` / `scaled` / `parse`. Arithmetic via `Numeric` (`+` `-` `*` `/`), plus `div` (scaled, half-up, taking a `NonZero`), `round`, `to_i`, `to_float`. JSON-encodes to a `<mantissa>e<exponent>` string. |
68
+ | `Range` | An inclusive span over any `Comparable`, open at either end or both. Opaque; build with `a..b` or `between` / `from` / `to` / `empty` / `all`, where `between` reads a descending pair as `empty` rather than as a range. Ask with `contains?`, `overlaps?`, `intersect`, `clamp`, `lower`, `upper`, `bounded?`, `empty?`. Not the same thing as a range pattern: `in 0..3` is checked for coverage when the code is compiled, and a `Range` is a value. |
67
69
 
68
70
  Stdlib operations compile inline rather than through a runtime dispatch layer,
69
71
  so the generated Ruby calls the underlying operation directly.
data/docs/syntax.md CHANGED
@@ -90,6 +90,57 @@ Anonymous records do not coerce into nominal structs: passing
90
90
 
91
91
  → [`examples/records.jd`](../examples/records.jd)
92
92
 
93
+ ## Type aliases
94
+
95
+ A name for another type, with no runtime identity of its own:
96
+
97
+ ```jade
98
+ type alias UserId = Int
99
+ type alias Point = (Float, Float)
100
+ type alias Handler = Int -> Int
101
+ type alias Pair(a) = (a, a)
102
+
103
+ type alias User = {
104
+ name: String,
105
+ age: Int
106
+ }
107
+ ```
108
+
109
+ `UserId` and `Int` are the same type, and a record literal satisfies `User`
110
+ directly:
111
+
112
+ ```jade
113
+ u1 = { name: "Paul", age: 55 }
114
+ u2 = { u1 | age: 56 }
115
+ name = u1.name
116
+ ```
117
+
118
+ An alias never has its own constructors or interface implementations, so
119
+ `implements Show(UserId)` is a compile error — two aliases over the same body
120
+ are the same type, so the implementation would land on `Int` and collide with
121
+ every other one. Reach for `struct` when you want a distinct type, or a
122
+ single-variant `type` when you want a newtype around an inner shape.
123
+
124
+ Interfaces an alias *inherits* work as you would expect, because the alias is
125
+ gone by the time they are resolved: `Encode.encode` on a `UserId` is
126
+ `Encode.encode` on an `Int`, and on a `User` it encodes the record.
127
+
128
+ | | identity | construct with |
129
+ | --- | --- | --- |
130
+ | `struct User = { … }` | nominal, distinct from a same-shaped record | `User("Paul", 55)` |
131
+ | `type User = User({ … })` | nominal newtype wrapping an inner shape | `User({ … })` |
132
+ | `type alias User = { … }` | structural, a readability name | `{ … }` |
133
+
134
+ Recursive aliases are rejected: use a `type` for a recursive shape. A cycle is
135
+ reported once, against the first alias in it that was declared.
136
+
137
+ `alias` is only a keyword directly after `type`, so a field or argument may
138
+ still be called `alias`.
139
+
140
+ The name survives into messages — a mismatch against a `UserId` reads
141
+ `should be UserId (= Int)` — and hovering one in an editor gives its
142
+ declaration.
143
+
93
144
  ## Anonymous records and tuples
94
145
 
95
146
  ```jade
@@ -184,6 +235,51 @@ def area(shape: Shape) -> Float
184
235
  end
185
236
  ```
186
237
 
238
+ **Ranges** match a span of `Int`, with `..` including both bounds. Either
239
+ bound may be left off to run to infinity:
240
+
241
+ ```jade
242
+ module Bands exposing (band)
243
+
244
+ def band(age: Int) -> String
245
+ case age
246
+ in ..-1 then "unborn"
247
+ in 0..2 then "infant"
248
+ in 3..12 then "child"
249
+ in 13.. then "adult"
250
+ end
251
+ end
252
+ ```
253
+
254
+ Those four arms partition `Int`, so the `case` needs no `else`. A range that
255
+ leaves a gap is reported with the gap named — omitting `in 3..12` above gives
256
+ *Missing cases: `3..12`* — and one already covered by an earlier arm is
257
+ reported as unreachable.
258
+
259
+ There is no exclusive form. Over `Int` every half-open range has an exact
260
+ inclusive spelling (`3...12` is `3..11`), so a second operator would buy
261
+ nothing and cost a lifetime of remembering which is which. Ranges are `Int`
262
+ only for the same reason: `0..2` and `3..12` sit next to each other with
263
+ nothing in between them to be missed, which is not true of `Float`.
264
+
265
+ The same spelling builds a `Range` value in expression position, where the
266
+ bounds are ordinary expressions rather than literals:
267
+
268
+ ```jade
269
+ module Bookings exposing (clashes?)
270
+
271
+ def clashes?(check_in: Date, check_out: Date, other: Range(Date)) -> Bool
272
+ Range.overlaps?(check_in..check_out, other)
273
+ end
274
+ ```
275
+
276
+ `..` binds looser than `+` and `-`, so `0..n - 1` is `0..(n - 1)`. It is
277
+ non-associative — `a..b..c` is an error, not a nesting — and it is infix
278
+ only: the endless forms are `Range.from(a)` and `Range.to(b)`, because a
279
+ token that is prefix, infix and postfix by turns costs more to read than two
280
+ names do. A descending pair is `Range.empty` rather than an error, since
281
+ `0..(n - 1)` with `n = 0` is the ordinary way an empty range arises.
282
+
187
283
  **Lists** match with `[]` and `[head | tail]`; the rest after `|` must be a
188
284
  name or wildcard:
189
285
 
data/lib/jade/api.rb CHANGED
@@ -70,6 +70,7 @@ module Jade
70
70
  KIND_ORDER = {
71
71
  'type' => 0,
72
72
  'struct' => 0,
73
+ 'alias' => 0,
73
74
  'interface' => 1,
74
75
  'constructor' => 2,
75
76
  'function' => 3,
@@ -153,6 +154,7 @@ module Jade
153
154
  case symbol
154
155
  in Symbol::Union then 'type'
155
156
  in Symbol::Struct then 'struct'
157
+ in Symbol::Alias then 'alias'
156
158
  in Symbol::Interface then 'interface'
157
159
  in Symbol::Constructor | Symbol::Variant then 'constructor'
158
160
  else 'function'
@@ -167,6 +169,9 @@ module Jade
167
169
  in Symbol::Struct
168
170
  "struct #{symbol.name}#{type_params(symbol.type_params)}#{fields(symbol)}"
169
171
 
172
+ in Symbol::Alias
173
+ "type alias #{symbol.name}#{type_params(symbol.type_params)} = #{body(symbol)}"
174
+
170
175
  in Symbol::Interface
171
176
  "interface #{symbol.name}(#{symbol.type_param.name})"
172
177
 
@@ -192,6 +197,10 @@ module Jade
192
197
  end
193
198
  end
194
199
 
200
+ def body(symbol)
201
+ declared(symbol.body).then { |(type, _)| type }
202
+ end
203
+
195
204
  def fields(symbol)
196
205
  return '' unless symbol.record_type
197
206