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
@@ -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
@@ -82,14 +82,14 @@ module Jade
82
82
  def derive_struct(constraint, struct_sym, type_args, registry, lookup, entry_name)
83
83
  fields = struct_fields(struct_sym, type_args, registry)
84
84
 
85
- [:struct_constructor, struct_sym.qualified_name, fields.size]
85
+ [:struct_class, struct_sym.qualified_name]
86
86
  .then { derive_record(constraint, fields, it, lookup, entry_name) }
87
87
  end
88
88
 
89
89
  def derive_anonymous_record(constraint, fields, lookup, entry_name)
90
90
  keys = fields.keys.map(&:to_s)
91
91
 
92
- [:anon_record_constructor, keys]
92
+ [:anon_record_class, keys]
93
93
  .then { derive_record(constraint, fields.to_a, it, lookup, entry_name) }
94
94
  end
95
95
 
@@ -121,19 +121,14 @@ module Jade
121
121
  end
122
122
 
123
123
  def record_body(fields, constructor_ref)
124
- seed = [:call, [:stdlib_fn, 'Decode.succeed'], [constructor_ref]]
125
-
126
- fields.each_with_index.reduce(seed) do |acc, ((field_name, _), idx)|
127
- field_decoder = [:call,
128
- [:stdlib_fn, 'Decode.field'],
129
- [
130
- field_name.to_s,
131
- [:impl_arg, idx, 'decoder'],
132
- ],
133
- ]
134
-
135
- [:call, [:stdlib_fn, 'Decode.and_map'], [acc, field_decoder]]
136
- 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
+ ]
137
132
  end
138
133
 
139
134
  # Free-var inner constraints fall back to the constraint marker
@@ -97,27 +97,18 @@ module Jade
97
97
  field_deps = fields
98
98
  .map { |_, field_type| Type.constraint(INTERFACE, field_type, nil) }
99
99
 
100
- pair_irs = fields
100
+ body = fields
101
101
  .each_with_index
102
102
  .map do |(field_name, _), idx|
