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.
- checksums.yaml +4 -4
- data/AGENTS.md +164 -0
- data/CHANGELOG.md +180 -1
- 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/boundary/cache.rb +1 -2
- data/lib/jade/codegen/boundary/specialized/list.rb +9 -4
- data/lib/jade/codegen/boundary/specialized/maybe.rb +15 -3
- data/lib/jade/codegen/boundary/specialized/record.rb +7 -3
- data/lib/jade/codegen/boundary/specialized.rb +7 -3
- data/lib/jade/codegen/emitter.rb +11 -2
- data/lib/jade/codegen/function_declaration.rb +7 -6
- 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 +12 -4
- data/lib/jade/interop/error.rb +25 -5
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- data/lib/jade/module_loader/module_name.rb +46 -0
- data/lib/jade/module_loader.rb +5 -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/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 +14 -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
|
|
|
@@ -51,12 +50,21 @@ module Jade
|
|
|
51
50
|
|
|
52
51
|
def hash(label, v)
|
|
53
52
|
case v
|
|
54
|
-
when ::Hash then v
|
|
53
|
+
when ::Hash then string_keyed!(label, v)
|
|
55
54
|
when ::Data then v.to_h.transform_keys(&:to_s)
|
|
56
55
|
else type_error!(label, v)
|
|
57
56
|
end
|
|
58
57
|
end
|
|
59
58
|
|
|
59
|
+
# Mixed keys are a real shape mismatch; the per-field errors say more
|
|
60
|
+
# than a guess about intent would.
|
|
61
|
+
def string_keyed!(label, v)
|
|
62
|
+
return v if v.empty? || v.keys.any?(::String)
|
|
63
|
+
|
|
64
|
+
v.keys.grep(::Symbol)
|
|
65
|
+
.then { it.empty? ? v : raise(Jade::Interop::SymbolKeys.new(label, it)) }
|
|
66
|
+
end
|
|
67
|
+
|
|
60
68
|
def type_error!(label, v)
|
|
61
69
|
raise Jade::Interop::DecodeError.new(
|
|
62
70
|
Jade::Decode::WrongType[label, Jade::Decode.type_name(v)],
|
data/lib/jade/interop/error.rb
CHANGED
|
@@ -40,12 +40,31 @@ module Jade
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Caught where the whole hash is in scope; downstream each field just
|
|
44
|
+
# reads nil and blames its own type.
|
|
45
|
+
class SymbolKeys < Error
|
|
46
|
+
def initialize(label, keys)
|
|
47
|
+
super(
|
|
48
|
+
"#{label} expects a Hash with string keys, got symbol keys " \
|
|
49
|
+
"(#{keys.take(4).map(&:inspect).join(', ')}). " \
|
|
50
|
+
"Values cross the boundary as wire data — " \
|
|
51
|
+
"try #{keys.first.to_s.inspect} rather than #{keys.first.inspect}."
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
class DecodeError < Error
|
|
44
|
-
attr_reader :decode_error, :value
|
|
57
|
+
attr_reader :decode_error, :value, :source
|
|
58
|
+
|
|
59
|
+
SUBJECTS = {
|
|
60
|
+
argument: 'Ruby passed a value that failed to decode',
|
|
61
|
+
port_return: 'Port returned a value that failed to decode',
|
|
62
|
+
}.freeze
|
|
45
63
|
|
|
46
|
-
def initialize(decode_error, value)
|
|
64
|
+
def initialize(decode_error, value, source: :argument)
|
|
47
65
|
@decode_error = decode_error
|
|
48
66
|
@value = value
|
|
67
|
+
@source = source
|
|
49
68
|
super(format(decode_error, value))
|
|
50
69
|
end
|
|
51
70
|
|
|
@@ -54,16 +73,17 @@ module Jade
|
|
|
54
73
|
def format(error, value)
|
|
55
74
|
path, leaf = unwind(error)
|
|
56
75
|
location = path.empty? ? "value" : path.join
|
|
76
|
+
subject = SUBJECTS.fetch(@source)
|
|
57
77
|
|
|
58
78
|
case leaf
|
|
59
79
|
in Jade::Decode::WrongType[expected, got]
|
|
60
|
-
"
|
|
80
|
+
"#{subject} at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
|
|
61
81
|
|
|
62
82
|
in Jade::Decode::MissingField[key]
|
|
63
|
-
"
|
|
83
|
+
"#{subject} at #{location}: missing field `#{key}` (#{value.inspect})"
|
|
64
84
|
|
|
65
85
|
in Jade::Decode::Custom[msg]
|
|
66
|
-
"
|
|
86
|
+
"#{subject} at #{location}: #{msg} (#{value.inspect})"
|
|
67
87
|
|
|
68
88
|
in Jade::Decode::Multiple[errors]
|
|
69
89
|
errors
|
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,46 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module ModuleLoader
|
|
3
|
+
# A module is keyed by the name its file implies — imports resolve
|
|
4
|
+
# `Shop.Cart` to `shop/cart.jd` and back. The wrapper takes the declared
|
|
5
|
+
# name while every type reference inside takes the file's, so a mismatch
|
|
6
|
+
# emits Ruby referring to a constant that was never defined.
|
|
7
|
+
module ModuleName
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def check(ast, source)
|
|
11
|
+
return Ok[ast] unless ast.is_a?(AST::Module)
|
|
12
|
+
|
|
13
|
+
source.to_module_name.then do |expected|
|
|
14
|
+
ast.name == expected ? Ok[ast] : Err[mismatch(ast, source, expected)]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def mismatch(ast, source, expected)
|
|
21
|
+
Diagnostics::List
|
|
22
|
+
.empty
|
|
23
|
+
.error(
|
|
24
|
+
"Module is declared as `#{ast.name}` but `#{source.uri}` defines `#{expected}`.",
|
|
25
|
+
source:,
|
|
26
|
+
span: name_span(ast),
|
|
27
|
+
label: "expected `#{expected}`",
|
|
28
|
+
)
|
|
29
|
+
.help("Rename it to `#{expected}`, or move the file to `#{path_for(ast.name)}`.")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `module` is skipped during parsing, so the node starts at the name.
|
|
33
|
+
def name_span(ast)
|
|
34
|
+
ast.range.begin...(ast.range.begin + ast.name.length)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path_for(module_name)
|
|
38
|
+
module_name
|
|
39
|
+
.split('.')
|
|
40
|
+
.map { Source.snake_case(it) }
|
|
41
|
+
.join('/')
|
|
42
|
+
.then { "#{it}.jd" }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/jade/module_loader.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'jade/codegen'
|
|
|
9
9
|
require 'jade/module_loader/cache'
|
|
10
10
|
require 'jade/module_loader/dependency_resolver'
|
|
11
11
|
require 'jade/module_loader/dependency_graph'
|
|
12
|
+
require 'jade/module_loader/module_name'
|
|
12
13
|
require 'jade/module_loader/normalize'
|
|
13
14
|
require 'jade/module_loader/topological_sort'
|
|
14
15
|
require 'jade/diagnostics'
|
|
@@ -132,6 +133,10 @@ module Jade
|
|
|
132
133
|
.then { raise CompilationError.new(it) }
|
|
133
134
|
end => Ok([raw_ast, comments])
|
|
134
135
|
|
|
136
|
+
ModuleName
|
|
137
|
+
.check(raw_ast, source)
|
|
138
|
+
.on_err { raise CompilationError.new(it) }
|
|
139
|
+
|
|
135
140
|
Frontend::CommentAttacher
|
|
136
141
|
.attach(raw_ast, comments, source)
|
|
137
142
|
.then { Registry.entry(source.to_module_name).with(ast: it, source:, entry:) }
|
|
@@ -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