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
@@ -2,15 +2,18 @@ require 'jade/type'
2
2
  require 'jade/frontend/type_checking/constraints'
3
3
  require 'jade/frontend/type_checking/var_gen'
4
4
  require 'jade/frontend/type_checking/error/port_not_decodable'
5
+ require 'jade/frontend/type_checking/error/port_not_encodable'
5
6
 
6
7
  module Jade
7
8
  module Frontend
8
9
  module TypeChecking
9
- # Resolves the Decode.Decodable instances each port needs for its ok/err
10
- # arms. Runs at the end of type-checking, when registry.implementations
11
- # is fully populated. The resolved Symbol::Implementation (or the :pass
12
- # sentinel for Decode.Value / Never) is stamped onto each
13
- # InteropFunction's `decoders` field so codegen can emit straight away.
10
+ # Resolves the instances each port needs to convert at the boundary:
11
+ # Decode.Decodable for its ok/err arms, Encode.Encodable for its
12
+ # arguments. Runs at the end of type-checking, when
13
+ # registry.implementations is fully populated. The resolved
14
+ # Symbol::Implementation (or the :pass sentinel for Decode.Value / Never)
15
+ # is stamped onto each InteropFunction's `decoders` / `encoders` fields
16
+ # so codegen can emit straight away.
14
17
  module PortResolution
15
18
  extend self
16
19
 
@@ -46,28 +49,61 @@ module Jade
46
49
  def resolve_port(interop_fn, entry, registry)
47
50
  interop_fn.return_type => Symbol::TypeApplication(args: [ok_sym, err_sym])
48
51
 
49
- # Single Type.from_symbol on the whole return so a var that appears
50
- # in both arms gets the same Type::Var id. PortDecoder relies on
51
- # those ids to build the call-site synthetic dict_env.
52
+ # Single Type.from_symbol over the whole port so a var appearing in
53
+ # more than one position gets the same Type::Var id. PortCodec relies
54
+ # on those ids to build the call-site synthetic dict_env.
52
55
  Type
53
- .from_symbol(interop_fn.return_type, registry, VarGen.new)
54
- .first => Type::Application(args: [ok_type, err_type])
56
+ .from_symbol(port_symbol(interop_fn), registry, VarGen.new)
57
+ .first => Type::Function(args: param_types, return_type: Type::Application(args: [ok_type, err_type]))
55
58
 
56
59
  ok, ok_errors = resolve_arm(ok_sym, ok_type, interop_fn, :ok, entry, registry)
57
60
  err, err_errors = resolve_arm(err_sym, err_type, interop_fn, :err, entry, registry)
61
+ encoders, param_errors = resolve_params(interop_fn, param_types, entry, registry)
58
62
 
59
63
  [
60
- interop_fn.with(decoders: { ok:, err: }),
61
- ok_errors + err_errors,
64
+ interop_fn.with(decoders: { ok:, err: }, encoders:),
65
+ ok_errors + err_errors + param_errors,
62
66
  ]
63
67
  end
64
68
 
69
+ # Without the constraints the shared VarGen would still hand every
70
+ # position its own var; `from_symbol` only ties them together through
71
+ # the map it threads across params and return.
72
+ def port_symbol(interop_fn)
73
+ Symbol.function_type(interop_fn.params, interop_fn.return_type)
74
+ end
75
+
76
+ def resolve_params(interop_fn, param_types, entry, registry)
77
+ interop_fn
78
+ .params
79
+ .zip(param_types)
80
+ .each_with_index
81
+ .map { |(sym, type), index| resolve_param(sym, type, interop_fn, index, entry, registry) }
82
+ .then { |results| [results.map(&:first), results.flat_map(&:last)] }
83
+ end
84
+
85
+ def resolve_param(type_sym, type, interop_fn, index, entry, registry)
86
+ return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
87
+
88
+ case type
89
+ in Type::Var(name:)
90
+ constraint_index_for(interop_fn, 'Encode.Encodable', name)
91
+ .then { [Symbol::InteropFunction::Dict.new(constraint_index: it), []] }
92
+
93
+ else
94
+ Type
95
+ .constraint('Encode.Encodable', type, nil)
96
+ .then { Constraints.resolve(it, registry, entry.name) }
97
+ .then { encoder_result(it, interop_fn, index, entry, type, span_of(type_sym, interop_fn)) }
98
+ end
99
+ end
100
+
65
101
  def resolve_arm(type_sym, type, interop_fn, arm, entry, registry)