103
- [:call,
104
- [:stdlib_fn, 'Tuple.pair'],
105
- [
106
- field_name.to_s,
107
- [:call,
108
- [:impl_arg, idx, 'encoder'],
109
- [[:access, [:var, 'rec'], field_name.to_s]],
110
- ],
103
+ [
104
+ field_name.to_s,
105
+ [:call,
106
+ [:impl_arg, idx, 'encoder'],
107
+ [[:access, [:var, 'rec'], field_name.to_s]],
111
108
  ],
112
109
  ]
113
110
  end
114
-
115
- body = [:call,
116
- [:stdlib_fn, 'Encode.object'],
117
- [
118
- [:list, pair_irs],
119
- ],
120
- ]
111
+ .then { [:hash, it] }
121
112
 
122
113
  field_deps
123
114
  .map { lookup.call(it) }
@@ -12,9 +12,8 @@ module Jade
12
12
  # weight at the boundary because failure always raises anyway. Skipping
13
13
  # it removes one allocation per arg per Ruby→Jade call.
14
14
  def decode_or_raise(decoder, value)
15
- case Jade::Decode::Runner.run(decoder, value)
16
- in Jade::Result::Ok[v] then v
17
- in Jade::Result::Err[e] then raise Jade::Interop::DecodeError.new(e, value)
15
+ Jade::Decode::Runner.run!(decoder, value) do |error|
16
+ raise Jade::Interop::DecodeError.new(error, value)
18
17
  end
19
18
  end
20
19
 
@@ -51,12 +50,21 @@ module Jade
51
50
 
52
51
  def hash(label, v)
53
52
  case v
54
- when ::Hash then v
53
+ when ::Hash then string_keyed!(label, v)
55
54
  when ::Data then v.to_h.transform_keys(&:to_s)
56
55
  else type_error!(label, v)
57
56
  end
58
57
  end
59
58
 
59
+ # Mixed keys are a real shape mismatch; the per-field errors say more
60
+ # than a guess about intent would.
61
+ def string_keyed!(label, v)
62
+ return v if v.empty? || v.keys.any?(::String)
63
+
64
+ v.keys.grep(::Symbol)
65
+ .then { it.empty? ? v : raise(Jade::Interop::SymbolKeys.new(label, it)) }
66
+ end
67
+
60
68
  def type_error!(label, v)
61
69
  raise Jade::Interop::DecodeError.new(
62
70
  Jade::Decode::WrongType[label, Jade::Decode.type_name(v)],
@@ -40,12 +40,31 @@ module Jade
40
40
  end
41
41
  end
42
42
 
43
+ # Caught where the whole hash is in scope; downstream each field just
44
+ # reads nil and blames its own type.
45
+ class SymbolKeys < Error
46
+ def initialize(label, keys)
47
+ super(
48
+ "#{label} expects a Hash with string keys, got symbol keys " \
49
+ "(#{keys.take(4).map(&:inspect).join(', ')}). " \
50
+ "Values cross the boundary as wire data — " \
51
+ "try #{keys.first.to_s.inspect} rather than #{keys.first.inspect}."
52
+ )
53
+ end
54
+ end
55
+
43
56
  class DecodeError < Error
44
- attr_reader :decode_error, :value
57
+ attr_reader :decode_error, :value, :source
58
+
59
+ SUBJECTS = {
60
+ argument: 'Ruby passed a value that failed to decode',
61
+ port_return: 'Port returned a value that failed to decode',
62
+ }.freeze
45
63
 
46
- def initialize(decode_error, value)
64
+ def initialize(decode_error, value, source: :argument)
47
65
  @decode_error = decode_error
48
66
  @value = value
67
+ @source = source
49
68
  super(format(decode_error, value))
50
69
  end
51
70
 
@@ -54,16 +73,17 @@ module Jade
54
73
  def format(error, value)
55
74
  path, leaf = unwind(error)
56
75
  location = path.empty? ? "value" : path.join
76
+ subject = SUBJECTS.fetch(@source)
57
77
 
58
78
  case leaf
59
79
  in Jade::Decode::WrongType[expected, got]
60
- "Port returned a value that failed to decode at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
80
+ "#{subject} at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
61
81
 
62
82
  in Jade::Decode::MissingField[key]
63
- "Port returned a value that failed to decode at #{location}: missing field `#{key}` (#{value.inspect})"
83
+ "#{subject} at #{location}: missing field `#{key}` (#{value.inspect})"
64
84
 
65
85
  in Jade::Decode::Custom[msg]
66
- "Port returned a value that failed to decode at #{location}: #{msg} (#{value.inspect})"
86
+ "#{subject} at #{location}: #{msg} (#{value.inspect})"
67
87
 
68
88
  in Jade::Decode::Multiple[errors]
69
89
  errors
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'jade/signature'
2
3
 
3
4
  module Jade
4
5
  module LSP
@@ -432,21 +433,7 @@ module Jade
432
433
  end
433
434
 
434
435
  def render_signature(name, type, constraints)
435
- "#{name} : #{constraint_prefix(constraints)}#{type}"
436
- end
437
-
438
- def constraint_prefix(constraints)
439
- return '' if constraints.empty?
440
-
441
- constraints
442
- .map { short_constraint(it) }
443
- .uniq
444
- .join(', ')
445
- .then { "#{it} => " }
446
- end
447
-
448
- def short_constraint(constraint)
449
- "#{constraint.interface.split('.').last} #{constraint.type}"
436
+ Signature.render(name, type, constraints)
450
437
  end
451
438
 
452
439
  # First node in the path (innermost-to-outermost) that resolves to a
@@ -1,7 +1,14 @@
1
1
  module Jade
2
2
  module LSP
3
3
  module Snippets
4
- Snippet = Data.define(:label, :detail, :body)
4
+ TAB_STOP = /\$\{\d+:?([^}]*)\}/
5
+
6
+ Snippet = Data.define(:label, :detail, :body) do
7
+ # Keeps the hint text, drops the tab stops an editor needs.
8
+ def source
9
+ body.gsub(TAB_STOP, '\1')
10
+ end
11
+ end
5
12
 
6
13
  ALL = [
7
14
  Snippet.new(
@@ -33,11 +40,11 @@ module Jade
33
40
  ),
34
41
  Snippet.new(
35
42
  label: 'case',
36
- detail: 'case expression with else fallback',
43
+ detail: 'case expression, one branch per variant',
37
44
  body: <<~SNIP.chomp,
38
45
  case ${1:expr}
39
46
  in ${2:pattern} then ${3:result}
40
- else ${0:fallback}
47
+ in ${4:pattern} then ${0:result}
41
48
  end
42
49
  SNIP
43
50
  ),
@@ -95,6 +102,17 @@ module Jade
95
102
  body: '(${1:args}) -> { ${0:body} }',
96
103
  ),
97
104
  ].freeze
105
+
106
+ module_function
107
+
108
+ # Every form, as plain Jade — what `jade q syntax` serves.
109
+ def catalog
110
+ ALL.map { { form: it.label, detail: it.detail, source: it.source } }
111
+ end
112
+
113
+ def find(form)
114
+ catalog.find { it[:form] == form }
115
+ end
98
116
  end
99
117
  end
100
118
  end
@@ -0,0 +1,46 @@
1
+ module Jade
2
+ module ModuleLoader
3
+ # A module is keyed by the name its file implies — imports resolve
4
+ # `Shop.Cart` to `shop/cart.jd` and back. The wrapper takes the declared
5
+ # name while every type reference inside takes the file's, so a mismatch
6
+ # emits Ruby referring to a constant that was never defined.
7
+ module ModuleName
8
+ extend self
9
+
10
+ def check(ast, source)
11
+ return Ok[ast] unless ast.is_a?(AST::Module)
12
+
13
+ source.to_module_name.then do |expected|
14
+ ast.name == expected ? Ok[ast] : Err[mismatch(ast, source, expected)]
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def mismatch(ast, source, expected)
21
+ Diagnostics::List
22
+ .empty
23
+ .error(
24
+ "Module is declared as `#{ast.name}` but `#{source.uri}` defines `#{expected}`.",
25
+ source:,
26
+ span: name_span(ast),
27
+ label: "expected `#{expected}`",
28
+ )
29
+ .help("Rename it to `#{expected}`, or move the file to `#{path_for(ast.name)}`.")
30
+ end
31
+
32
+ # `module` is skipped during parsing, so the node starts at the name.
33
+ def name_span(ast)
34
+ ast.range.begin...(ast.range.begin + ast.name.length)
35
+ end
36
+
37
+ def path_for(module_name)
38
+ module_name
39
+ .split('.')
40
+ .map { Source.snake_case(it) }
41
+ .join('/')
42
+ .then { "#{it}.jd" }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -9,6 +9,7 @@ require 'jade/codegen'
9
9
  require 'jade/module_loader/cache'
10
10
  require 'jade/module_loader/dependency_resolver'
11
11
  require 'jade/module_loader/dependency_graph'
12
+ require 'jade/module_loader/module_name'
12
13
  require 'jade/module_loader/normalize'
13
14
  require 'jade/module_loader/topological_sort'
14
15
  require 'jade/diagnostics'
@@ -132,6 +133,10 @@ module Jade
132
133
  .then { raise CompilationError.new(it) }
133
134
  end => Ok([raw_ast, comments])
134
135
 
136
+ ModuleName
137
+ .check(raw_ast, source)
138
+ .on_err { raise CompilationError.new(it) }
139
+
135
140
  Frontend::CommentAttacher
136
141
  .attach(raw_ast, comments, source)
137
142
  .then { Registry.entry(source.to_module_name).with(ast: it, source:, entry:) }
@@ -0,0 +1,47 @@
1
+ require 'jade/frontend/type_checking/substitution'
2
+
3
+ module Jade
4
+ # One-line rendering of a name and its type, shared by hover and `q api`.
5
+ module Signature
6
+ extend self
7
+
8
+ LETTERS = ('a'..'z').to_a.freeze
9
+
10
+ def render(name, type, constraints)
11
+ naming(type, constraints)
12
+ .then { |sub| [sub.apply(type), constraints.map { sub.apply(it) }] }
13
+ .then { |(renamed, cs)| "#{name} : #{constraint_prefix(cs)}#{renamed}" }
14
+ end
15
+
16
+ # Vars compare by id but print as their name, which nothing keeps unique.
17
+ def naming(type, constraints)
18
+ (type.unbound_vars + constraints.flat_map(&:unbound_vars))
19
+ .uniq(&:id)
20
+ .reduce([{}, []]) do |(mappings, taken), var|
21
+ display_name(var, taken)
22
+ .then { [mappings.merge(var.id => Type.var(var.id, it)), taken + [it]] }
23
+ end
24
+ .then { |(mappings, _)| Frontend::TypeChecking::Substitution[mappings] }
25
+ end
26
+
27
+ def display_name(var, taken)
28
+ return var.name if var.name && !taken.include?(var.name)
29
+
30
+ (LETTERS - taken).first || "#{var.name}#{taken.size}"
31
+ end
32
+
33
+ def constraint_prefix(constraints)
34
+ return '' if constraints.empty?
35
+
36
+ constraints
37
+ .map { short_constraint(it) }
38
+ .uniq
39
+ .join(', ')
40
+ .then { "#{it} => " }
41
+ end
42
+
43
+ def short_constraint(constraint)
44
+ "#{constraint.interface.split('.').last} #{constraint.type}"
45
+ end
46
+ end
47
+ end
@@ -317,24 +317,25 @@ module Jade
317
317
  end
318
318
 
319
319
 
320
+ # Days counted in 400-year eras from March, so the leap day lands
321
+ # at the end of an era and month lengths repeat; `mp` is the month
322
+ # in that March-based year, shifted back at the end. Int division
323
+ # floors, which is what makes it hold before year 1.
320
324
  def from_rata_die(rd: Int) -> Date
321
- y = bump_year(rd, (rd - 1) / 366 + 1)
322
- doy = rd - days_before_year(y)
323
- case search_month(y, doy, 12)
324
- in (m, d) then from_calendar_date(y, m, d)
325
- end
326
- end
327
-
328
-
329
- def bump_year(rd: Int, y: Int) -> Int
330
- days_before_year(y + 1) < rd ? bump_year(rd, y + 1) : y
331
- end
325
+ z = rd + 305
326
+ era = z / 146097
327
+ doe = z - era * 146097
328
+ yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
329
+ doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
330
+ mp = (5 * doy + 2) / 153
331
+ d = doy - (153 * mp + 2) / 5 + 1
332
+ mi = mp < 10 ? mp + 3 : mp - 9
332
333
 
333
-
334
- def search_month(y: Int, doy: Int, mi: Int) -> (Month, Int)
335
- m = month_from_int(mi)
336
- offset = days_before_month(y, m)
337
- doy > offset ? (m, doy - offset) : search_month(y, doy, mi - 1)
334
+ from_calendar_date(
335
+ yoe + era * 400 + (mi <= 2 ? 1 : 0),
336
+ month_from_int(mi),
337
+ d,
338
+ )
338
339
  end
339
340
 
340
341
 
@@ -390,9 +391,9 @@ module Jade
390
391
  end
391
392
 
392
393
 
393
- implements Decodable(Date) with
394
- decoder: -> { Decode.string |> Decode.and_then(parse_date) }
395
- end
394
+ # Decodable(Date) is registered by Decode, which reads this same
395
+ # wire form in Ruby. `from_iso_string` stays exposed and is what
396
+ # the native parser is checked against.
396
397
 
397
398
 
398
399
  implements Encodable(Date) with
@@ -154,7 +154,7 @@ module Jade
154
154
 
155
155
  def on_date(i: Instant) -> Calendar.Date
156
156
  Instant(ms) = i
157
- Calendar.from_rata_die(floor_div(ms, 86400000) + 719163)
157
+ Calendar.from_rata_die(ms / 86400000 + 719163)
158
158
  end
159
159
 
160
160
 
@@ -175,12 +175,6 @@ module Jade
175
175
  end
176
176
 
177
177
 
178
- def floor_div(a: Int, b: Int) -> Int
179
- q = a / b
180
- a < 0 && mod(a, b) != 0 ? q - 1 : q
181
- end
182
-
183
-
184
178
  def to_iso(i: Instant) -> String
185
179
  d = on_date(i)
186
180
  tod = at_time(i)
@@ -317,14 +311,14 @@ module Jade
317
311
  end
318
312
 
319
313
 
320
- implements Decodable(Instant) with
321
- decoder: -> { Decode.string |> Decode.and_then(parse_instant) }
322
- end
314
+ # Decodable(Instant) is registered by Decode, which reads this
315
+ # same wire form in Ruby. `from_iso` is still the parser of
316
+ # record and stays exposed; the two are differential-tested.
323
317
 
324
318
 
325
- implements Encodable(Instant) with
326
- encoder: (i) -> { Encode.string(to_iso(i)) }
327
- end
319
+ # Encodable(Instant) is registered by Encode, which writes this
320
+ # same string in Ruby. `to_iso` stays exposed and is what the
321
+ # native writer is differential-tested against.
328
322
 
329
323
 
330
324
  implements Decodable(Duration) with
@@ -208,22 +208,9 @@ module Jade
208
208
  end
209
209
 
210
210
 
211
- def from_wire(s: String) -> Decoder(Decimal)
212
- case String.split(s, "e")
213
- in [m, e]
214
- case (String.to_int(m), String.to_int(e))
215
- in (Just(mi), Just(ei)) then Decode.succeed(of(mi, ei))
216
- else Decode.fail("invalid decimal: " ++ s)
217
- end
218
-
219
- else Decode.fail("invalid decimal: " ++ s)
220
- end
221
- end
222
-
223
-
224
- implements Decodable(Decimal) with
225
- decoder: -> { Decode.string |> Decode.and_then(from_wire) }
226
- end
211
+ # Decodable(Decimal) is registered by Decode itself, which reads
212
+ # this same wire form in Ruby — a numeric column decodes about
213
+ # three times faster than the Jade version of the parse did.
227
214
 
228
215
 
229
216
  implements Encodable(Decimal) with
@@ -1,5 +1,6 @@
1
1
  require 'jade/stdlib/intrinsics'
2
2
  require 'jade/decode'
3
+ require 'jade/stdlib/text'
3
4
 
4
5
  module Jade
5
6
  module Stdlib
@@ -148,6 +149,21 @@ module Jade
148
149
  Jade::Decode::Decoder[Jade::Decode::Desc::AndMap[wrapped.desc, decoder.desc]]
149
150
  }
