jade-lang 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38f4067e761c85016ad9baa6c733e6567aa93eea5cc0ffb755c0ac469a6a0002
4
- data.tar.gz: 97dd4372b13bcb7e23bdc3ac2dcbe231d067410e9a99634afbbd3bd1a485452e
3
+ metadata.gz: 7b817b13aad1b314b6a8bf578f8a604246d19091712a77375376515732b6dcbc
4
+ data.tar.gz: 7be6b4091ac7c57c6befe21fb0ffbea302b9e33bd2940479b7c4227dd37cd1b6
5
5
  SHA512:
6
- metadata.gz: 415bcc6027fbefa912960d30c004d3d442000b37d6b3e6a27485b9ef9a12685bad3dc79855f7ccd99378987ea38fb6810fafe8f371dad5c50ce9b605c464f756
7
- data.tar.gz: 8891cbe3dfe48e80cdb2ae73ec96a13b09a3adc8287adf812e7a30639ca4918b417cc02e45148222bacd2f7b1d412b58ce78d15f783dceed0868d0175e194daf
6
+ metadata.gz: 9b758da80e8b9033dd6b5ce840188f7d69a8d482cf496a3731c4a4f7b5bcb90ddd0119e5b5eb6e9b1763bddffca4f3df49b6cba7d6a26d9304538efde7a8aa49
7
+ data.tar.gz: 22abc8374f37d7f9ff3c03f5f2a35861322f234227c3d0932e80c56e17bb49de5ce6ac67dc7c1f0b1896ce2b9a5c3562a2d8197f697f74fdd5d18e5db64094f6
data/CHANGELOG.md CHANGED
@@ -6,10 +6,60 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - **`Jade::Extensions`, where a gem hooks into compilation.** Two kinds, both
12
+ read-only: a *deriver* builds an implementation for an interface it owns,
13
+ and a *check* reads a call site and returns errors. Checks register against
14
+ a named phase — `:call` today — and receive the call's AST node alongside
15
+ its resolved argument types, because types alone cannot see a raw SQL
16
+ string's placeholders or a constant predicate. Only gems named in
17
+ `Extensions::ALLOWED` may register, so the compiler knows who extends it and
18
+ nothing about what they do.
19
+
20
+ ### Changed
21
+
22
+ - **The `Sql.Assignable` deriver moves to jade-sql.** The deriver list named
23
+ jade-sql's interface outright, with a comment apologising for it; it now
24
+ holds only the built-ins and whatever an allowed gem registers.
25
+
26
+ ### Fixed
27
+
28
+ - **A long chain of `Task.and_then` no longer exhausts the Ruby stack.** Each
29
+ `and_then` ran the next task from inside the previous one's `run`, so depth
30
+ cost a stack frame and anything built by recursion, a batch loop or a retry,
31
+ died with `SystemStackError` somewhere past ten thousand links. `run` now
32
+ drives an explicit stack of continuations from one loop, so a chain is
33
+ bounded by memory: 500,000 links run where 100,000 used to fail.
34
+
35
+ - **Two implementations for the same head type is now an error.** An
36
+ implementation is registered under `[interface, head type]`, so a second
37
+ `implements Assignable(Box(Cols2, Val2))` overwrote
38
+ `implements Assignable(Box(Cols, Val))` and every call — including ones whose
39
+ types matched the first — dispatched to the second. It compiled clean and
40
+ died at run time in the implementation body, or worse, didn't. The second
41
+ declaration is now reported, with the first as a secondary label and a note
42
+ that type arguments do not select between implementations. Duplicates can
43
+ only arise within one module: the orphan rule and cycle detection between
44
+ them rule out the cross-module case.
45
+
9
46
  ## [0.8.0]
10
47
 
11
48
  ### Added
12
49
 
50
+ - **A struct written to a table is checked against its columns.** jade-sql maps
51
+ a struct's fields onto columns by name, and the mapping derives, so nothing
52
+ compared the two — a field the table has no column for reached Postgres as
53
+ invalid SQL, and a field whose type disagreed with its column failed at
54
+ decode. Calls to `Sql.Mutation.insert`, `insert_all` and `update` now report
55
+ both at compile time, naming the field. Both types are concrete at the call
56
+ site, so this is a check rather than anything the type system has to carry.
57
+ A call through a generic helper of your own has neither type in hand and
58
+ stays unchecked. Inert without jade-sql, since nothing else declares those
59
+ functions.
60
+
61
+ ### Added
62
+
13
63
  - `Assignable` also derives for structs, naming one column per field in
