jade-lang 0.9.0 → 0.10.1

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