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
@@ -146,7 +146,8 @@ module Jade
146
146
 
147
147
  in AST::VariableReference | AST::ConstructorReference | AST::TypeDeclaration |
148
148
  AST::ImportDeclaration | AST::Literal | AST::CharLiteral | AST::RecordAccessSugar | AST::InteropImportDeclaration |
149
- AST::StructDeclaration | AST::QualifiedAccess | AST::Placeholder | AST::InterfaceDeclaration
149
+ AST::StructDeclaration | AST::QualifiedAccess | AST::Placeholder | AST::InterfaceDeclaration |
150
+ AST::RecordUpdateSugar
150
151
 
151
152
  node
152
153
 
@@ -164,7 +165,7 @@ module Jade
164
165
  flatten(left) + [operator] + flatten(right)
165
166
 
166
167
  else
167
- [ast]
168
+ [fix(ast)]
168
169
  end
169
170
  end
170
171
 
@@ -47,8 +47,8 @@ module Jade
47
47
  end
48
48
 
49
49
  def fn_type_to_interop(interop_mod_name, function_node, symbol, entry, registry)
50
- lifted_errors = Interop::Lowering
51
- .validate(symbol.return_type, registry, entry)
50
+ lifted_errors = [symbol.return_type, *symbol.params]
51
+ .flat_map { Interop::Lowering.validate(it, registry, entry) }
52
52
  .map { Error::TypeNotLowerable.new(entry, function_node.range, message: it.message) }
53
53
 
54
54
  Symbol
@@ -57,7 +57,8 @@ module Jade
57
57
  symbol.params,
58
58
  symbol.return_type,
59
59
  interop_mod_name.name,
60
- constraints: implicit_decodable_constraints(symbol.return_type),
60
+ constraints: implicit_decodable_constraints(symbol.return_type) +
61
+ implicit_encodable_constraints(symbol.params),
61
62
  )
62
63
  .then { [it, lifted_errors] }
63
64
  end
@@ -78,6 +79,15 @@ module Jade
78
79
  end
79
80
  end
80
81
 
82
+ # Arguments travel the other way, so a free variable in a parameter
83
+ # needs the caller's Encodable instance rather than its Decodable one.
84
+ def implicit_encodable_constraints(params)
85
+ params
86
+ .flat_map { collect_var_names(it) }
87
+ .uniq
88
+ .map { ['Encode.Encodable', it] }
89
+ end
90
+
81
91
  def collect_var_names(type_sym)
82
92
  case type_sym
83
93
  in Symbol::Variable(name:)
@@ -39,7 +39,7 @@ module Jade
39
39
  with(rows: rows + other.rows)
40
40
  end
41
41
 
42
- def missing_patterns(env, seen_recursive_types = ::Set.new)
42
+ def missing_patterns(env)
43
43
  if types.empty?
44
44
  return rows.empty? ? Matrix[[[]], types] : Matrix.empty
45
45
  end
@@ -49,37 +49,72 @@ module Jade
49
49
  return Matrix.empty if never?(type)
50
50
  return Matrix.wildcard(types) if rows.empty?
51
51
 
52
- if infinite?(type) || type_var?(type) || seen_recursive_types.include?(type)
53
- matrix = default
54
- .missing_patterns(env, seen_recursive_types)
52
+ heads = head_constructors
55
53
 
56
- matrix
57
- .map { [Wildcard[]] + it }
54
+ if infinite?(type) || type_var?(type)
55
+ unmatched_column(env)
58
56
 
59
57
  elsif expandable?(type, env)
60
- expand(env).missing_patterns(env, seen_recursive_types)
58
+ expand(env).missing_patterns(env)
59
+
60
+ elsif heads.empty?
61
+ unmatched_column(env)
61
62
 
62
63
  else
63
- new_seen = seen_recursive_types | ::Set[type]
64
64
  constructors_of(type, env)
65
65
  .reduce(Matrix.empty.with(types:)) do |acc, constructor|
66
- missing = specialize(constructor)
67
- .missing_patterns(env, new_seen)
68
-
69
- missing
70
- .map do |row|
71
- new_args = row.take(constructor.args.size)
72
- tail = row.drop(constructor.args.size)
73
-
74
- [Constructor[constructor.name, new_args]] + tail
75
- end
76
- .then { acc.concat(it) }
66
+ witnesses(constructor, env, heads.include?(constructor.name))
67
+ .then { acc.concat(it) }
77
68
  end
