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.
- checksums.yaml +4 -4
- data/AGENTS.md +164 -0
- data/CHANGELOG.md +180 -1
- 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/boundary/cache.rb +1 -2
- data/lib/jade/codegen/boundary/specialized/list.rb +9 -4
- data/lib/jade/codegen/boundary/specialized/maybe.rb +15 -3
- data/lib/jade/codegen/boundary/specialized/record.rb +7 -3
- data/lib/jade/codegen/boundary/specialized.rb +7 -3
- data/lib/jade/codegen/emitter.rb +11 -2
- data/lib/jade/codegen/function_declaration.rb +7 -6
- 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 +12 -4
- data/lib/jade/interop/error.rb +25 -5
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- data/lib/jade/module_loader/module_name.rb +46 -0
- data/lib/jade/module_loader.rb +5 -0
- 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 +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.
|