jade-lang 0.4.0 → 0.5.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/AGENTS.md +164 -0
- data/CHANGELOG.md +141 -0
- data/README.md +5 -1
- data/docs/interop.md +208 -0
- data/docs/json.md +163 -0
- data/docs/lsp.md +105 -0
- data/docs/stdlib.md +69 -0
- data/docs/syntax.md +458 -0
- data/docs/testing.md +70 -0
- data/lib/jade/api.rb +244 -0
- data/lib/jade/cli/check.rb +97 -0
- data/lib/jade/cli/q.rb +47 -1
- data/lib/jade/cli.rb +4 -2
- data/lib/jade/codegen/emitter.rb +11 -2
- data/lib/jade/codegen/helpers.rb +6 -0
- data/lib/jade/codegen/inlines.rb +5 -0
- data/lib/jade/decode.rb +332 -212
- data/lib/jade/frontend/fixity_fixer.rb +3 -2
- data/lib/jade/frontend/semantic_analysis/error/variable_not_found.rb +9 -2
- data/lib/jade/frontend/semantic_analysis/member_access.rb +11 -1
- data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +10 -15
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +7 -16
- data/lib/jade/interop/boundary.rb +2 -3
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- data/lib/jade/signature.rb +47 -0
- data/lib/jade/stdlib/calendar.rb +20 -19
- data/lib/jade/stdlib/clock.rb +7 -13
- data/lib/jade/stdlib/decimal.rb +3 -16
- data/lib/jade/stdlib/decode.rb +35 -12
- data/lib/jade/stdlib/encode.rb +9 -0
- data/lib/jade/stdlib/intrinsics.rb +9 -1
- data/lib/jade/stdlib/result.rb +1 -1
- data/lib/jade/stdlib/text.rb +131 -0
- data/lib/jade/task.rb +2 -3
- data/lib/jade/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1bbeb934a5bbbeb19cfeddeb18f59798c37380a34d514110f42e07dab206525
|
|
4
|
+
data.tar.gz: 1f68a227a953fa57f875274f409bf93aa7269bbadfc4b3abc7634ea1d6b283c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87f7c7e84b193ee2dba6e989f493ca36cf4bc6a58ba7d4c38519aedf5052908263cea802e917b11555396edce72c1ae4af5ff687676f13e123d37858c9643e8f
|
|
7
|
+
data.tar.gz: 0d2f3e20d4e1863a567310355a04bc7eb0fb73c49b629db3f0f6162ce2a009ddf821420558962e35052f0a5859d035b196483ead990659f0c73700ad97b4abfb
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Writing Jade
|
|
2
|
+
|
|
3
|
+
Idiom and gotchas. This file is deliberately not a function list — that's
|
|
4
|
+
what `jade q api` is for, and a copy here would rot.
|
|
5
|
+
|
|
6
|
+
## Ask the compiler, not the source
|
|
7
|
+
|
|
8
|
+
| Question | Command |
|
|
9
|
+
|---|---|
|
|
10
|
+
| Does this function exist? What does it take? | `jade q api List` / `jade q api List.fold` |
|
|
11
|
+
| Which module has a `fold`? | `jade q find fold` |
|
|
12
|
+
| How is a lambda / `implements` block written? | `jade q syntax lambda` |
|
|
13
|
+
| Did what I just wrote compile? | `jade check path/to/file.jd` |
|
|
14
|
+
|
|
15
|
+
**Grepping the stdlib does not work, and fails quietly.** Stdlib modules are
|
|
16
|
+
written two ways — Jade in a heredoc (`Maybe`, `Result`, `Decimal`) and a Ruby
|
|
17
|
+
DSL (`List`, `String`, `Dict`, `Decode`) — so half of them never spell `def`,
|
|
18
|
+
and none contains the string `List.map` you would search for. Extension
|
|
19
|
+
modules (`Sql.*`) live in a gem that isn't in the tree at all. A grep that
|
|
20
|
+
comes back empty tells you nothing.
|
|
21
|
+
|
|
22
|
+
Inside a project, `jade q api` covers the stdlib, your own modules, and any
|
|
23
|
+
extension gem's, each tagged with an `origin`.
|
|
24
|
+
|
|
25
|
+
## Reach for the combinator
|
|
26
|
+
|
|
27
|
+
A `case` that just unwraps and rewraps is the long way round.
|
|
28
|
+
|
|
29
|
+
| Instead of | Write |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `case m in Just(x) then f(x) in Nothing then Nothing end` | `Maybe.map(m, f)` |
|
|
32
|
+
| `case m in Just(x) then x in Nothing then d end` | `Maybe.with_default(m, d)` |
|
|
33
|
+
| `case m in Just(x) then f(x) in Nothing then Nothing end` where `f` returns `Maybe` | `Maybe.and_then(m, f)` |
|
|
34
|
+
| `case r in Ok(x) then Ok(f(x)) in Err(e) then Err(e) end` | `Result.map(r, f)` |
|
|
35
|
+
| `case r in Ok(x) then x in Err(_) then d end` | `Result.with_default(r, d)` |
|
|
36
|
+
| a hand-written `==` | nothing — `Eq` derives |
|
|
37
|
+
| a hand-written decoder for a struct | nothing — `Decode.from_json` derives it |
|
|
38
|
+
|
|
39
|
+
These read best piped:
|
|
40
|
+
|
|
41
|
+
```jade
|
|
42
|
+
def label(m: Maybe(User)) -> String
|
|
43
|
+
m
|
|
44
|
+
|> Maybe.map((u) -> { u.name })
|
|
45
|
+
|> Maybe.with_default("anonymous")
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`case` earns its place when you're actually distinguishing variants — the
|
|
50
|
+
`Result` arm that reports a different error, the union with five constructors.
|
|
51
|
+
Write one `in` branch per variant rather than an `else` fallback; `else` is for
|
|
52
|
+
matching literals, where exhaustiveness isn't available.
|
|
53
|
+
|
|
54
|
+
**`map` changes the element type.** `Maybe.map : (Maybe(a), (a) -> b) ->
|
|
55
|
+
Maybe(b)`. If you were told otherwise, that was hover, which reports a
|
|
56
|
+
collapsed `(a) -> a` for the stdlib functions backing an interface — a known
|
|
57
|
+
bug, see `~/vault/claude/jade/bugs/hover-collapses-stdlib-type-vars.md`.
|
|
58
|
+
`jade q api` reads the declaration and is right.
|
|
59
|
+
|
|
60
|
+
## Imports
|
|
61
|
+
|
|
62
|
+
Auto-imported, no `import` needed: **`Basics`, `Maybe`, `Tuple`, `List`,
|
|
63
|
+
`Char`, `String`, `Result`, `Task`, `Bytes`**.
|
|
64
|
+
|
|
65
|
+
Everything else needs an explicit `import`: **`Dict`, `Set`, `Decode`,
|
|
66
|
+
`Decode.Params`, `Encode`, `Calendar`, `Clock`, `Decimal`, `Show`, `Debug`**.
|
|
67
|
+
`Show.show(x)` without `import Show` is "I cannot find a `Show.show`
|
|
68
|
+
variable" — the function exists, the import doesn't.
|
|
69
|
+
|
|
70
|
+
`import Dict` gets you the module. To name its *type* unqualified you have to
|
|
71
|
+
ask for it:
|
|
72
|
+
|
|
73
|
+
```jade
|
|
74
|
+
import Dict exposing (Dict) -- then: def go -> Dict(String, Int)
|
|
75
|
+
import Dict -- then: def go -> Dict.Dict(String, Int)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Zero-argument entries are values, not calls.** `Dict.empty`, `Decode.bool`,
|
|
79
|
+
`Encode.null` — no parentheses. `Dict.empty()` is a compile error.
|
|
80
|
+
|
|
81
|
+
## Interfaces
|
|
82
|
+
|
|
83
|
+
The built-ins live in `Basics`: `Eq`, `Comparable`, `Appendable`, `Mappable`,
|
|
84
|
+
`Chainable`, `Numeric`. Plus `Show` (in `Show`), `Decodable` (in `Decode`),
|
|
85
|
+
`Encodable` (in `Encode`).
|
|
86
|
+
|
|
87
|
+
Declare and implement:
|
|
88
|
+
|
|
89
|
+
```jade
|
|
90
|
+
interface Sized(a) with
|
|
91
|
+
size : a -> Int
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
implements Sized(Basket) with
|
|
96
|
+
size: (b) -> { List.length(b.items) }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The right-hand side is an inline lambda or a function reference
|
|
101
|
+
(`size: basket_size`).
|
|
102
|
+
|
|
103
|
+
Two things to keep straight:
|
|
104
|
+
|
|
105
|
+
- **The interface parameter is the constructor, not the applied type.**
|
|
106
|
+
`Mappable f` has `map : f(a), (a -> b) -> f(b)` — `f` is `Maybe`, not
|
|
107
|
+
`Maybe(a)`. Getting this wrong is why the impl target reads
|
|
108
|
+
`Mappable(Maybe(a))`.
|
|
109
|
+
- **`jade q api` tells you what already has an instance.** `jade q api
|
|
110
|
+
Decode.Decodable` lists every implementing type under `implemented_by`;
|
|
111
|
+
a struct's entry lists what it `implements`. Check before writing one.
|
|
112
|
+
|
|
113
|
+
### What derives, what doesn't
|
|
114
|
+
|
|
115
|
+
Verified against the compiler:
|
|
116
|
+
|
|
117
|
+
| | Structs / unions |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `Eq` (`==`) | derives |
|
|
120
|
+
| `Show` | derives |
|
|
121
|
+
| `Encodable` / `Decodable` | derives |
|
|
122
|
+
| `Comparable` (`<`, `List.sort`) | **does not derive** |
|
|
123
|
+
|
|
124
|
+
`List.sort` on a list of structs is `No implementation of Basics.Comparable
|
|
125
|
+
for Point`. Write the instance, or sort by a projection with
|
|
126
|
+
`List.sort_by`.
|
|
127
|
+
|
|
128
|
+
## Encoding and decoding
|
|
129
|
+
|
|
130
|
+
`Encode.encode(value)` derives the encoder from the value's type;
|
|
131
|
+
`Decode.from_json(json)` derives the decoder from the **return type**. A
|
|
132
|
+
struct round-trips with neither written by hand:
|
|
133
|
+
|
|
134
|
+
```jade
|
|
135
|
+
def parse(json: String) -> Result(User, DecodeError)
|
|
136
|
+
Decode.from_json(json)
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Derivation reaches through structural types, so anything built from encodable
|
|
141
|
+
parts is encodable: `List(a)`/`Set(a)` → array, `Maybe(a)` → the value or
|
|
142
|
+
`null`, tuples → positional array, `Dict(k, v)` → array of `[k, v]` pairs (a
|
|
143
|
+
JSON object only admits string keys), a struct → object keyed by field name, a
|
|
144
|
+
union whose variants take no arguments → the variant name in snake_case.
|
|
145
|
+
|
|
146
|
+
Outside that list — a union carrying arguments, say — write
|
|
147
|
+
`implements Encodable(T)` / `Decodable(T)` yourself; nothing but you knows the
|
|
148
|
+
shape it should take.
|
|
149
|
+
|
|
150
|
+
Reach for the explicit combinators (`Decode.field`, `Decode.required`,
|
|
151
|
+
`Decode.index`) when the JSON doesn't match the struct one-to-one. Use
|
|
152
|
+
`Decode.Params` for PATCH-style input, where a missing field means "don't
|
|
153
|
+
touch" rather than an error.
|
|
154
|
+
|
|
155
|
+
Full treatment: [docs/json.md](docs/json.md).
|
|
156
|
+
|
|
157
|
+
## Where else to look
|
|
158
|
+
|
|
159
|
+
- [docs/syntax.md](docs/syntax.md) — the language, form by form.
|
|
160
|
+
- [examples/](examples/) — nine files, compiled and asserted by
|
|
161
|
+
`spec/examples_spec.rb`, so they're verified idiom rather than samples that
|
|
162
|
+
drifted. `interfaces.jd` and `pattern_matching.jd` earn their keep.
|
|
163
|
+
- [docs/interop.md](docs/interop.md) — the Ruby boundary, ports, `uses`.
|
|
164
|
+
- [docs/stdlib.md](docs/stdlib.md) — what each module is for.
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,147 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`jade check [FILE...]`** type-checks and prints diagnostics without
|
|
14
|
+
generating anything — the same front end the language server runs, exiting 1
|
|
15
|
+
if there were errors. With no arguments it checks every `.jd` under the
|
|
16
|
+
source root. Closes the loop for editors, CI, and anything that just edited a
|
|
17
|
+
file and wants to know whether it invented a function.
|
|
18
|
+
- **`jade q api [MODULE|NAME]`** and **`jade q find TERM`** report the public
|
|
19
|
+
surface of everything a module can call — signatures, interface constraints,
|
|
20
|
+
a struct's fields, a type's variants and what it implements, an interface and
|
|
21
|
+
what implements it — read out of the registry rather than the source. Source
|
|
22
|
+
is the wrong thing to read: stdlib modules are written two ways (Jade in a
|
|
23
|
+
heredoc, a Ruby DSL), so grepping `lib/jade/stdlib/` finds neither `List.map`
|
|
24
|
+
nor half the modules' functions at all, and an extension gem's modules aren't
|
|
25
|
+
in your tree to grep. Inside a project the listing spans the stdlib, the
|
|
26
|
+
project's own modules and any extension gem's, each tagged with its `origin`;
|
|
27
|
+
without a `jade.json` it falls back to the stdlib, so the query still answers
|
|
28
|
+
from any directory. A module that won't compile is reported under `skipped`
|
|
29
|
+
rather than silently missing.
|
|
30
|
+
- **`jade q syntax [FORM]`** reports how a form is written — `lambda` is
|
|
31
|
+
`(args) -> { body }`, plus `def`, `type`, `struct`, `case`, `if`, `module`,
|
|
32
|
+
`import`, `interface`, `implements`, `uses`. Signatures answer "does this
|
|
33
|
+
function exist and what does it take", never "how is a lambda spelled", and
|
|
34
|
+
that second question has its own wrong answers. It serves the corpus the
|
|
35
|
+
editor already offers as completions, so the two can't drift; needs no
|
|
36
|
+
project and reads no files.
|
|
37
|
+
|
|
38
|
+
- **`AGENTS.md`** — idiom and gotchas for writing Jade: reach for `Maybe.map` /
|
|
39
|
+
`with_default` over a `case` that only unwraps and rewraps, which stdlib
|
|
40
|
+
modules are auto-imported and which need an `import`, that zero-argument
|
|
41
|
+
entries are values and `Dict.empty()` is a compile error, what derives (`Eq`,
|
|
42
|
+
`Show`, `Encodable`, `Decodable`) and what doesn't (`Comparable`), and how
|
|
43
|
+
interfaces and encode/decode fit together. Deliberately not a function list —
|
|
44
|
+
that's `jade q api`, and a copy would rot. Every claim in it was checked
|
|
45
|
+
against the compiler.
|
|
46
|
+
- The gem now ships `docs/` and `AGENTS.md`, so a project that installs
|
|
47
|
+
jade-lang gets them instead of README links pointing at nothing.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Decoding a row returned from a port is roughly eight times cheaper.**
|
|
52
|
+
A derived decoder was a composition of combinators rebuilt and walked
|
|
53
|
+
for every row; a profile put 88% of the time inside the interpreter and
|
|
54
|
+
70 allocations on a four-field row. Deriving now emits one node
|
|
55
|
+
carrying every key, its decoder and the constructor, applied once
|
|
56
|
+
rather than curried through a closure per field; the interpreter
|
|
57
|
+
returns values unwrapped, with failures on a sentinel, so nothing
|
|
58
|
+
allocates a `Result` per node; and each descriptor decodes itself
|
|
59
|
+
instead of being matched out of a twenty-branch `case`. A four-field
|
|
60
|
+
row went 9.58 to 1.19 µs and 70 allocations to 5.
|
|
61
|
+
- **`Decimal`, `Clock.Instant` and `Calendar.Date` read their text form
|
|
62
|
+
in Ruby** rather than parsing it through Jade-level combinators, which
|
|
63
|
+
cost a fresh decoder, two `Maybe`s and a tuple for every value. The
|
|
64
|
+
text forms are unchanged, so JSON output and existing ports are
|
|
65
|
+
unaffected. One input changes meaning: a trailing exponent marker
|
|
66
|
+
(`"825e-4e"`) used to read as `825e-4`, because splitting on every
|
|
67
|
+
`"e"` dropped the empty trailing piece, and is rejected now.
|
|
68
|
+
- **Encoding a value back out to Ruby is cheaper by the same route.** A
|
|
69
|
+
derived record encoder built a pair per field and folded the list back
|
|
70
|
+
into a hash, with an intrinsic lookup for both per field per value; it
|
|
71
|
+
builds the hash directly. `Encodable(Instant)` writes its string in
|
|
72
|
+
Ruby next to the reader that parses it. Handing a four-field struct
|
|
73
|
+
back to a Ruby caller cost 2.26 µs/row over the internal form and now
|
|
74
|
+
costs 0.17 — what a caller waits for and what `Internal` measures have
|
|
75
|
+
converged.
|
|
76
|
+
- **`Calendar.from_rata_die` is closed form.** It walked a year at a time
|
|
77
|
+
to find the year, then December backwards to find the month, allocating
|
|
78
|
+
a `Month` and a tuple per step through two twelve-branch `case`
|
|
79
|
+
statements. It is reached from `Clock.to_iso`, `Clock.on_date` and
|
|
80
|
+
`Calendar.add`, so this is not only an interop win — any Jade code
|
|
81
|
+
doing date arithmetic pays it.
|
|
82
|
+
- The `case` completion snippet offers one `in` branch per variant instead of
|
|
83
|
+
an `else` fallback. `else` is for matching literals, where exhaustiveness
|
|
84
|
+
isn't available — not the default shape of a `case`.
|
|
85
|
+
- **A missing qualified name now suggests the one you meant.** `List.fold_left`
|
|
86
|
+
answers ``help: did you mean `List.fold`?`` instead of a bare "not found" —
|
|
87
|
+
the candidates are the module's exposed values, so the suggestion is drawn
|
|
88
|
+
from what actually exists. Suggestions go through the alias the module was
|
|
89
|
+
imported under (`L.post`, not `Ledger.post`), since that's what the call site
|
|
90
|
+
can say. Local variables, types and constructors already did this;
|
|
91
|
+
module-qualified access was the gap.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Operator precedence was lost inside an infix chain's operands.**
|
|
96
|
+
`(1 + 2 * 3) + 0` was 9, `(10 - 4 / 2) + 0` was 3, and
|
|
97
|
+
`id(1 + 2 * 3) + 0` was 9. Shunting-yard ran on a chain's own operators
|
|
98
|
+
but treated every operand as an atom and returned it unfixed, so a
|
|
99
|
+
grouping, a call argument, a lambda body, a ternary branch or a record
|
|
100
|
+
value sitting in a chain kept the shape the parser built — left to
|
|
101
|
+
right, no precedence. Standing alone the same expression was fine,
|
|
102
|
+
which is why `1 + 2 * 3` and `(1 + 2 * 3)` both gave 7 and nothing
|
|
103
|
+
caught it. **This changes generated output for code that hit it**: a
|
|
104
|
+
project that commits generated artifacts and CI-checks them for drift
|
|
105
|
+
will see a diff, and the new bytes are the correct arithmetic.
|
|
106
|
+
- **`Clock.on_date` reported the previous day for pre-1970 instants.**
|
|
107
|
+
`Clock.floor_div` subtracted one from the quotient for a negative
|
|
108
|
+
dividend with a remainder — the right correction when `/` truncates
|
|
109
|
+
toward zero, which Jade's doesn't; it already floors, so it floored
|
|
110
|
+
twice. `at_time` was unaffected, so `to_iso(-1)` read
|
|
111
|
+
`"1969-12-30T23:59:59Z"`: the time right, the date a day out. Exact
|
|
112
|
+
midnight was always correct, because the double correction only applies
|
|
113
|
+
when there is a remainder.
|
|
114
|
+
- **Anonymous records decoded from the same shape now compare equal.**
|
|
115
|
+
The derived decoder built its constructor from a bare `Data.define`,
|
|
116
|
+
which mints a fresh class each time the decoder is built, so
|
|
117
|
+
`{ x: 1, y: "a" }` decoded twice compared false. It resolves through
|
|
118
|
+
the same interned registry record literals already used. Note this does
|
|
119
|
+
not unify decoded records with *literals* in the general case —
|
|
120
|
+
literals hoist to a per-module constant, so two modules with the same
|
|
121
|
+
shape still get different classes.
|
|
122
|
+
- **The tuple decoders no longer build their constructor with
|
|
123
|
+
`Method#curry`.** A curried `Method` built once and called on every
|
|
124
|
+
decode corrupts the heap under GC compaction — a near-null "try to mark
|
|
125
|
+
T_NONE object" crash after enough rows. It is the pattern the
|
|
126
|
+
constructor-curry regression spec exists to prevent; that spec reads
|
|
127
|
+
generated code, so the stdlib's own copy was invisible to it.
|
|
128
|
+
- **`Result.on_error` can change the error type**, as its signature has always
|
|
129
|
+
claimed. The `Ok` arm handed back the input rather than rebuilding it, which
|
|
130
|
+
unified the outgoing error type with the incoming one, so
|
|
131
|
+
`Result(Int, String) -> Result(Int, Int)` did not compile. Recovering into a
|
|
132
|
+
different error type is the reason the function takes `e -> Result(a, f)`.
|
|
133
|
+
- **A signature no longer renders two distinct type variables as one.** Hover
|
|
134
|
+
reported `Maybe.map : (Maybe(a), (a) -> a) -> Maybe(a)` — a function unable
|
|
135
|
+
to change the element type, which is a different function from the one that
|
|
136
|
+
exists. Variables are identified by an id but print as a name, and nothing
|
|
137
|
+
upstream keeps names distinct; rendering now re-letters a clash instead of
|
|
138
|
+
emitting the same name twice. Declared names are kept where they don't
|
|
139
|
+
collide, so annotated signatures read as written.
|
|
140
|
+
|
|
141
|
+
- **`docs/stdlib.md` named nine functions that don't exist.** `Char.is_digit`,
|
|
142
|
+
`is_alpha`, `is_alpha_num`, `is_upper`, `is_lower` were renamed to `digit?`,
|
|
143
|
+
`alpha?`, `alpha_numeric?`, `upper?`, `lower?` when predicates took the `?`
|
|
144
|
+
suffix and the doc never followed; `String.contains` is `contains?`,
|
|
145
|
+
`Dict.member` is `member?`, `Tuple.map_first` / `map_second` were never
|
|
146
|
+
implemented, and `Decode.at` doesn't exist (`Decode.index` does). Every
|
|
147
|
+
qualified name in `docs/stdlib.md` and `AGENTS.md` now checks out against the
|
|
148
|
+
registry.
|
|
149
|
+
|
|
9
150
|
## [0.4.0]
|
|
10
151
|
|
|
11
152
|
### Changed
|
data/README.md
CHANGED
|
@@ -333,7 +333,9 @@ Worst case: you wrote Ruby with a nicer authoring layer for a while.
|
|
|
333
333
|
|
|
334
334
|
There's a language server — type errors, inferred types, and jump-to-definition
|
|
335
335
|
in any editor that speaks LSP. For tools that don't, `jade q` answers the same
|
|
336
|
-
questions as one-shot JSON (hover, definition, references, symbols)
|
|
336
|
+
questions as one-shot JSON (hover, definition, references, symbols), `jade q
|
|
337
|
+
api` reports the stdlib's signatures, and `jade check` type-checks a file and
|
|
338
|
+
prints what's wrong.
|
|
337
339
|
|
|
338
340
|
In our experience coding agents like Claude Code and Cursor handle Jade well:
|
|
339
341
|
the syntax is close enough to the ML family (Elm, OCaml, Haskell) that models
|
|
@@ -346,9 +348,11 @@ for us so far.
|
|
|
346
348
|
A single `jade` binary fronts the toolchain:
|
|
347
349
|
|
|
348
350
|
```
|
|
351
|
+
jade check [file...] # type-check; exits 1 on errors, generates nothing
|
|
349
352
|
jade fmt [-i|-c] [file] # format .jd source (stdin or file)
|
|
350
353
|
jade lsp # language server over stdio (hover, defn, refs, diagnostics)
|
|
351
354
|
jade q hover FILE:L:C # headless JSON queries — hover/symbols/defn/refs
|
|
355
|
+
jade q api List.fold # stdlib signatures, read from the registry
|
|
352
356
|
```
|
|
353
357
|
|
|
354
358
|
`jade fmt` is deterministic and idempotent; wire it into your editor or a
|
data/docs/interop.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Interop with Ruby
|
|
2
|
+
|
|
3
|
+
Jade has no implicit side effects. All interaction with the outside world goes
|
|
4
|
+
through `uses` blocks, and every port returns a `Task`.
|
|
5
|
+
|
|
6
|
+
## Declaring a port
|
|
7
|
+
|
|
8
|
+
The `uses` block declares the boundary types; the names come into scope
|
|
9
|
+
unqualified. **Declarations are separated by commas** — a newline alone ends
|
|
10
|
+
the block, so a second entry without a comma before it reads as a parse error
|
|
11
|
+
(`Unexpected token "entries", expected end`).
|
|
12
|
+
|
|
13
|
+
A port takes as many arguments as its type says: `A, B -> Task(…)` is a
|
|
14
|
+
two-argument port, called `entries(key, limit)`. Parenthesising the arguments
|
|
15
|
+
means something else — `(A, B) -> Task(…)` is a *one*-argument port taking a
|
|
16
|
+
tuple, called `entries((key, limit))`.
|
|
17
|
+
|
|
18
|
+
```jade
|
|
19
|
+
module Store exposing (page)
|
|
20
|
+
|
|
21
|
+
uses KeyValue with
|
|
22
|
+
members : String -> Task(List(String), String),
|
|
23
|
+
entries : String, Int -> Task(List(String), String)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def page(key: String) -> Task(List(String), String)
|
|
28
|
+
entries(key, 20)
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On the Ruby side, register the port with `Jade::Port`. The block receives a
|
|
33
|
+
helper `t` for `t.ok(value)` / `t.err(error)`, then one parameter per declared
|
|
34
|
+
argument:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
module KeyValue
|
|
38
|
+
extend Jade::Port
|
|
39
|
+
|
|
40
|
+
task :members do |t, key|
|
|
41
|
+
t.ok(REDIS.smembers(key))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
task :entries do |t, key, limit|
|
|
45
|
+
t.ok(REDIS.lrange(key, 0, limit - 1))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The block must return `t.ok(value)` or `t.err(error)` — never another `Task`.
|
|
51
|
+
Composition (`map`, `and_then`, `sequence`) lives in Jade. (Pick a port module
|
|
52
|
+
name that doesn't shadow a Ruby constant you rely on — `task :members` defines
|
|
53
|
+
`KeyValue.members`.)
|
|
54
|
+
|
|
55
|
+
## What crosses the boundary
|
|
56
|
+
|
|
57
|
+
A port is the same boundary as a Jade function called from Ruby, pointed the
|
|
58
|
+
other way, and it converts in both directions: **arguments are encoded on the
|
|
59
|
+
way out, and the return value is decoded on the way back**. Ruby sees wire
|
|
60
|
+
values — strings, numbers, hashes, arrays — never Jade's internal
|
|
61
|
+
representation.
|
|
62
|
+
|
|
63
|
+
```jade
|
|
64
|
+
uses Scheduling with
|
|
65
|
+
shift_months : Instant, Int -> Task(Instant, Never)
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
module Scheduling
|
|
71
|
+
extend Jade::Port
|
|
72
|
+
|
|
73
|
+
task :shift_months do |t, iso, months|
|
|
74
|
+
# iso is "2026-08-01T12:00:00Z", not a Jade::Clock::Instant
|
|
75
|
+
t.ok(Time.iso8601(iso).then { |at| (at << -months).iso8601 })
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Every argument type therefore needs an `Encodable` instance, the same way every
|
|
81
|
+
`Task` arm needs a `Decodable` one. A type with neither is a compile error
|
|
82
|
+
naming the argument:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Port `shift_months` cannot encode argument 1 (`Shape`): no Encodable instance
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Declare the argument as `Decode.Value` to opt out and hand Ruby the value
|
|
89
|
+
untouched — the arg-side counterpart of a `Decode.Value` return arm. `Value` has
|
|
90
|
+
instances on both sides, so the opt-out holds when it's nested too: a
|
|
91
|
+
`List(Value)` argument, or a struct with a `Value` field, crosses element by
|
|
92
|
+
element with each one left alone.
|
|
93
|
+
|
|
94
|
+
## Calling Jade from Ruby
|
|
95
|
+
|
|
96
|
+
An exposed function gets two callable forms:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# Boundary form — args decoded, return encoded, Task runs eagerly
|
|
100
|
+
Store.page("recent") # => ["ok", ["a", "b"]]
|
|
101
|
+
Store.page!("recent") # => ["a", "b"] (bang form unwraps, raises on err)
|
|
102
|
+
|
|
103
|
+
# Internal form — keeps the Task as a value you can compose
|
|
104
|
+
task = Store::Internal.page("recent")
|
|
105
|
+
task.run # => Jade::Result::Ok[["a", "b"]]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
At the boundary, Ruby values are **decoded into Jade values** on the way in and
|
|
109
|
+
**encoded back to Ruby** on the way out. For `Task` functions the ok-arm decoder
|
|
110
|
+
runs against whatever the port returned, so a signature like `Task(User, String)`
|
|
111
|
+
keeps its declared error type — you don't thread a separate `DecodeError`
|
|
112
|
+
through every call.
|
|
113
|
+
|
|
114
|
+
## When a function is callable from Ruby
|
|
115
|
+
|
|
116
|
+
The unit here is the **function**, not the type. A function is exposed to Ruby
|
|
117
|
+
only when **all of its parameters are `Decodable` and its return type is
|
|
118
|
+
`Encodable`**. If any parameter can't be decoded, or the return can't be
|
|
119
|
+
encoded, that whole function isn't exposed — it compiles fine and its `Internal`
|
|
120
|
+
form still works, but calling the public `Module.fn` from plain Ruby raises
|
|
121
|
+
`Jade::Interop::NotExposed`. The error names the part that disqualified it
|
|
122
|
+
(e.g. `argument 1 of type Shape has no Decodable instance`).
|
|
123
|
+
|
|
124
|
+
What takes a function out of Ruby's reach is a parameter or return type with no
|
|
125
|
+
`Decodable` / `Encodable` instance — a function value, an unbound type variable,
|
|
126
|
+
or a custom union you haven't given an instance.
|
|
127
|
+
|
|
128
|
+
If a port returns something that doesn't decode to the declared type, the
|
|
129
|
+
boundary raises `Jade::Interop::DecodeError`. This is on purpose: a port
|
|
130
|
+
returning the wrong shape is a programming bug, not a runtime condition to
|
|
131
|
+
recover from, so the boundary raises rather than pass on a malformed value.
|
|
132
|
+
|
|
133
|
+
```jade
|
|
134
|
+
module Users exposing (fetch)
|
|
135
|
+
|
|
136
|
+
struct User = {
|
|
137
|
+
id: Int,
|
|
138
|
+
name: String
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
uses Backend with
|
|
143
|
+
raw_fetch : Int -> Task(User, String)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def fetch(id: Int) -> Task(User, String)
|
|
148
|
+
raw_fetch(id)
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
module Backend
|
|
154
|
+
extend Jade::Port
|
|
155
|
+
|
|
156
|
+
task :raw_fetch do |t, id|
|
|
157
|
+
t.ok({ name: "Paul" }) # oops — missing :id
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
Users.fetch(1)
|
|
162
|
+
# => raises Jade::Interop::DecodeError:
|
|
163
|
+
# Port returned a value that failed to decode at value: missing field `id` ({name: "Paul"})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The Jade caller never sees a malformed `User` — the bug is caught at the entry
|
|
167
|
+
point, and the error arm (here `String`) stays meaningful for real failures.
|
|
168
|
+
|
|
169
|
+
## What the compiled boundary looks like
|
|
170
|
+
|
|
171
|
+
For a function with a primitive argument:
|
|
172
|
+
|
|
173
|
+
```jade
|
|
174
|
+
module Sample exposing (absolute)
|
|
175
|
+
|
|
176
|
+
def absolute(n: Int) -> Int
|
|
177
|
+
n < 0 ? 0 - n : n
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
the compiler emits:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
module Sample
|
|
185
|
+
extend self
|
|
186
|
+
|
|
187
|
+
module Internal
|
|
188
|
+
extend self
|
|
189
|
+
|
|
190
|
+
def absolute(n)
|
|
191
|
+
if ((n < 0))
|
|
192
|
+
(0 - n)
|
|
193
|
+
else
|
|
194
|
+
n
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def self.absolute(n)
|
|
200
|
+
Internal.absolute(Jade::Interop::Boundary.integer("Int", n))
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Two surface methods — `Internal.absolute` (pure) and `self.absolute` (the
|
|
206
|
+
boundary). `Int` has a specialized fast-path coercion; richer types decode
|
|
207
|
+
through cached `Decode` constants instead. Either way the boundary work is
|
|
208
|
+
visible in the file, not hidden inside a runtime hook.
|