14
64
  declaration order. A field renamed to dodge a keyword (`type_`) maps back to
15
65
  the column it came from. Generic structs derive at the type they are applied
@@ -83,7 +133,6 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
83
133
  anonymous record. A project that CI-checks committed artifacts will see a
84
134
  diff.
85
135
 
86
-
87
136
  ## [0.6.0]
88
137
 
89
138
  ### Fixed
data/lib/jade/error.rb CHANGED
@@ -21,6 +21,10 @@ module Jade
21
21
  []
22
22
  end
23
23
 
24
+ def secondary
25
+ []
26
+ end
27
+
24
28
  def queried_name
25
29
  nil
26
30
  end
@@ -39,6 +43,7 @@ module Jade
39
43
  Jade::Diagnostics::Diagnostic.error(
40
44
  message,
41
45
  primary: Jade::Diagnostics::Label[source, span, label],
46
+ secondary: secondary.map { |sp, text| Jade::Diagnostics::Label[source, sp, text] },
42
47
  annotations: notes + did_you_mean_notes,
43
48
  )
44
49
  end
@@ -0,0 +1,70 @@
1
+ module Jade
2
+ # Where an extension gem hooks into compilation. Only gems named in ALLOWED
3
+ # may register, so jade knows who extends it and nothing about what they do.
4
+ module Extensions
5
+ extend self
6
+
7
+ ALLOWED = %w[jade-sql].freeze
8
+ PHASES = %i[call].freeze
9
+
10
+ class NotAllowed < StandardError
11
+ def initialize(gem_name)
12
+ super(
13
+ "#{gem_name} may not extend the compiler; " \
14
+ "allowed: #{ALLOWED.join(', ')}"
15
+ )
16
+ end
17
+ end
18
+
19
+ class UnknownPhase < StandardError
20
+ def initialize(phase)
21
+ super("no such check phase #{phase.inspect}; known: #{PHASES.join(', ')}")
22
+ end
23
+ end
24
+
25
+ def register_deriver(gem_name, deriver)
26
+ allow!(gem_name)
27
+ @derivers = derivers | [deriver]
28
+ end
29
+
30
+ # A `:call` check answers `watches -> [qualified name]` and
31
+ # `check(context) -> [error]`. Watching by name keeps every other call in
32
+ # the program off the check's path entirely.
33
+ def register_check(gem_name, phase, check)
34
+ allow!(gem_name)
35
+ fail UnknownPhase.new(phase) unless PHASES.include?(phase)
36
+
37
+ @call_checks = check
38
+ .watches
39
+ .reduce(call_checks) { |acc, name| acc.merge(name => acc.fetch(name, []) | [check]) }
40
+ end
41
+
42
+ def derivers = @derivers ||= []
43
+
44
+ def call_checks = @call_checks ||= {}
45
+
46
+ # The node travels with the types because a literal's value — a SQL string,
47
+ # a constant predicate — is not in them.
48
+ CallContext = Data.define(:name, :arg_types, :node, :registry, :entry_name, :span)
49
+
50
+ def check_call(name, arg_types, node, registry, entry_name, span)
51
+ call_checks[name].then do |watching|
52
+ next [] if watching.nil?
53
+
54
+ CallContext[name, arg_types, node, registry, entry_name, span]
55
+ .then { |ctx| watching.flat_map { it.check(ctx) } }
56
+ end
57
+ end
58
+
59
+ def reset!
60
+ @derivers = []
61
+ @call_checks = {}
62
+ end
63
+
64
+ private
65
+
66
+ def allow!(gem_name)
67
+ fail NotAllowed.new(gem_name) unless ALLOWED.include?(gem_name)
68
+ end
69
+ end
70
+ end
@@ -24,9 +24,37 @@ module Jade
24
24
  end
25
25
 
26
26
  analyze_in_sequence(expressions, registry, scope, entry)
