jade-lang 0.4.0 → 0.6.0

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