78
69
  end
79
70
  end
80
71
 
81
72
  protected
82
73
 
74
+ # Nothing in this column narrows anything — a type with no constructors
75
+ # to enumerate, or one every row left open. Whatever is missing lives in
76
+ # the columns after it.
77
+ def unmatched_column(env)
78
+ default
79
+ .missing_patterns(env)
80
+ .map { [Wildcard[]] + it }
81
+ end
82
+
83
+ # A constructor the first column matches is explored by specializing on
84
+ # it. One it never matches can only be missing as a whole, so its
85
+ # witnesses come from the rows that would have covered it — the ones
86
+ # headed by a wildcard. Recursing there instead of into the constructor
87
+ # is what keeps a recursive type from expanding forever.
88
+ def witnesses(constructor, env, matched)
89
+ return Matrix.empty if constructor.args.any? { never?(it) }
90
+
91
+ arity = constructor.args.size
92
+
93
+ if matched
94
+ specialize(constructor)
95
+ .missing_patterns(env)
96
+ .map { [Constructor[constructor.name, it.take(arity)]] + it.drop(arity) }
97
+
98
+ else
99
+ default
100
+ .missing_patterns(env)
101
+ .map { [Constructor[constructor.name, Array.new(arity) { Wildcard[] }]] + it }
102
+ end
103
+ end
104
+
105
+ def head_constructors
106
+ rows.filter_map { head_constructor(it.first) }
107
+ end
108
+
109
+ def head_constructor(pattern)
110
+ case pattern
111
+ in Constructor(constructor: name) then name
112
+ in Literal(value: true) then 'Basics.True'
113
+ in Literal(value: false) then 'Basics.False'
114
+ in Literal | Record | Wildcard then nil
115
+ end
116
+ end
117
+
83
118
  def expandable?(type, env)
84
119
  case type
85
120
  in Type::AnonymousRecord
@@ -125,9 +160,8 @@ module Jade
125
160
  TypeChecking::ConstructorDef['Basics.True', 'Basics.Bool', []],
126
161
  TypeChecking::ConstructorDef['Basics.False', 'Basics.Bool', []],
127
162
  ]
128
- in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple([2-4])$/ => name))
129
- n = name[-1].to_i
130
- [TypeChecking::Definition.constructor(name, name, Array.new(n) { env.fresh })]
163
+ in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple[2-4]$/ => name), args:)
164
+ [TypeChecking::Definition.constructor(name, name, args)]
131
165
 
132
166
  in Type::Application(constructor: Type::Constructor(name: 'List.List'), args: [elem_type])
133
167
  [
@@ -181,7 +215,7 @@ module Jade
181
215
  end
182
216
  end
183
217
  .then { with(rows: it) }
184
- .then { it.with(types: constructor.args) }
218
+ .then { it.with(types: constructor.args + types.drop(1)) }
185
219
  end
186
220
 
187
221
  def default
@@ -17,7 +17,7 @@ module Jade
17
17
  entry.name,
18
18
  node.range,
19
19
  name:,
20
- exposed_type_module: exposed_type_origin(name, entry, registry),
20
+ hint: constructor_hint(name, entry, registry),
21
21
  candidates: scope.bindings.keys.select { it.match?(/\A[A-Z]/) },
22
22
  ))
23
23
  .then do
@@ -49,14 +49,40 @@ module Jade
49
49
  &.then { klass.new(entry.name, span, arity: it) }
50
50
  end
51
51
 
52
- def exposed_type_origin(name, entry, registry)
53
- type_ref = entry.imported_types[name]
54
- return nil unless type_ref
52
+ # An imported module owning a constructor by this name is the whole
53
+ # story: either it keeps its constructors private, or it exposes them
54
+ # and the import here asked for the type alone.
55
+ def constructor_hint(name, entry, registry)
56
+ entry
57
+ .imports
58
+ .filter_map { hint_for(it.module_name, name, registry) }
59
+ .first
60
+ end
61
+
62
+ def hint_for(module_name, name, registry)
63
+ target = registry.get(module_name)
64
+ return nil if target.nil? || Stdlib.is_stdlib?(target)
65
+
66
+ type_name = owning_type(target, name)
67
+ return nil unless type_name && target.exposed_type(type_name)
55
68
 