66
102
  return [Symbol::InteropFunction::PASS, []] if pass_through?(type_sym)
67
103
 
68
104
  case type
69
105
  in Type::Var(name:)
70
- constraint_index_for(interop_fn, name)
106
+ constraint_index_for(interop_fn, 'Decode.Decodable', name)
71
107
  .then { [Symbol::InteropFunction::Dict.new(constraint_index: it), []] }
72
108
 
73
109
  else
@@ -81,11 +117,11 @@ module Jade
81
117
  end
82
118
  end
83
119
 
84
- def constraint_index_for(interop_fn, var_name)
120
+ def constraint_index_for(interop_fn, interface, var_name)
85
121
  interop_fn
86
122
  .constraints
87
- .index { |_iface, name| name == var_name }
88
- .tap { fail "no Decodable constraint for #{var_name.inspect}" if it.nil? }
123
+ .index { |iface, name| iface == interface && name == var_name }
124
+ .tap { fail "no #{interface} constraint for #{var_name.inspect}" if it.nil? }
89
125
  end
90
126
 
91
127
  def decoder_result(constraint_result, interop_fn, arm, entry, type, span)
@@ -100,6 +136,18 @@ module Jade
100
136
  end
101
137
  end
102
138
 
139
+ def encoder_result(constraint_result, interop_fn, index, entry, type, span)
140
+ case constraint_result
141
+ in Ok[impl]
142
+ [impl, []]
143
+
144
+ in Err
145
+ Error::PortNotEncodable
146
+ .new(entry, span, port_name: interop_fn.name, position: index + 1, type:)
147
+ .then { [nil, [it]] }
148
+ end
149
+ end
150
+
103
151
  def span_of(type_sym, interop_fn)
104
152
  case type_sym
105
153
  in Symbol::TypeApplication(span:) then span
@@ -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
 
@@ -3,7 +3,7 @@ require 'jade/interop/error'
3
3
  module Jade
4
4
  module Interop
5
5
  module Runtime
6
- def task_call(interop_module_name, function_name, ok_decoder, err_decoder)
6
+ def task_call(interop_module_name, function_name, ok_decoder, err_decoder, arg_encoders)
7
7
  ->(*args) do
8
8
  interop_module_name
9
9
  .send(function_name)
@@ -12,13 +12,21 @@ module Jade
12
12
  fail(Interop::PortNotRegistered.new(interop_module_name, function_name))
13
13
 
14
14
  Jade::Task::Decoded.new(
15
- Jade::Task::Dispatch.new(port, args),
15
+ Jade::Task::Dispatch.new(port, encode_args(args, arg_encoders)),
16
16
  ok_decoder,
17
17
  err_decoder,
18
18
  )
19
19
  end
20
20
  end
21
21
  end
22
+
23
+ # Ports are the boundary in the other direction: what Ruby gets handed is
24
+ # encoded the same way a return value is decoded on the way back.
25
+ def encode_args(args, encoders)
26
+ args
27
+ .each_with_index
28
+ .map { |arg, i| encoders.fetch(i).call(arg) }
29
+ end
22
30
  end
23
31
  end
24
32
  end
@@ -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
@@ -58,11 +58,20 @@ module Jade
58
58
  super
59
59
  end
60
60
 
