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
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
|
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
|
|
|
@@ -180,7 +180,7 @@ module Jade
|
|
|
180
180
|
in Symbol::InteropFunction
|
|
181
181
|
registry
|
|
182
182
|
.lookup(callee.symbol.to_ref)
|
|
183
|
-
.then {
|
|
183
|
+
.then { PortCodec.task_call(it, registry, dictionaries) }
|
|
184
184
|
|
|
185
185
|
in Symbol::StdlibFunction => symbol if symbol.constraints.any?
|
|
186
186
|
dictionaries
|
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
|
@@ -16,6 +16,15 @@ module Jade
|
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
18
|
INLINES = {
|
|
19
|
+
'Debug.log' => ->(l, v) { "Jade::Debug.log(#{l}, #{v})" },
|
|
20
|
+
'Show.int_show' => ->(n) { "#{n}.to_s" },
|
|
21
|
+
'Show.float_show' => ->(f) { "#{f}.to_s" },
|
|
22
|
+
'Show.bool_show' => ->(b) { "#{b}.to_s" },
|
|
23
|
+
'Show.str_show' => ->(s) { "#{s}.inspect" },
|
|
24
|
+
'Show.char_show' => ->(c) { "#{c}.inspect" },
|
|
25
|
+
# Never is uninhabited, so reaching this is a compiler bug, not a
|
|
26
|
+
# rendering problem — say so rather than print a plausible string.
|
|
27
|
+
'Show.never_show' => ->(_) { 'fail("Show.never_show: Never has no values")' },
|
|
19
28
|
'Basics.identity' => ->(a) { a },
|
|
20
29
|
'Basics.always' => ->(x) { "->(_) { #{x} }" },
|
|
21
30
|
'Basics.int_add' => ->(a, b) { "(#{a} + #{b})" },
|
|
@@ -174,6 +183,9 @@ module Jade
|
|
|
174
183
|
Decode.and_map
|
|
175
184
|
Decode.and_then
|
|
176
185
|
Decode.bool
|
|
186
|
+
Decode.decimal
|
|
187
|
+
Decode.instant
|
|
188
|
+
Decode.iso_date
|
|
177
189
|
Decode.decode
|
|
178
190
|
Decode.decode_string
|
|
179
191
|
Decode.dict
|
|
@@ -189,8 +201,10 @@ module Jade
|
|
|
189
201
|
Decode.one_of
|
|
190
202
|
Decode.optional
|
|
191
203
|
Decode.optional_field
|
|
204
|
+
Decode.record
|
|
192
205
|
Decode.required
|
|
193
206
|
Decode.sequence
|
|
207
|
+
Decode.set
|
|
194
208
|
Decode.string
|
|
195
209
|
Decode.string_enum
|
|
196
210
|
Decode.succeed
|
|
@@ -198,9 +212,11 @@ module Jade
|
|
|
198
212
|
Decode.tuple3
|
|
199
213
|
Decode.tuple4
|
|
200
214
|
Decode.type_
|
|
215
|
+
Decode.value
|
|
201
216
|
Decode.variant
|
|
202
217
|
Encode.bool
|
|
203
218
|
Encode.dict
|
|
219
|
+
Encode.instant
|
|
204
220
|
Encode.encode_to_string
|
|
205
221
|
Encode.field
|
|
206
222
|
Encode.float
|
|
@@ -209,10 +225,12 @@ module Jade
|
|
|
209
225
|
Encode.null
|
|
210
226
|
Encode.nullable
|
|
211
227
|
Encode.object
|
|
228
|
+
Encode.set
|
|
212
229
|
Encode.string
|
|
213
230
|
Encode.tuple
|
|
214
231
|
Encode.tuple3
|
|
215
232
|
Encode.tuple4
|
|
233
|
+
Encode.value
|
|
216
234
|
Encode.variant
|
|
217
235
|
Task.and_then
|
|
218
236
|
Task.fail
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module Codegen
|
|
3
|
-
module
|
|
3
|
+
module PortCodec
|
|
4
4
|
extend self
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
PASS_DECODER = "Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]"
|
|
7
|
+
PASS_ENCODER = "->(v) { v }"
|
|
7
8
|
|
|
8
9
|
# `dictionaries` is the call site's constraint-attachment list; only
|
|
9
|
-
# used when an arm is a Dict marker.
|
|
10
|
+
# used when an arm or a parameter is a Dict marker.
|
|
10
11
|
def task_call(interop_fn, registry, dictionaries = [])
|
|
11
12
|
[
|
|
12
13
|
interop_fn.interop_module_name,
|
|
13
14
|
":#{interop_fn.name}",
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
codec(interop_fn.decoders.fetch(:ok), 'decoder', interop_fn, registry, dictionaries),
|
|
16
|
+
codec(interop_fn.decoders.fetch(:err), 'decoder', interop_fn, registry, dictionaries),
|
|
17
|
+
encoders(interop_fn, registry, dictionaries),
|
|
16
18
|
]
|
|
17
19
|
.join(', ')
|
|
18
20
|
.then { "Jade::Runtime.task_call(#{it})" }
|
|
@@ -20,32 +22,42 @@ module Jade
|
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def encoders(interop_fn, registry, dictionaries)
|
|
26
|
+
interop_fn
|
|
27
|
+
.encoders
|
|
28
|
+
.map { codec(it, 'encoder', interop_fn, registry, dictionaries) }
|
|
29
|
+
.join(', ')
|
|
30
|
+
.then { "[#{it}]" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Per-arm, per-argument codec. PortResolution stamps one of:
|
|
34
|
+
# - :pass — Decode.Value or Never, nothing to convert
|
|
25
35
|
# - Symbol::Implementation — concrete OR partial impl (with marker
|
|
26
36
|
# deps for free vars). We push a synthetic dict_env mapping the
|
|
27
37
|
# port's vars to the call-site dictionaries so generate_impl_dispatch
|
|
28
38
|
# can resolve markers uniformly.
|
|
29
|
-
# - Symbol::InteropFunction::Dict — bare-var
|
|
39
|
+
# - Symbol::InteropFunction::Dict — bare-var position; the codec comes
|
|
30
40
|
# from the caller's threaded dictionary at the given constraint index
|
|
31
|
-
def
|
|
32
|
-
case
|
|
41
|
+
def codec(resolution, kind, interop_fn, registry, dictionaries)
|
|
42
|
+
case resolution
|
|
33
43
|
in Symbol::InteropFunction::PASS
|
|
34
|
-
|
|
44
|
+
kind == 'decoder' ? PASS_DECODER : PASS_ENCODER
|
|
35
45
|
|
|
36
46
|
in Symbol::Implementation => impl
|
|
37
|
-
|
|
47
|
+
emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
|
|
38
48
|
|
|
39
49
|
in Symbol::InteropFunction::Dict(constraint_index:)
|
|
40
|
-
|
|
50
|
+
FunctionCall
|
|
51
|
+
.dispatch_value(dictionaries.fetch(constraint_index), registry)
|
|
52
|
+
.then { "#{it}[#{kind.inspect}]" }
|
|
41
53
|
end
|
|
42
54
|
end
|
|
43
55
|
|
|
44
|
-
def
|
|
56
|
+
def emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
|
|
45
57
|
synthetic_env = build_synthetic_dict_env(impl, interop_fn, dictionaries, registry)
|
|
46
58
|
|
|
47
59
|
Codegen.with_dict_env(synthetic_env) {
|
|
48
|
-
FunctionCall.generate_impl_dispatch(impl, registry).fetch(
|
|
60
|
+
FunctionCall.generate_impl_dispatch(impl, registry).fetch(kind)
|
|
49
61
|
}
|
|
50
62
|
end
|
|
51
63
|
|
data/lib/jade/codegen.rb
CHANGED
|
@@ -19,7 +19,7 @@ require 'jade/codegen/transforms/fold_shape'
|
|
|
19
19
|
require 'jade/codegen/function_declaration'
|
|
20
20
|
require 'jade/codegen/function_call'
|
|
21
21
|
require 'jade/codegen/implementation'
|
|
22
|
-
require 'jade/codegen/
|
|
22
|
+
require 'jade/codegen/port_codec'
|
|
23
23
|
|
|
24
24
|
module Jade
|
|
25
25
|
module Codegen
|
|
@@ -159,7 +159,7 @@ module Jade
|
|
|
159
159
|
in Symbol::InteropFunction => sym
|
|
160
160
|
registry
|
|
161
161
|
.lookup(sym.to_ref)
|
|
162
|
-
.then {
|
|
162
|
+
.then { PortCodec.task_call(it, registry) }
|
|
163
163
|
|
|
164
164
|
in Symbol::StdlibFunction(codegen:)
|
|
165
165
|
codegen
|