27
- .add_errors(duplicate_errors)
27
+ .add_errors(duplicate_errors + duplicate_implementation_errors(expressions, entry))
28
28
  .map_node { node.with(expressions: it) }
29
29
  end
30
+
31
+ private
32
+
33
+ def duplicate_implementation_errors(expressions, entry)
34
+ expressions
35
+ .select { it.is_a?(AST::Implementation) }
36
+ .group_by { implementation_key(it, entry) }
37
+ .reject { |key, impls| key.nil? || impls.size < 2 }
38
+ .flat_map do |(interface, type), (first, *rest)|
39
+ rest.map do |dup|
40
+ Error::DuplicateImplementation.new(
41
+ entry.name,
42
+ dup.range,
43
+ interface:,
44
+ type:,
45
+ first_span: first.range,
46
+ parameterized: dup.applied_type.args.any?,
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ def implementation_key(node, entry)
53
+ interface = entry.lookup_type(node.interface)
54
+ type = lookup_applied_type(node.applied_type, entry)
55
+
56
+ [interface.qname, type.qname] if interface && type
57
+ end
30
58
  end
31
59
  end
32
60
  end
@@ -0,0 +1,39 @@
1
+ module Jade
2
+ module Frontend
3
+ module SemanticAnalysis
4
+ module Error
5
+ class DuplicateImplementation < Jade::Error
6
+ def initialize(entry, span, interface:, type:, first_span:, parameterized:)
7
+ @interface = interface
8
+ @type = type
9
+ @first_span = first_span
10
+ @parameterized = parameterized
11
+ super(entry:, span:)
12
+ end
13
+
14
+ def message
15
+ "Duplicate implementation of #{@interface} for #{@type}"
16
+ end
17
+
18
+ def label
19
+ "already implemented"
20
+ end
21
+
22
+ def secondary
23
+ [[@first_span, 'first implemented here']]
24
+ end
25
+
26
+ def notes
27
+ return [] unless @parameterized
28
+
29
+ [Jade::Diagnostics::Annotation[
30
+ :note,
31
+ "an implementation is chosen by the head type `#{@type}` alone — " \
32
+ 'its type arguments do not select between implementations',
33
+ ]]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -7,6 +7,7 @@ require 'jade/frontend/semantic_analysis/error/constructor_pattern_arity_mismatc
7
7
  require 'jade/frontend/semantic_analysis/error/duplicate_field'
8
8
  require 'jade/frontend/semantic_analysis/error/placeholder_not_allowed'
9
9
  require 'jade/frontend/semantic_analysis/error/duplicate_function_declaration'
10
+ require 'jade/frontend/semantic_analysis/error/duplicate_implementation'
10
11
  require 'jade/frontend/semantic_analysis/error/duplicate_record_field'
11
12
  require 'jade/frontend/semantic_analysis/error/invalid_list_rest_pattern'
12
13
  require 'jade/frontend/semantic_analysis/error/kwargs_on_non_constructor'
@@ -49,6 +49,16 @@ module Jade
49
49
  Result[results.map(&:node), results.flat_map(&:errors), scope]
50
50
  end
51
51
 
52
+ private def lookup_applied_type(applied_type, entry)
53
+ case applied_type.constructor
54
+ in AST::TypeName(type:)
55
+ entry.lookup_type(type)
56
+ in AST::QualifiedTypeName(path:)
57
+ *module_parts, type_name = path
58
+ entry.lookup_qualified_type(module_parts.join('.'), type_name)
59
+ end
60
+ end
61
+
52
62
  def analyze_duplicate_fields(fields, entry)
53
63
  fields
54
64
  .group_by(&:key)
@@ -73,16 +73,6 @@ module Jade
73
73
 
74
74
  private
75
75
 
76
- def lookup_applied_type(applied_type, entry)
77
- case applied_type.constructor
78
- in AST::TypeName(type:)
79
- entry.lookup_type(type)
80
- in AST::QualifiedTypeName(path:)
81
- *module_parts, type_name = path
82
- entry.lookup_qualified_type(module_parts.join('.'), type_name)
83
- end
84
- end
85
-
86
76
  def local_type_name(applied_type)
87
77
  case applied_type.constructor
88
78
  in AST::TypeName(type:) then type
@@ -2,7 +2,6 @@ require_relative './deriving/helpers.rb'
2
2
  require_relative './deriving/eq.rb'
3
3
  require_relative './deriving/decodable.rb'
4
4
  require_relative './deriving/encodable.rb'
5
- require_relative './deriving/assignable.rb'
6
5
  require_relative './deriving/show.rb'
7
6
 
8
7
  module Jade
@@ -12,14 +11,16 @@ module Jade
12
11
  module Deriving
13
12
  extend self
14
13
 
15
- DERIVERS = [Eq, Show, Decodable, Encodable, Assignable]
14
+ BUILTIN = [Eq, Show, Decodable, Encodable].freeze
15
+
16
+ def derivers = BUILTIN + Extensions.derivers
16
17
 
17
18
  def derivable?(interface)
18
- DERIVERS.any? { it.supports?(interface) }
19
+ derivers.any? { it.supports?(interface) }
19
20
  end
20
21
 
21
22
  def derive(constraint, registry, entry_name, &lookup)
22
- DERIVERS
23
+ derivers
23
24
  .find { it.supports?(constraint.interface) }
24
25
  .then { it.derive(constraint, registry, entry_name, &lookup) }
25
26
  end
@@ -102,8 +102,17 @@ module Jade
102
102
  end
103
103
  end
104
104
 
105
+ column_errors = Extensions.check_call(
106
+ callee_name(callee),
107
+ args_acc.types.map { st.env.substitution.apply(it) },
108
+ node,
109
+ registry,
110
+ st.env.entry_name,
111
+ node.range,
112
+ )
113
+
105
114
  st
106
- .add_errors(callee_errors + args_errors)
115
+ .add_errors(callee_errors + args_errors + column_errors)
107
116
  .then { [it, base_rs.with(constraints: propagated)] }
108
117
  end
109
118
  end
@@ -114,6 +123,17 @@ module Jade
114
123
  # still holds free vars (`Decodable(List(a))`) surfaces its deps as
115
124
  # markers of their own — unindexed, since they occupy no slot in
116
125
  # this call's dictionary list.
126
+ # A call through a local binding or a lambda has no name to give a
127
+ # check, which asks by qualified name.
128
+ def callee_name(callee)
129
+ case callee
130
+ in AST::VariableReference(symbol: Symbol::Variable) then nil
131
+ in AST::VariableReference(symbol:) then symbol.qualified_name
132
+ in AST::QualifiedAccess(symbol:) then symbol.qualified_name
133
+ else nil
134
+ end
135
+ end
136
+
117
137
  def propagate(constraint, registry, entry_name)
118
138
  return [constraint] if constraint.type.is_a?(Type::Var)
119
139
  return [] if constraint.unbound_vars.empty?
data/lib/jade/task.rb CHANGED
@@ -2,101 +2,121 @@ require 'jade/tasks'
2
2
 
3
3
  module Jade
4
4
  module Task
5
- Literal = Data.define(:result) do
6
- include Task
7
-
8
- def run
9
- result
10
- end
11
- end
12
-
13
- Dispatch = Data.define(:task_def, :args) do
14
- include Task
15
-
16
- def run
17
- Jade::Tasks.dispatch(task_def, *args)
18
- end
19
- end
20
-
21
- Map = Data.define(:task, :fn) do
22
- include Task
23
-
24
- def run
25
- case task.run
26
- in Jade::Result::Ok[value] then Jade::Result::Ok[fn.call(value)]
27
- in Jade::Result::Err => err then err
5
+ # Every node is a value describing work, and `run` drives them from one
6
+ # loop with an explicit stack of continuations. Composing tasks therefore
7
+ # costs heap, not Ruby stack, so a chain built by recursion (a batch loop,
8
+ # a retry) is bounded by memory rather than by SystemStackError.
9
+ def run
10
+ task = self
11
+ pending = []
12
+
13
+ loop do
14
+ case task
15
+ in AndThen[inner, fn]
16
+ task = inner
17
+ pending << [:ok, fn]
18
+
19
+ in OnError[inner, fn]
20
+ task = inner
21
+ pending << [:err, fn]
22
+
23
+ in Map[inner, fn]
24
+ task = inner
25
+ pending << [:map, fn]
26
+
27
+ in MapError[inner, fn]
28
+ task = inner
29
+ pending << [:map_err, fn]
30
+
31
+ in Decoded[inner, ok_decoder, err_decoder]
32
+ task = inner
33
+ pending << [:decode, [ok_decoder, err_decoder]]
34
+
35
+ else
36
+ result = task.step
37
+ return result if pending.empty?
38
+
39
+ task = resume(result, pending)
40
+ return task if task.is_a?(Jade::Result::Ok) || task.is_a?(Jade::Result::Err)
28
41
  end
29
42
  end
30
43
  end
31
44
 
32
- AndThen = Data.define(:task, :fn) do
33
- include Task
34
-
35
- def run
36
- case task.run
37
- in Jade::Result::Ok[value] then fn.call(value).run
38
- in Jade::Result::Err => err then err
39
- end
45
+ private
46
+
47
+ # Applies continuations to a settled result until one of them produces a
48
+ # fresh task to run, or the stack empties.
49
+ def resume(result, pending)
50
+ until pending.empty?
51
+ kind, fn = pending.pop
52
+
53
+ result =
54
+ case [kind, result]
55
+ in [:decode, _] then Decoded.decode_result(result, *fn)
56
+ in [:ok, Jade::Result::Ok[value]] then return fn.call(value)
57
+ in [:err, Jade::Result::Err[error]] then return fn.call(error)
58
+ in [:map, Jade::Result::Ok[value]] then Jade::Result::Ok[fn.call(value)]
59
+ in [:map_err, Jade::Result::Err[error]] then Jade::Result::Err[fn.call(error)]
60
+ else result
61
+ end
40
62
  end
63
+
64
+ result
41
65
  end
42
66
 
43
- OnError = Data.define(:task, :fn) do
67
+ Literal = Data.define(:result) do
44
68
  include Task
45
69
 
46
- def run
47
- case task.run
48
- in Jade::Result::Ok => ok then ok
49
- in Jade::Result::Err[error] then fn.call(error).run
50
- end
70
+ def step
71
+ result
51
72
  end
52
73
  end
53
74
 
54
- MapError = Data.define(:task, :fn) do
75
+ Dispatch = Data.define(:task_def, :args) do
55
76
  include Task
56
77
 
57
- def run
58
- case task.run
59
- in Jade::Result::Ok => ok then ok
60
- in Jade::Result::Err[error] then Jade::Result::Err[fn.call(error)]
61
- end
78
+ def step
79
+ Jade::Tasks.dispatch(task_def, *args)
62
80
  end
63
81
  end
64
82
 
65
83
  Sequence = Data.define(:tasks) do
66
84
  include Task
67
85
 
68
- def run
86
+ def step
69
87
  values = []
88
+
70
89
  tasks.each do |task|
71
90
  case task.run
72
91
  in Jade::Result::Ok[value] then values << value
73
92
  in Jade::Result::Err => err then return err
74
93
  end
75
94
  end
95
+
76
96
  Jade::Result::Ok[values]
77
97
  end
78
98
  end
79
99
 
100
+ Map = Data.define(:task, :fn) { include Task }
101
+ AndThen = Data.define(:task, :fn) { include Task }
102
+ OnError = Data.define(:task, :fn) { include Task }
103
+ MapError = Data.define(:task, :fn) { include Task }
104
+
80
105
  Decoded = Data.define(:task, :ok_decoder, :err_decoder) do
81
106
  include Task
82
107
 
83
- def run
84
- case task.run
85
- in Jade::Result::Ok[value]
86
- Jade::Result::Ok[decode(ok_decoder, value)]
87
- in Jade::Result::Err[error]
88
- Jade::Result::Err[decode(err_decoder, error)]
108
+ def self.decode_result(result, ok_decoder, err_decoder)
109
+ case result
110
+ in Jade::Result::Ok[value] then Jade::Result::Ok[decode(ok_decoder, value)]
111
+ in Jade::Result::Err[error] then Jade::Result::Err[decode(err_decoder, error)]
89
112
  end
90
113
  end
91
114
 
92
- private
93
-
94
- def decode(decoder, value)
115
+ def self.decode(decoder, value)
95
116
  Jade::Decode::Runner.run!(decoder, value) do |error|
96
117
  fail Jade::Interop::DecodeError.new(error, value, source: :port_return)
97
118
  end
98
119
  end
99
120
  end
100
-
101
121
  end
102
122
  end
data/lib/jade/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.8.0'
2
+ VERSION = '0.9.0'
3
3
  end
data/lib/jade.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'jade/version'
2
+ require 'jade/extensions'
2
3
  require 'jade/project'
3
4
  require 'jade/did_you_mean'
4
5
  require 'jade/symbol'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jade-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agustin Cornu
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-19 00:00:00.000000000 Z
10
+ date: 2026-08-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -93,6 +93,7 @@ files:
93
93
  - lib/jade/did_you_mean.rb
94
94
  - lib/jade/entry.rb
95
95
  - lib/jade/error.rb
96
+ - lib/jade/extensions.rb
96
97
  - lib/jade/formatter.rb
97
98
  - lib/jade/formatter/accesses.rb
98
99
  - lib/jade/formatter/bindings.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/jade/frontend/semantic_analysis/error/constructor_pattern_arity_mismatch.rb
158
159
  - lib/jade/frontend/semantic_analysis/error/duplicate_field.rb
159
160
  - lib/jade/frontend/semantic_analysis/error/duplicate_function_declaration.rb
161
+ - lib/jade/frontend/semantic_analysis/error/duplicate_implementation.rb
160
162
  - lib/jade/frontend/semantic_analysis/error/duplicate_record_field.rb
161
163
  - lib/jade/frontend/semantic_analysis/error/invalid_list_rest_pattern.rb
162
164
  - lib/jade/frontend/semantic_analysis/error/kwargs_on_non_constructor.rb
@@ -218,7 +220,6 @@ files:
218
220
  - lib/jade/frontend/type_checking/canonicalize.rb
219
221
  - lib/jade/frontend/type_checking/constraints.rb
220
222
  - lib/jade/frontend/type_checking/constraints/deriving.rb
221
- - lib/jade/frontend/type_checking/constraints/deriving/assignable.rb
222
223
  - lib/jade/frontend/type_checking/constraints/deriving/decodable.rb
223
224
  - lib/jade/frontend/type_checking/constraints/deriving/encodable.rb
224
225
  - lib/jade/frontend/type_checking/constraints/deriving/eq.rb
@@ -1,170 +0,0 @@
1
- module Jade
2
- module Frontend
3
- module TypeChecking
4
- module Constraints
5
- module Deriving
6
- # jade-sql's interface, derived here rather than there: the
7
- # deriving framework is not extensible, and this stays inert
8
- # without jade-sql, since nothing else names the interface.
9
- module Assignable
10
- extend self
11
- include Helpers
12
-
13
- INTERFACE = 'Sql.Assignable'
14
- ASSIGNMENT = 'Sql.Assignment'
15
- ASSIGNMENT_FIELDS = %w[col value_sql params].freeze
16
-
17
- def supports?(interface) = interface == INTERFACE
18
-
19
- def derive(constraint, registry, entry_name, &lookup)
20
- return failed(constraint, entry_name) unless assignment_matches?(registry)
21
-
22
- case constraint.type
23
- in Type::Application(constructor: Type::Constructor(name:), args:)
24
- Symbol
25
- .type_ref_from_qualified_name(name)
26
- .then { registry.lookup(it) }
27
- .then { derive_for(constraint, it, args, registry, lookup, entry_name) }
28
-
29
- else
30
- failed(constraint, entry_name)
31
- end
32
- end
33
-
34
- private
35
-
36
- # The interface is matched by name, so some other module called
37
- # `Sql` would otherwise be derived against as though it were
38
- # jade-sql — building its Assignment with the wrong arity, and
39
- # failing at runtime rather than here.
40
- def assignment_matches?(registry)
41
- Symbol
42
- .type_ref_from_qualified_name(ASSIGNMENT)
43
- .then { registry.lookup(it) }
44
- .then do
45
- it in Symbol::Struct(record_type: { fields: }) and
46
- fields.keys.map(&:to_s) == ASSIGNMENT_FIELDS
47
- end
48
- end
49
-
50
- def derive_for(constraint, symbol, args, registry, lookup, entry_name)
51
- case symbol
52
- in Symbol::Union if args.empty? && single_payload?(symbol, registry)
53
- derive_union(constraint, symbol, registry, lookup, entry_name)
54
-
55
- in Symbol::Struct
56
- derive_struct(constraint, symbol, args, registry, lookup, entry_name)
57
-
58
- else
59
- failed(constraint, entry_name)
60
- end
61
- end
62
-
63
- # One column per variant, so each variant carries exactly the
64
- # value that column is set to.
65
- def single_payload?(union_sym, registry)
66
- variants(union_sym, registry)
67
- .then { it.any? && it.all? { it.args.size == 1 } }
68
- end
69
-
70
- def derive_union(constraint, union_sym, registry, lookup, entry_name)
71
- vs = variants(union_sym, registry)
72
-
73
- vs
74
- .map { encodable_dep(it, registry) }
75
- .map { lookup.call(it) }
76
- .then { Results.sequence(it) }
77
- .map { implementation(constraint, union_body(vs), it) }
78
- end
79
-
80
- def derive_struct(constraint, struct_sym, args, registry, lookup, entry_name)
81
- fields = struct_fields(struct_sym, args, registry)
82
-
83
- fields
84
- .map { |_, type| Type.constraint('Encode.Encodable', type, nil) }
85
- .map { lookup.call(it) }
86
- .then { Results.sequence(it) }
87
- .map { implementation(constraint, struct_body(fields), it) }
88
- end
89
-
90
- def struct_body(fields)
91
- fields
92
- .each_with_index
93
- .map { |(name, _), idx| field_assignment(name, idx) }
94
- .then { [:list, it] }
95
- end
96
-
97
- def field_assignment(name, idx)
98
- [:call,
99
- [:struct_constructor, ASSIGNMENT, 3],
100
- [
101
- column_name(name),
102
- '?',
103
- [:list,
104
- [[:call, [:impl_arg, idx, 'encoder'], [[:access, [:var, 'f'], name.to_s]]]],
105
- ],
106
- ],
107
- ]
108
- end
109
-
110
- def column_name(field)
111
- field
112
- .to_s
113
- .then { it.end_with?('_') ? it.delete_suffix('_') : it }
114
- .then { Lexer::KEYWORDS.include?(it) ? it : field.to_s }
115
- end
116
-
117
- def encodable_dep(variant, registry)
118
- variant
119
- .args
120
- .first
121
- .then { instantiate(it, {}, registry) }
122
- .then { Type.constraint('Encode.Encodable', it, nil) }
123
- end
124
-
125
- def union_body(variants)
126
- variants
127
- .each_with_index
128
- .map { |v, idx| [[:constructor, v.qualified_name, ['x']], [assignment(v, idx)]] }
129
- .then { [:case, [:var, 'f'], it] }
130
- end
131
-
132
- def assignment(variant, idx)
133
- [:call,
134
- [:struct_constructor, ASSIGNMENT, 3],
135
- [
136
- wire_name(variant),
137
- '?',
138
- [:list, [[:call, [:impl_arg, idx, 'encoder'], [[:var, 'x']]]]],
139
- ],
140
- ]
141
- .then { [:list, [it]] }
142
- end
143
-
144
- def failed(constraint, entry_name)
145
- Err[
146
- Error::DerivationFailed.new(
147
- entry_name, constraint.origin&.range, constraint:, trace: [],
148
- )
149
- ]
150
- end
151
-
152
- def implementation(constraint, body, deps)
153
- Symbol::Implementation.new(
154
- module_name: nil,
155
- interface: Symbol.type_ref_from_qualified_name(constraint.interface),
156
- type: constraint.type,
157
- type_params: [],
158
- constraints: [],
159
- functions: { 'to_assigns' => Symbol::DerivedFunction.new(params: ['f'], body:) },
160
- deps:,
161
- extends: [],
162
- decl_span: nil,
163
- )
164
- end
165
- end
166
- end
167
- end
168
- end
169
- end
170
- end