56
- target_entry = registry.get(type_ref.module_name)
57
- return nil if target_entry.nil? || Stdlib.is_stdlib?(target_entry)
69
+ exposes_constructor?(target, type_name, name)
70
+ .then { it ? :import : :expose }
71
+ .then { Error::ConstructorNotFound::Hint[module_name, type_name, it] }
72
+ end
73
+
74
+ def owning_type(target, name)
75
+ target
76
+ .defined_types
77
+ .find { |_, sym| sym.constructor_refs.any? { it.name == name } }
78
+ &.first
79
+ end
58
80
 
59
- type_ref.module_name
81
+ def exposes_constructor?(target, type_name, name)
82
+ target
83
+ .exposed_type_variants(type_name)
84
+ .to_a
85
+ .any? { it.name == name }
60
86
  end
61
87
  end
62
88
  end
@@ -3,21 +3,36 @@ module Jade
3
3
  module SemanticAnalysis
4
4
  module Error
5
5
  class ConstructorNotFound < Jade::Error
6
+ # `fix` is where the `(..)` is missing: `:expose` when the module
7
+ # that owns the type keeps its constructors to itself, `:import`
8
+ # when it exposes them and this module's import didn't ask for them.
9
+ Hint = Data.define(:module_name, :type_name, :fix)
10
+
6
11
  attr_reader :candidates
7
12
 
8
- def initialize(entry, span, name:, exposed_type_module: nil, candidates: [])
13
+ def initialize(entry, span, name:, hint: nil, candidates: [])
9
14
  @name = name
10
- @exposed_type_module = exposed_type_module
15
+ @hint = hint
11
16
  @candidates = candidates
12
17
  super(entry:, span:)
13
18
  end
14
19
 
15
20
  def message
16
21
  base = "I cannot find a `#{@name}` constructor"
17
- return base unless @exposed_type_module
18
22
 
19
- "#{base}. The type `#{@name}` is exposed by `#{@exposed_type_module}` but its " \
20
- "constructor is private — add `#{@name}(..)` to that module's `exposing` list."
23
+ case @hint
24
+ in nil
25
+ base
26
+
27
+ in Hint(module_name:, type_name:, fix: :expose)
28
+ "#{base}. The type `#{type_name}` is exposed by `#{module_name}` but its " \
29
+ "constructor is private — add `#{type_name}(..)` to that module's " \
30
+ "`exposing` list."
31
+
32
+ in Hint(module_name:, type_name:, fix: :import)
33
+ "#{base}. `#{module_name}` exposes it — add `#{type_name}(..)` to this " \
34
+ "module's `import #{module_name} exposing (...)`."
35
+ end
21
36
  end
22
37
 
23
38
  def label
@@ -3,11 +3,12 @@ module Jade
3
3
  module SemanticAnalysis
4
4
  module Error
5
5
  class VariableNotFound < Jade::Error
6
- attr_reader :causes
6
+ attr_reader :causes, :candidates
7
7
 
8
- def initialize(entry, span, name:, causes: [])
8
+ def initialize(entry, span, name:, causes: [], candidates: [])
9
9
  @name = name
10
10
  @causes = causes
11
+ @candidates = candidates
11
12
  super(entry:, span:)
12
13
  end
13
14
 
@@ -18,6 +19,12 @@ module Jade
18
19
  def label
19
20
  "not found"
20
21
  end
22
+
23
+ # Qualified, like `candidates`: bare `fold_left` against `fold` is
24
+ # too short to clear the spell checker's threshold.
25
+ def queried_name
26
+ @name
27
+ end
21
28
  end
22
29
  end
23
30
  end
@@ -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 registry.get(import.module_name).exposed_value(name.name)
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
@@ -11,13 +11,23 @@ module Jade
11
11
 
12
12
  def supports?(interface) = interface == INTERFACE
13
13
 
14
+ # One decoder combinator per structural type, taking a decoder per
15
+ # element. `Tuple2(a, b)` is `Decode.tuple(dec_a, dec_b)` the same
16
+ # way `List(a)` is `Decode.list(dec_a)`.
17
+ STRUCTURAL = {
18
+ 'List.List' => 'Decode.list',
19
+ 'Maybe.Maybe' => 'Decode.nullable',
20
+ 'Set.Set' => 'Decode.set',
21
+ 'Dict.Dict' => 'Decode.dict',
22
+ 'Tuple.Tuple2' => 'Decode.tuple',
23
+ 'Tuple.Tuple3' => 'Decode.tuple3',
24
+ 'Tuple.Tuple4' => 'Decode.tuple4',
25
+ }.freeze
26
+
14
27
  def derive(constraint, registry, entry_name, &lookup)
