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
data/lib/jade/api.rb
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
require 'jade'
|
|
2
|
+
require 'jade/module_loader'
|
|
3
|
+
require 'jade/signature'
|
|
4
|
+
|
|
5
|
+
module Jade
|
|
6
|
+
# The public surface of everything a module can call, read from the registry
|
|
7
|
+
# rather than the source. Stdlib modules are written both as Jade heredocs
|
|
8
|
+
# and as a Ruby DSL, and an extension gem's modules aren't in the tree at
|
|
9
|
+
# all, so no grep of the source finds them.
|
|
10
|
+
Api = Data.define(:registry, :source_root, :skipped) do
|
|
11
|
+
def self.load(project = Project.find)
|
|
12
|
+
project ? of_project(project) : stdlib
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.stdlib
|
|
16
|
+
new(registry: stdlib_registry, source_root: nil, skipped: [])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.stdlib_registry
|
|
20
|
+
@stdlib_registry ||= Stdlib.load(Registry.new)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.of_project(project)
|
|
24
|
+
sources(project)
|
|
25
|
+
.reduce([stdlib_registry, []]) { |acc, source| absorb(acc, project, source) }
|
|
26
|
+
.then do |(registry, skipped)|
|
|
27
|
+
new(registry:, source_root: project.source_root, skipped:)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A module that won't compile is named rather than dropped: an absent
|
|
32
|
+
# module reads as "that function doesn't exist".
|
|
33
|
+
def self.absorb((registry, skipped), project, (root, uri))
|
|
34
|
+
case load_module(project, root, uri)
|
|
35
|
+
in nil then [registry, skipped + [uri]]
|
|
36
|
+
in loaded then [merge(registry, loaded), skipped]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.load_module(project, root, uri)
|
|
41
|
+
ModuleLoader.load(root, uri, cache_dir: project.cache_path, tolerant: true)
|
|
42
|
+
rescue CompilationError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.merge(into, from)
|
|
47
|
+
into.with(
|
|
48
|
+
modules: into.modules.merge(from.modules),
|
|
49
|
+
implementations: into.implementations.merge(from.implementations),
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.sources(project)
|
|
54
|
+
[project.source_root, *Jade.extensions]
|
|
55
|
+
.uniq
|
|
56
|
+
.flat_map { |root| jade_files(root).map { [root, it] } }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.jade_files(root)
|
|
60
|
+
Dir
|
|
61
|
+
.glob(File.join(root, '**', '*.jd'))
|
|
62
|
+
.map { Pathname.new(it).relative_path_from(root).to_s }
|
|
63
|
+
.sort
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_class_method :of_project, :absorb, :load_module, :merge, :sources, :jade_files
|
|
67
|
+
|
|
68
|
+
ORIGIN_ORDER = { 'project' => 0, 'extension' => 1, 'stdlib' => 2 }.freeze
|
|
69
|
+
|
|
70
|
+
KIND_ORDER = {
|
|
71
|
+
'type' => 0,
|
|
72
|
+
'struct' => 0,
|
|
73
|
+
'interface' => 1,
|
|
74
|
+
'constructor' => 2,
|
|
75
|
+
'function' => 3,
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
def modules
|
|
79
|
+
registry
|
|
80
|
+
.modules
|
|
81
|
+
.map { |name, entry| { name:, origin: origin(entry) } }
|
|
82
|
+
.sort_by { [ORIGIN_ORDER.fetch(it[:origin]), it[:name]] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def describe(module_name)
|
|
86
|
+
registry
|
|
87
|
+
.modules[module_name]
|
|
88
|
+
&.then do
|
|
89
|
+
{
|
|
90
|
+
module: module_name,
|
|
91
|
+
origin: origin(it),
|
|
92
|
+
symbols: entries_for(module_name, it),
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def lookup(qualified_name)
|
|
98
|
+
*module_parts, name = qualified_name.split('.')
|
|
99
|
+
module_parts
|
|
100
|
+
.join('.')
|
|
101
|
+
.then { describe(it) }
|
|
102
|
+
&.fetch(:symbols)
|
|
103
|
+
&.find { it[:name] == name }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def search(term)
|
|
107
|
+
registry
|
|
108
|
+
.modules
|
|
109
|
+
.flat_map { |name, entry| entries_for(name, entry) }
|
|
110
|
+
.select { it[:qualified_name].downcase.include?(term.downcase) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# `Source#root` is nil for the stdlib, which never comes off disk.
|
|
116
|
+
def origin(entry)
|
|
117
|
+
case entry.source&.root
|
|
118
|
+
in nil then 'stdlib'
|
|
119
|
+
in ^(source_root) then 'project'
|
|
120
|
+
else 'extension'
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def entries_for(module_name, entry)
|
|
125
|
+
entry
|
|
126
|
+
.exposes
|
|
127
|
+
.uniq { [it.class, it.name] }
|
|
128
|
+
.filter_map { symbol_for(entry, it) }
|
|
129
|
+
.map { entry_for(module_name, it) }
|
|
130
|
+
.reject { Stdlib.private_constructor?(it[:qualified_name]) }
|
|
131
|
+
.sort_by { [KIND_ORDER.fetch(it[:kind], 9), it[:name]] }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def symbol_for(entry, ref)
|
|
135
|
+
ref.is_a?(Symbol::TypeRef) ?
|
|
136
|
+
entry.defined_types[ref.name] :
|
|
137
|
+
entry.defined_values[ref.name]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def entry_for(module_name, symbol)
|
|
141
|
+
"#{module_name}.#{symbol.name}".then do |qualified_name|
|
|
142
|
+
{
|
|
143
|
+
name: symbol.name,
|
|
144
|
+
qualified_name:,
|
|
145
|
+
kind: kind_of(symbol),
|
|
146
|
+
signature: render(symbol, qualified_name),
|
|
147
|
+
**extras(symbol),
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def kind_of(symbol)
|
|
153
|
+
case symbol
|
|
154
|
+
in Symbol::Union then 'type'
|
|
155
|
+
in Symbol::Struct then 'struct'
|
|
156
|
+
in Symbol::Interface then 'interface'
|
|
157
|
+
in Symbol::Constructor | Symbol::Variant then 'constructor'
|
|
158
|
+
else 'function'
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def render(symbol, qualified_name)
|
|
163
|
+
case symbol
|
|
164
|
+
in Symbol::Union
|
|
165
|
+
"type #{symbol.name}#{type_params(symbol.type_params)}"
|
|
166
|
+
|
|
167
|
+
in Symbol::Struct
|
|
168
|
+
"struct #{symbol.name}#{type_params(symbol.type_params)}#{fields(symbol)}"
|
|
169
|
+
|
|
170
|
+
in Symbol::Interface
|
|
171
|
+
"interface #{symbol.name}(#{symbol.type_param.name})"
|
|
172
|
+
|
|
173
|
+
else
|
|
174
|
+
declared(symbol)
|
|
175
|
+
.then { |(type, cs)| Signature.render(qualified_name, nullary(type), cs) }
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The declared annotation, never the module env's scheme: the env collapses
|
|
180
|
+
# distinct type variables on stdlib functions backing an interface, so
|
|
181
|
+
# `Result.map` reads there as `(a) -> a` — unable to change the element
|
|
182
|
+
# type, which it can.
|
|
183
|
+
def declared(symbol)
|
|
184
|
+
Type.from_symbol(symbol, registry, Frontend::TypeChecking::VarGen.new)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# `Dict.empty()` is a compile error — it's a value, not a nullary call.
|
|
188
|
+
def nullary(type)
|
|
189
|
+
case type
|
|
190
|
+
in Type::Function(args: [], return_type:) then return_type
|
|
191
|
+
else type
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def fields(symbol)
|
|
196
|
+
return '' unless symbol.record_type
|
|
197
|
+
|
|
198
|
+
declared(symbol.record_type)
|
|
199
|
+
.then { |(type, _)| " = #{type}" }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def type_params(params)
|
|
203
|
+
return '' if params.nil? || params.empty?
|
|
204
|
+
|
|
205
|
+
params
|
|
206
|
+
.map { it.respond_to?(:name) ? it.name : it.to_s }
|
|
207
|
+
.join(', ')
|
|
208
|
+
.then { "(#{it})" }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def extras(symbol)
|
|
212
|
+
case symbol
|
|
213
|
+
in Symbol::Union
|
|
214
|
+
{ implements: implements(symbol), variants: symbol.constructor_refs.map(&:name) }
|
|
215
|
+
|
|
216
|
+
in Symbol::Struct
|
|
217
|
+
{ implements: implements(symbol) }
|
|
218
|
+
|
|
219
|
+
in Symbol::Interface
|
|
220
|
+
{ implemented_by: implemented_by(symbol) }
|
|
221
|
+
|
|
222
|
+
else
|
|
223
|
+
{}
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def implements(type_symbol)
|
|
228
|
+
instances { it[1] == type_symbol.qualified_name }.map { it[0] }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def implemented_by(interface_symbol)
|
|
232
|
+
instances { it[0] == interface_symbol.qualified_name }.map { it[1] }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def instances(&)
|
|
236
|
+
registry
|
|
237
|
+
.implementations
|
|
238
|
+
.keys
|
|
239
|
+
.select(&)
|
|
240
|
+
.uniq
|
|
241
|
+
.sort
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'jade'
|
|
2
|
+
require 'jade/module_loader'
|
|
3
|
+
require 'jade/diagnostics/renderer'
|
|
4
|
+
|
|
5
|
+
module Jade
|
|
6
|
+
module CLI
|
|
7
|
+
# Type-checks without generating anything.
|
|
8
|
+
module Check
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(argv)
|
|
12
|
+
usage if argv.any? { it == '-h' || it == '--help' }
|
|
13
|
+
|
|
14
|
+
Project
|
|
15
|
+
.find!
|
|
16
|
+
.then { report(diagnose(it, argv), it) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def diagnose(project, files)
|
|
20
|
+
entries(project, files)
|
|
21
|
+
.flat_map { compile(project, it) }
|
|
22
|
+
.uniq { |d| [d.primary&.source&.uri, d.message, d.primary&.span] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def entries(project, files)
|
|
26
|
+
return sources(project) if files.empty?
|
|
27
|
+
|
|
28
|
+
files.map { project.source_relative(it) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sources(project)
|
|
32
|
+
Dir
|
|
33
|
+
.glob(File.join(project.source_root, '**', '*.jd'))
|
|
34
|
+
.map { Pathname.new(it).relative_path_from(project.source_root).to_s }
|
|
35
|
+
.sort
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Tolerant, so a module yields every diagnostic rather than the first.
|
|
39
|
+
def compile(project, entry)
|
|
40
|
+
ModuleLoader
|
|
41
|
+
.load(project.source_root, entry, cache_dir: project.cache_path, tolerant: true)
|
|
42
|
+
.then { collect(it) }
|
|
43
|
+
rescue CompilationError => e
|
|
44
|
+
e.diagnostics.items
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collect(registry)
|
|
48
|
+
registry
|
|
49
|
+
.modules
|
|
50
|
+
.each_value
|
|
51
|
+
.reject { Stdlib.is_stdlib?(it) }
|
|
52
|
+
.reject { it.source.nil? }
|
|
53
|
+
.flat_map { it.diagnostics.items }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def report(items, project)
|
|
57
|
+
return if items.empty?
|
|
58
|
+
|
|
59
|
+
Diagnostics::Renderer
|
|
60
|
+
.new(colors: $stdout.tty?)
|
|
61
|
+
.render_all(Diagnostics::List.new(items:))
|
|
62
|
+
.then { warn(it) }
|
|
63
|
+
|
|
64
|
+
warn(summary(items, project))
|
|
65
|
+
exit 1 if items.any?(&:error?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def summary(items, project)
|
|
69
|
+
items
|
|
70
|
+
.partition(&:error?)
|
|
71
|
+
.then { |(errors, rest)| [count(errors, 'error'), count(rest, 'warning')] }
|
|
72
|
+
.compact
|
|
73
|
+
.join(', ')
|
|
74
|
+
.then { "\n#{it} in #{project.source_roots.first}" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def count(items, noun)
|
|
78
|
+
return nil if items.empty?
|
|
79
|
+
|
|
80
|
+
"#{items.size} #{noun}#{'s' unless items.size == 1}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def usage
|
|
84
|
+
warn <<~USAGE
|
|
85
|
+
Usage: jade check [FILE...]
|
|
86
|
+
|
|
87
|
+
Type-checks FILEs and everything they import, writing diagnostics
|
|
88
|
+
to stderr. Checks every .jd file under the source root when given
|
|
89
|
+
no arguments. Exits 1 if there were errors, 0 otherwise.
|
|
90
|
+
|
|
91
|
+
FILE is relative to the project root or the source root.
|
|
92
|
+
USAGE
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/jade/cli/q.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'jade'
|
|
2
|
+
require 'jade/api'
|
|
2
3
|
require 'jade/lsp'
|
|
3
4
|
require 'json'
|
|
4
5
|
|
|
@@ -15,11 +16,20 @@ module Jade
|
|
|
15
16
|
symbols FILE Document symbols (outline) for FILE.
|
|
16
17
|
defn FILE:LINE:COL Goto-definition location.
|
|
17
18
|
refs FILE:LINE:COL Find all references (incl. declaration).
|
|
19
|
+
api [MODULE|NAME] Stdlib signatures. No argument lists the
|
|
20
|
+
modules; `Maybe` describes one; `List.map`
|
|
21
|
+
picks a single symbol.
|
|
22
|
+
find TERM Symbols whose name matches TERM.
|
|
23
|
+
syntax [FORM] How a form is written (`lambda`, `case`,
|
|
24
|
+
`interface`, …). No argument lists them all.
|
|
18
25
|
|
|
19
26
|
FILE is relative to the project root (cwd).
|
|
20
27
|
LINE and COL are 0-indexed (LSP convention).
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
`api` and `find` cover the stdlib plus, inside a project, its own and
|
|
30
|
+
its extensions' modules. `syntax` needs nothing at all. Everything
|
|
31
|
+
else compiles the project, caching at .jade/cache so repeat queries
|
|
32
|
+
are fast.
|
|
23
33
|
TXT
|
|
24
34
|
|
|
25
35
|
module_function
|
|
@@ -30,12 +40,48 @@ module Jade
|
|
|
30
40
|
when 'symbols' then dump(symbols(argv[1]))
|
|
31
41
|
when 'defn' then dump(defn(argv[1]))
|
|
32
42
|
when 'refs' then dump(refs(argv[1]))
|
|
43
|
+
when 'api' then dump(api(argv[1]))
|
|
44
|
+
when 'find' then dump(find(argv[1]))
|
|
45
|
+
when 'syntax' then dump(syntax(argv[1]))
|
|
33
46
|
else
|
|
34
47
|
warn USAGE
|
|
35
48
|
exit 1
|
|
36
49
|
end
|
|
37
50
|
end
|
|
38
51
|
|
|
52
|
+
def api(name)
|
|
53
|
+
Api.load.then do |api|
|
|
54
|
+
next listing(api) unless name
|
|
55
|
+
|
|
56
|
+
(api.describe(name) || api.lookup(name))
|
|
57
|
+
.then { it || fail("no module or symbol named #{name}") }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def find(term)
|
|
62
|
+
fail 'TERM required' unless term
|
|
63
|
+
|
|
64
|
+
Api
|
|
65
|
+
.load
|
|
66
|
+
.then { { matches: it.search(term), **skipped(it) } }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def syntax(form)
|
|
70
|
+
return { forms: LSP::Snippets.catalog } unless form
|
|
71
|
+
|
|
72
|
+
LSP::Snippets
|
|
73
|
+
.find(form)
|
|
74
|
+
.then { it || fail("no syntax form named #{form}") }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def listing(api)
|
|
78
|
+
{ modules: api.modules, **skipped(api) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def skipped(api)
|
|
82
|
+
api.skipped.empty? ? {} : { skipped: api.skipped }
|
|
83
|
+
end
|
|
84
|
+
|
|
39
85
|
def hover(arg)
|
|
40
86
|
file, line, col = parse_position(arg)
|
|
41
87
|
Context.new(file).then do |ctx|
|
data/lib/jade/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@ require 'jade'
|
|
|
3
3
|
module Jade
|
|
4
4
|
module CLI
|
|
5
5
|
SUBCOMMANDS = {
|
|
6
|
+
'check' => 'Check',
|
|
6
7
|
'fmt' => 'Fmt',
|
|
7
8
|
'lsp' => 'Lsp',
|
|
8
9
|
'q' => 'Q',
|
|
@@ -26,7 +27,7 @@ module Jade
|
|
|
26
27
|
usage($stderr)
|
|
27
28
|
exit 1
|
|
28
29
|
end
|
|
29
|
-
rescue Project::NotFound => e
|
|
30
|
+
rescue Project::NotFound, RuntimeError => e
|
|
30
31
|
warn "jade: #{e.message}"
|
|
31
32
|
exit 1
|
|
32
33
|
end
|
|
@@ -35,9 +36,10 @@ module Jade
|
|
|
35
36
|
io.puts <<~TXT
|
|
36
37
|
Usage: jade COMMAND [ARGS]
|
|
37
38
|
|
|
39
|
+
check Type-check the project (or the given files).
|
|
38
40
|
fmt Format .jd source (stdin or file).
|
|
39
41
|
lsp Run the language server (stdio JSON-RPC).
|
|
40
|
-
q Headless query interface (hover/symbols/defn/refs).
|
|
42
|
+
q Headless query interface (hover/symbols/defn/refs/api).
|
|
41
43
|
|
|
42
44
|
Run `jade COMMAND --help` for command-specific options.
|
|
43
45
|
TXT
|
|
@@ -36,8 +36,7 @@ module Jade
|
|
|
36
36
|
Boundary::Specialized.decode_expr(t, '_', registry)
|
|
37
37
|
},
|
|
38
38
|
encoders: cache_map(per_fn.flat_map { it[:encoders] }, 'ENC') { |t|
|
|
39
|
-
Boundary::Specialized.
|
|
40
|
-
Boundary::Specialized.encode_expr(t, '_', registry)
|
|
39
|
+
Boundary::Specialized.encode_expr(t, '_', registry)
|
|
41
40
|
},
|
|
42
41
|
}
|
|
43
42
|
end
|
|
@@ -19,11 +19,16 @@ module Jade
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def encode(type, value_expr, registry)
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
inner_of(type)
|
|
23
|
+
&.then { Specialized.encode_expr(it, '_1', registry) }
|
|
24
|
+
&.then { map_expr(it, value_expr) }
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
def map_expr(elem, value_expr)
|
|
28
|
+
case elem
|
|
29
|
+
in ::String then "#{value_expr}.map { #{elem} }"
|
|
30
|
+
in :identity then nil
|
|
31
|
+
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def identity_encoder?(type)
|
|
@@ -17,10 +17,22 @@ module Jade
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def encode(type, value_expr, registry)
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
inner_of(type)
|
|
21
|
+
&.then { element_encoder(it, registry) }
|
|
22
|
+
&.then { unwrap_expr(it, value_expr) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unwrap_expr(elem, value_expr)
|
|
26
|
+
"#{value_expr}.then { it.is_a?(::Jade::Maybe::Just) ? #{elem} : nil }"
|
|
27
|
+
end
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
# Never identity even when the element is: the Just still unwraps.
|
|
30
|
+
def element_encoder(inner, registry)
|
|
31
|
+
case Specialized.encode_expr(inner, 'it._1', registry)
|
|
32
|
+
in :identity then 'it._1'
|
|
33
|
+
in ::String => expr then expr
|
|
34
|
+
in nil then nil
|
|
35
|
+
end
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def specializable?(type, registry, seen)
|
|
@@ -138,7 +138,7 @@ module Jade
|
|
|
138
138
|
|
|
139
139
|
def encode_helper(struct, registry)
|
|
140
140
|
struct.record_type.fields
|
|
141
|
-
.map { |k, t| "#{k.inspect} => #{
|
|
141
|
+
.map { |k, t| "#{k.inspect} => #{field_encode(k, t, "p.#{k}", registry)}" }
|
|
142
142
|
.then { "{ #{it.join(', ')} }" }
|
|
143
143
|
.then { Pretty.block("def self.#{encode_helper_name(struct)}(p)", it) }
|
|
144
144
|
end
|
|
@@ -148,8 +148,12 @@ module Jade
|
|
|
148
148
|
fail("non-specializable field type for #{key}: #{type}")
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
def
|
|
152
|
-
Specialized.encode_expr(type, accessor, registry)
|
|
151
|
+
def field_encode(key, type, accessor, registry)
|
|
152
|
+
case Specialized.encode_expr(type, accessor, registry)
|
|
153
|
+
in :identity then accessor
|
|
154
|
+
in ::String => expr then expr
|
|
155
|
+
in nil then fail("non-encodable field type for #{key}: #{type}")
|
|
156
|
+
end
|
|
153
157
|
end
|
|
154
158
|
|
|
155
159
|
def snake(name)
|
|
@@ -28,10 +28,14 @@ module Jade
|
|
|
28
28
|
Record.decode(type, input, registry)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# How to encode `value_expr` to the wire form:
|
|
32
|
+
#
|
|
33
|
+
# String inline expression
|
|
34
|
+
# :identity the value is already wire-shaped; emit it unchanged
|
|
35
|
+
# nil not specializable; fall back to the cached encoder
|
|
34
36
|
def encode_expr(type, value_expr, registry)
|
|
37
|
+
return :identity if identity_encoder?(type)
|
|
38
|
+
|
|
35
39
|
Record.encode(type, value_expr, registry) ||
|
|
36
40
|
List.encode(type, value_expr, registry) ||
|
|
37
41
|
Maybe.encode(type, value_expr, registry)
|
data/lib/jade/codegen/emitter.rb
CHANGED
|
@@ -50,8 +50,11 @@ module Jade
|
|
|
50
50
|
in [:struct_constructor, qualified_name, arity]
|
|
51
51
|
"Jade::Runtime.curry(::#{to_qualified(qualified_name)}.method(:[]), #{arity})"
|
|
52
52
|
|
|
53
|
-
in [:
|
|
54
|
-
"
|
|
53
|
+
in [:struct_class, qualified_name]
|
|
54
|
+
"::#{to_qualified(qualified_name)}"
|
|
55
|
+
|
|
56
|
+
in [:anon_record_class, keys]
|
|
57
|
+
record_class(keys)
|
|
55
58
|
|
|
56
59
|
in [:list, exprs]
|
|
57
60
|
exprs
|
|
@@ -59,6 +62,12 @@ module Jade
|
|
|
59
62
|
.join(', ')
|
|
60
63
|
.then { "[#{it}]" }
|
|
61
64
|
|
|
65
|
+
in [:hash, pairs]
|
|
66
|
+
pairs
|
|
67
|
+
.map { |key, value| "#{key.inspect} => #{emit(value)}" }
|
|
68
|
+
.join(', ')
|
|
69
|
+
.then { it.empty? ? '{}' : "{ #{it} }" }
|
|
70
|
+
|
|
62
71
|
in [:access, expr, key]
|
|
63
72
|
"#{emit(expr)}.#{key}"
|
|
64
73
|
|
|
@@ -80,14 +80,15 @@ module Jade
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def encode_return(return_type, call_expr, registry)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
case Codegen::Boundary::Specialized.encode_expr(return_type, call_expr, registry)
|
|
84
|
+
in :identity then call_expr
|
|
85
|
+
|
|
86
|
+
in ::String => expr then expr
|
|
87
|
+
|
|
88
|
+
in nil
|
|
88
89
|
Codegen::Boundary::Cache
|
|
89
90
|
.encoder_for(return_type, registry)
|
|
90
|
-
.then {
|
|
91
|
+
.then { "#{it}.call(#{call_expr})" }
|
|
91
92
|
end
|
|
92
93
|
end
|
|
93
94
|
|
data/lib/jade/codegen/helpers.rb
CHANGED
|
@@ -22,6 +22,12 @@ module Jade
|
|
|
22
22
|
"Data.define(#{fields.map { ":#{it}" }.join(', ')})"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Record literals resolve through the same registry, so two records
|
|
26
|
+
# with the same keys compare equal however they were built.
|
|
27
|
+
def record_class(keys)
|
|
28
|
+
"Jade::Runtime.record(#{keys.map { ":#{it}" }.join(', ')})"
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
def generate_node(node, registry)
|
|
26
32
|
Codegen.generate(node, registry)
|
|
27
33
|
end
|
data/lib/jade/codegen/inlines.rb
CHANGED
|
@@ -183,6 +183,9 @@ module Jade
|
|
|
183
183
|
Decode.and_map
|
|
184
184
|
Decode.and_then
|
|
185
185
|
Decode.bool
|
|
186
|
+
Decode.decimal
|
|
187
|
+
Decode.instant
|
|
188
|
+
Decode.iso_date
|
|
186
189
|
Decode.decode
|
|
187
190
|
Decode.decode_string
|
|
188
191
|
Decode.dict
|
|
@@ -198,6 +201,7 @@ module Jade
|
|
|
198
201
|
Decode.one_of
|
|
199
202
|
Decode.optional
|
|
200
203
|
Decode.optional_field
|
|
204
|
+
Decode.record
|
|
201
205
|
Decode.required
|
|
202
206
|
Decode.sequence
|
|
203
207
|
Decode.set
|
|
@@ -212,6 +216,7 @@ module Jade
|
|
|
212
216
|
Decode.variant
|
|
213
217
|
Encode.bool
|
|
214
218
|
Encode.dict
|
|
219
|
+
Encode.instant
|
|
215
220
|
Encode.encode_to_string
|
|
216
221
|
Encode.field
|
|
217
222
|
Encode.float
|