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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +164 -0
  3. data/CHANGELOG.md +215 -1
  4. data/README.md +17 -12
  5. data/docs/interop.md +208 -0
  6. data/docs/json.md +163 -0
  7. data/docs/lsp.md +105 -0
  8. data/docs/stdlib.md +69 -0
  9. data/docs/syntax.md +458 -0
  10. data/docs/testing.md +70 -0
  11. data/lib/jade/api.rb +244 -0
  12. data/lib/jade/cli/check.rb +97 -0
  13. data/lib/jade/cli/q.rb +47 -1
  14. data/lib/jade/cli.rb +4 -2
  15. data/lib/jade/codegen/emitter.rb +11 -2
  16. data/lib/jade/codegen/function_call.rb +1 -1
  17. data/lib/jade/codegen/helpers.rb +6 -0
  18. data/lib/jade/codegen/inlines.rb +18 -0
  19. data/lib/jade/codegen/{port_decoder.rb → port_codec.rb} +27 -15
  20. data/lib/jade/codegen.rb +2 -2
  21. data/lib/jade/debug.rb +59 -0
  22. data/lib/jade/decode.rb +332 -212
  23. data/lib/jade/diagnostics/renderer.rb +16 -5
  24. data/lib/jade/frontend/fixity_fixer.rb +3 -2
  25. data/lib/jade/frontend/forward_declaration/interop_import_declaration.rb +13 -3
  26. data/lib/jade/frontend/pattern_analysis/matrix.rb +57 -23
  27. data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +33 -7
  28. data/lib/jade/frontend/semantic_analysis/error/constructor_not_found.rb +20 -5
  29. data/lib/jade/frontend/semantic_analysis/error/variable_not_found.rb +9 -2
  30. data/lib/jade/frontend/semantic_analysis/member_access.rb +11 -1
  31. data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +35 -32
  32. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +32 -50
  33. data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +51 -175
  34. data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +133 -0
  35. data/lib/jade/frontend/type_checking/constraints/deriving/show.rb +186 -0
  36. data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
  37. data/lib/jade/frontend/type_checking/error/port_not_encodable.rb +38 -0
  38. data/lib/jade/frontend/type_checking/port_resolution.rb +64 -16
  39. data/lib/jade/interop/boundary.rb +2 -3
  40. data/lib/jade/interop/runtime.rb +10 -2
  41. data/lib/jade/lsp/converters.rb +2 -15
  42. data/lib/jade/lsp/snippets.rb +21 -3
  43. data/lib/jade/parsing/error.rb +19 -0
  44. data/lib/jade/runtime.rb +1 -0
  45. data/lib/jade/signature.rb +47 -0
  46. data/lib/jade/stdlib/calendar.rb +20 -19
  47. data/lib/jade/stdlib/clock.rb +7 -13
  48. data/lib/jade/stdlib/debug.rb +12 -0
  49. data/lib/jade/stdlib/decimal.rb +3 -16
  50. data/lib/jade/stdlib/decode.rb +57 -12
  51. data/lib/jade/stdlib/encode.rb +26 -0
  52. data/lib/jade/stdlib/intrinsics.rb +12 -1
  53. data/lib/jade/stdlib/result.rb +1 -1
  54. data/lib/jade/stdlib/show.rb +40 -0
  55. data/lib/jade/stdlib/text.rb +131 -0
  56. data/lib/jade/stdlib.rb +5 -2
  57. data/lib/jade/symbol/interop_function.rb +3 -1
  58. data/lib/jade/symbol.rb +2 -2
  59. data/lib/jade/task.rb +2 -3
  60. data/lib/jade/version.rb +1 -1
  61. 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
- Compile cache lives at .jade/cache, so repeat queries are fast.
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
@@ -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 [:anon_record_constructor, keys]
54
- "Jade::Runtime.curry(#{data_define(keys)}.method(:[]), #{keys.size})"
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 { PortDecoder.task_call(it, registry, dictionaries) }
183
+ .then { PortCodec.task_call(it, registry, dictionaries) }
184
184
 
185
185
  in Symbol::StdlibFunction => symbol if symbol.constraints.any?
186
186
  dictionaries
@@ -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
@@ -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 PortDecoder
3
+ module PortCodec
4
4
  extend self
5
5
 
6
- PASS = "Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]"
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
- decoder(interop_fn, :ok, registry, dictionaries),
15
- decoder(interop_fn, :err, registry, dictionaries),
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
- # Per-arm decoder. PortResolution stamps one of:
24
- # - :pass — Decode.Value or Never arm, no decoder needed
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 arm, decoder comes
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 decoder(interop_fn, arm, registry, dictionaries)
32
- case interop_fn.decoders.fetch(arm)
41
+ def codec(resolution, kind, interop_fn, registry, dictionaries)
42
+ case resolution
33
43
  in Symbol::InteropFunction::PASS
34
- PASS
44
+ kind == 'decoder' ? PASS_DECODER : PASS_ENCODER
35
45
 
36
46
  in Symbol::Implementation => impl
37
- emit_impl_decoder(impl, interop_fn, dictionaries, registry)
47
+ emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
38
48
 
39
49
  in Symbol::InteropFunction::Dict(constraint_index:)
40
- "#{FunctionCall.dispatch_value(dictionaries.fetch(constraint_index), registry)}[\"decoder\"]"
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 emit_impl_decoder(impl, interop_fn, dictionaries, registry)
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('decoder')
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/port_decoder'
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 { PortDecoder.task_call(it, registry) }
162
+ .then { PortCodec.task_call(it, registry) }
163
163
 
164
164
  in Symbol::StdlibFunction(codegen:)
165
165
  codegen