jade-lang 0.3.1 → 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 +215 -1
- data/README.md +17 -12
- 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/function_call.rb +1 -1
- data/lib/jade/codegen/helpers.rb +6 -0
- data/lib/jade/codegen/inlines.rb +18 -0
- data/lib/jade/codegen/{port_decoder.rb → port_codec.rb} +27 -15
- data/lib/jade/codegen.rb +2 -2
- data/lib/jade/debug.rb +59 -0
- data/lib/jade/decode.rb +332 -212
- data/lib/jade/diagnostics/renderer.rb +16 -5
- data/lib/jade/frontend/fixity_fixer.rb +3 -2
- data/lib/jade/frontend/forward_declaration/interop_import_declaration.rb +13 -3
- data/lib/jade/frontend/pattern_analysis/matrix.rb +57 -23
- data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +33 -7
- data/lib/jade/frontend/semantic_analysis/error/constructor_not_found.rb +20 -5
- 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 +35 -32
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +32 -50
- data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +51 -175
- data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +133 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/show.rb +186 -0
- data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
- data/lib/jade/frontend/type_checking/error/port_not_encodable.rb +38 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +64 -16
- data/lib/jade/interop/boundary.rb +2 -3
- data/lib/jade/interop/runtime.rb +10 -2
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- data/lib/jade/parsing/error.rb +19 -0
- data/lib/jade/runtime.rb +1 -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/debug.rb +12 -0
- data/lib/jade/stdlib/decimal.rb +3 -16
- data/lib/jade/stdlib/decode.rb +57 -12
- data/lib/jade/stdlib/encode.rb +26 -0
- data/lib/jade/stdlib/intrinsics.rb +12 -1
- data/lib/jade/stdlib/result.rb +1 -1
- data/lib/jade/stdlib/show.rb +40 -0
- data/lib/jade/stdlib/text.rb +131 -0
- data/lib/jade/stdlib.rb +5 -2
- data/lib/jade/symbol/interop_function.rb +3 -1
- data/lib/jade/symbol.rb +2 -2
- data/lib/jade/task.rb +2 -3
- data/lib/jade/version.rb +1 -1
- metadata +19 -3
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.
|
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.
|