15
28
  case constraint.type
16
- in Type::Application(constructor: Type::Constructor(name: 'List.List'), args: [inner])
17
- derive_unary(constraint, inner, 'Decode.list', lookup, entry_name)
18
-
19
- in Type::Application(constructor: Type::Constructor(name: 'Maybe.Maybe'), args: [inner])
20
- derive_unary(constraint, inner, 'Decode.nullable', lookup, entry_name)
29
+ in Type::Application(constructor: Type::Constructor(name:), args:) if STRUCTURAL.key?(name)
30
+ derive_structural(constraint, args, STRUCTURAL.fetch(name), lookup, entry_name)
21
31
 
22
32
  in Type::Application(constructor: Type::Constructor(name:), args:)
23
33
  resolved_sym = Symbol
@@ -56,32 +66,30 @@ module Jade
56
66
  ]
57
67
  end
58
68
 
59
- def derive_unary(constraint, inner_type, stdlib_fn, lookup, entry_name)
60
- dep = Type.constraint(INTERFACE, inner_type, nil)
61
-
62
- resolve_dep(dep, lookup, entry_name).and_then do |dep_impl|
63
- body = [:call,
64
- [:stdlib_fn, stdlib_fn],
65
- [
66
- [:impl_arg, 0, 'decoder'],
67
- ],
68
- ]
69
+ def derive_structural(constraint, inner_types, stdlib_fn, lookup, entry_name)
70
+ body = [:call,
71
+ [:stdlib_fn, stdlib_fn],
72
+ inner_types.each_index.map { [:impl_arg, it, 'decoder'] },
73
+ ]
69
74
 
70
- Ok[implementation(constraint, body, [dep_impl])]
71
- end
75
+ inner_types
76
+ .map { Type.constraint(INTERFACE, it, nil) }
77
+ .map { resolve_dep(it, lookup, entry_name) }
78
+ .then { Results.sequence(it) }
79
+ .map { implementation(constraint, body, it) }
72
80
  end
73
81
 
74
82
  def derive_struct(constraint, struct_sym, type_args, registry, lookup, entry_name)
75
83
  fields = struct_fields(struct_sym, type_args, registry)
76
84
 
77
- [:struct_constructor, struct_sym.qualified_name, fields.size]
85
+ [:struct_class, struct_sym.qualified_name]
78
86
  .then { derive_record(constraint, fields, it, lookup, entry_name) }
79
87
  end
80
88
 
81
89
  def derive_anonymous_record(constraint, fields, lookup, entry_name)
82
90
  keys = fields.keys.map(&:to_s)
83
91
 
84
- [:anon_record_constructor, keys]
92
+ [:anon_record_class, keys]
85
93
  .then { derive_record(constraint, fields.to_a, it, lookup, entry_name) }
86
94
  end
87
95
 
@@ -113,19 +121,14 @@ module Jade
113
121
  end
114
122
 
115
123
  def record_body(fields, constructor_ref)
116
- seed = [:call, [:stdlib_fn, 'Decode.succeed'], [constructor_ref]]
117
-
118
- fields.each_with_index.reduce(seed) do |acc, ((field_name, _), idx)|
119
- field_decoder = [:call,
120
- [:stdlib_fn, 'Decode.field'],
121
- [
122
- field_name.to_s,
123
- [:impl_arg, idx, 'decoder'],
124
- ],
125
- ]
126
-
127
- [:call, [:stdlib_fn, 'Decode.and_map'], [acc, field_decoder]]
128
- 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
+ ]
129
132
  end
130
133
 
131
134
  # Free-var inner constraints fall back to the constraint marker
@@ -13,13 +13,23 @@ module Jade
13
13
  interface == INTERFACE
14
14
  end
15
15
 