150
151
 
152
+ # `keys` and `decoders` are positionally paired; `ctor` takes one
153
+ # argument per key. Private because its type cannot be spelled — the
154
+ # arity of `ctor` is only known to the deriver.
155
+ function(
156
+ 'record',
157
+ { keys: 'List(String)', decoders: 'List(Decoder(a))', ctor: 'b' },
158
+ 'Decoder(b)',
159
+ private: true,
160
+ ) { |keys, decoders, ctor|
161
+ keys
162
+ .zip(decoders)
163
+ .map { |key, decoder| Jade::Decode::Desc::RecordField[key, key.to_sym, decoder.desc] }
164
+ .then { Jade::Decode::Decoder[Jade::Decode::Desc::Record[it, ctor]] }
165
+ }
166
+
151
167
  function(
152
168
  'required',
153
169
  { wrapped: 'Decoder(a -> b)', key: 'String', field_decoder: 'Decoder(a)' },
@@ -312,9 +328,7 @@ module Jade
312
328
  { a: 'Decoder(a)', b: 'Decoder(b)' },
313
329
  'Decoder(Tuple2(a, b))',
314
330
  ) { |da, db|
315
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple2.method(:[]).curry(2)]
316
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
317
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
331
+ Jade::Decode::Desc::Indexed[[da.desc, db.desc], Jade::Tuple::Tuple2]
318
332
  .then { Jade::Decode::Decoder[it] }