61
+ # Blocks whose entries are comma-separated, so a newline between two of
62
+ # them reads as the end of the block.
63
+ COMMA_SEPARATED_BLOCKS = [
64
+ 'interop import declaration',
65
+ 'interface declaration',
66
+ 'implementation',
67
+ ].freeze
68
+
61
69
  def hint
62
70
  return leading_pipe_hint if leading_pipe_in_type_decl?
63
71
  return reserved_keyword_hint if reserved_keyword_as_name?
64
72
  return colon_not_eq_hint if eq_where_colon_expected?
65
73
  return record_eq_hint if eq_where_record_pipe_expected?
74
+ return missing_comma_hint if entry_after_unterminated_entry?
66
75
  nil
67
76
  end
68
77
 
@@ -115,6 +124,16 @@ module Jade
115
124
  def record_eq_hint
116
125
  "(record fields use `:`, not `=` — write `{ name: value }`)"
117
126
  end
127
+
128
+ def entry_after_unterminated_entry?
129
+ expected == :end &&
130
+ [:identifier, :lparen].include?(actual.type) &&
131
+ (@context & COMMA_SEPARATED_BLOCKS).any?
132
+ end
133
+
134
+ def missing_comma_hint
135
+ "(entries here are separated by `,` — add one to the line above)"
136
+ end
118
137
  end
119
138
 
120
139
  # Specific case-branch shape: `in <pat> <body>` on the same source
data/lib/jade/runtime.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'jade/debug'
1
2
  require 'base64'
2
3
 
3
4
  require 'jade/interop/runtime'
@@ -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
@@ -0,0 +1,12 @@
1
+ require 'jade/debug'
2
+ require 'jade/stdlib/intrinsics'
3
+
4
+ module Jade
5
+ module Stdlib
6
+ module Debug
7
+ extend Intrinsics
8
+
9
+ function(:log, { label: 'String', value: 'a' }, 'a') { |l, v| Jade::Debug.log(l, v) }
10
+ end
11
+ end
12
+ end
@@ -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
@@ -10,6 +11,7 @@ module Jade
10
11
  import Result
11
12
  import List
12
13
  import Dict
14
+ import Set
13
15
 
14
16
  union :DecodeError
15
17
  variant :MissingField, of: :DecodeError, args: ['String']
@@ -46,6 +48,13 @@ module Jade
46
48
  Jade::Decode::Decoder[Jade::Decode::Desc::Bool[]]
47
49
  }
48
50
 
51
+ # Keeps whatever came in. `Value` is the un-decoded value, so this is
52
+ # what makes it usable nested — `List(Value)`, a field of a struct —
53
+ # and not only as a whole arm.
54
+ function('value', {}, 'Decoder(Value)') {
55
+ Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]
56
+ }
57
+
49
58
  # Structural
50
59
 
51
60
  function(
@@ -88,6 +97,19 @@ module Jade
88
97
  Jade::Decode::Decoder[Jade::Decode::Desc::Lst[decoder.desc]]
89
98
  }
90
99
 
100
+ # Reads the same shape a list does, dropping duplicates. Like
101
+ # `Decode.dict`, it builds the keyed structure directly, so it asks
102
+ # nothing of the element beyond being decodable.
103
+ function(
104
+ 'set',
105
+ { decoder: 'Decoder(a)' },
106
+ 'Decoder(Set(a))',
107
+ ) { |decoder|
108
+ Jade::Decode::Desc::Lst[decoder.desc]
109
+ .then { Jade::Decode::Desc::Map[->(vs) { Jade::Set::Set[vs.to_h { [it, true] }] }, it] }
110
+ .then { Jade::Decode::Decoder[it] }
111
+ }
112
+
91
113
  # Decodes either a Hash (natural Ruby/JSON object form) or a list
92
114
  # of `[k, v]` pairs (what Encode.dict emits — also the only shape
93
115
  # that round-trips non-String key types).
@@ -127,6 +149,21 @@ module Jade
127
149
  Jade::Decode::Decoder[Jade::Decode::Desc::AndMap[wrapped.desc, decoder.desc]]
