jade-lang 0.9.0 → 0.10.1
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/CHANGELOG.md +442 -12
- data/README.md +1 -0
- data/docs/interop.md +18 -1
- data/docs/json.md +34 -3
- data/docs/stdlib.md +3 -1
- data/docs/syntax.md +96 -0
- data/lib/jade/api.rb +9 -0
- data/lib/jade/ast.rb +37 -1
- data/lib/jade/cli/eject.rb +132 -0
- data/lib/jade/cli/init.rb +86 -0
- data/lib/jade/cli.rb +4 -0
- data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
- data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
- data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
- data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
- data/lib/jade/codegen/boundary/specialized.rb +13 -5
- data/lib/jade/codegen/boundary.rb +12 -2
- data/lib/jade/codegen/context.rb +17 -0
- data/lib/jade/codegen/error.rb +1 -0
- data/lib/jade/codegen/function_call.rb +38 -7
- data/lib/jade/codegen/function_declaration.rb +25 -13
- data/lib/jade/codegen/helpers.rb +27 -4
- data/lib/jade/codegen/implementation.rb +2 -2
- data/lib/jade/codegen/inline.rb +1 -1
- data/lib/jade/codegen/inlines.rb +6 -1
- data/lib/jade/codegen/names.rb +31 -0
- data/lib/jade/codegen/pattern/constructor.rb +2 -2
- data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
- data/lib/jade/codegen.rb +76 -15
- data/lib/jade/compiler.rb +2 -0
- data/lib/jade/entry.rb +1 -1
- data/lib/jade/formatter/calls.rb +68 -11
- data/lib/jade/formatter/collections.rb +4 -4
- data/lib/jade/formatter/declarations.rb +13 -15
- data/lib/jade/formatter/helper.rb +20 -0
- data/lib/jade/formatter/infix_application.rb +19 -4
- data/lib/jade/formatter/lambda.rb +24 -12
- data/lib/jade/formatter/pattern.rb +3 -0
- data/lib/jade/formatter/type.rb +3 -5
- data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
- data/lib/jade/frontend/desugaring.rb +29 -9
- data/lib/jade/frontend/fixity_fixer.rb +6 -4
- data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
- data/lib/jade/frontend/forward_declaration/error.rb +3 -0
- data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
- data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
- data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
- data/lib/jade/frontend/forward_declaration/module.rb +4 -0
- data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
- data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration.rb +2 -0
- data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
- data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
- data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
- data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
- data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
- data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
- data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
- data/lib/jade/frontend/pattern_analysis.rb +5 -38
- data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
- data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
- data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
- data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
- data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
- data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
- data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
- data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
- data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
- data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
- data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis.rb +5 -0
- data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
- data/lib/jade/frontend/type_checking/cascade.rb +90 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
- data/lib/jade/frontend/type_checking/constraints.rb +18 -3
- data/lib/jade/frontend/type_checking/error/display.rb +49 -0
- data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
- data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
- data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
- data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
- data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
- data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
- data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
- data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
- data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
- data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
- data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
- data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
- data/lib/jade/frontend/type_checking/error.rb +7 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
- data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
- data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
- data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
- data/lib/jade/frontend/type_checking/inference/record_update.rb +8 -1
- data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
- data/lib/jade/frontend/type_checking/inference.rb +2 -0
- data/lib/jade/frontend/type_checking/loader.rb +11 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
- data/lib/jade/frontend/type_checking/substitution.rb +2 -2
- data/lib/jade/frontend/type_checking.rb +3 -1
- data/lib/jade/frontend/usage_analysis.rb +5 -1
- data/lib/jade/interop/boundary.rb +64 -24
- data/lib/jade/interop/error.rb +35 -7
- data/lib/jade/interop/lowering.rb +8 -2
- data/lib/jade/lsp/converters.rb +83 -2
- data/lib/jade/lsp/handlers.rb +122 -25
- data/lib/jade/lsp/snippets.rb +12 -0
- data/lib/jade/lsp/state.rb +11 -4
- data/lib/jade/module_loader/build.rb +36 -0
- data/lib/jade/module_loader.rb +1 -0
- data/lib/jade/parsing/combinators.rb +78 -2
- data/lib/jade/parsing/error.rb +43 -6
- data/lib/jade/parsing/type.rb +6 -4
- data/lib/jade/parsing.rb +140 -26
- data/lib/jade/runtime.rb +32 -13
- data/lib/jade/source.rb +6 -2
- data/lib/jade/stdlib/basics.rb +22 -3
- data/lib/jade/stdlib/char.rb +2 -0
- data/lib/jade/stdlib/clock.rb +6 -2
- data/lib/jade/stdlib/decimal.rb +51 -14
- data/lib/jade/stdlib/decode.rb +11 -0
- data/lib/jade/stdlib/intrinsics.rb +59 -6
- data/lib/jade/stdlib/list.rb +37 -1
- data/lib/jade/stdlib/number.rb +47 -0
- data/lib/jade/stdlib/range.rb +211 -0
- data/lib/jade/stdlib.rb +11 -4
- data/lib/jade/symbol/alias.rb +11 -0
- data/lib/jade/symbol/parser.rb +1 -1
- data/lib/jade/symbol/stdlib_function.rb +1 -1
- data/lib/jade/symbol.rb +13 -1
- data/lib/jade/task.rb +0 -2
- data/lib/jade/type/anonymous_record.rb +11 -2
- data/lib/jade/type/application.rb +11 -2
- data/lib/jade/type/base.rb +26 -0
- data/lib/jade/type/function.rb +11 -3
- data/lib/jade/type.rb +36 -10
- data/lib/jade/version.rb +1 -1
- metadata +35 -2
data/docs/syntax.md
CHANGED
|
@@ -90,6 +90,57 @@ Anonymous records do not coerce into nominal structs: passing
|
|
|
90
90
|
|
|
91
91
|
→ [`examples/records.jd`](../examples/records.jd)
|
|
92
92
|
|
|
93
|
+
## Type aliases
|
|
94
|
+
|
|
95
|
+
A name for another type, with no runtime identity of its own:
|
|
96
|
+
|
|
97
|
+
```jade
|
|
98
|
+
type alias UserId = Int
|
|
99
|
+
type alias Point = (Float, Float)
|
|
100
|
+
type alias Handler = Int -> Int
|
|
101
|
+
type alias Pair(a) = (a, a)
|
|
102
|
+
|
|
103
|
+
type alias User = {
|
|
104
|
+
name: String,
|
|
105
|
+
age: Int
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`UserId` and `Int` are the same type, and a record literal satisfies `User`
|
|
110
|
+
directly:
|
|
111
|
+
|
|
112
|
+
```jade
|
|
113
|
+
u1 = { name: "Paul", age: 55 }
|
|
114
|
+
u2 = { u1 | age: 56 }
|
|
115
|
+
name = u1.name
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
An alias never has its own constructors or interface implementations, so
|
|
119
|
+
`implements Show(UserId)` is a compile error — two aliases over the same body
|
|
120
|
+
are the same type, so the implementation would land on `Int` and collide with
|
|
121
|
+
every other one. Reach for `struct` when you want a distinct type, or a
|
|
122
|
+
single-variant `type` when you want a newtype around an inner shape.
|
|
123
|
+
|
|
124
|
+
Interfaces an alias *inherits* work as you would expect, because the alias is
|
|
125
|
+
gone by the time they are resolved: `Encode.encode` on a `UserId` is
|
|
126
|
+
`Encode.encode` on an `Int`, and on a `User` it encodes the record.
|
|
127
|
+
|
|
128
|
+
| | identity | construct with |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `struct User = { … }` | nominal, distinct from a same-shaped record | `User("Paul", 55)` |
|
|
131
|
+
| `type User = User({ … })` | nominal newtype wrapping an inner shape | `User({ … })` |
|
|
132
|
+
| `type alias User = { … }` | structural, a readability name | `{ … }` |
|
|
133
|
+
|
|
134
|
+
Recursive aliases are rejected: use a `type` for a recursive shape. A cycle is
|
|
135
|
+
reported once, against the first alias in it that was declared.
|
|
136
|
+
|
|
137
|
+
`alias` is only a keyword directly after `type`, so a field or argument may
|
|
138
|
+
still be called `alias`.
|
|
139
|
+
|
|
140
|
+
The name survives into messages — a mismatch against a `UserId` reads
|
|
141
|
+
`should be UserId (= Int)` — and hovering one in an editor gives its
|
|
142
|
+
declaration.
|
|
143
|
+
|
|
93
144
|
## Anonymous records and tuples
|
|
94
145
|
|
|
95
146
|
```jade
|
|
@@ -184,6 +235,51 @@ def area(shape: Shape) -> Float
|
|
|
184
235
|
end
|
|
185
236
|
```
|
|
186
237
|
|
|
238
|
+
**Ranges** match a span of `Int`, with `..` including both bounds. Either
|
|
239
|
+
bound may be left off to run to infinity:
|
|
240
|
+
|
|
241
|
+
```jade
|
|
242
|
+
module Bands exposing (band)
|
|
243
|
+
|
|
244
|
+
def band(age: Int) -> String
|
|
245
|
+
case age
|
|
246
|
+
in ..-1 then "unborn"
|
|
247
|
+
in 0..2 then "infant"
|
|
248
|
+
in 3..12 then "child"
|
|
249
|
+
in 13.. then "adult"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Those four arms partition `Int`, so the `case` needs no `else`. A range that
|
|
255
|
+
leaves a gap is reported with the gap named — omitting `in 3..12` above gives
|
|
256
|
+
*Missing cases: `3..12`* — and one already covered by an earlier arm is
|
|
257
|
+
reported as unreachable.
|
|
258
|
+
|
|
259
|
+
There is no exclusive form. Over `Int` every half-open range has an exact
|
|
260
|
+
inclusive spelling (`3...12` is `3..11`), so a second operator would buy
|
|
261
|
+
nothing and cost a lifetime of remembering which is which. Ranges are `Int`
|
|
262
|
+
only for the same reason: `0..2` and `3..12` sit next to each other with
|
|
263
|
+
nothing in between them to be missed, which is not true of `Float`.
|
|
264
|
+
|
|
265
|
+
The same spelling builds a `Range` value in expression position, where the
|
|
266
|
+
bounds are ordinary expressions rather than literals:
|
|
267
|
+
|
|
268
|
+
```jade
|
|
269
|
+
module Bookings exposing (clashes?)
|
|
270
|
+
|
|
271
|
+
def clashes?(check_in: Date, check_out: Date, other: Range(Date)) -> Bool
|
|
272
|
+
Range.overlaps?(check_in..check_out, other)
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`..` binds looser than `+` and `-`, so `0..n - 1` is `0..(n - 1)`. It is
|
|
277
|
+
non-associative — `a..b..c` is an error, not a nesting — and it is infix
|
|
278
|
+
only: the endless forms are `Range.from(a)` and `Range.to(b)`, because a
|
|
279
|
+
token that is prefix, infix and postfix by turns costs more to read than two
|
|
280
|
+
names do. A descending pair is `Range.empty` rather than an error, since
|
|
281
|
+
`0..(n - 1)` with `n = 0` is the ordinary way an empty range arises.
|
|
282
|
+
|
|
187
283
|
**Lists** match with `[]` and `[head | tail]`; the rest after `|` must be a
|
|
188
284
|
name or wildcard:
|
|
189
285
|
|
data/lib/jade/api.rb
CHANGED
|
@@ -70,6 +70,7 @@ module Jade
|
|
|
70
70
|
KIND_ORDER = {
|
|
71
71
|
'type' => 0,
|
|
72
72
|
'struct' => 0,
|
|
73
|
+
'alias' => 0,
|
|
73
74
|
'interface' => 1,
|
|
74
75
|
'constructor' => 2,
|
|
75
76
|
'function' => 3,
|
|
@@ -153,6 +154,7 @@ module Jade
|
|
|
153
154
|
case symbol
|
|
154
155
|
in Symbol::Union then 'type'
|
|
155
156
|
in Symbol::Struct then 'struct'
|
|
157
|
+
in Symbol::Alias then 'alias'
|
|
156
158
|
in Symbol::Interface then 'interface'
|
|
157
159
|
in Symbol::Constructor | Symbol::Variant then 'constructor'
|
|
158
160
|
else 'function'
|
|
@@ -167,6 +169,9 @@ module Jade
|
|
|
167
169
|
in Symbol::Struct
|
|
168
170
|
"struct #{symbol.name}#{type_params(symbol.type_params)}#{fields(symbol)}"
|
|
169
171
|
|
|
172
|
+
in Symbol::Alias
|
|
173
|
+
"type alias #{symbol.name}#{type_params(symbol.type_params)} = #{body(symbol)}"
|
|
174
|
+
|
|
170
175
|
in Symbol::Interface
|
|
171
176
|
"interface #{symbol.name}(#{symbol.type_param.name})"
|
|
172
177
|
|
|
@@ -192,6 +197,10 @@ module Jade
|
|
|
192
197
|
end
|
|
193
198
|
end
|
|
194
199
|
|
|
200
|
+
def body(symbol)
|
|
201
|
+
declared(symbol.body).then { |(type, _)| type }
|
|
202
|
+
end
|
|
203
|
+
|
|
195
204
|
def fields(symbol)
|
|
196
205
|
return '' unless symbol.record_type
|
|
197
206
|
|
data/lib/jade/ast.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Jade
|
|
|
32
32
|
define(:TypeParam, :name)
|
|
33
33
|
define(:ImportDeclaration, :module_name, :as, :exposing)
|
|
34
34
|
define(:StructDeclaration, :name, :type_params, :record_type)
|
|
35
|
+
define(:TypeAliasDeclaration, :name, :type_params, :body_type)
|
|
35
36
|
|
|
36
37
|
define(:ExposeAll)
|
|
37
38
|
define(:ExposeNone)
|
|
@@ -84,6 +85,7 @@ module Jade
|
|
|
84
85
|
|
|
85
86
|
define(:Wildcard)
|
|
86
87
|
define(:Literal, :literal)
|
|
88
|
+
define(:Range, :from, :to)
|
|
87
89
|
define(:Binding, :name)
|
|
88
90
|
define(:Constructor, :constructor, :patterns)
|
|
89
91
|
define(:Record, :fields)
|
|
@@ -364,11 +366,17 @@ module Jade
|
|
|
364
366
|
module_parts.map(&:value).join('.'),
|
|
365
367
|
exposing,
|
|
366
368
|
body,
|
|
367
|
-
module_parts.first.range.begin...(body
|
|
369
|
+
module_parts.first.range.begin...module_end(module_parts, body),
|
|
368
370
|
]
|
|
369
371
|
end
|
|
370
372
|
end
|
|
371
373
|
|
|
374
|
+
# Tolerant mode drops a declaration it cannot parse, so a module whose
|
|
375
|
+
# only declaration is half-written has an empty body.
|
|
376
|
+
def module_end(module_parts, body)
|
|
377
|
+
body.expressions.last&.range&.end || module_parts.last.range.end
|
|
378
|
+
end
|
|
379
|
+
|
|
372
380
|
def if_then_else
|
|
373
381
|
->((if_token, condition, if_branch, else_branch, end_token)) do
|
|
374
382
|
IfThenElse[
|
|
@@ -442,6 +450,22 @@ module Jade
|
|
|
442
450
|
end
|
|
443
451
|
end
|
|
444
452
|
|
|
453
|
+
def range_pattern
|
|
454
|
+
->((from, dotdot, to)) do
|
|
455
|
+
Pattern::Range[
|
|
456
|
+
from,
|
|
457
|
+
to,
|
|
458
|
+
from.range.begin...(to&.range&.end || dotdot.range.end),
|
|
459
|
+
]
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def unbounded_below_pattern
|
|
464
|
+
->((dotdot, to)) do
|
|
465
|
+
Pattern::Range[nil, to, dotdot.range.begin...to.range.end]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
445
469
|
def binding_pattern
|
|
446
470
|
->(identifier) do
|
|
447
471
|
Pattern::Binding[identifier.value, identifier.range]
|
|
@@ -676,6 +700,18 @@ module Jade
|
|
|
676
700
|
end
|
|
677
701
|
end
|
|
678
702
|
|
|
703
|
+
def type_alias_declaration
|
|
704
|
+
->((type_token, name, type_params_list, body_type)) do
|
|
705
|
+
TypeAliasDeclaration.new(
|
|
706
|
+
name: name.value,
|
|
707
|
+
type_params: type_params_list.items,
|
|
708
|
+
body_type:,
|
|
709
|
+
trailing_comma: type_params_list.trailing_comma,
|
|
710
|
+
range: type_token.range.begin...body_type.range.end,
|
|
711
|
+
)
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
679
715
|
def implementation
|
|
680
716
|
->((implements_token, interface, applied_type, extends, functions, end_token)) do
|
|
681
717
|
Implementation[
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'rbconfig'
|
|
3
|
+
|
|
4
|
+
require 'jade'
|
|
5
|
+
require 'jade/module_loader'
|
|
6
|
+
|
|
7
|
+
module Jade
|
|
8
|
+
module CLI
|
|
9
|
+
# Writes the project as Ruby that runs without the gem: the compiled
|
|
10
|
+
# modules, the runtime they call, and requires that point at each other
|
|
11
|
+
# rather than at a load path.
|
|
12
|
+
module Eject
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
DEFAULT_OUT = 'ejected'.freeze
|
|
16
|
+
|
|
17
|
+
def run(argv)
|
|
18
|
+
usage if argv.any? { it == '-h' || it == '--help' }
|
|
19
|
+
|
|
20
|
+
Project
|
|
21
|
+
.find!
|
|
22
|
+
.then { [it, out_dir(argv, it)] }
|
|
23
|
+
.then { |project, out| eject(project, out) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def eject(project, out)
|
|
27
|
+
FileUtils.rm_rf(out)
|
|
28
|
+
|
|
29
|
+
compile_all(project, out)
|
|
30
|
+
vendor_runtime(out)
|
|
31
|
+
rewrite_requires(out)
|
|
32
|
+
|
|
33
|
+
report(out)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Every module, not only the ones something imports: an ejected tree
|
|
37
|
+
# that drops a file is not the project.
|
|
38
|
+
def compile_all(project, out)
|
|
39
|
+
sources(project).each do |entry|
|
|
40
|
+
ModuleLoader
|
|
41
|
+
.load(project.source_root, entry, cache_dir: project.cache_path)
|
|
42
|
+
.then { ModuleLoader.emit(it, path: out) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sources(project)
|
|
47
|
+
Dir
|
|
48
|
+
.glob(File.join(project.source_root, '**', '*.jd'))
|
|
49
|
+
.map { Pathname.new(it).relative_path_from(Pathname.new(project.source_root)).to_s }
|
|
50
|
+
.sort
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whatever a booted runtime loads is what the ejected code needs, so
|
|
54
|
+
# ask one rather than keeping a list here.
|
|
55
|
+
def vendor_runtime(out)
|
|
56
|
+
runtime_files.each do |file|
|
|
57
|
+
File.join(out, file)
|
|
58
|
+
.tap { FileUtils.mkdir_p(File.dirname(it)) }
|
|
59
|
+
.then { FileUtils.cp(File.join(gem_lib, file), it) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def runtime_files
|
|
64
|
+
[RbConfig.ruby, '-I', gem_lib, '-e', BOOT]
|
|
65
|
+
.then { IO.popen(it, err: [:child, :out], &:read) }
|
|
66
|
+
.lines(chomp: true)
|
|
67
|
+
.select { it.start_with?("#{gem_lib}/") }
|
|
68
|
+
.map { it.delete_prefix("#{gem_lib}/") }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
BOOT = "require 'jade/runtime'; Jade::Runtime.boot!; puts $LOADED_FEATURES".freeze
|
|
72
|
+
|
|
73
|
+
def gem_lib
|
|
74
|
+
File.expand_path('../../..', __dir__).then { File.join(it, 'lib') }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# `require 'jade/x'` finds the gem; the point is not to have one.
|
|
78
|
+
def rewrite_requires(out)
|
|
79
|
+
Dir
|
|
80
|
+
.glob(File.join(out, '**', '*.rb'))
|
|
81
|
+
.each { rewrite(it, out) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rewrite(path, out)
|
|
85
|
+
File
|
|
86
|
+
.read(path)
|
|
87
|
+
.gsub(/^\$LOAD_PATH\.push\(File\.expand_path\("lib"\)\)\.uniq!\n/, '')
|
|
88
|
+
.gsub(/require ['"](jade\/[a-z_\/]+)['"]/) { "require_relative '#{up_to(path, out)}#{$1}'" }
|
|
89
|
+
.then { File.write(path, it) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def up_to(path, out)
|
|
93
|
+
Pathname
|
|
94
|
+
.new(out)
|
|
95
|
+
.expand_path
|
|
96
|
+
.relative_path_from(Pathname.new(File.dirname(File.expand_path(path))))
|
|
97
|
+
.then { it.to_s == '.' ? '' : "#{it}/" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def report(out)
|
|
101
|
+
puts <<~TXT
|
|
102
|
+
Ejected to #{out}/ (#{Dir.glob(File.join(out, '**', '*.rb')).size} files).
|
|
103
|
+
|
|
104
|
+
Nothing under #{out}/ requires the jade gem. To leave:
|
|
105
|
+
|
|
106
|
+
1. point your app at #{out}/ instead of the build directory
|
|
107
|
+
2. run your suite
|
|
108
|
+
3. drop jade-lang from your Gemfile and delete the .jd sources
|
|
109
|
+
TXT
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def out_dir(argv, project)
|
|
113
|
+
argv
|
|
114
|
+
.each_cons(2)
|
|
115
|
+
.find { |flag, _| flag == '--out' }
|
|
116
|
+
&.last
|
|
117
|
+
.then { File.expand_path(it || DEFAULT_OUT, project.root) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def usage
|
|
121
|
+
warn <<~USAGE
|
|
122
|
+
Usage: jade eject [--out DIR]
|
|
123
|
+
|
|
124
|
+
Compiles the project and writes it, plus the runtime it calls, as
|
|
125
|
+
Ruby that does not require the jade gem. Writes to ./ejected by
|
|
126
|
+
default. The directory is replaced if it exists.
|
|
127
|
+
USAGE
|
|
128
|
+
exit 1
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require 'jade'
|
|
5
|
+
|
|
6
|
+
module Jade
|
|
7
|
+
module CLI
|
|
8
|
+
# Writes the manifest every tool outside the app reads. Without one,
|
|
9
|
+
# the CLI and the language server have to guess where the sources are,
|
|
10
|
+
# which is why `Project::NotFound` says to write this file by hand.
|
|
11
|
+
module Init
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
IGNORED = '.jade/'.freeze
|
|
15
|
+
|
|
16
|
+
def run(argv)
|
|
17
|
+
usage if argv.any? { it == '-h' || it == '--help' }
|
|
18
|
+
|
|
19
|
+
source_root(argv)
|
|
20
|
+
.then { [it, File.expand_path(Project::MANIFEST)] }
|
|
21
|
+
.then { |root, manifest| write(root, manifest) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write(root, manifest)
|
|
25
|
+
refuse(manifest) if File.exist?(manifest)
|
|
26
|
+
|
|
27
|
+
File.write(manifest, "#{JSON.pretty_generate(config(root))}\n")
|
|
28
|
+
FileUtils.mkdir_p(root)
|
|
29
|
+
|
|
30
|
+
report(root, manifest)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Only what the defaults do not already say. A manifest naming every
|
|
34
|
+
# setting reads as though each were a decision.
|
|
35
|
+
def config(root)
|
|
36
|
+
{ 'source_roots' => [root], 'extensions' => [] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def source_root(argv)
|
|
40
|
+
argv
|
|
41
|
+
.each_cons(2)
|
|
42
|
+
.find { |flag, _| flag == '--source-root' }
|
|
43
|
+
&.last || Project::DEFAULTS[:source_roots].first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def refuse(manifest)
|
|
47
|
+
warn "jade: #{File.basename(manifest)} already exists in #{File.dirname(manifest)}"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def report(root, manifest)
|
|
52
|
+
puts <<~TXT
|
|
53
|
+
Wrote #{File.basename(manifest)} and #{root}/.
|
|
54
|
+
|
|
55
|
+
Put a module in #{root}/, then:
|
|
56
|
+
|
|
57
|
+
jade check type-check it
|
|
58
|
+
jade fmt #{root}/x.jd#{' ' * [0, 7 - root.length].max} format it
|
|
59
|
+
|
|
60
|
+
#{gitignore_note}
|
|
61
|
+
TXT
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build and cache output, which nobody wants in a diff.
|
|
65
|
+
def gitignore_note
|
|
66
|
+
path = File.expand_path('.gitignore')
|
|
67
|
+
return "Add #{IGNORED} to .gitignore." unless File.exist?(path)
|
|
68
|
+
return "#{IGNORED} is already ignored." if File.read(path).match?(/^\.jade\b/)
|
|
69
|
+
|
|
70
|
+
File.write(path, "#{File.read(path).chomp}\n#{IGNORED}\n")
|
|
71
|
+
"Added #{IGNORED} to .gitignore."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def usage
|
|
75
|
+
warn <<~USAGE
|
|
76
|
+
Usage: jade init [--source-root DIR]
|
|
77
|
+
|
|
78
|
+
Writes jade.json, which is how every tool outside the app finds
|
|
79
|
+
your sources, and creates the source directory. Refuses to
|
|
80
|
+
overwrite an existing manifest.
|
|
81
|
+
USAGE
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/jade/cli.rb
CHANGED
|
@@ -4,7 +4,9 @@ module Jade
|
|
|
4
4
|
module CLI
|
|
5
5
|
SUBCOMMANDS = {
|
|
6
6
|
'check' => 'Check',
|
|
7
|
+
'eject' => 'Eject',
|
|
7
8
|
'fmt' => 'Fmt',
|
|
9
|
+
'init' => 'Init',
|
|
8
10
|
'lsp' => 'Lsp',
|
|
9
11
|
'q' => 'Q',
|
|
10
12
|
}.freeze
|
|
@@ -37,7 +39,9 @@ module Jade
|
|
|
37
39
|
Usage: jade COMMAND [ARGS]
|
|
38
40
|
|
|
39
41
|
check Type-check the project (or the given files).
|
|
42
|
+
eject Write the project as Ruby that runs without the gem.
|
|
40
43
|
fmt Format .jd source (stdin or file).
|
|
44
|
+
init Write jade.json and the source directory.
|
|
41
45
|
lsp Run the language server (stdio JSON-RPC).
|
|
42
46
|
q Headless query interface (hover/symbols/defn/refs/api).
|
|
43
47
|
|
|
@@ -11,11 +11,11 @@ module Jade
|
|
|
11
11
|
module List
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
-
def decode(type, input, registry)
|
|
14
|
+
def decode(type, input, registry, where = nil)
|
|
15
15
|
inner = inner_of(type) or return nil
|
|
16
16
|
|
|
17
|
-
scalar_optimized(inner, input) ||
|
|
18
|
-
generic_decode(type, inner, input, registry)
|
|
17
|
+
scalar_optimized(inner, input, where) ||
|
|
18
|
+
generic_decode(type, inner, input, registry, where)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def encode(type, value_expr, registry)
|
|
@@ -51,17 +51,21 @@ module Jade
|
|
|
51
51
|
|
|
52
52
|
# `List(scalar)` fast path: validate elements with a single
|
|
53
53
|
# C-loop `all?` check, no per-element decoder call.
|
|
54
|
-
def scalar_optimized(inner, input)
|
|
54
|
+
def scalar_optimized(inner, input, where)
|
|
55
55
|
qname = Scalar.qname_for(inner) or return nil
|
|
56
56
|
klass = Scalar::LIST_ELEM_CLASS[qname]
|
|
57
57
|
label = "List(#{Scalar::LABEL[qname]})".inspect
|
|
58
|
-
"Jade::Interop::Boundary.list_of(#{klass}, #{label}, #{input})"
|
|
58
|
+
"Jade::Interop::Boundary.list_of(#{klass}, #{label}, #{input}#{Specialized.where_arg(where)})"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
# The index comes from `elements`, so the element decoder itself
|
|
62
|
+
# carries no path of its own.
|
|
63
|
+
def generic_decode(type, inner, input, registry, where)
|
|
62
64
|
elem = Specialized.decode_expr(inner, '_1', registry) or return nil
|
|
63
65
|
label = "List(#{Specialized.type_label(inner)})".inspect
|
|
64
|
-
|
|
66
|
+
at = Specialized.where_arg(where)
|
|
67
|
+
array = "Jade::Interop::Boundary.array(#{label}, #{input}#{at})"
|
|
68
|
+
"Jade::Interop::Boundary.elements(#{array}#{at}) { #{elem} }"
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|
|
@@ -9,9 +9,9 @@ module Jade
|
|
|
9
9
|
module Maybe
|
|
10
10
|
extend self
|
|
11
11
|
|
|
12
|
-
def decode(type, input, registry)
|
|
12
|
+
def decode(type, input, registry, where = nil)
|
|
13
13
|
inner = inner_of(type) or return nil
|
|
14
|
-
elem = Specialized.decode_expr(inner, 'it', registry) or return nil
|
|
14
|
+
elem = Specialized.decode_expr(inner, 'it', registry, where) or return nil
|
|
15
15
|
|
|
16
16
|
"#{input}.then { it.nil? ? Jade::Maybe::Nothing[] : Jade::Maybe::Just[#{elem}] }"
|
|
17
17
|
end
|
|
@@ -16,9 +16,13 @@ module Jade
|
|
|
16
16
|
extend self
|
|
17
17
|
extend Helpers
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# The helper is shared across call sites, so its fields can only
|
|
20
|
+
# know where they sit inside the struct: the segment that leads
|
|
21
|
+
# to the struct itself goes on at the call site.
|
|
22
|
+
def decode(type, input, registry, where = nil)
|
|
20
23
|
struct = struct_for(type, registry) or return nil
|
|
21
|
-
"#{decode_helper_name(struct)}(#{input})"
|
|
24
|
+
call = "#{decode_helper_name(struct)}(#{input})"
|
|
25
|
+
where ? "Jade::Interop::Boundary.at(#{where.inspect}) { #{call} }" : call
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def encode(type, value_expr, registry)
|
|
@@ -132,7 +136,7 @@ module Jade
|
|
|
132
136
|
struct.record_type.fields
|
|
133
137
|
.map { |k, t| field_decode(k, t, registry) }
|
|
134
138
|
.then { Pretty.call(ctor, it, open: '[', close: ']') }
|
|
135
|
-
.then { Pretty.block("#{hash_call}.then do |h|", it) }
|
|
139
|
+
.then { Pretty.block("#{hash_call}.then do |h|", it, blaming_the_key(struct)) }
|
|
136
140
|
.then { Pretty.block("def self.#{decode_helper_name(struct)}(value)", it) }
|
|
137
141
|
end
|
|
138
142
|
|
|
@@ -144,7 +148,7 @@ module Jade
|
|
|
144
148
|
end
|
|
145
149
|
|
|
146
150
|
def field_decode(key, type, registry)
|
|
147
|
-
Specialized.decode_expr(type, "h[#{key.to_s.inspect}]", registry) ||
|
|
151
|
+
Specialized.decode_expr(type, "h[#{key.to_s.inspect}]", registry, ".#{key}") ||
|
|
148
152
|
fail("non-specializable field type for #{key}: #{type}")
|
|
149
153
|
end
|
|
150
154
|
|
|
@@ -156,6 +160,17 @@ module Jade
|
|
|
156
160
|
end
|
|
157
161
|
end
|
|
158
162
|
|
|
163
|
+
# A field the hash never had reads as nil to its decoder, which
|
|
164
|
+
# blames the type. The key it sat under is right here, and so is
|
|
165
|
+
# the hash that was supposed to hold it.
|
|
166
|
+
def blaming_the_key(struct)
|
|
167
|
+
[
|
|
168
|
+
'rescue Jade::Interop::DecodeError => e',
|
|
169
|
+
" raise Jade::Interop::Boundary.blame(e, h, #{struct.name.inspect})",
|
|
170
|
+
'end',
|
|
171
|
+
].join("\n")
|
|
172
|
+
end
|
|
173
|
+
|
|
159
174
|
def snake(name)
|
|
160
175
|
name
|
|
161
176
|
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
@@ -31,10 +31,10 @@ module Jade
|
|
|
31
31
|
'String.String' => '::String',
|
|
32
32
|
}.freeze
|
|
33
33
|
|
|
34
|
-
def decode(type, input)
|
|
34
|
+
def decode(type, input, where = nil)
|
|
35
35
|
qname = qname_for(type) or return nil
|
|
36
36
|
label = LABEL[qname].inspect
|
|
37
|
-
"Jade::Interop::Boundary.#{HELPER[qname]}(#{label}, #{input})"
|
|
37
|
+
"Jade::Interop::Boundary.#{HELPER[qname]}(#{label}, #{input}#{Specialized.where_arg(where)})"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Scalar encoders are identity (Ruby int IS the JSON int, etc.),
|
|
@@ -21,11 +21,13 @@ module Jade
|
|
|
21
21
|
|
|
22
22
|
# Ruby expression that validates `input` and yields the decoded
|
|
23
23
|
# value, or `nil` if `type` isn't specializable.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
# `where` is the path segment this value sits at, `".cents"`,
|
|
25
|
+
# baked into the emitted call so a failure can name it.
|
|
26
|
+
def decode_expr(type, input, registry, where = nil)
|
|
27
|
+
Scalar.decode(type, input, where) ||
|
|
28
|
+
List.decode(type, input, registry, where) ||
|
|
29
|
+
Maybe.decode(type, input, registry, where) ||
|
|
30
|
+
Record.decode(type, input, registry, where)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# How to encode `value_expr` to the wire form:
|
|
@@ -66,6 +68,12 @@ module Jade
|
|
|
66
68
|
Record.emit_helpers(structs, registry)
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# Trailing argument for the runtime helpers, which all take the
|
|
72
|
+
# path segment last and default it to nil.
|
|
73
|
+
def where_arg(where)
|
|
74
|
+
where ? ", #{where.inspect}" : ''
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
# --- type-shape helpers shared across shape modules ---
|
|
70
78
|
|
|
71
79
|
# Both `Type::Application` (from inferred boundary types) and
|
|
@@ -18,6 +18,9 @@ module Jade
|
|
|
18
18
|
in Type::Application(constructor: Type::Constructor(name:), args:)
|
|
19
19
|
decode_app(name, args, registry)
|
|
20
20
|
|
|
21
|
+
in Type::AnonymousRecord
|
|
22
|
+
derived_for(DECODABLE, type, registry)&.dig('decoder')
|
|
23
|
+
|
|
21
24
|
else
|
|
22
25
|
nil
|
|
23
26
|
end
|
|
@@ -28,6 +31,9 @@ module Jade
|
|
|
28
31
|
in Type::Application(constructor: Type::Constructor(name:), args:)
|
|
29
32
|
encode_app(name, args, registry)
|
|
30
33
|
|
|
34
|
+
in Type::AnonymousRecord
|
|
35
|
+
derived_for(ENCODABLE, type, registry)&.dig('encoder')
|
|
36
|
+
|
|
31
37
|
else
|
|
32
38
|
nil
|
|
33
39
|
end
|
|
@@ -153,7 +159,11 @@ module Jade
|
|
|
153
159
|
end
|
|
154
160
|
|
|
155
161
|
def derived_dispatch(interface_qname, type_qname, args, registry)
|
|
156
|
-
type
|
|
162
|
+
type = Type.constructor(type_qname).apply(args)
|
|
163
|
+
derived_for(interface_qname, type, registry)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def derived_for(interface_qname, type, registry)
|
|
157
167
|
constraint = Type.constraint(interface_qname, type, nil)
|
|
158
168
|
|
|
159
169
|
case Frontend::TypeChecking::Constraints.resolve(constraint, registry, nil)
|
|
@@ -175,7 +185,7 @@ module Jade
|
|
|
175
185
|
|
|
176
186
|
case fn_sym
|
|
177
187
|
in Symbol::Function
|
|
178
|
-
"#{
|
|
188
|
+
"#{internal(fn_sym.module_name)}.method(:#{fn_sym.name})"
|
|
179
189
|
|
|
180
190
|
in Symbol::StdlibFunction(codegen:) if codegen.is_a?(String)
|
|
181
191
|
codegen
|
data/lib/jade/codegen/context.rb
CHANGED
|
@@ -22,6 +22,23 @@ module Jade
|
|
|
22
22
|
@dict_env = prev
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Dictionary source => constant name, filled while a module emits.
|
|
26
|
+
# A dictionary built entirely from concrete implementations depends
|
|
27
|
+
# on nothing at the call site, so it is built once at load instead
|
|
28
|
+
# of on every call. Nil outside a module: bare expressions have no
|
|
29
|
+
# constants to reference.
|
|
30
|
+
def dict_consts
|
|
31
|
+
@dict_consts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_dict_consts(table)
|
|
35
|
+
prev = @dict_consts
|
|
36
|
+
@dict_consts = table
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
@dict_consts = prev
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
# When set, references with this name emit as `self` (and field accesses
|
|
26
43
|
# on them as bare method calls). Used to rewrite operator-impl lambda
|
|
27
44
|
# bodies — `(a, b) -> { a.amount == b.amount }` becomes
|
data/lib/jade/codegen/error.rb
CHANGED