319
333
  }
320
334
 
@@ -323,10 +337,7 @@ module Jade
323
337
  { a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)' },
324
338
  'Decoder(Tuple3(a, b, c))',
325
339
  ) { |da, db, dc|
326
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple3.method(:[]).curry(3)]
327
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
328
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
329
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
340
+ Jade::Decode::Desc::Indexed[[da.desc, db.desc, dc.desc], Jade::Tuple::Tuple3]
330
341
  .then { Jade::Decode::Decoder[it] }
331
342
  }
332
343
 
@@ -335,11 +346,7 @@ module Jade
335
346
  { a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)', d: 'Decoder(d)' },
336
347
  'Decoder(Tuple4(a, b, c, d))',
337
348
  ) { |da, db, dc, dd|
338
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple4.method(:[]).curry(4)]
339
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
340
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
341
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
342
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[3, dd.desc]] }
349
+ Jade::Decode::Desc::Indexed[[da.desc, db.desc, dc.desc, dd.desc], Jade::Tuple::Tuple4]
343
350
  .then { Jade::Decode::Decoder[it] }
344
351
  }
345
352
 
@@ -350,6 +357,22 @@ module Jade
350
357
  implementation('Decodable', 'Basics.Bool', 'decoder' => 'bool')
351
358
  implementation('Decodable', 'String.String', 'decoder' => 'string')