128
150
  }
129
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
+
130
167
  function(
131
168
  'required',
132
169
  { wrapped: 'Decoder(a -> b)', key: 'String', field_decoder: 'Decoder(a)' },
@@ -291,9 +328,7 @@ module Jade
291
328
  { a: 'Decoder(a)', b: 'Decoder(b)' },
292
329
  'Decoder(Tuple2(a, b))',
293
330
  ) { |da, db|
294
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple2.method(:[]).curry(2)]
295
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
296
- .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]
297
332
  .then { Jade::Decode::Decoder[it] }
298
333
  }
299
334
 
@@ -302,10 +337,7 @@ module Jade
302
337
  { a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)' },
303
338
  'Decoder(Tuple3(a, b, c))',
304
339
  ) { |da, db, dc|
305
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple3.method(:[]).curry(3)]
306
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
307
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
308
- .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]
309
341
  .then { Jade::Decode::Decoder[it] }
310
342
  }
311
343
 
@@ -314,11 +346,7 @@ module Jade
314
346
  { a: 'Decoder(a)', b: 'Decoder(b)', c: 'Decoder(c)', d: 'Decoder(d)' },
315
347
  'Decoder(Tuple4(a, b, c, d))',
316
348
  ) { |da, db, dc, dd|
317
- Jade::Decode::Desc::Succeed[Jade::Tuple::Tuple4.method(:[]).curry(4)]
318
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[0, da.desc]] }
319
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[1, db.desc]] }
320
- .then { Jade::Decode::Desc::AndMap[it, Jade::Decode::Desc::Idx[2, dc.desc]] }
321
- .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]
322
350
  .then { Jade::Decode::Decoder[it] }
323
351
  }
324
352
 
@@ -328,6 +356,23 @@ module Jade
328
356
  implementation('Decodable', 'Basics.Float', 'decoder' => 'float')
329
357
  implementation('Decodable', 'Basics.Bool', 'decoder' => 'bool')
330
358
  implementation('Decodable', 'String.String', 'decoder' => 'string')
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
331
376
  end
332
377
  end
333
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
@@ -10,6 +11,7 @@ module Jade
10
11
  import List
11
12
  import Tuple
12
13
  import Dict
14
+ import Set
13
15
 
14
16
  interface(
15
17
  'Encodable',
@@ -25,6 +27,11 @@ module Jade
25
27
  function('bool', { b: 'Bool' }, 'Value') { it }
26
28
  function('null', {}, 'Value') { nil }
27
29
 
30
+ # A `Value` is already encoded, so its encoder is identity. Ports and
31
+ # boundaries take it as the opt-out; the instance is what carries that
32
+ # through `List(Value)` and friends.
33
+ function('value', { v: 'Value' }, 'Value') { it }
34
+
28
35
  # Structural
29
36
 
30
37
  function(
@@ -57,6 +64,16 @@ module Jade
57
64
  dict.hash.map { |k, v| [k_enc.call(k), v_enc.call(v)] }
58
65
  }
59
66
 
67
+ # A set is its elements, in insertion order, with no duplicates —
68
+ # the same shape a list encodes to.
69
+ function(
70
+ 'set',
71
+ { encoder: 'a -> Value', set: 'Set(a)' },
72
+ 'Value',
73
+ ) { |encoder, set|
74
+ set.hash.keys.map { encoder.call(it) }
75
+ }
76
+
60
77
  function(
61
78
  'object',
62
79
  { pairs: 'List(Tuple2(String, Value))' },
@@ -138,6 +155,15 @@ module Jade
138
155
  implementation('Encodable', 'Basics.Float', 'encoder' => 'float')
139
156
  implementation('Encodable', 'Basics.Bool', 'encoder' => 'bool')
140
157
  implementation('Encodable', 'String.String', 'encoder' => 'string')
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')
141
167
  end
142
168
  end
143
169
  end