16
+ # One encoder combinator per structural type, taking an encoder per
17
+ # element plus the value itself. The name is what the emitted
18
+ # encoder binds its argument to.
19
+ STRUCTURAL = {
20
+ 'List.List' => ['Encode.list', 'items'],
21
+ 'Maybe.Maybe' => ['Encode.nullable', 'maybe'],
22
+ 'Set.Set' => ['Encode.set', 'set'],
23
+ 'Dict.Dict' => ['Encode.dict', 'dict'],
24
+ 'Tuple.Tuple2' => ['Encode.tuple', 'pair'],
25
+ 'Tuple.Tuple3' => ['Encode.tuple3', 'triple'],
26
+ 'Tuple.Tuple4' => ['Encode.tuple4', 'quad'],
27
+ }.freeze
28
+
16
29
  def derive(constraint, registry, entry_name, &lookup)
17
30
  case constraint.type
18
- in Type::Application(constructor: Type::Constructor(name: 'List.List'), args: [inner])
19
- derive_list(constraint, inner, lookup, entry_name)
20
-
21
- in Type::Application(constructor: Type::Constructor(name: 'Maybe.Maybe'), args: [inner])
22
- derive_nullable(constraint, inner, lookup, entry_name)
31
+ in Type::Application(constructor: Type::Constructor(name:), args:) if STRUCTURAL.key?(name)
32
+ derive_structural(constraint, args, *STRUCTURAL.fetch(name), lookup, entry_name)
23
33
 
24
34
  in Type::Application(constructor: Type::Constructor(name:), args:)
25
35
  resolved_sym = Symbol
@@ -55,36 +65,17 @@ module Jade
55
65
  ]
56
66
  end
57
67
 
58
- def derive_list(constraint, inner_type, lookup, entry_name)
59
- dep = Type.constraint(INTERFACE, inner_type, nil)
60
-
61
- lookup.call(dep).and_then do |dep_impl|
62
- body = [:call,
63
- [:stdlib_fn, 'Encode.list'],
64
- [
65
- [:impl_arg, 0, 'encoder'],
66
- [:var, 'items'],
67
- ],
68
- ]
69
-
70
- Ok[implementation(constraint, params: ['items'], body:, deps: [dep_impl])]
71
- end
72
- end
73
-
74
- def derive_nullable(constraint, inner_type, lookup, entry_name)
75
- dep = Type.constraint(INTERFACE, inner_type, nil)
76
-
77
- lookup.call(dep).and_then do |dep_impl|
78
- body = [:call,
79
- [:stdlib_fn, 'Encode.nullable'],
80
- [
81
- [:impl_arg, 0, 'encoder'],
82
- [:var, 'maybe'],
83
- ],
84
- ]
68
+ def derive_structural(constraint, inner_types, stdlib_fn, param, lookup, entry_name)
69
+ body = [:call,
70
+ [:stdlib_fn, stdlib_fn],
71
+ inner_types.each_index.map { [:impl_arg, it, 'encoder'] } + [[:var, param]],
72
+ ]
85
73
 
86
- Ok[implementation(constraint, params: ['maybe'], body:, deps: [dep_impl])]
87
- end
74
+ inner_types
75
+ .map { Type.constraint(INTERFACE, it, nil) }
76
+ .map { lookup.call(it) }
77
+ .then { Results.sequence(it) }
78
+ .map { implementation(constraint, params: [param], body:, deps: it) }
88
79
  end
89
80
 
90
81
  def derive_nullary_union(constraint, union_sym, registry)
@@ -106,27 +97,18 @@ module Jade
106
97
  field_deps = fields
107
98
  .map { |_, field_type| Type.constraint(INTERFACE, field_type, nil) }
108
99
 
109
- pair_irs = fields
100
+ body = fields
110
101
  .each_with_index
111
102
  .map do |(field_name, _), idx|
112
- [:call,
113
- [:stdlib_fn, 'Tuple.pair'],
114
- [
115
- field_name.to_s,
116
- [:call,
117
- [:impl_arg, idx, 'encoder'],
118
- [[:access, [:var, 'rec'], field_name.to_s]],
119
- ],
103
+ [
104
+ field_name.to_s,
105
+ [:call,
106
+ [:impl_arg, idx, 'encoder'],
107
+ [[:access, [:var, 'rec'], field_name.to_s]],
120
108
  ],
121
109
  ]
122
110
  end
123
-
124
- body = [:call,
125
- [:stdlib_fn, 'Encode.object'],
126
- [
127
- [:list, pair_irs],
128
- ],
129
- ]
111
+ .then { [:hash, it] }
130
112
 
131
113
  field_deps
132
114
  .map { lookup.call(it) }