352
359
  implementation('Decodable', 'Decode.Value', 'decoder' => 'value')
360
+
361
+ # Registered here, not by the modules owning the types: they import
362
+ # Decode, so the instance cannot live the other way round. Private
363
+ # for the same reason `record` is — Decode cannot name those types
364
+ # back, and `Decoder(a)` would be a hole if anything could reach it.
365
+ {
366
+ 'decimal' => [Stdlib::Text.method(:to_decimal), 'Decimal', 'Decimal.Decimal'],
367
+ 'instant' => [Stdlib::Text.method(:to_instant), 'Instant', 'Clock.Instant'],
368
+ 'iso_date' => [Stdlib::Text.method(:to_date), 'Date', 'Calendar.Date'],
369
+ }.each do |fn_name, (parse, label, type_name)|
370
+ function(fn_name, {}, 'Decoder(a)', private: true) {
371
+ Jade::Decode::Decoder[Jade::Decode::Desc::FromString[label, parse]]
372
+ }
373
+
374
+ implementation('Decodable', type_name, 'decoder' => fn_name)
375
+ end
353
376
  end
354
377
  end
355
378
  end
@@ -1,5 +1,6 @@
1
1
  require 'json'
2
2
  require 'jade/stdlib/intrinsics'
3
+ require 'jade/stdlib/text'
3
4
 
