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/syntax.md ADDED
@@ -0,0 +1,458 @@
1
+ # Syntax reference
2
+
3
+ A tour of the language. The shorter "what does Jade look like" answer is in the
4
+ [README](../README.md); this page covers what you'll actually write. Every
5
+ snippet here compiles.
6
+
7
+ ## Functions
8
+
9
+ ```jade
10
+ module Greetings exposing (greet)
11
+
12
+ def greet(name: String) -> String
13
+ "Hello, " ++ name ++ "!"
14
+ end
15
+ ```
16
+
17
+ Annotations are required on function signatures; everything else is inferred.
18
+ Bodies close with `end`. A zero-argument function is written without parens —
19
+ `def greet -> String`.
20
+
21
+ → [`examples/basics_examples.jd`](../examples/basics_examples.jd)
22
+
23
+ ## Union types
24
+
25
+ ```jade
26
+ module Shapes exposing (Shape)
27
+
28
+ type Shape
29
+ = Circle(Float)
30
+ | Rectangle(Float, Float)
31
+ ```
32
+
33
+ Variants carry positional payloads (above) or keyed payloads when the
34
+ positional args get hard to read:
35
+
36
+ ```jade
37
+ module Charges exposing (paid)
38
+
39
+ type Charge
40
+ = Refund(Int)
41
+ | Settled(paid_amount: Int, tax_amount: Int, issued_amount: Int)
42
+
43
+
44
+ def paid(c: Charge) -> Int
45
+ case c
46
+ in Refund(n) then n
47
+ in Settled(r) then r.paid_amount + r.tax_amount
48
+ end
49
+ end
50
+ ```
51
+
52
+ The keyed payload is an anonymous record, so `r.paid_amount` and
53
+ `{ r | paid_amount: 0 }` work as usual.
54
+
55
+ → [`examples/custom_types.jd`](../examples/custom_types.jd)
56
+
57
+ ## Structs (named records)
58
+
59
+ ```jade
60
+ module People exposing (make, make_kw, older, older_pipe)
61
+
62
+ struct Person = {
63
+ name: String,
64
+ age: Int
65
+ }
66
+
67
+
68
+ def make -> Person
69
+ Person("Paul", 55)
70
+ end
71
+
72
+
73
+ def make_kw -> Person
74
+ Person(name: "Paul", age: 55)
75
+ end
76
+
77
+
78
+ def older(p: Person) -> Person
79
+ { p | age: p.age + 1 }
80
+ end
81
+
82
+
83
+ def older_pipe(p: Person) -> Person
84
+ p |> .age=(p.age + 1)
85
+ end
86
+ ```
87
+
88
+ Anonymous records do not coerce into nominal structs: passing
89
+ `{ name: "Paul", age: 55 }` where a `Person` is expected is a type error.
90
+
91
+ → [`examples/records.jd`](../examples/records.jd)
92
+
93
+ ## Anonymous records and tuples
94
+
95
+ ```jade
96
+ module Pairs exposing (origin, swap)
97
+
98
+ def origin -> { x: Int, y: Int }
99
+ { x: 0, y: 0 }
100
+ end
101
+
102
+
103
+ def swap(pair: (Int, String)) -> (String, Int)
104
+ (Tuple.second(pair), Tuple.first(pair))
105
+ end
106
+ ```
107
+
108
+ Tuple elements are read with `Tuple.first` / `Tuple.second`, or matched in a
109
+ `case` (`in (a, b) then …`).
110
+
111
+ ## Primitives and `Never`
112
+
113
+ `Int`, `Float`, `Bool`, `String`, `Char`. `Char` literals are single
114
+ characters in single quotes: `'a'`, `'Z'`.
115
+
116
+ `Never` is a type with no values — a variant that can't be built. The compiler
117
+ knows this, so you can destructure past it:
118
+
119
+ ```jade
120
+ module Unwrap exposing (unwrap)
121
+
122
+ def unwrap(r: Result(Int, Never)) -> Int
123
+ Ok(n) = r
124
+ n
125
+ end
126
+ ```
127
+
128
+ ## Zero-arg functions as values
129
+
130
+ A zero-arg function is referenced by name, with or without parens:
131
+
132
+ ```jade
133
+ module Geometry exposing (area)
134
+
135
+ def pi -> Float
136
+ 3.14
137
+ end
138
+
139
+
140
+ def area(radius: Float) -> Float
141
+ pi * radius * radius
142
+ end
143
+ ```
144
+
145
+ Bare constructors are values too: `True`, `Nothing`. Constructors with payloads
146
+ are called: `Just(x)`, `Ok(value)`.
147
+
148
+ ## Pattern matching
149
+
150
+ ```jade
151
+ module Sizes exposing (describe_size)
152
+
153
+ def describe_size(n: Int) -> String
154
+ case n
155
+ in 0 then "empty"
156
+ in 1 then "one"
157
+ else "many"
158
+ end
159
+ end
160
+ ```
161
+
162
+ `case` must cover every shape the value can take — exhaustiveness is enforced by
163
+ the compiler. The trailing wildcard is written `else`.
164
+
165
+ A branch with multiple statements drops `then` and indents under the header:
166
+
167
+ ```jade
168
+ module Areas exposing (area)
169
+
170
+ type Shape
171
+ = Circle(Float)
172
+ | Rectangle(Float, Float)
173
+ | Triangle(Float, Float, Float)
174
+
175
+
176
+ def area(shape: Shape) -> Float
177
+ case shape
178
+ in Circle(r) then 3.14 * r * r
179
+ in Rectangle(w, h) then w * h
180
+ in Triangle(a, b, c)
181
+ s = (a + b + c) / 2.0
182
+ s * (s - a) * (s - b) * (s - c)
183
+ end
184
+ end
185
+ ```
186
+
187
+ **Lists** match with `[]` and `[head | tail]`; the rest after `|` must be a
188
+ name or wildcard:
189
+
190
+ ```jade
191
+ module Sums exposing (sum)
192
+
193
+ def sum(list: List(Int)) -> Int
194
+ case list
195
+ in [] then 0
196
+ in [x | xs] then x + sum(xs)
197
+ end
198
+ end
199
+ ```
200
+
201
+ **Destructuring with `=`** binds the fields of a record (use `case` for a list
202
+ — a partial `=` binding on a list is rejected):
203
+
204
+ ```jade
205
+ module Names exposing (greet)
206
+
207
+ struct Person = {
208
+ name: String,
209
+ age: Int
210
+ }
211
+
212
+
213
+ def greet(person: Person) -> String
214
+ { name:, age: } = person
215
+ "Hi " ++ name
216
+ end
217
+ ```
218
+
219
+ **The `<-` operator** unwraps the value inside a `Maybe`, `Result`, or `Task`
220
+ and short-circuits if it isn't there, so you write the success path top to
221
+ bottom:
222
+
223
+ ```jade
224
+ module SafeSum exposing (safe_sum)
225
+
226
+ def safe_sum(a: Maybe(Int), b: Maybe(Int)) -> Maybe(Int)
227
+ x <- a
228
+ y <- b
229
+ Just(x + y)
230
+ end
231
+ ```
232
+
233
+ If `a` or `b` is `Nothing`, the rest of the function is skipped. `Result`
234
+ short-circuits on `Err`; `Task` on its failure arm.
235
+
236
+ → [`examples/pattern_matching.jd`](../examples/pattern_matching.jd),
237
+ [`examples/maybe_examples.jd`](../examples/maybe_examples.jd)
238
+
239
+ ## Conditionals
240
+
241
+ A ternary for one-liners, a block `if`/`else`/`end` when a branch needs more
242
+ than one statement:
243
+
244
+ ```jade
245
+ module Abs exposing (abs)
246
+
247
+ def abs(n: Int) -> Int
248
+ n < 0 ? 0 - n : n
249
+ end
250
+ ```
251
+
252
+ ## Pipes, lambdas, currying
253
+
254
+ ```jade
255
+ module Nums exposing (clean)
256
+
257
+ def clean(numbers: List(Int)) -> List(Int)
258
+ numbers
259
+ |> List.filter((x) -> { x > 0 })
260
+ |> List.map((x) -> { x * 2 })
261
+ end
262
+ ```
263
+
264
+ Lambdas are `(params) -> { body }` — the one construct that uses braces.
265
+
266
+ A `_` in an argument position curries the call: each `_` becomes a parameter,
267
+ left to right, so `add(_, 5)` is a one-argument function.
268
+
269
+ ```jade
270
+ module Curry exposing (add_five, add_one)
271
+
272
+ def add(a: Int, b: Int) -> Int
273
+ a + b
274
+ end
275
+
276
+
277
+ def add_five(n: Int) -> Int
278
+ add(_, 5)(n)
279
+ end
280
+
281
+
282
+ def add_one(n: Int) -> Int
283
+ add(1, _)(n)
284
+ end
285
+ ```
286
+
287
+ It's most useful for building decoders field by field:
288
+
289
+ ```jade
290
+ module Decoding exposing (person)
291
+
292
+ import Decode exposing (DecodeError)
293
+
294
+
295
+ struct Person = {
296
+ name: String,
297
+ age: Int
298
+ }
299
+
300
+
301
+ def person(json: String) -> Result(Person, DecodeError)
302
+ decoder = Decode.succeed(Person(_, _))
303
+ |> Decode.required("name", Decode.string)
304
+ |> Decode.required("age", Decode.int)
305
+
306
+ Decode.decode_string(decoder, json)
307
+ end
308
+ ```
309
+
310
+ `_` is only valid as a direct argument inside a call — not as a bare expression
311
+ or operator operand.
312
+
313
+ ## Interfaces
314
+
315
+ Resolved at compile time from the argument types, like a Ruby module you
316
+ `extend` — except the compiler picks the implementation. `Eq` (`==` / `!=`),
317
+ `Comparable` (`compare`, returning `LT` / `EQ` / `GT`), and `Appendable` (`++`
318
+ on `String` and `List`) ship built in:
319
+
320
+ ```jade
321
+ module Compare exposing (are_equal, larger)
322
+
323
+ def are_equal(a: a, b: a) -> Bool
324
+ a == b
325
+ end
326
+
327
+
328
+ def larger(a: a, b: a) -> a
329
+ case compare(a, b)
330
+ in GT then a
331
+ else b
332
+ end
333
+ end
334
+ ```
335
+
336
+ You can define your own, with implementations dispatched by type:
337
+
338
+ ```jade
339
+ module Shows exposing (describe)
340
+
341
+ struct Person = {
342
+ name: String,
343
+ age: Int
344
+ }
345
+
346
+
347
+ interface Show(a) with
348
+ show : a -> String
349
+ end
350
+
351
+
352
+ implements Show(Person) with
353
+ show: (p) -> { p.name ++ " (age " ++ String.from_int(p.age) ++ ")" }
354
+ end
355
+
356
+
357
+ def describe(p: Person) -> String
358
+ show(p)
359
+ end
360
+ ```
361
+
362
+ The right-hand side of an `implements` clause is an inline lambda or a function
363
+ reference (`show: show_person`).
364
+
365
+ → [`examples/interfaces.jd`](../examples/interfaces.jd)
366
+
367
+ ## Modules and imports
368
+
369
+ One module per file; `exposing` lists the public surface (alphabetized).
370
+ `import` pulls names in by module or selectively — `Maybe(..)` brings the type
371
+ in along with its constructors:
372
+
373
+ ```jade
374
+ module Wallet exposing (balance)
375
+
376
+ import Maybe exposing (Maybe(..), map)
377
+ import List
378
+
379
+
380
+ def balance(amount: Maybe(Int)) -> Int
381
+ Maybe.with_default(amount, 0)
382
+ end
383
+ ```
384
+
385
+ ## How it compiles
386
+
387
+ Source on the left, the Ruby it compiles to on the right. Nothing in the
388
+ compiled column is machinery you can't trace.
389
+
390
+ <table>
391
+ <tr><th>Jade</th><th>Compiled Ruby</th></tr>
392
+ <tr><td>
393
+
394
+ ```jade
395
+ module Sample exposing (area)
396
+
397
+ type Shape
398
+ = Circle(Float)
399
+ | Rectangle(Float, Float)
400
+
401
+
402
+ def area(shape: Shape) -> Float
403
+ case shape
404
+ in Circle(r) then 3.14 * r * r
405
+ in Rectangle(w, h) then w * h
406
+ end
407
+ end
408
+ ```
409
+
410
+ </td><td>
411
+
412
+ ```ruby
413
+ module Sample
414
+ extend self
415
+
416
+ Circle = Data.define(:_1) do
417
+ def circle?; true; end
418
+ def rectangle?; false; end
419
+ end
420
+
421
+ Rectangle = Data.define(:_1, :_2) do
422
+ def circle?; false; end
423
+ def rectangle?; true; end
424
+ end
425
+
426
+ module Internal
427
+ extend self
428
+
429
+ def area(shape)
430
+ case shape
431
+ in Sample::Circle(r) then ((3.14 * r) * r)
432
+ in Sample::Rectangle(w, h) then (w * h)
433
+ end
434
+ end
435
+ end
436
+
437
+ def self.area(*)
438
+ raise Jade::Interop::NotExposed.new(
439
+ module_name: "Sample", function_name: :area,
440
+ hint: "argument 1 of type Shape has no Decodable instance",
441
+ )
442
+ end
443
+ end
444
+ ```
445
+
446
+ </td></tr>
447
+ </table>
448
+
449
+ (The compiled file also carries a short `require` header for the runtime and
450
+ stdlib.) When something behaves unexpectedly, the path is the same as in any
451
+ Ruby project: open the file, read the code.
452
+
453
+ A function is exposed to Ruby only when all its parameters are `Decodable` and
454
+ its return is `Encodable`; the public `Sample.area` is generated alongside
455
+ `Internal.area` to decode the Ruby args, run the function, and encode the
456
+ return. Here `area`'s parameter is a `Shape`, which has no `Decodable` instance —
457
+ so the function isn't exposed, and the public method raises `NotExposed` if
458
+ called. See [interop.md](interop.md) for that side.
data/docs/testing.md ADDED
@@ -0,0 +1,70 @@
1
+ # Testing
2
+
3
+ Most Jade tests are plain unit tests over pure functions: pass in data, assert
4
+ on the return value, no mocks. The only thing that needs special support is a
5
+ `Task` — code that talks to the outside world. That's what this page covers.
6
+
7
+ ## Setup
8
+
9
+ Include one of two RSpec helpers:
10
+
11
+ ```ruby
12
+ # strict: any unstubbed Task is a test failure
13
+ RSpec.configure { |c| c.include Jade::Tasks::RSpec }
14
+
15
+ # loose: real port bodies run unless you've replaced them
16
+ RSpec.configure { |c| c.include Jade::Tasks::RSpec::Loose }
17
+ ```
18
+
19
+ Use **strict** for unit specs (an unstubbed `Task` means the test reached the
20
+ outside world by accident). Use **loose** for higher-level specs that should let
21
+ real bodies through unless you've stubbed them.
22
+
23
+ ## Stubbing a Task
24
+
25
+ `all_calls_to(task, …)` sets a persistent answer; `next_call_to(task, …)` queues
26
+ a one-shot. Both take a value or a block `{ |t, *args| t.ok(…) }`:
27
+
28
+ ```ruby
29
+ it 'sends a welcome mail to the new address' do
30
+ all_calls_to(Mailer.deliver) { |t, _email| t.ok(true) }
31
+
32
+ expect(Signup::Internal.run('ada@example.com').run).to be_ok(true)
33
+ expect(Mailer.deliver).to have_been_called.with('ada@example.com')
34
+ end
35
+ ```
36
+
37
+ Queued answers win until exhausted, then the persistent one takes over:
38
+
39
+ ```ruby
40
+ next_call_to(Rng.roll, 1)
41
+ next_call_to(Rng.roll, 2)
42
+ all_calls_to(Rng.roll, 0) # call 1 → 1, call 2 → 2, call 3+ → 0
43
+ ```
44
+
45
+ `have_been_called` chains `.with(...)`, `.once`, `.times(n)`, and negates with
46
+ `not_to`.
47
+
48
+ ## Matchers
49
+
50
+ ```ruby
51
+ expect(result).to be_ok # is Ok
52
+ expect(result).to be_ok(42) # Ok(42)
53
+ expect(result).to be_err("smtp down")
54
+ expect(maybe).to be_just(5)
55
+ expect(maybe).to be_nothing
56
+
57
+ # look_like matches a union variant by name and payload
58
+ expect(shape).to look_like(:Circle, 10.0)
59
+ expect(shape).to look_like(:Square, 1.0)
60
+ ```
61
+
62
+ `be_ok` and friends compose with ordinary matchers:
63
+
64
+ ```ruby
65
+ expect(result).to be_ok(have_attributes(name: 'Ada', age: 40))
66
+ expect(result).to be_ok(kind_of(Integer))
67
+ ```
68
+
69
+ Pass a `'Module::Name'` string to `look_like` when the short variant name is
70
+ ambiguous.