jade-lang 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +164 -0
  3. data/CHANGELOG.md +180 -1
  4. data/README.md +5 -1
  5. data/docs/interop.md +208 -0
  6. data/docs/json.md +163 -0
  7. data/docs/lsp.md +105 -0
  8. data/docs/stdlib.md +69 -0
  9. data/docs/syntax.md +458 -0
  10. data/docs/testing.md +70 -0
  11. data/lib/jade/api.rb +244 -0
  12. data/lib/jade/cli/check.rb +97 -0
  13. data/lib/jade/cli/q.rb +47 -1
  14. data/lib/jade/cli.rb +4 -2
  15. data/lib/jade/codegen/boundary/cache.rb +1 -2
  16. data/lib/jade/codegen/boundary/specialized/list.rb +9 -4
  17. data/lib/jade/codegen/boundary/specialized/maybe.rb +15 -3
  18. data/lib/jade/codegen/boundary/specialized/record.rb +7 -3
  19. data/lib/jade/codegen/boundary/specialized.rb +7 -3
  20. data/lib/jade/codegen/emitter.rb +11 -2
  21. data/lib/jade/codegen/function_declaration.rb +7 -6
  22. data/lib/jade/codegen/helpers.rb +6 -0
  23. data/lib/jade/codegen/inlines.rb +5 -0
  24. data/lib/jade/decode.rb +332 -212
  25. data/lib/jade/frontend/fixity_fixer.rb +3 -2
  26. data/lib/jade/frontend/semantic_analysis/error/variable_not_found.rb +9 -2
  27. data/lib/jade/frontend/semantic_analysis/member_access.rb +11 -1
  28. data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +10 -15
  29. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +7 -16
  30. data/lib/jade/interop/boundary.rb +12 -4
  31. data/lib/jade/interop/error.rb +25 -5
  32. data/lib/jade/lsp/converters.rb +2 -15
  33. data/lib/jade/lsp/snippets.rb +21 -3
  34. data/lib/jade/module_loader/module_name.rb +46 -0
  35. data/lib/jade/module_loader.rb +5 -0
  36. data/lib/jade/signature.rb +47 -0
  37. data/lib/jade/stdlib/calendar.rb +20 -19
  38. data/lib/jade/stdlib/clock.rb +7 -13
  39. data/lib/jade/stdlib/decimal.rb +3 -16
  40. data/lib/jade/stdlib/decode.rb +35 -12
  41. data/lib/jade/stdlib/encode.rb +9 -0
  42. data/lib/jade/stdlib/intrinsics.rb +9 -1
  43. data/lib/jade/stdlib/result.rb +1 -1
  44. data/lib/jade/stdlib/text.rb +131 -0
  45. data/lib/jade/task.rb +2 -3
  46. data/lib/jade/version.rb +1 -1
  47. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1cb9527b610f268fc9ae7d784abcbf41018cf99f91b0bdaea462a655a8f12bfb
4
- data.tar.gz: d888ed1e08771d41147879b5576c0b894f94795af1e4f849f6ca31044661366b
3
+ metadata.gz: ec12f8f9ca54c6ac59b3d8d521a9ad192eb87dd5a2a5e62620f3abe00b636cee
4
+ data.tar.gz: 940c0c3e78b8dcbd013050b298508323e5c0f60f8ddcb4749ede2416b14d9e16
5
5
  SHA512:
6
- metadata.gz: 95f7b32d5542bb1d386d18a7e1e8749de7a7f5c6ecfffaacee351dcb0d4205d698269954fe4fc023f2fed3cc7fe9f32c0f6344f9415770916ea6237be3f16e8f
7
- data.tar.gz: 897851f20e95afe86c92a7c4fe104299b46a4239644f06ea26c5580f5d47660834560e5c218336efc3d569f56e5754b8c68e45b1e68524166d347bdf5d178bb6
6
+ metadata.gz: e2f05e1f66e7b893e547a2ccb4d31a9678705daecd35c4c806a94f65c47444ad8b69b334c14b18a3a67417963870debdc4f28530ac334c0106699ee278c1f199
7
+ data.tar.gz: 5711c4a232e7728699e89ddcf168e7dc5b1b86913e34579c7b0b1b2d3df6e069f2f4a12aeb35a800619fdb945169c142a2434f322ce582b1415c524259ab2646
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,183 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0]
10
+
11
+ ### Fixed
12
+
13
+ - **A `Maybe` return encodes its element.** A function returning `Maybe(T)`
14
+ handed Ruby the internal `Data` object whenever `T` was a union, or a struct
15
+ holding one — no error, just an un-encoded value across the boundary. The
16
+ specialized encoder fell back to emitting the raw element when it couldn't
17
+ specialize it, instead of declining so the generic encoder could take over;
18
+ `decode`, five lines above it, already declined correctly. `Maybe(Int)` and
19
+ `Maybe(SomeStruct)` were unaffected, which is why it stayed hidden — identity
20
+ happens to be right for primitives. Both derived and hand-written
21
+ `Encodable` instances are now applied, and a `Maybe` field of a struct
22
+ unwraps the same way a `Maybe` return does.
23
+ - **Boundary decode errors name the right side.** `DecodeError` reported
24
+ "Port returned a value that failed to decode" for every failure, including
25
+ values Ruby passed *into* an exposed function, where no port is involved —
26
+ sending you to look for a `uses` block that may not exist. Argument failures
27
+ now read "Ruby passed a value that failed to decode"; port returns are
28
+ unchanged.
29
+ - **A symbol-keyed Hash says so.** Structs cross the boundary as a Hash with
30
+ string keys, so `Person.birthday(name: "Ada", age: 40)` used to fail with
31
+ `expected String, got null` — each field independently reading `nil`, naming
32
+ neither the cause nor the fix. It is now caught where the whole hash is in
33
+ scope, listing the offending keys and the correction. Hashes mixing string
34
+ and symbol keys are left alone; that is a real shape mismatch, and the
35
+ per-field errors describe it better than a guess would.
36
+ - **A module declared under the wrong file name is now a compile error.** A
37
+ module is keyed by the name its file implies — imports resolve `Shop.Cart`
38
+ to `shop/cart.jd` and back — but nothing checked that against what the file
39
+ declared. `module Cart` in `orders.jd` compiled clean and emitted a `Cart`
40
+ module whose own type references pointed at `Orders`, so the first call
41
+ raised `NameError: uninitialized constant Cart::Internal::Orders` far from
42
+ the cause. It now fails at compile time, pointing at the declared name and
43
+ offering both the rename and the move.
44
+
45
+ ## [0.5.0]
46
+
47
+ ### Added
48
+
49
+ - **`jade check [FILE...]`** type-checks and prints diagnostics without
50
+ generating anything — the same front end the language server runs, exiting 1
51
+ if there were errors. With no arguments it checks every `.jd` under the
52
+ source root. Closes the loop for editors, CI, and anything that just edited a
53
+ file and wants to know whether it invented a function.
54
+ - **`jade q api [MODULE|NAME]`** and **`jade q find TERM`** report the public
55
+ surface of everything a module can call — signatures, interface constraints,
56
+ a struct's fields, a type's variants and what it implements, an interface and
57
+ what implements it — read out of the registry rather than the source. Source
58
+ is the wrong thing to read: stdlib modules are written two ways (Jade in a
59
+ heredoc, a Ruby DSL), so grepping `lib/jade/stdlib/` finds neither `List.map`
60
+ nor half the modules' functions at all, and an extension gem's modules aren't
61
+ in your tree to grep. Inside a project the listing spans the stdlib, the
62
+ project's own modules and any extension gem's, each tagged with its `origin`;
63
+ without a `jade.json` it falls back to the stdlib, so the query still answers
64
+ from any directory. A module that won't compile is reported under `skipped`
65
+ rather than silently missing.
66
+ - **`jade q syntax [FORM]`** reports how a form is written — `lambda` is
67
+ `(args) -> { body }`, plus `def`, `type`, `struct`, `case`, `if`, `module`,
68
+ `import`, `interface`, `implements`, `uses`. Signatures answer "does this
69
+ function exist and what does it take", never "how is a lambda spelled", and
70
+ that second question has its own wrong answers. It serves the corpus the
71
+ editor already offers as completions, so the two can't drift; needs no
72
+ project and reads no files.
73
+
74
+ - **`AGENTS.md`** — idiom and gotchas for writing Jade: reach for `Maybe.map` /
75
+ `with_default` over a `case` that only unwraps and rewraps, which stdlib
76
+ modules are auto-imported and which need an `import`, that zero-argument
77
+ entries are values and `Dict.empty()` is a compile error, what derives (`Eq`,
78
+ `Show`, `Encodable`, `Decodable`) and what doesn't (`Comparable`), and how
79
+ interfaces and encode/decode fit together. Deliberately not a function list —
80
+ that's `jade q api`, and a copy would rot. Every claim in it was checked
81
+ against the compiler.
82
+ - The gem now ships `docs/` and `AGENTS.md`, so a project that installs
83
+ jade-lang gets them instead of README links pointing at nothing.
84
+
85
+ ### Changed
86
+
87
+ - **Decoding a row returned from a port is roughly eight times cheaper.**
88
+ A derived decoder was a composition of combinators rebuilt and walked
89
+ for every row; a profile put 88% of the time inside the interpreter and
90
+ 70 allocations on a four-field row. Deriving now emits one node
91
+ carrying every key, its decoder and the constructor, applied once
92
+ rather than curried through a closure per field; the interpreter
93
+ returns values unwrapped, with failures on a sentinel, so nothing
94
+ allocates a `Result` per node; and each descriptor decodes itself
95
+ instead of being matched out of a twenty-branch `case`. A four-field
96
+ row went 9.58 to 1.19 µs and 70 allocations to 5.
97
+ - **`Decimal`, `Clock.Instant` and `Calendar.Date` read their text form
98
+ in Ruby** rather than parsing it through Jade-level combinators, which
99
+ cost a fresh decoder, two `Maybe`s and a tuple for every value. The
100
+ text forms are unchanged, so JSON output and existing ports are
101
+ unaffected. One input changes meaning: a trailing exponent marker
102
+ (`"825e-4e"`) used to read as `825e-4`, because splitting on every
103
+ `"e"` dropped the empty trailing piece, and is rejected now.
104
+ - **Encoding a value back out to Ruby is cheaper by the same route.** A
105
+ derived record encoder built a pair per field and folded the list back
106
+ into a hash, with an intrinsic lookup for both per field per value; it
107
+ builds the hash directly. `Encodable(Instant)` writes its string in
108
+ Ruby next to the reader that parses it. Handing a four-field struct
109
+ back to a Ruby caller cost 2.26 µs/row over the internal form and now
110
+ costs 0.17 — what a caller waits for and what `Internal` measures have
111
+ converged.
112
+ - **`Calendar.from_rata_die` is closed form.** It walked a year at a time
113
+ to find the year, then December backwards to find the month, allocating
114
+ a `Month` and a tuple per step through two twelve-branch `case`
115
+ statements. It is reached from `Clock.to_iso`, `Clock.on_date` and
116
+ `Calendar.add`, so this is not only an interop win — any Jade code
117
+ doing date arithmetic pays it.
118
+ - The `case` completion snippet offers one `in` branch per variant instead of
119
+ an `else` fallback. `else` is for matching literals, where exhaustiveness
120
+ isn't available — not the default shape of a `case`.
121
+ - **A missing qualified name now suggests the one you meant.** `List.fold_left`
122
+ answers ``help: did you mean `List.fold`?`` instead of a bare "not found" —
123
+ the candidates are the module's exposed values, so the suggestion is drawn
124
+ from what actually exists. Suggestions go through the alias the module was
125
+ imported under (`L.post`, not `Ledger.post`), since that's what the call site
126
+ can say. Local variables, types and constructors already did this;
127
+ module-qualified access was the gap.
128
+
129
+ ### Fixed
130
+
131
+ - **Operator precedence was lost inside an infix chain's operands.**
132
+ `(1 + 2 * 3) + 0` was 9, `(10 - 4 / 2) + 0` was 3, and
133
+ `id(1 + 2 * 3) + 0` was 9. Shunting-yard ran on a chain's own operators
134
+ but treated every operand as an atom and returned it unfixed, so a
135
+ grouping, a call argument, a lambda body, a ternary branch or a record
136
+ value sitting in a chain kept the shape the parser built — left to
137
+ right, no precedence. Standing alone the same expression was fine,
138
+ which is why `1 + 2 * 3` and `(1 + 2 * 3)` both gave 7 and nothing
139
+ caught it. **This changes generated output for code that hit it**: a
140
+ project that commits generated artifacts and CI-checks them for drift
141
+ will see a diff, and the new bytes are the correct arithmetic.
142
+ - **`Clock.on_date` reported the previous day for pre-1970 instants.**
143
+ `Clock.floor_div` subtracted one from the quotient for a negative
144
+ dividend with a remainder — the right correction when `/` truncates
145
+ toward zero, which Jade's doesn't; it already floors, so it floored
146
+ twice. `at_time` was unaffected, so `to_iso(-1)` read
147
+ `"1969-12-30T23:59:59Z"`: the time right, the date a day out. Exact
148
+ midnight was always correct, because the double correction only applies
149
+ when there is a remainder.
150
+ - **Anonymous records decoded from the same shape now compare equal.**
151
+ The derived decoder built its constructor from a bare `Data.define`,
152
+ which mints a fresh class each time the decoder is built, so
153
+ `{ x: 1, y: "a" }` decoded twice compared false. It resolves through
154
+ the same interned registry record literals already used. Note this does
155
+ not unify decoded records with *literals* in the general case —
156
+ literals hoist to a per-module constant, so two modules with the same
157
+ shape still get different classes.
158
+ - **The tuple decoders no longer build their constructor with
159
+ `Method#curry`.** A curried `Method` built once and called on every
160
+ decode corrupts the heap under GC compaction — a near-null "try to mark
161
+ T_NONE object" crash after enough rows. It is the pattern the
162
+ constructor-curry regression spec exists to prevent; that spec reads
163
+ generated code, so the stdlib's own copy was invisible to it.
164
+ - **`Result.on_error` can change the error type**, as its signature has always
165
+ claimed. The `Ok` arm handed back the input rather than rebuilding it, which
166
+ unified the outgoing error type with the incoming one, so
167
+ `Result(Int, String) -> Result(Int, Int)` did not compile. Recovering into a
168
+ different error type is the reason the function takes `e -> Result(a, f)`.
169
+ - **A signature no longer renders two distinct type variables as one.** Hover
170
+ reported `Maybe.map : (Maybe(a), (a) -> a) -> Maybe(a)` — a function unable
171
+ to change the element type, which is a different function from the one that
172
+ exists. Variables are identified by an id but print as a name, and nothing
173
+ upstream keeps names distinct; rendering now re-letters a clash instead of
174
+ emitting the same name twice. Declared names are kept where they don't
175
+ collide, so annotated signatures read as written.
176
+
177
+ - **`docs/stdlib.md` named nine functions that don't exist.** `Char.is_digit`,
178
+ `is_alpha`, `is_alpha_num`, `is_upper`, `is_lower` were renamed to `digit?`,
179
+ `alpha?`, `alpha_numeric?`, `upper?`, `lower?` when predicates took the `?`
180
+ suffix and the doc never followed; `String.contains` is `contains?`,
181
+ `Dict.member` is `member?`, `Tuple.map_first` / `map_second` were never
182
+ implemented, and `Decode.at` doesn't exist (`Decode.index` does). Every
183
+ qualified name in `docs/stdlib.md` and `AGENTS.md` now checks out against the
184
+ registry.
185
+
9
186
  ## [0.4.0]
10
187
 
11
188
  ### Changed
@@ -187,7 +364,9 @@ Initial release.
187
364
  - `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
188
365
  - Language server and headless query interface for editor/agent tooling.
189
366
 
190
- [Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.4.0...HEAD
367
+ [Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.6.0...HEAD
368
+ [0.6.0]: https://github.com/agustinrhcp/jade/compare/v0.5.0...v0.6.0
369
+ [0.5.0]: https://github.com/agustinrhcp/jade/compare/v0.4.0...v0.5.0
191
370
  [0.4.0]: https://github.com/agustinrhcp/jade/compare/v0.3.1...v0.4.0
192
371
  [0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
193
372
  [0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
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.