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
data/docs/json.md ADDED
@@ -0,0 +1,163 @@
1
+ # Decoding and encoding
2
+
3
+ `Decode` turns untyped JSON / Ruby data into typed Jade values; `Encode` turns
4
+ typed values back into JSON. Both are explicit pipelines, and both auto-derive
5
+ for `struct` types so the common case needs no hand-written decoder or encoder.
6
+
7
+ ## Decoding
8
+
9
+ A decoder is a value; `Decode.decode_string(decoder, json)` runs one and returns
10
+ a `Result(a, DecodeError)`. The combinators compose:
11
+
12
+ ```jade
13
+ module DecodeJson exposing (
14
+ age,
15
+ coords,
16
+ tags,
17
+ user,
18
+ )
19
+
20
+ import Decode exposing (DecodeError)
21
+
22
+
23
+ struct User = {
24
+ name: String,
25
+ age: Int
26
+ }
27
+
28
+
29
+ def age(json: String) -> Result(Int, DecodeError)
30
+ Decode.decode_string(Decode.field("age", Decode.int), json)
31
+ end
32
+
33
+
34
+ def tags(json: String) -> Result(List(String), DecodeError)
35
+ Decode.decode_string(Decode.list(Decode.string), json)
36
+ end
37
+
38
+
39
+ def coords(json: String) -> Result(Maybe(Int), DecodeError)
40
+ Decode.decode_string(Decode.nullable(Decode.int), json)
41
+ end
42
+
43
+
44
+ def user(json: String) -> Result(User, DecodeError)
45
+ decoder = Decode.succeed(User(_, _))
46
+ |> Decode.required("name", Decode.string)
47
+ |> Decode.required("age", Decode.int)
48
+
49
+ Decode.decode_string(decoder, json)
50
+ end
51
+ ```
52
+
53
+ ```ruby
54
+ DecodeJson::Internal.age('{"age":40}') # => Ok(40)
55
+ DecodeJson::Internal.tags('["a","b"]') # => Ok(["a", "b"])
56
+ DecodeJson::Internal.coords('null') # => Ok(Nothing)
57
+ DecodeJson::Internal.coords('7') # => Ok(Just(7))
58
+ DecodeJson::Internal.user('{"name":"Ada","age":40}')
59
+ # => Ok(User(name: "Ada", age: 40))
60
+ DecodeJson::Internal.user('{"name":"Ada"}')
61
+ # => Err(MissingField("age"))
62
+ ```
63
+
64
+ The struct decoder is `Decode.succeed(User(_, _))` piped through one
65
+ `Decode.required` per field — the `_` placeholders are the constructor's holes,
66
+ filled left to right as each field decodes.
67
+
68
+ ## Encoding
69
+
70
+ `Encode.encode_to_string(value)` serializes; the combinators mirror `Decode`:
71
+
72
+ ```jade
73
+ module EncodeJson exposing (n, point, user, xs)
74
+
75
+ import Encode
76
+
77
+
78
+ struct User = {
79
+ name: String,
80
+ age: Int
81
+ }
82
+
83
+
84
+ def n -> String
85
+ Encode.encode_to_string(Encode.int(42))
86
+ end
87
+
88
+
89
+ def xs -> String
90
+ Encode.encode_to_string(Encode.list(Encode.int, [1, 2, 3]))
91
+ end
92
+
93
+
94
+ def point -> String
95
+ pairs = [
96
+ Encode.field("x", Encode.int, 1),
97
+ Encode.field("y", Encode.int, 2),
98
+ ]
99
+
100
+ Encode.encode_to_string(Encode.object(pairs))
101
+ end
102
+
103
+
104
+ def user(u: User) -> String
105
+ Encode.encode_to_string(Encode.encode(u))
106
+ end
107
+ ```
108
+
109
+ ```ruby
110
+ EncodeJson.n # => "42"
111
+ EncodeJson.xs # => "[1,2,3]"
112
+ EncodeJson.point # => '{"x":1,"y":2}'
113
+ EncodeJson::Internal.user(user)
114
+ # => '{"name":"Ada","age":40}'
115
+ ```
116
+
117
+ ## Auto-derivation
118
+
119
+ `Encode.encode(value)` derives the encoder from the value's type, and
120
+ `Decode.from_json(json)` derives the decoder from the **return type** — so a
121
+ struct round-trips without writing either by hand:
122
+
123
+ ```jade
124
+ module Api exposing (parse, render)
125
+
126
+ import Encode
127
+ import Decode exposing (DecodeError)
128
+
129
+
130
+ struct User = {
131
+ name: String,
132
+ age: Int
133
+ }
134
+
135
+
136
+ def parse(json: String) -> Result(User, DecodeError)
137
+ Decode.from_json(json)
138
+ end
139
+
140
+
141
+ def render(user: User) -> String
142
+ Encode.encode_to_string(Encode.encode(user))
143
+ end
144
+ ```
145
+
146
+ Reach for the explicit combinators above when the JSON shape doesn't match the
147
+ struct one-to-one — renamed keys, nested lookups, optional fields.
148
+
149
+ Derivation reaches through the structural types to their elements, so anything
150
+ built out of encodable parts is itself encodable:
151
+
152
+ | Type | Wire form |
153
+ |------|-----------|
154
+ | `List(a)`, `Set(a)` | array — a set drops duplicates on the way back |
155
+ | `Maybe(a)` | the value, or `null` |
156
+ | `(a, b)` … `(a, b, c, d)` | array, positional |
157
+ | `Dict(k, v)` | array of `[key, value]` pairs — a JSON object only admits string keys |
158
+ | a `struct` | object, keyed by field name |
159
+ | a union whose variants take no arguments | string, the variant name in snake_case |
160
+
161
+ A type outside that list needs its own `implements Encodable(T)` /
162
+ `Decodable(T)` — a union carrying arguments, say, where nothing but you knows
163
+ which shape it should take.
data/docs/lsp.md ADDED
@@ -0,0 +1,105 @@
1
+ # Language server
2
+
3
+ Jade ships a language server, run as `jade lsp`. It speaks LSP over stdio and
4
+ works in any editor with an LSP client.
5
+
6
+ ## What it does
7
+
8
+ - **Diagnostics.** Type and parse errors are streamed to the editor as you type.
9
+ - **Hover.** Hover any name, local, or expression to see its inferred type.
10
+ - **Go to definition.** Jump from a use site to where a name is declared —
11
+ across modules, into the stdlib, and to interface implementations.
12
+ - **Find references.** Every use of a name across the project, declaration
13
+ included.
14
+ - **Document symbols.** The editor's outline ("show all functions in this
15
+ file") works.
16
+
17
+ ## Editor setup
18
+
19
+ The server is `jade lsp` on stdio. Point your editor's LSP client at it for
20
+ files matching `*.jd`.
21
+
22
+ ### Neovim
23
+
24
+ ```lua
25
+ -- Treat *.jd files as filetype "jd"
26
+ vim.filetype.add({ extension = { jd = 'jd' } })
27
+
28
+ vim.lsp.start({
29
+ name = 'jade-lsp',
30
+ cmd = { 'jade', 'lsp' },
31
+ root_dir = vim.fs.dirname(vim.fs.find({ 'Gemfile', '.git' }, { upward = true })[1]),
32
+ filetypes = { 'jd' },
33
+ })
34
+ ```
35
+
36
+ ### VS Code
37
+
38
+ No published extension yet. The server works with any LSP-client extension that
39
+ lets you point at a custom command (e.g. `vscode-languageclient` with a small
40
+ wrapper that runs `jade lsp`).
41
+
42
+ ### Other editors
43
+
44
+ If your editor speaks LSP, it'll work — the server depends on no editor-specific
45
+ bridge.
46
+
47
+ ## For agents that don't speak LSP
48
+
49
+ `jade q` exposes the same compiler intelligence as one-shot JSON over stdout —
50
+ handy for agents and scripts that don't want to manage a JSON-RPC session:
51
+
52
+ ```
53
+ jade q hover file.jd:LINE:COL # type info at a position
54
+ jade q defn file.jd:LINE:COL # goto-definition target
55
+ jade q refs file.jd:LINE:COL # all references (incl. declaration)
56
+ jade q symbols file.jd # document outline
57
+ jade q api [MODULE|NAME] # signatures, by module or symbol
58
+ jade q find TERM # every symbol matching TERM
59
+ jade q syntax [FORM] # how a form is written
60
+ ```
61
+
62
+ `LINE` and `COL` are 0-indexed (LSP convention). Paths are relative to the
63
+ project root; compile results are cached at `.jade/cache`, so repeat queries are
64
+ fast.
65
+
66
+ `api` and `find` cover the stdlib, the project's own modules, and any extension
67
+ gem's — each tagged with its `origin`. Without a `jade.json` they fall back to
68
+ the stdlib alone, so they still answer from any directory. See
69
+ [stdlib.md](stdlib.md#finding-a-function).
70
+
71
+ `syntax` answers the other half. A signature says a function exists and what it
72
+ takes; it never says how a lambda or an `implements` block is written:
73
+
74
+ ```
75
+ $ jade q syntax lambda
76
+ { "form": "lambda", "detail": "anonymous function", "source": "(args) -> { body }" }
77
+ ```
78
+
79
+ The forms are `def`, `type`, `struct`, `case`, `if`, `module`, `import`,
80
+ `interface`, `implements`, `uses`, `lambda` — the same corpus the editor
81
+ offers as completions, so the two can't drift. They're templates, not
82
+ compilable programs. It reads no files and needs no project.
83
+
84
+ ## Checking without an editor
85
+
86
+ `jade check` type-checks and prints diagnostics, generating nothing:
87
+
88
+ ```
89
+ jade check # every .jd under the source root
90
+ jade check lib/orders.jd # that file and everything it imports
91
+ ```
92
+
93
+ Exit 1 if there were errors, 0 otherwise. This is the loop to close after
94
+ editing a file — it's the same front end the LSP runs, so an invented function
95
+ or a wrong argument order surfaces immediately instead of at the next build.
96
+
97
+ ## What it doesn't do yet
98
+
99
+ - Autocomplete
100
+ - Refactor / rename
101
+ - Code actions (e.g. "add the missing case to this `case`")
102
+ - Workspace-wide symbol search
103
+
104
+ These are the natural next features; the current server is the minimum useful
105
+ surface.
data/docs/stdlib.md ADDED
@@ -0,0 +1,69 @@
1
+ # Standard library
2
+
3
+ ## Finding a function
4
+
5
+ Ask the compiler, don't grep:
6
+
7
+ ```
8
+ jade q api # every module, and where it came from
9
+ jade q api Dict # one module, with signatures
10
+ jade q api List.fold # one symbol
11
+ jade q find fold # everything named `fold`
12
+ ```
13
+
14
+ Grep is the wrong tool here, and quietly so. Stdlib modules are written two
15
+ ways — Jade source in a heredoc (`Maybe`, `Result`, `Decimal`, …) and a Ruby
16
+ DSL (`List`, `String`, `Dict`, `Decode`, …) — so half of them never spell `def`
17
+ at all, and none of them contains the string `List.map` you'd search for.
18
+ Extension modules aren't in your tree at all; they live in a gem. A grep that
19
+ comes back empty means nothing.
20
+
21
+ `jade q api` reads the registry the compiler itself resolves against, so it
22
+ can't drift from what exists. It answers what grep can't: the argument order
23
+ (`List.fold` folds with `(b, a) -> b`, `Dict.fold` with `(k, v, b) -> b`), the
24
+ interface constraints (`List.sort : Comparable a => …`), a struct's fields
25
+ (`struct Date = { year : Int, month : Month, day : Int }`), and which types
26
+ implement `Decodable` or `Encodable`.
27
+
28
+ Run inside a project and it covers three origins, each tagged in the output:
29
+
30
+ | `origin` | what |
31
+ |---|---|
32
+ | `stdlib` | the modules below |
33
+ | `project` | your own `.jd` files, under the source root |
34
+ | `extension` | modules an extension gem ships (`jade-sql`'s `Sql.Query`, …) |
35
+
36
+ Without a `jade.json` it falls back to the stdlib alone, so the query still
37
+ works from anywhere. A file that won't compile is named under `skipped` rather
38
+ than quietly missing — an absent module reads as "that function doesn't
39
+ exist", which is the failure this is here to prevent.
40
+
41
+ ## What's where
42
+
43
+ Every module's source lives in [`lib/jade/stdlib/`](../lib/jade/stdlib/) — short,
44
+ readable Ruby. This is a map of the territory; `jade q api` is the atlas.
45
+
46
+ | Module | What's there |
47
+ |--------|--------------|
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
+ | `Maybe` | Optional values without `nil`: `Just(a)` / `Nothing`, with `map`, `and_then`, `with_default`. |
50
+ | `Result` | Errors as values, no exceptions: `Ok(a)` / `Err(e)`, with `map`, `and_then`, `map_error`, `on_error`, `sequence`. |
51
+ | `List` | Immutable lists: `map`, `filter`, `fold`, `zip`, `sort`, `length`, `range`, `head`, `tail`, `take`, `drop`, … |
52
+ | `String` | Text: `length`, `reverse`, `split`, `trim`, `to_int`, `contains?`, `uncons`, `cons`, `from_char`, `map`. |
53
+ | `Char` | Character predicates and codes: `to_code`, `from_code`, `digit?`, `alpha?`, `alpha_numeric?`, `upper?`, `lower?`. |
54
+ | `Tuple` | Pair accessors: `first`, `second`, `pair`. |
55
+ | `Task` | Side-effecting actions: `succeed`, `fail`, `map`, `and_then`, `on_error`, `sequence`. See [interop.md](interop.md) for how Tasks cross the Ruby boundary. |
56
+ | `Decode` | Parse JSON / Ruby data into typed values: `string`, `int`, `float`, `bool`, `list`, `field`, `index`, `succeed`, `required`, `optional`, `nullable`, `map`, `and_then`. Auto-derived for `struct` types. See [json.md](json.md). |
57
+ | `Decode.Params` | PATCH-style decoders: only fields present in the input appear in the output, missing fields don't error. For partial-update endpoints. |
58
+ | `Encode` | Symmetric to `Decode`: `string`, `int`, `float`, `bool`, `list`, `object`. Auto-derived for `struct` types via `Encodable`. |
59
+ | `Dict` | Immutable key-value map with structural equality: `empty`, `get`, `member?`, `insert`, `update`, `remove`, `keys`, `values`, `to_list`, `from_list`, `map`, `filter`, `fold`, `union`, `merge`. |
60
+ | `Set` | Immutable set: `empty`, `insert`, `remove`, `member?`, `to_list`, `from_list`, `map`, `filter`, `fold`, `union`, `intersect`, `diff`. |
61
+ | `Bytes` | Opaque byte buffer: `empty`, `width`, `from_list` / `to_list`, `from_string` / `to_string`. Implements `Eq` and `Appendable`. |
62
+ | `Calendar` | Dates and date arithmetic: `Date`, `today`. Days, months, years; no time of day — use `Clock`. |
63
+ | `Clock` | Timestamps and monotonic timing: `Instant`, `now`. Sub-second precision; the bridge to wall-clock time. |
64
+ | `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
+ | `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
+
68
+ Stdlib operations compile inline rather than through a runtime dispatch layer,
69
+ so the generated Ruby calls the underlying operation directly.