jade-lang 0.4.0 → 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 +141 -0
- 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/emitter.rb +11 -2
- 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 +2 -3
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- 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 +13 -2
|
@@ -55,13 +55,14 @@ module Jade
|
|
|
55
55
|
import = entry
|
|
56
56
|
.imports
|
|
57
57
|
.find { it.alias == module_name }
|
|
58
|
+
imported = import&.then { registry.get(it.module_name) }
|
|
58
59
|
|
|
59
60
|
if import.nil?
|
|
60
61
|
Error::ModuleNotFound
|
|
61
62
|
.new(entry.name, node.target.range, name: import)
|
|
62
63
|
|
|
63
64
|
else
|
|
64
|
-
case
|
|
65
|
+
case imported.exposed_value(name.name)
|
|
65
66
|
in nil
|
|
66
67
|
Error::ValueNotExposed
|
|
67
68
|
.new(entry.name, name.range, module_name:, name: name.name)
|
|
@@ -77,10 +78,19 @@ module Jade
|
|
|
77
78
|
node.range,
|
|
78
79
|
name: [module_name, name.name].join('.'),
|
|
79
80
|
causes: [it],
|
|
81
|
+
candidates: exposed_value_names(imported, module_name),
|
|
80
82
|
)
|
|
81
83
|
end
|
|
82
84
|
.then { Err[it] }
|
|
83
85
|
end
|
|
86
|
+
|
|
87
|
+
def exposed_value_names(imported, module_name)
|
|
88
|
+
return [] unless imported
|
|
89
|
+
|
|
90
|
+
imported
|
|
91
|
+
.exposes
|
|
92
|
+
.filter_map { "#{module_name}.#{it.name}" if it.is_a?(Symbol::ValueRef) }
|
|
93
|
+
end
|
|
84
94
|
end
|
|
85
95
|
end
|
|
86
96
|
end
|
|
@@ -82,14 +82,14 @@ module Jade
|
|
|
82
82
|
def derive_struct(constraint, struct_sym, type_args, registry, lookup, entry_name)
|
|
83
83
|
fields = struct_fields(struct_sym, type_args, registry)
|
|
84
84
|
|
|
85
|
-
[:
|
|
85
|
+
[:struct_class, struct_sym.qualified_name]
|
|
86
86
|
.then { derive_record(constraint, fields, it, lookup, entry_name) }
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def derive_anonymous_record(constraint, fields, lookup, entry_name)
|
|
90
90
|
keys = fields.keys.map(&:to_s)
|
|
91
91
|
|
|
92
|
-
[:
|
|
92
|
+
[:anon_record_class, keys]
|
|
93
93
|
.then { derive_record(constraint, fields.to_a, it, lookup, entry_name) }
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -121,19 +121,14 @@ module Jade
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def record_body(fields, constructor_ref)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
[:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
],
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
[:call, [:stdlib_fn, 'Decode.and_map'], [acc, field_decoder]]
|
|
136
|
-
end
|
|
124
|
+
[:call,
|
|
125
|
+
[:stdlib_fn, 'Decode.record'],
|
|
126
|
+
[
|
|
127
|
+
[:list, fields.map { |name, _| name.to_s }],
|
|
128
|
+
[:list, fields.each_index.map { [:impl_arg, it, 'decoder'] }],
|
|
129
|
+
constructor_ref,
|
|
130
|
+
],
|
|
131
|
+
]
|
|
137
132
|
end
|
|
138
133
|
|
|
139
134
|
# Free-var inner constraints fall back to the constraint marker
|
|
@@ -97,27 +97,18 @@ module Jade
|
|
|
97
97
|
field_deps = fields
|
|
98
98
|
.map { |_, field_type| Type.constraint(INTERFACE, field_type, nil) }
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
body = fields
|
|
101
101
|
.each_with_index
|
|
102
102
|
.map do |(field_name, _), idx|
|
|
103
|
-
[
|
|
104
|
-
|
|
105
|
-
[
|
|
106
|
-
|
|
107
|
-
[:
|
|
108
|
-
[:impl_arg, idx, 'encoder'],
|
|
109
|
-
[[:access, [:var, 'rec'], field_name.to_s]],
|
|
110
|
-
],
|
|
103
|
+
[
|
|
104
|
+
field_name.to_s,
|
|
105
|
+
[:call,
|
|
106
|
+
[:impl_arg, idx, 'encoder'],
|
|
107
|
+
[[:access, [:var, 'rec'], field_name.to_s]],
|
|
111
108
|
],
|
|
112
109
|
]
|
|
113
110
|
end
|
|
114
|
-
|
|
115
|
-
body = [:call,
|
|
116
|
-
[:stdlib_fn, 'Encode.object'],
|
|
117
|
-
[
|
|
118
|
-
[:list, pair_irs],
|
|
119
|
-
],
|
|
120
|
-
]
|
|
111
|
+
.then { [:hash, it] }
|
|
121
112
|
|
|
122
113
|
field_deps
|
|
123
114
|
.map { lookup.call(it) }
|
|
@@ -12,9 +12,8 @@ module Jade
|
|
|
12
12
|
# weight at the boundary because failure always raises anyway. Skipping
|
|
13
13
|
# it removes one allocation per arg per Ruby→Jade call.
|
|
14
14
|
def decode_or_raise(decoder, value)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
in Jade::Result::Err[e] then raise Jade::Interop::DecodeError.new(e, value)
|
|
15
|
+
Jade::Decode::Runner.run!(decoder, value) do |error|
|
|
16
|
+
raise Jade::Interop::DecodeError.new(error, value)
|
|
18
17
|
end
|
|
19
18
|
end
|
|
20
19
|
|
data/lib/jade/lsp/converters.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'jade/signature'
|
|
2
3
|
|
|
3
4
|
module Jade
|
|
4
5
|
module LSP
|
|
@@ -432,21 +433,7 @@ module Jade
|
|
|
432
433
|
end
|
|
433
434
|
|
|
434
435
|
def render_signature(name, type, constraints)
|
|
435
|
-
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def constraint_prefix(constraints)
|
|
439
|
-
return '' if constraints.empty?
|
|
440
|
-
|
|
441
|
-
constraints
|
|
442
|
-
.map { short_constraint(it) }
|
|
443
|
-
.uniq
|
|
444
|
-
.join(', ')
|
|
445
|
-
.then { "#{it} => " }
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def short_constraint(constraint)
|
|
449
|
-
"#{constraint.interface.split('.').last} #{constraint.type}"
|
|
436
|
+
Signature.render(name, type, constraints)
|
|
450
437
|
end
|
|
451
438
|
|
|
452
439
|
# First node in the path (innermost-to-outermost) that resolves to a
|
data/lib/jade/lsp/snippets.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module LSP
|
|
3
3
|
module Snippets
|
|
4
|
-
|
|
4
|
+
TAB_STOP = /\$\{\d+:?([^}]*)\}/
|
|
5
|
+
|
|
6
|
+
Snippet = Data.define(:label, :detail, :body) do
|
|
7
|
+
# Keeps the hint text, drops the tab stops an editor needs.
|
|
8
|
+
def source
|
|
9
|
+
body.gsub(TAB_STOP, '\1')
|
|
10
|
+
end
|
|
11
|
+
end
|
|
5
12
|
|
|
6
13
|
ALL = [
|
|
7
14
|
Snippet.new(
|
|
@@ -33,11 +40,11 @@ module Jade
|
|
|
33
40
|
),
|
|
34
41
|
Snippet.new(
|
|
35
42
|
label: 'case',
|
|
36
|
-
detail: 'case expression
|
|
43
|
+
detail: 'case expression, one branch per variant',
|
|
37
44
|
body: <<~SNIP.chomp,
|
|
38
45
|
case ${1:expr}
|
|
39
46
|
in ${2:pattern} then ${3:result}
|
|
40
|
-
|
|
47
|
+
in ${4:pattern} then ${0:result}
|
|
41
48
|
end
|
|
42
49
|
SNIP
|
|
43
50
|
),
|
|
@@ -95,6 +102,17 @@ module Jade
|
|
|
95
102
|
body: '(${1:args}) -> { ${0:body} }',
|
|
96
103
|
),
|
|
97
104
|
].freeze
|
|
105
|
+
|
|
106
|
+
module_function
|
|
107
|
+
|
|
108
|
+
# Every form, as plain Jade — what `jade q syntax` serves.
|
|
109
|
+
def catalog
|
|
110
|
+
ALL.map { { form: it.label, detail: it.detail, source: it.source } }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find(form)
|
|
114
|
+
catalog.find { it[:form] == form }
|
|
115
|
+
end
|
|
98
116
|
end
|
|
99
117
|
end
|
|
100
118
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'jade/frontend/type_checking/substitution'
|
|
2
|
+
|
|
3
|
+
module Jade
|
|
4
|
+
# One-line rendering of a name and its type, shared by hover and `q api`.
|
|
5
|
+
module Signature
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
LETTERS = ('a'..'z').to_a.freeze
|
|
9
|
+
|
|
10
|
+
def render(name, type, constraints)
|
|
11
|
+
naming(type, constraints)
|
|
12
|
+
.then { |sub| [sub.apply(type), constraints.map { sub.apply(it) }] }
|
|
13
|
+
.then { |(renamed, cs)| "#{name} : #{constraint_prefix(cs)}#{renamed}" }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Vars compare by id but print as their name, which nothing keeps unique.
|
|
17
|
+
def naming(type, constraints)
|
|
18
|
+
(type.unbound_vars + constraints.flat_map(&:unbound_vars))
|
|
19
|
+
.uniq(&:id)
|
|
20
|
+
.reduce([{}, []]) do |(mappings, taken), var|
|
|
21
|
+
display_name(var, taken)
|
|
22
|
+
.then { [mappings.merge(var.id => Type.var(var.id, it)), taken + [it]] }
|
|
23
|
+
end
|
|
24
|
+
.then { |(mappings, _)| Frontend::TypeChecking::Substitution[mappings] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def display_name(var, taken)
|
|
28
|
+
return var.name if var.name && !taken.include?(var.name)
|
|
29
|
+
|
|
30
|
+
(LETTERS - taken).first || "#{var.name}#{taken.size}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def constraint_prefix(constraints)
|
|
34
|
+
return '' if constraints.empty?
|
|
35
|
+
|
|
36
|
+
constraints
|
|
37
|
+
.map { short_constraint(it) }
|
|
38
|
+
.uniq
|
|
39
|
+
.join(', ')
|
|
40
|
+
.then { "#{it} => " }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def short_constraint(constraint)
|
|
44
|
+
"#{constraint.interface.split('.').last} #{constraint.type}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/jade/stdlib/calendar.rb
CHANGED
|
@@ -317,24 +317,25 @@ module Jade
|
|
|
317
317
|
end
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
# Days counted in 400-year eras from March, so the leap day lands
|
|
321
|
+
# at the end of an era and month lengths repeat; `mp` is the month
|
|
322
|
+
# in that March-based year, shifted back at the end. Int division
|
|
323
|
+
# floors, which is what makes it hold before year 1.
|
|
320
324
|
def from_rata_die(rd: Int) -> Date
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def bump_year(rd: Int, y: Int) -> Int
|
|
330
|
-
days_before_year(y + 1) < rd ? bump_year(rd, y + 1) : y
|
|
331
|
-
end
|
|
325
|
+
z = rd + 305
|
|
326
|
+
era = z / 146097
|
|
327
|
+
doe = z - era * 146097
|
|
328
|
+
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
|
|
329
|
+
doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
|
|
330
|
+
mp = (5 * doy + 2) / 153
|
|
331
|
+
d = doy - (153 * mp + 2) / 5 + 1
|
|
332
|
+
mi = mp < 10 ? mp + 3 : mp - 9
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
from_calendar_date(
|
|
335
|
+
yoe + era * 400 + (mi <= 2 ? 1 : 0),
|
|
336
|
+
month_from_int(mi),
|
|
337
|
+
d,
|
|
338
|
+
)
|
|
338
339
|
end
|
|
339
340
|
|
|
340
341
|
|
|
@@ -390,9 +391,9 @@ module Jade
|
|
|
390
391
|
end
|
|
391
392
|
|
|
392
393
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
# Decodable(Date) is registered by Decode, which reads this same
|
|
395
|
+
# wire form in Ruby. `from_iso_string` stays exposed and is what
|
|
396
|
+
# the native parser is checked against.
|
|
396
397
|
|
|
397
398
|
|
|
398
399
|
implements Encodable(Date) with
|
data/lib/jade/stdlib/clock.rb
CHANGED
|
@@ -154,7 +154,7 @@ module Jade
|
|
|
154
154
|
|
|
155
155
|
def on_date(i: Instant) -> Calendar.Date
|
|
156
156
|
Instant(ms) = i
|
|
157
|
-
Calendar.from_rata_die(
|
|
157
|
+
Calendar.from_rata_die(ms / 86400000 + 719163)
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
|
|
@@ -175,12 +175,6 @@ module Jade
|
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def floor_div(a: Int, b: Int) -> Int
|
|
179
|
-
q = a / b
|
|
180
|
-
a < 0 && mod(a, b) != 0 ? q - 1 : q
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
|
|
184
178
|
def to_iso(i: Instant) -> String
|
|
185
179
|
d = on_date(i)
|
|
186
180
|
tod = at_time(i)
|
|
@@ -317,14 +311,14 @@ module Jade
|
|
|
317
311
|
end
|
|
318
312
|
|
|
319
313
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
314
|
+
# Decodable(Instant) is registered by Decode, which reads this
|
|
315
|
+
# same wire form in Ruby. `from_iso` is still the parser of
|
|
316
|
+
# record and stays exposed; the two are differential-tested.
|
|
323
317
|
|
|
324
318
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
319
|
+
# Encodable(Instant) is registered by Encode, which writes this
|
|
320
|
+
# same string in Ruby. `to_iso` stays exposed and is what the
|
|
321
|
+
# native writer is differential-tested against.
|
|
328
322
|
|
|
329
323
|
|
|
330
324
|
implements Decodable(Duration) with
|
data/lib/jade/stdlib/decimal.rb
CHANGED
|
@@ -208,22 +208,9 @@ module Jade
|
|
|
208
208
|
end
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
case (String.to_int(m), String.to_int(e))
|
|
215
|
-
in (Just(mi), Just(ei)) then Decode.succeed(of(mi, ei))
|
|
216
|
-
else Decode.fail("invalid decimal: " ++ s)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
else Decode.fail("invalid decimal: " ++ s)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
implements Decodable(Decimal) with
|
|
225
|
-
decoder: -> { Decode.string |> Decode.and_then(from_wire) }
|
|
226
|
-
end
|
|
211
|
+
# Decodable(Decimal) is registered by Decode itself, which reads
|
|
212
|
+
# this same wire form in Ruby — a numeric column decodes about
|
|
213
|
+
# three times faster than the Jade version of the parse did.
|
|
227
214
|
|
|
228
215
|
|
|
229
216
|
implements Encodable(Decimal) with
|
data/lib/jade/stdlib/decode.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'jade/stdlib/intrinsics'
|
|
2
2
|
require 'jade/decode'
|
|
3
|
+
require 'jade/stdlib/text'
|
|
3
4
|
|
|
4
5
|
module Jade
|
|
5
6
|
module Stdlib
|
|
@@ -148,6 +149,21 @@ module Jade
|
|
|
148
149
|
Jade::Decode::Decoder[Jade::Decode::Desc::AndMap[wrapped.desc, decoder.desc]]
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
# `keys` and `decoders` are positionally paired; `ctor` takes one
|
|
153
|
+
# argument per key. Private because its type cannot be spelled — the
|
|
154
|
+
# arity of `ctor` is only known to the deriver.
|
|
155
|
+
function(
|
|
156
|
+
'record',
|
|
157
|
+
{ keys: 'List(String)', decoders: 'List(Decoder(a))', ctor: 'b' },
|
|
158
|
+
'Decoder(b)',
|
|
159
|
+
private: true,
|
|
160
|
+
) { |keys, decoders, ctor|
|
|
161
|
+
keys
|
|
162
|
+
.zip(decoders)
|
|
163
|
+
.map { |key, decoder| Jade::Decode::Desc::RecordField[key, key.to_sym, decoder.desc] }
|
|
164
|
+
.then { Jade::Decode::Decoder[Jade::Decode::Desc::Record[it, ctor]] }
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
function(
|
|
152
168
|
'required',
|
|
153
169
|
{ wrapped: 'Decoder(a -> b)', key: 'String', field_decoder: 'Decoder(a)' },
|
|
@@ -312,9 +328,7 @@ module Jade
|
|
|
312
328
|
{ a: 'Decoder(a)', b: 'Decoder(b)' },
|
|
313
329
|
'Decoder(Tuple2(a, b))',
|
|
314
330
|
) { |da, db|
|
|
315
|
-
Jade::Decode::Desc::
|
|
316
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
317
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
|
|
331
|
+
Jade::Decode::Desc::Indexed[[da.desc, db.desc], Jade::Tuple::Tuple2]
|
|
318
332
|
.then { Jade::Decode::Decoder[it] }
|
|
319
333
|
}
|
|
320
334
|
|
|
@@ -323,10 +337,7 @@ module Jade
|
|
|
323
337
|
{ a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)' },
|
|
324
338
|
'Decoder(Tuple3(a, b, c))',
|
|
325
339
|
) { |da, db, dc|
|
|
326
|
-
Jade::Decode::Desc::
|
|
327
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
328
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
|
|
329
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
|
|
340
|
+
Jade::Decode::Desc::Indexed[[da.desc, db.desc, dc.desc], Jade::Tuple::Tuple3]
|
|
330
341
|
.then { Jade::Decode::Decoder[it] }
|
|
331
342
|
}
|
|
332
343
|
|
|
@@ -335,11 +346,7 @@ module Jade
|
|
|
335
346
|
{ a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)', d: 'Decoder(d)' },
|
|
336
347
|
'Decoder(Tuple4(a, b, c, d))',
|
|
337
348
|
) { |da, db, dc, dd|
|
|
338
|
-
Jade::Decode::Desc::
|
|
339
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
340
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
|
|
341
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
|
|
342
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[3, dd.desc]] }
|
|
349
|
+
Jade::Decode::Desc::Indexed[[da.desc, db.desc, dc.desc, dd.desc], Jade::Tuple::Tuple4]
|
|
343
350
|
.then { Jade::Decode::Decoder[it] }
|
|
344
351
|
}
|
|
345
352
|
|
|
@@ -350,6 +357,22 @@ module Jade
|
|
|
350
357
|
implementation('Decodable', 'Basics.Bool', 'decoder' => 'bool')
|
|
351
358
|
implementation('Decodable', 'String.String', 'decoder' => 'string')
|
|
352
359
|
implementation('Decodable', 'Decode.Value', 'decoder' => 'value')
|
|
360
|
+
|
|
361
|
+
# Registered here, not by the modules owning the types: they import
|
|
362
|
+
# Decode, so the instance cannot live the other way round. Private
|
|
363
|
+
# for the same reason `record` is — Decode cannot name those types
|
|
364
|
+
# back, and `Decoder(a)` would be a hole if anything could reach it.
|
|
365
|
+
{
|
|
366
|
+
'decimal' => [Stdlib::Text.method(:to_decimal), 'Decimal', 'Decimal.Decimal'],
|
|
367
|
+
'instant' => [Stdlib::Text.method(:to_instant), 'Instant', 'Clock.Instant'],
|
|
368
|
+
'iso_date' => [Stdlib::Text.method(:to_date), 'Date', 'Calendar.Date'],
|
|
369
|
+
}.each do |fn_name, (parse, label, type_name)|
|
|
370
|
+
function(fn_name, {}, 'Decoder(a)', private: true) {
|
|
371
|
+
Jade::Decode::Decoder[Jade::Decode::Desc::FromString[label, parse]]
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
implementation('Decodable', type_name, 'decoder' => fn_name)
|
|
375
|
+
end
|
|
353
376
|
end
|
|
354
377
|
end
|
|
355
378
|
end
|
data/lib/jade/stdlib/encode.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'jade/stdlib/intrinsics'
|
|
3
|
+
require 'jade/stdlib/text'
|
|
3
4
|
|
|
4
5
|
module Jade
|
|
5
6
|
module Stdlib
|
|
@@ -155,6 +156,14 @@ module Jade
|
|
|
155
156
|
implementation('Encodable', 'Basics.Bool', 'encoder' => 'bool')
|
|
156
157
|
implementation('Encodable', 'String.String', 'encoder' => 'string')
|
|
157
158
|
implementation('Encodable', 'Decode.Value', 'encoder' => 'value')
|
|
159
|
+
|
|
160
|
+
# Registered here because Clock imports Encode, so the instance
|
|
161
|
+
# cannot live the other way round. `Stdlib::Text` writes the string.
|
|
162
|
+
function('instant', { i: 'a' }, 'Value', private: true) { |i|
|
|
163
|
+
Stdlib::Text.from_instant(i)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
implementation('Encodable', 'Clock.Instant', 'encoder' => 'instant')
|
|
158
167
|
end
|
|
159
168
|
end
|
|
160
169
|
end
|
|
@@ -76,10 +76,17 @@ module Jade
|
|
|
76
76
|
constraints:,
|
|
77
77
|
)
|
|
78
78
|
.with(module_name:)
|
|
79
|
-
.tap { store(it)
|
|
79
|
+
.tap { store(it) }
|
|
80
|
+
.tap { private_names << name.to_s if private }
|
|
80
81
|
.tap { Runtime.register(qualified_fn_name, &impl) }
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# Private means defined but not exposed: codegen and `implementation`
|
|
85
|
+
# resolve it, user modules cannot name it.
|
|
86
|
+
def private_names
|
|
87
|
+
@private_names ||= []
|
|
88
|
+
end
|
|
89
|
+
|
|
83
90
|
# Approach A: the inline template is the single source of truth for a
|
|
84
91
|
# function's behaviour. A hand-written block always wins. With no block
|
|
85
92
|
# and a `body:` codegen override the function is codegen/dictionary-only
|
|
@@ -219,6 +226,7 @@ module Jade
|
|
|
219
226
|
def exposes
|
|
220
227
|
@symbols
|
|
221
228
|
.reject { it.is_a?(Symbol::Implementation) }
|
|
229
|
+
.reject { it.is_a?(Symbol::StdlibFunction) && private_names.include?(it.name) }
|
|
222
230
|
.map { it.to_ref }
|
|
223
231
|
end
|
|
224
232
|
|
data/lib/jade/stdlib/result.rb
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Stdlib
|
|
3
|
+
# Text representations of stdlib types, read in Ruby. Lives here rather
|
|
4
|
+
# than in Decimal / Clock / Calendar: those import Decode, so the
|
|
5
|
+
# instance cannot be registered the other way round. The types are
|
|
6
|
+
# compiled Jade, so constants resolve on first use.
|
|
7
|
+
#
|
|
8
|
+
# Accepts exactly what the Jade parsers did, down to `String.to_int`
|
|
9
|
+
# being `Integer(s, 10)` — whitespace and underscores included.
|
|
10
|
+
module Text
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
EPOCH_RATA_DIE = 719163
|
|
14
|
+
DAY_MS = 86_400_000
|
|
15
|
+
|
|
16
|
+
def to_decimal(s)
|
|
17
|
+
coefficient, exponent = s.split('e', 2)
|
|
18
|
+
return nil unless exponent
|
|
19
|
+
|
|
20
|
+
m = int(coefficient)
|
|
21
|
+
e = int(exponent)
|
|
22
|
+
|
|
23
|
+
m && e ? Jade::Decimal::Decimal[m, e] : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# No timezone offsets: `Clock.from_iso` never took them either.
|
|
27
|
+
def to_instant(s)
|
|
28
|
+
date_part, time_part = split_dt(s)
|
|
29
|
+
return nil unless date_part
|
|
30
|
+
|
|
31
|
+
d = to_date(date_part)
|
|
32
|
+
time = split_time(time_part)
|
|
33
|
+
return nil unless d && time
|
|
34
|
+
|
|
35
|
+
hour, minute, second, sub_ms = time
|
|
36
|
+
days = Jade::Calendar::Internal.to_rata_die(d) - EPOCH_RATA_DIE
|
|
37
|
+
|
|
38
|
+
Jade::Clock::Instant[
|
|
39
|
+
days * DAY_MS +
|
|
40
|
+
hour * 3_600_000 + minute * 60_000 + second * 1_000 + sub_ms
|
|
41
|
+
]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_date(s)
|
|
45
|
+
parts = s.split('-')
|
|
46
|
+
return nil unless parts.length == 3
|
|
47
|
+
|
|
48
|
+
year, month, day = parts.map { int(it) }
|
|
49
|
+
return nil unless year && month && day
|
|
50
|
+
|
|
51
|
+
# Answers Jan outside 1..12 and never validates the day — calling
|
|
52
|
+
# it keeps that. Don't cache the Months: a second compile
|
|
53
|
+
# redefines the class.
|
|
54
|
+
Jade::Calendar::Date[
|
|
55
|
+
year,
|
|
56
|
+
Jade::Calendar::Internal.month_from_int(month),
|
|
57
|
+
day,
|
|
58
|
+
]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Seconds precision, always UTC. `rjust` matches `pad_to`, which
|
|
62
|
+
# left-pads without truncating: a negative year renders "00-5".
|
|
63
|
+
def from_instant(instant)
|
|
64
|
+
ms = instant._1
|
|
65
|
+
day_ms = ms % DAY_MS
|
|
66
|
+
d = Jade::Calendar::Internal.from_rata_die(ms / DAY_MS + EPOCH_RATA_DIE)
|
|
67
|
+
|
|
68
|
+
[
|
|
69
|
+
pad(d.year, 4),
|
|
70
|
+
'-', pad(Jade::Calendar::Internal.month_to_int(d.month), 2),
|
|
71
|
+
'-', pad(d.day, 2),
|
|
72
|
+
'T', pad(day_ms / 3_600_000, 2),
|
|
73
|
+
':', pad(day_ms % 3_600_000 / 60_000, 2),
|
|
74
|
+
':', pad(day_ms % 60_000 / 1_000, 2),
|
|
75
|
+
'Z',
|
|
76
|
+
].join
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def int(s) = Integer(s, 10, exception: false)
|
|
82
|
+
|
|
83
|
+
def pad(n, width) = n.to_s.rjust(width, '0')
|
|
84
|
+
|
|
85
|
+
def split_dt(s)
|
|
86
|
+
parts = s.split('T')
|
|
87
|
+
parts = s.split(' ') unless parts.length == 2
|
|
88
|
+
|
|
89
|
+
parts.length == 2 ? parts : nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def split_time(s)
|
|
93
|
+
parts = strip_z(s).split(':')
|
|
94
|
+
return nil unless parts.length == 3
|
|
95
|
+
|
|
96
|
+
hour = int(parts[0])
|
|
97
|
+
minute = int(parts[1])
|
|
98
|
+
second, sub_ms = seconds(parts[2])
|
|
99
|
+
|
|
100
|
+
hour && minute && second && sub_ms ? [hour, minute, second, sub_ms] : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A stray second "Z" falls through unstripped, as it did before.
|
|
104
|
+
def strip_z(s)
|
|
105
|
+
parts = s.split('Z')
|
|
106
|
+
|
|
107
|
+
parts.length == 1 || parts.length == 2 ? parts[0] : s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def seconds(s)
|
|
111
|
+
parts = s.split('.')
|
|
112
|
+
|
|
113
|
+
case parts.length
|
|
114
|
+
when 1 then [int(parts[0]), 0]
|
|
115
|
+
when 2 then [int(parts[0]), millis(int(parts[1]), parts[1].length)]
|
|
116
|
+
else [nil, nil]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def millis(n, digits)
|
|
121
|
+
return nil unless n
|
|
122
|
+
|
|
123
|
+
case digits <=> 3
|
|
124
|
+
when 0 then n
|
|
125
|
+
when -1 then n * 10.pow(3 - digits)
|
|
126
|
+
else n / 10.pow(digits - 3)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/jade/task.rb
CHANGED
|
@@ -92,9 +92,8 @@ module Jade
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
94
|
def decode(decoder, value)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
in Jade::Result::Err[e] then fail Jade::Interop::DecodeError.new(e, value)
|
|
95
|
+
Jade::Decode::Runner.run!(decoder, value) do |error|
|
|
96
|
+
fail Jade::Interop::DecodeError.new(error, value)
|
|
98
97
|
end
|
|
99
98
|
end
|
|
100
99
|
end
|