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
|
@@ -2,15 +2,18 @@ require 'jade/type'
|
|
|
2
2
|
require 'jade/frontend/type_checking/constraints'
|
|
3
3
|
require 'jade/frontend/type_checking/var_gen'
|
|
4
4
|
require 'jade/frontend/type_checking/error/port_not_decodable'
|
|
5
|
+
require 'jade/frontend/type_checking/error/port_not_encodable'
|
|
5
6
|
|
|
6
7
|
module Jade
|
|
7
8
|
module Frontend
|
|
8
9
|
module TypeChecking
|
|
9
|
-
# Resolves the
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
10
|
+
# Resolves the instances each port needs to convert at the boundary:
|
|
11
|
+
# Decode.Decodable for its ok/err arms, Encode.Encodable for its
|
|
12
|
+
# arguments. Runs at the end of type-checking, when
|
|
13
|
+
# registry.implementations is fully populated. The resolved
|
|
14
|
+
# Symbol::Implementation (or the :pass sentinel for Decode.Value / Never)
|
|
15
|
+
# is stamped onto each InteropFunction's `decoders` / `encoders` fields
|
|
16
|
+
# so codegen can emit straight away.
|
|
14
17
|
module PortResolution
|
|
15
18
|
extend self
|
|
16
19
|
|
|
@@ -46,28 +49,61 @@ module Jade
|
|
|
46
49
|
def resolve_port(interop_fn, entry, registry)
|
|
47
50
|
interop_fn.return_type => Symbol::TypeApplication(args: [ok_sym, err_sym])
|
|
48
51
|
|
|
49
|
-
# Single Type.from_symbol
|
|
50
|
-
#
|
|
51
|
-
# those ids to build the call-site synthetic dict_env.
|
|
52
|
+
# Single Type.from_symbol over the whole port so a var appearing in
|
|
53
|
+
# more than one position gets the same Type::Var id. PortCodec relies
|
|
54
|
+
# on those ids to build the call-site synthetic dict_env.
|
|
52
55
|
Type
|
|
53
|
-
.from_symbol(interop_fn
|
|
54
|
-
.first => Type::Application(args: [ok_type, err_type])
|
|
56
|
+
.from_symbol(port_symbol(interop_fn), registry, VarGen.new)
|
|
57
|
+
.first => Type::Function(args: param_types, return_type: Type::Application(args: [ok_type, err_type]))
|
|
55
58
|
|
|
56
59
|
ok, ok_errors = resolve_arm(ok_sym, ok_type, interop_fn, :ok, entry, registry)
|
|
57
60
|
err, err_errors = resolve_arm(err_sym, err_type, interop_fn, :err, entry, registry)
|
|
61
|
+
encoders, param_errors = resolve_params(interop_fn, param_types, entry, registry)
|
|
58
62
|
|
|
59
63
|
[
|
|
60
|
-
interop_fn.with(decoders: { ok:, err: }),
|
|
61
|
-
ok_errors + err_errors,
|
|
64
|
+
interop_fn.with(decoders: { ok:, err: }, encoders:),
|
|
65
|
+
ok_errors + err_errors + param_errors,
|
|
62
66
|
]
|
|
63
67
|
end
|
|
64
68
|
|
|
69
|
+
# Without the constraints the shared VarGen would still hand every
|
|
70
|
+
# position its own var; `from_symbol` only ties them together through
|
|
71
|
+
# the map it threads across params and return.
|
|
72
|
+
def port_symbol(interop_fn)
|
|
73
|
+
Symbol.function_type(interop_fn.params, interop_fn.return_type)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve_params(interop_fn, param_types, entry, registry)
|
|
77
|
+
interop_fn
|
|
78
|
+
.params
|
|
79
|
+
.zip(param_types)
|
|
80
|
+
.each_with_index
|
|
81
|
+
.map { |(sym, type), index| resolve_param(sym, type, interop_fn, index, entry, registry) }
|
|
82
|
+
.then { |results| [results.map(&:first), results.flat_map(&:last)] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_param(type_sym, type, interop_fn, index, entry, registry)
|
|
86
|
+
return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
|
|
87
|
+
|
|
88
|
+
case type
|
|
89
|
+
in Type::Var(name:)
|
|
90
|
+
constraint_index_for(interop_fn, 'Encode.Encodable', name)
|
|
91
|
+
.then { [Symbol::InteropFunction::Dict.new(constraint_index: it), []] }
|
|
92
|
+
|
|
93
|
+
else
|
|
94
|
+
Type
|
|
95
|
+
.constraint('Encode.Encodable', type, nil)
|
|
96
|
+
.then { Constraints.resolve(it, registry, entry.name) }
|
|
97
|
+
.then { encoder_result(it, interop_fn, index, entry, type, span_of(type_sym, interop_fn)) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
65
101
|
def resolve_arm(type_sym, type, interop_fn, arm, entry, registry)
|
|
66
102
|
return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
|
|
67
103
|
|
|
68
104
|
case type
|
|
69
105
|
in Type::Var(name:)
|
|
70
|
-
constraint_index_for(interop_fn, name)
|
|
106
|
+
constraint_index_for(interop_fn, 'Decode.Decodable', name)
|
|
71
107
|
.then { [Symbol::InteropFunction::Dict.new(constraint_index: it), []] }
|
|
72
108
|
|
|
73
109
|
else
|
|
@@ -81,11 +117,11 @@ module Jade
|
|
|
81
117
|
end
|
|
82
118
|
end
|
|
83
119
|
|
|
84
|
-
def constraint_index_for(interop_fn, var_name)
|
|
120
|
+
def constraint_index_for(interop_fn, interface, var_name)
|
|
85
121
|
interop_fn
|
|
86
122
|
.constraints
|
|
87
|
-
.index { |
|
|
88
|
-
.tap { fail "no
|
|
123
|
+
.index { |iface, name| iface == interface && name == var_name }
|
|
124
|
+
.tap { fail "no #{interface} constraint for #{var_name.inspect}" if it.nil? }
|
|
89
125
|
end
|
|
90
126
|
|
|
91
127
|
def decoder_result(constraint_result, interop_fn, arm, entry, type, span)
|
|
@@ -100,6 +136,18 @@ module Jade
|
|
|
100
136
|
end
|
|
101
137
|
end
|
|
102
138
|
|
|
139
|
+
def encoder_result(constraint_result, interop_fn, index, entry, type, span)
|
|
140
|
+
case constraint_result
|
|
141
|
+
in Ok[impl]
|
|
142
|
+
[impl, []]
|
|
143
|
+
|
|
144
|
+
in Err
|
|
145
|
+
Error::PortNotEncodable
|
|
146
|
+
.new(entry, span, port_name: interop_fn.name, position: index + 1, type:)
|
|
147
|
+
.then { [nil, [it]] }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
103
151
|
def span_of(type_sym, interop_fn)
|
|
104
152
|
case type_sym
|
|
105
153
|
in Symbol::TypeApplication(span:) then span
|
|
@@ -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/interop/runtime.rb
CHANGED
|
@@ -3,7 +3,7 @@ require 'jade/interop/error'
|
|
|
3
3
|
module Jade
|
|
4
4
|
module Interop
|
|
5
5
|
module Runtime
|
|
6
|
-
def task_call(interop_module_name, function_name, ok_decoder, err_decoder)
|
|
6
|
+
def task_call(interop_module_name, function_name, ok_decoder, err_decoder, arg_encoders)
|
|
7
7
|
->(*args) do
|
|
8
8
|
interop_module_name
|
|
9
9
|
.send(function_name)
|
|
@@ -12,13 +12,21 @@ module Jade
|
|
|
12
12
|
fail(Interop::PortNotRegistered.new(interop_module_name, function_name))
|
|
13
13
|
|
|
14
14
|
Jade::Task::Decoded.new(
|
|
15
|
-
Jade::Task::Dispatch.new(port, args),
|
|
15
|
+
Jade::Task::Dispatch.new(port, encode_args(args, arg_encoders)),
|
|
16
16
|
ok_decoder,
|
|
17
17
|
err_decoder,
|
|
18
18
|
)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Ports are the boundary in the other direction: what Ruby gets handed is
|
|
24
|
+
# encoded the same way a return value is decoded on the way back.
|
|
25
|
+
def encode_args(args, encoders)
|
|
26
|
+
args
|
|
27
|
+
.each_with_index
|
|
28
|
+
.map { |arg, i| encoders.fetch(i).call(arg) }
|
|
29
|
+
end
|
|
22
30
|
end
|
|
23
31
|
end
|
|
24
32
|
end
|
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
|
data/lib/jade/parsing/error.rb
CHANGED
|
@@ -58,11 +58,20 @@ module Jade
|
|
|
58
58
|
super
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Blocks whose entries are comma-separated, so a newline between two of
|
|
62
|
+
# them reads as the end of the block.
|
|
63
|
+
COMMA_SEPARATED_BLOCKS = [
|
|
64
|
+
'interop import declaration',
|
|
65
|
+
'interface declaration',
|
|
66
|
+
'implementation',
|
|
67
|
+
].freeze
|
|
68
|
+
|
|
61
69
|
def hint
|
|
62
70
|
return leading_pipe_hint if leading_pipe_in_type_decl?
|
|
63
71
|
return reserved_keyword_hint if reserved_keyword_as_name?
|
|
64
72
|
return colon_not_eq_hint if eq_where_colon_expected?
|
|
65
73
|
return record_eq_hint if eq_where_record_pipe_expected?
|
|
74
|
+
return missing_comma_hint if entry_after_unterminated_entry?
|
|
66
75
|
nil
|
|
67
76
|
end
|
|
68
77
|
|
|
@@ -115,6 +124,16 @@ module Jade
|
|
|
115
124
|
def record_eq_hint
|
|
116
125
|
"(record fields use `:`, not `=` — write `{ name: value }`)"
|
|
117
126
|
end
|
|
127
|
+
|
|
128
|
+
def entry_after_unterminated_entry?
|
|
129
|
+
expected == :end &&
|
|
130
|
+
[:identifier, :lparen].include?(actual.type) &&
|
|
131
|
+
(@context & COMMA_SEPARATED_BLOCKS).any?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def missing_comma_hint
|
|
135
|
+
"(entries here are separated by `,` — add one to the line above)"
|
|
136
|
+
end
|
|
118
137
|
end
|
|
119
138
|
|
|
120
139
|
# Specific case-branch shape: `in <pat> <body>` on the same source
|
data/lib/jade/runtime.rb
CHANGED
|
@@ -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
|
|
@@ -10,6 +11,7 @@ module Jade
|
|
|
10
11
|
import Result
|
|
11
12
|
import List
|
|
12
13
|
import Dict
|
|
14
|
+
import Set
|
|
13
15
|
|
|
14
16
|
union :DecodeError
|
|
15
17
|
variant :MissingField, of: :DecodeError, args: ['String']
|
|
@@ -46,6 +48,13 @@ module Jade
|
|
|
46
48
|
Jade::Decode::Decoder[Jade::Decode::Desc::Bool[]]
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
# Keeps whatever came in. `Value` is the un-decoded value, so this is
|
|
52
|
+
# what makes it usable nested — `List(Value)`, a field of a struct —
|
|
53
|
+
# and not only as a whole arm.
|
|
54
|
+
function('value', {}, 'Decoder(Value)') {
|
|
55
|
+
Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
# Structural
|
|
50
59
|
|
|
51
60
|
function(
|
|
@@ -88,6 +97,19 @@ module Jade
|
|
|
88
97
|
Jade::Decode::Decoder[Jade::Decode::Desc::Lst[decoder.desc]]
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
# Reads the same shape a list does, dropping duplicates. Like
|
|
101
|
+
# `Decode.dict`, it builds the keyed structure directly, so it asks
|
|
102
|
+
# nothing of the element beyond being decodable.
|
|
103
|
+
function(
|
|
104
|
+
'set',
|
|
105
|
+
{ decoder: 'Decoder(a)' },
|
|
106
|
+
'Decoder(Set(a))',
|
|
107
|
+
) { |decoder|
|
|
108
|
+
Jade::Decode::Desc::Lst[decoder.desc]
|
|
109
|
+
.then { Jade::Decode::Desc::Map[->(vs) { Jade::Set::Set[vs.to_h { [it, true] }] }, it] }
|
|
110
|
+
.then { Jade::Decode::Decoder[it] }
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
# Decodes either a Hash (natural Ruby/JSON object form) or a list
|
|
92
114
|
# of `[k, v]` pairs (what Encode.dict emits — also the only shape
|
|
93
115
|
# that round-trips non-String key types).
|
|
@@ -127,6 +149,21 @@ module Jade
|
|
|
127
149
|
Jade::Decode::Decoder[Jade::Decode::Desc::AndMap[wrapped.desc, decoder.desc]]
|
|
128
150
|
}
|
|
129
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
|
+
|
|
130
167
|
function(
|
|
131
168
|
'required',
|
|
132
169
|
{ wrapped: 'Decoder(a -> b)', key: 'String', field_decoder: 'Decoder(a)' },
|
|
@@ -291,9 +328,7 @@ module Jade
|
|
|
291
328
|
{ a: 'Decoder(a)', b: 'Decoder(b)' },
|
|
292
329
|
'Decoder(Tuple2(a, b))',
|
|
293
330
|
) { |da, db|
|
|
294
|
-
Jade::Decode::Desc::
|
|
295
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
296
|
-
.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]
|
|
297
332
|
.then { Jade::Decode::Decoder[it] }
|
|
298
333
|
}
|
|
299
334
|
|
|
@@ -302,10 +337,7 @@ module Jade
|
|
|
302
337
|
{ a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)' },
|
|
303
338
|
'Decoder(Tuple3(a, b, c))',
|
|
304
339
|
) { |da, db, dc|
|
|
305
|
-
Jade::Decode::Desc::
|
|
306
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
307
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
|
|
308
|
-
.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]
|
|
309
341
|
.then { Jade::Decode::Decoder[it] }
|
|
310
342
|
}
|
|
311
343
|
|
|
@@ -314,11 +346,7 @@ module Jade
|
|
|
314
346
|
{ a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)', d: 'Decoder(d)' },
|
|
315
347
|
'Decoder(Tuple4(a, b, c, d))',
|
|
316
348
|
) { |da, db, dc, dd|
|
|
317
|
-
Jade::Decode::Desc::
|
|
318
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
|
|
319
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
|
|
320
|
-
.then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
|
|
321
|
-
.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]
|
|
322
350
|
.then { Jade::Decode::Decoder[it] }
|
|
323
351
|
}
|
|
324
352
|
|
|
@@ -328,6 +356,23 @@ module Jade
|
|
|
328
356
|
implementation('Decodable', 'Basics.Float', 'decoder' => 'float')
|
|
329
357
|
implementation('Decodable', 'Basics.Bool', 'decoder' => 'bool')
|
|
330
358
|
implementation('Decodable', 'String.String', 'decoder' => 'string')
|
|
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
|
|
331
376
|
end
|
|
332
377
|
end
|
|
333
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
|
|
@@ -10,6 +11,7 @@ module Jade
|
|
|
10
11
|
import List
|
|
11
12
|
import Tuple
|
|
12
13
|
import Dict
|
|
14
|
+
import Set
|
|
13
15
|
|
|
14
16
|
interface(
|
|
15
17
|
'Encodable',
|
|
@@ -25,6 +27,11 @@ module Jade
|
|
|
25
27
|
function('bool', { b: 'Bool' }, 'Value') { it }
|
|
26
28
|
function('null', {}, 'Value') { nil }
|
|
27
29
|
|
|
30
|
+
# A `Value` is already encoded, so its encoder is identity. Ports and
|
|
31
|
+
# boundaries take it as the opt-out; the instance is what carries that
|
|
32
|
+
# through `List(Value)` and friends.
|
|
33
|
+
function('value', { v: 'Value' }, 'Value') { it }
|
|
34
|
+
|
|
28
35
|
# Structural
|
|
29
36
|
|
|
30
37
|
function(
|
|
@@ -57,6 +64,16 @@ module Jade
|
|
|
57
64
|
dict.hash.map { |k, v| [k_enc.call(k), v_enc.call(v)] }
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
# A set is its elements, in insertion order, with no duplicates —
|
|
68
|
+
# the same shape a list encodes to.
|
|
69
|
+
function(
|
|
70
|
+
'set',
|
|
71
|
+
{ encoder: 'a -> Value', set: 'Set(a)' },
|
|
72
|
+
'Value',
|
|
73
|
+
) { |encoder, set|
|
|
74
|
+
set.hash.keys.map { encoder.call(it) }
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
function(
|
|
61
78
|
'object',
|
|
62
79
|
{ pairs: 'List(Tuple2(String, Value))' },
|
|
@@ -138,6 +155,15 @@ module Jade
|
|
|
138
155
|
implementation('Encodable', 'Basics.Float', 'encoder' => 'float')
|
|
139
156
|
implementation('Encodable', 'Basics.Bool', 'encoder' => 'bool')
|
|
140
157
|
implementation('Encodable', 'String.String', 'encoder' => 'string')
|
|
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')
|
|
141
167
|
end
|
|
142
168
|
end
|
|
143
169
|
end
|