jade-lang 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +439 -2
- data/docs/interop.md +18 -1
- data/docs/stdlib.md +3 -1
- data/docs/syntax.md +96 -0
- data/lib/jade/api.rb +9 -0
- data/lib/jade/ast.rb +37 -1
- data/lib/jade/cli/eject.rb +132 -0
- data/lib/jade/cli.rb +2 -0
- data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
- data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
- data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
- data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
- data/lib/jade/codegen/boundary/specialized.rb +13 -5
- data/lib/jade/codegen/boundary.rb +12 -2
- data/lib/jade/codegen/context.rb +17 -0
- data/lib/jade/codegen/error.rb +1 -0
- data/lib/jade/codegen/function_call.rb +23 -7
- data/lib/jade/codegen/function_declaration.rb +25 -13
- data/lib/jade/codegen/helpers.rb +27 -4
- data/lib/jade/codegen/implementation.rb +2 -2
- data/lib/jade/codegen/inline.rb +1 -1
- data/lib/jade/codegen/inlines.rb +6 -1
- data/lib/jade/codegen/names.rb +31 -0
- data/lib/jade/codegen/pattern/constructor.rb +2 -2
- data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
- data/lib/jade/codegen.rb +76 -15
- data/lib/jade/compiler.rb +2 -0
- data/lib/jade/entry.rb +1 -1
- data/lib/jade/error.rb +5 -0
- data/lib/jade/extensions.rb +70 -0
- data/lib/jade/formatter/calls.rb +68 -11
- data/lib/jade/formatter/collections.rb +4 -4
- data/lib/jade/formatter/declarations.rb +13 -15
- data/lib/jade/formatter/helper.rb +20 -0
- data/lib/jade/formatter/infix_application.rb +19 -4
- data/lib/jade/formatter/lambda.rb +24 -12
- data/lib/jade/formatter/pattern.rb +3 -0
- data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
- data/lib/jade/frontend/desugaring.rb +29 -9
- data/lib/jade/frontend/fixity_fixer.rb +6 -4
- data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
- data/lib/jade/frontend/forward_declaration/error.rb +3 -0
- data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
- data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
- data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
- data/lib/jade/frontend/forward_declaration/module.rb +4 -0
- data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
- data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration.rb +2 -0
- data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
- data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
- data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
- data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
- data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
- data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
- data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
- data/lib/jade/frontend/pattern_analysis.rb +5 -38
- data/lib/jade/frontend/semantic_analysis/body.rb +29 -1
- data/lib/jade/frontend/semantic_analysis/error/duplicate_implementation.rb +39 -0
- data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
- data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
- data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +4 -0
- data/lib/jade/frontend/semantic_analysis/helper.rb +41 -0
- data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -10
- data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
- data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
- data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
- data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
- data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
- data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis.rb +5 -0
- data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
- data/lib/jade/frontend/type_checking/cascade.rb +90 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
- data/lib/jade/frontend/type_checking/constraints/deriving.rb +5 -4
- data/lib/jade/frontend/type_checking/constraints.rb +18 -3
- data/lib/jade/frontend/type_checking/error/display.rb +49 -0
- data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
- data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
- data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
- data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
- data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
- data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
- data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
- data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
- data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
- data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
- data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
- data/lib/jade/frontend/type_checking/error.rb +6 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
- data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
- data/lib/jade/frontend/type_checking/inference/function_call.rb +47 -2
- data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
- data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
- data/lib/jade/frontend/type_checking/inference.rb +2 -0
- data/lib/jade/frontend/type_checking/loader.rb +11 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
- data/lib/jade/frontend/type_checking/substitution.rb +2 -2
- data/lib/jade/frontend/type_checking.rb +3 -1
- data/lib/jade/frontend/usage_analysis.rb +5 -1
- data/lib/jade/interop/boundary.rb +64 -24
- data/lib/jade/interop/error.rb +35 -7
- data/lib/jade/interop/lowering.rb +8 -2
- data/lib/jade/lsp/converters.rb +83 -2
- data/lib/jade/lsp/handlers.rb +122 -25
- data/lib/jade/lsp/snippets.rb +12 -0
- data/lib/jade/lsp/state.rb +11 -4
- data/lib/jade/module_loader/build.rb +36 -0
- data/lib/jade/module_loader.rb +1 -0
- data/lib/jade/parsing/combinators.rb +78 -2
- data/lib/jade/parsing/error.rb +43 -6
- data/lib/jade/parsing/type.rb +5 -3
- data/lib/jade/parsing.rb +140 -26
- data/lib/jade/runtime.rb +32 -13
- data/lib/jade/source.rb +6 -2
- data/lib/jade/stdlib/basics.rb +22 -3
- data/lib/jade/stdlib/char.rb +2 -0
- data/lib/jade/stdlib/clock.rb +6 -2
- data/lib/jade/stdlib/decimal.rb +51 -14
- data/lib/jade/stdlib/intrinsics.rb +59 -6
- data/lib/jade/stdlib/list.rb +37 -1
- data/lib/jade/stdlib/number.rb +47 -0
- data/lib/jade/stdlib/range.rb +211 -0
- data/lib/jade/stdlib.rb +11 -4
- data/lib/jade/symbol/alias.rb +11 -0
- data/lib/jade/symbol/parser.rb +1 -1
- data/lib/jade/symbol.rb +13 -1
- data/lib/jade/task.rb +74 -56
- data/lib/jade/type/anonymous_record.rb +11 -2
- data/lib/jade/type/application.rb +11 -2
- data/lib/jade/type/base.rb +26 -0
- data/lib/jade/type/function.rb +11 -2
- data/lib/jade/type.rb +36 -10
- data/lib/jade/version.rb +1 -1
- data/lib/jade.rb +1 -0
- metadata +35 -3
- data/lib/jade/frontend/type_checking/constraints/deriving/assignable.rb +0 -170
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba543580eec5d50cae2249c010a03f7a56a96f276705b3b87df7f25f06ff8eeb
|
|
4
|
+
data.tar.gz: 15923caf94302e7326aef6cc8a0e581c349b7b46d54ae4898e6bd8d8529657cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3906343d3094e2b9142aebc9324722c584db45dfb0951d6546d917794559a8cb9249d9ff4243bb380dc383932e78b93609455743a9e349ec3b60086450693846
|
|
7
|
+
data.tar.gz: 841c318dc43aa3e99df702eba6ffda7621b5a8c0f97e9302f2d75ef2149a36bcc19a1287d26a3cef5091c1361508d333a964f9eb70094e30a5791e8c4f8394a9
|
data/CHANGELOG.md
CHANGED
|
@@ -4,12 +4,450 @@ 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
|
-
## [
|
|
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.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
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 (...)`.
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
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.
|
|
214
|
+
|
|
215
|
+
### Fixed
|
|
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
|
+
|
|
397
|
+
- **A long chain of `Task.and_then` no longer exhausts the Ruby stack.** Each
|
|
398
|
+
`and_then` ran the next task from inside the previous one's `run`, so depth
|
|
399
|
+
cost a stack frame and anything built by recursion, a batch loop or a retry,
|
|
400
|
+
died with `SystemStackError` somewhere past ten thousand links. `run` now
|
|
401
|
+
drives an explicit stack of continuations from one loop, so a chain is
|
|
402
|
+
bounded by memory: 500,000 links run where 100,000 used to fail.
|
|
403
|
+
|
|
404
|
+
- **Two implementations for the same head type is now an error.** An
|
|
405
|
+
implementation is registered under `[interface, head type]`, so a second
|
|
406
|
+
`implements Assignable(Box(Cols2, Val2))` overwrote
|
|
407
|
+
`implements Assignable(Box(Cols, Val))` and every call — including ones whose
|
|
408
|
+
types matched the first — dispatched to the second. It compiled clean and
|
|
409
|
+
died at run time in the implementation body, or worse, didn't. The second
|
|
410
|
+
declaration is now reported, with the first as a secondary label and a note
|
|
411
|
+
that type arguments do not select between implementations. Duplicates can
|
|
412
|
+
only arise within one module: the orphan rule and cycle detection between
|
|
413
|
+
them rule out the cross-module case.
|
|
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.
|
|
8
433
|
|
|
9
434
|
## [0.8.0]
|
|
10
435
|
|
|
11
436
|
### Added
|
|
12
437
|
|
|
438
|
+
- **A struct written to a table is checked against its columns.** jade-sql maps
|
|
439
|
+
a struct's fields onto columns by name, and the mapping derives, so nothing
|
|
440
|
+
compared the two — a field the table has no column for reached Postgres as
|
|
441
|
+
invalid SQL, and a field whose type disagreed with its column failed at
|
|
442
|
+
decode. Calls to `Sql.Mutation.insert`, `insert_all` and `update` now report
|
|
443
|
+
both at compile time, naming the field. Both types are concrete at the call
|
|
444
|
+
site, so this is a check rather than anything the type system has to carry.
|
|
445
|
+
A call through a generic helper of your own has neither type in hand and
|
|
446
|
+
stays unchecked. Inert without jade-sql, since nothing else declares those
|
|
447
|
+
functions.
|
|
448
|
+
|
|
449
|
+
### Added
|
|
450
|
+
|
|
13
451
|
- `Assignable` also derives for structs, naming one column per field in
|
|
14
452
|
declaration order. A field renamed to dodge a keyword (`type_`) maps back to
|
|
15
453
|
the column it came from. Generic structs derive at the type they are applied
|
|
@@ -83,7 +521,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
83
521
|
anonymous record. A project that CI-checks committed artifacts will see a
|
|
84
522
|
diff.
|
|
85
523
|
|
|
86
|
-
|
|
87
524
|
## [0.6.0]
|
|
88
525
|
|
|
89
526
|
### Fixed
|
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(
|
|
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
|
|