4
5
  module Jade
5
6
  module Stdlib
@@ -155,6 +156,14 @@ module Jade
155
156
  implementation('Encodable', 'Basics.Bool', 'encoder' => 'bool')
156
157
  implementation('Encodable', 'String.String', 'encoder' => 'string')
157
158
  implementation('Encodable', 'Decode.Value', 'encoder' => 'value')
159
+
160
+ # Registered here because Clock imports Encode, so the instance
161
+ # cannot live the other way round. `Stdlib::Text` writes the string.
162
+ function('instant', { i: 'a' }, 'Value', private: true) { |i|
163
+ Stdlib::Text.from_instant(i)
164
+ }
165
+
166
+ implementation('Encodable', 'Clock.Instant', 'encoder' => 'instant')
158
167
  end
159
168
  end
160
169
  end
@@ -76,10 +76,17 @@ module Jade
76
76
  constraints:,
77
77
  )
78
78
  .with(module_name:)
79
- .tap { store(it) unless private }
79
+ .tap { store(it) }
80
+ .tap { private_names << name.to_s if private }
80
81
  .tap { Runtime.register(qualified_fn_name, &impl) }
81
82
  end
82
83
 
84
+ # Private means defined but not exposed: codegen and `implementation`
85
+ # resolve it, user modules cannot name it.
86
+ def private_names
87
+ @private_names ||= []
88
+ end
89
+
83
90
  # Approach A: the inline template is the single source of truth for a
84
91
  # function's behaviour. A hand-written block always wins. With no block
85
92
  # and a `body:` codegen override the function is codegen/dictionary-only
@@ -219,6 +226,7 @@ module Jade
219
226
  def exposes
220
227
  @symbols
221
228
  .reject { it.is_a?(Symbol::Implementation) }
229
+ .reject { it.is_a?(Symbol::StdlibFunction) && private_names.include?(it.name) }
222
230
  .map { it.to_ref }
223
231
  end
224
232
 
@@ -111,7 +111,7 @@ module Jade
111
111
  def on_error(result: Result(a, e), fn: e -> Result(a, f)) -> Result(a, f)
112
112
  case result
113
113
  in Err(error) then error |> fn
114
- in Ok(_) then result
114
+ in Ok(value) then Ok(value)
115
115
  end
116
116
  end
117
117