jade-lang 0.3.0 → 0.4.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -1
  3. data/README.md +12 -11
  4. data/lib/jade/codegen/function_call.rb +1 -1
  5. data/lib/jade/codegen/inlines.rb +13 -0
  6. data/lib/jade/codegen/{port_decoder.rb → port_codec.rb} +27 -15
  7. data/lib/jade/codegen.rb +7 -3
  8. data/lib/jade/debug.rb +59 -0
  9. data/lib/jade/diagnostics/renderer.rb +16 -5
  10. data/lib/jade/frontend/forward_declaration/interop_import_declaration.rb +13 -3
  11. data/lib/jade/frontend/pattern_analysis/matrix.rb +57 -23
  12. data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +33 -7
  13. data/lib/jade/frontend/semantic_analysis/error/constructor_not_found.rb +20 -5
  14. data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +25 -17
  15. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +25 -34
  16. data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +51 -175
  17. data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +133 -0
  18. data/lib/jade/frontend/type_checking/constraints/deriving/show.rb +186 -0
  19. data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
  20. data/lib/jade/frontend/type_checking/env.rb +3 -0
  21. data/lib/jade/frontend/type_checking/error/port_not_encodable.rb +38 -0
  22. data/lib/jade/frontend/type_checking/port_resolution.rb +64 -16
  23. data/lib/jade/interop/runtime.rb +10 -2
  24. data/lib/jade/module_loader.rb +27 -3
  25. data/lib/jade/parsing/error.rb +19 -0
  26. data/lib/jade/runtime.rb +1 -0
  27. data/lib/jade/stdlib/debug.rb +12 -0
  28. data/lib/jade/stdlib/decode.rb +22 -0
  29. data/lib/jade/stdlib/encode.rb +17 -0
  30. data/lib/jade/stdlib/intrinsics.rb +3 -0
  31. data/lib/jade/stdlib/show.rb +40 -0
  32. data/lib/jade/stdlib.rb +5 -2
  33. data/lib/jade/symbol/interop_function.rb +3 -1
  34. data/lib/jade/symbol.rb +2 -2
  35. data/lib/jade/version.rb +1 -1
  36. metadata +8 -3
@@ -0,0 +1,186 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Constraints
5
+ module Deriving
6
+ module Show
7
+ extend self
8
+ include Helpers
9
+
10
+ INTERFACE = 'Show.Show'
11
+
12
+ def supports?(interface) = interface == INTERFACE
13
+
14
+ def derive(constraint, registry, entry_name, &lookup)
15
+ resolve_constraint(constraint, registry, entry_name, lookup)
16
+ end
17
+
18
+ private
19
+
20
+ # List has no variants to walk, so it is matched by name here rather
21
+ # than declared as an instance in the Show module. That is a stopgap:
22
+ # a parameterised stdlib instance needs a registration path the DSL
23
+ # does not have. Encodable hardcodes List and Maybe for the same
24
+ # reason. Contained because derivation is private — see
25
+ # vault jade/plans/jade-test-runner.md for the four layers involved.
26
+ LIST = 'List.List'
27
+
28
+ def special_case(constraint, lookup)
29
+ case constraint.type
30
+ in Type::Function
31
+ Ok[implementation(constraint, { 'show' => constant('<function>') })]
32
+
33
+ in Type::Application(constructor: Type::Constructor(name: LIST), args: [inner])
34
+ list_show(constraint, inner, lookup)
35
+
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ def list_show(constraint, inner, lookup)
42
+ lookup
43
+ .call(Type.constraint(INTERFACE, inner, constraint.origin))
44
+ .and_then do |dep|
45
+ body = concat([
46
+ '[',
47
+ [:call, [:stdlib_fn, 'String.join'], [
48
+ [:call, [:stdlib_fn, 'List.map'], [[:var, 'list'], [:impl_arg, 0, 'show']]],
49
+ ', ',
50
+ ]],
51
+ ']',
52
+ ])
53
+
54
+ Symbol::DerivedFunction
55
+ .new(params: ['list'], body:)
56
+ .then { Ok[implementation(constraint, { 'show' => it }, deps: [dep])] }
57
+ end
58
+ end
59
+
60
+ def constant(text)
61
+ Symbol::DerivedFunction.new(params: ['value'], body: text)
62
+ end
63
+
64
+ def concat(parts)
65
+ [:call, [:stdlib_fn, 'String.concat'], [[:list, parts]]]
66
+ end
67
+
68
+ def shown(dict_index, expr)
69
+ [:call, [:impl_arg, dict_index, 'show'], [expr]]
70
+ end
71
+
72
+ def derive_union(constraint, symbol, registry, lookup, entry_name)
73
+ type_vars = symbol.type_params.map(&:name)
74
+ index_map = type_vars.each_with_index.map.to_h
75
+ variants = symbol.variants.map { registry.lookup(it) }
76
+
77
+ concrete = variants
78
+ .flat_map(&:args)
79
+ .reject { it in Symbol::Variable }
80
+ .map { instantiate(it, {}, registry) }
81
+ .uniq
82
+
83
+ return failed(constraint, entry_name) if variants.empty?
84
+
85
+ concrete
86
+ .map { lookup.call(Type.constraint(INTERFACE, it, constraint.origin)) }
87
+ .then { Results.sequence(it) }
88
+ .and_then do |deps|
89
+ cases = variants.map {
90
+ variant_case(it, index_map, concrete, registry)
91
+ }
92
+
93
+ show_fn = Symbol::DerivedFunction.new(
94
+ params: ['value'],
95
+ body: [:case, [:var, 'value'], cases],
96
+ )
97
+
98
+ Ok[union_impl(constraint, type_vars, concrete, show_fn, deps)]
99
+ end
100
+ end
101
+
102
+ def union_impl(constraint, type_vars, concrete, show_fn, deps)
103
+ return implementation(constraint, { 'show' => show_fn }, deps:) if type_vars.empty?
104
+
105
+ Symbol::ImplementationTemplate.new(
106
+ interface: Symbol.type_ref_from_qualified_name(constraint.interface),
107
+ type: constraint.type,
108
+ type_params: type_vars.map { Type.var(it) },
109
+ constraints: union_constraints(constraint, type_vars, concrete),
110
+ functions: { 'show' => show_fn },
111
+ )
112
+ end
113
+
114
+ def variant_case(variant, index_map, concrete, registry)
115
+ vars = (0...variant.args.length).map { |i| "a#{i}" }
116
+ name = variant.qualified_name.split('.').last
117
+ pattern = [:constructor, variant.qualified_name, vars]
118
+
119
+ return [pattern, [name]] if vars.empty?
120
+
121
+ rendered = variant.args.each_with_index.map do |arg_type, i|
122
+ idx =
123
+ case arg_type
124
+ in Symbol::Variable(name: var_name) then index_map[var_name]
125
+ else
126
+ index_map.size + concrete.index(instantiate(arg_type, {}, registry))
127
+ end
128
+
129
+ shown(idx, [:var, vars[i]])
130
+ end
131
+
132
+ parts = [name, '('] + intersperse(rendered, ', ') + [')']
133
+
134
+ [pattern, [concat(parts)]]
135
+ end
136
+
137
+ def intersperse(items, separator)
138
+ items.flat_map { [separator, it] }.drop(1)
139
+ end
140
+
141
+ def derive_struct(constraint, struct_sym, type_args, registry, lookup, entry_name)
142
+ fields = struct_fields(struct_sym, type_args, registry)
143
+ name = struct_sym.qualified_name.split('.').last
144
+
145
+ resolve_field_deps(fields.map { |_, type| type }, lookup, constraint.origin)
146
+ .and_then do |deps|
147
+ rendered = fields.each_with_index.map { |(field, _), idx|
148
+ [:call, [:stdlib_fn, 'String.concat'], [[:list, [
149
+ "#{field}: ",
150
+ shown(idx, [:access, [:var, 'value'], field.to_s]),
151
+ ]]]]
152
+ }
153
+
154
+ parts = [name, ' { '] + intersperse(rendered, ', ') + [' }']
155
+
156
+ Ok[record_impl(constraint, concat(parts), deps)]
157
+ end
158
+ end
159
+
160
+ def derive_record(constraint, fields, lookup)
161
+ resolve_field_deps(fields.values, lookup, constraint.origin)
162
+ .and_then do |deps|
163
+ rendered = fields.keys.each_with_index.map { |field, idx|
164
+ [:call, [:stdlib_fn, 'String.concat'], [[:list, [
165
+ "#{field}: ",
166
+ shown(idx, [:access, [:var, 'value'], field.to_s]),
167
+ ]]]]
168
+ }
169
+
170
+ parts = ['{ '] + intersperse(rendered, ', ') + [' }']
171
+
172
+ Ok[record_impl(constraint, concat(parts), deps)]
173
+ end
174
+ end
175
+
176
+ def record_impl(constraint, body, deps)
177
+ Symbol::DerivedFunction
178
+ .new(params: ['value'], body:)
179
+ .then { implementation(constraint, { 'show' => it }, deps:) }
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -3,6 +3,7 @@ require_relative './deriving/eq.rb'
3
3
  require_relative './deriving/decodable.rb'
4
4
  require_relative './deriving/encodable.rb'
5
5
  require_relative './deriving/sql_mapper.rb'
6
+ require_relative './deriving/show.rb'
6
7
 
7
8
  module Jade
8
9
  module Frontend
@@ -11,7 +12,7 @@ module Jade
11
12
  module Deriving
12
13
  extend self
13
14
 
14
- DERIVERS = [Eq, Decodable, Encodable, SqlMapper]
15
+ DERIVERS = [Eq, Show, Decodable, Encodable, SqlMapper]
15
16
 
16
17
  def derivable?(interface)
17
18
  DERIVERS.any? { it.supports?(interface) }
@@ -53,6 +53,9 @@ module Jade
53
53
  in Placeholder => placeholder
54
54
  Scheme[placeholder.free_vars, placeholder.type, placeholder.constraints]
55
55
  .then { Instantiation.instantiate(it, var_gen) }
56
+
57
+ in nil
58
+ fail "no type binding for `#{key}`"
56
59
  end
57
60
 
58
61
  Result.init(type, constraints)
@@ -0,0 +1,38 @@
1
+ module Jade
2
+ module Frontend
3
+ module TypeChecking
4
+ module Error
5
+ class PortNotEncodable < Jade::Error
6
+ attr_reader :port_name, :position, :type
7
+
8
+ def initialize(entry, span, port_name:, position:, type:)
9
+ @port_name = port_name
10
+ @position = position
11
+ @type = type
12
+ super(entry:, span:)
13
+ end
14
+
15
+ def message
16
+ "Port `#{@port_name}` cannot encode argument #{@position} (`#{@type}`): " \
17
+ "no Encodable instance"
18
+ end
19
+
20
+ def label
21
+ "no Encodable instance for `#{@type}`"
22
+ end
23
+
24
+ def notes
25
+ [
26
+ Jade::Diagnostics::Annotation[
27
+ :help,
28
+ "implement Encodable for `#{@type}` so it can be encoded on the " \
29
+ "way out, or declare the argument as `Decode.Value` to hand " \
30
+ "Ruby the value untouched",
31
+ ],
32
+ ]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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
@@ -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
@@ -51,13 +51,37 @@ module Jade
51
51
  .modules_in_topo_order
52
52
  .reject { Stdlib.is_stdlib?(it) }
53
53
  .reduce([registry, {}]) do |(acc, digests), entry|
54
- compiled, digest = compile_with_cache(entry, acc, digests, cache_dir, tolerant)
55
-
56
- [acc.update_module(compiled), digests.merge(entry.name => digest)]
54
+ case broken_dependency(entry, acc)
55
+ in String => dependency
56
+ [acc.update_module(blocked(entry, dependency)), digests]
57
+
58
+ else
59
+ compile_with_cache(entry, acc, digests, cache_dir, tolerant)
60
+ .then { |(compiled, digest)| [acc.update_module(compiled), digests.merge(entry.name => digest)] }
61
+ end
57
62
  end
58
63
  .first
59
64
  end
60
65
 
66
+ def broken_dependency(entry, registry)
67
+ direct_deps(entry.name, registry)
68
+ .filter_map { registry.modules[it] }
69
+ .find { it.diagnostics.any_errors? }
70
+ &.name
71
+ end
72
+
73
+ def blocked(entry, dependency)
74
+ Diagnostics::List
75
+ .empty
76
+ .add(
77
+ Diagnostics::Diagnostic.error(
78
+ "Not checked: #{dependency} failed to compile",
79
+ primary: nil,
80
+ ),
81
+ )
82
+ .then { entry.with(diagnostics: it) }
83
+ end
84
+
61
85
  def compile_with_cache(entry, registry, digests, cache_dir, tolerant)
62
86
  return [compile_one(entry, registry, tolerant:), nil] unless cache_dir
63
87
 
@@ -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,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
@@ -10,6 +10,7 @@ module Jade
10
10
  import Result
11
11
  import List
12
12
  import Dict
13
+ import Set
13
14
 
14
15
  union :DecodeError
15
16
  variant :MissingField, of: :DecodeError, args: ['String']
@@ -46,6 +47,13 @@ module Jade
46
47
  Jade::Decode::Decoder[Jade::Decode::Desc::Bool[]]
47
48
  }
48
49
 
50
+ # Keeps whatever came in. `Value` is the un-decoded value, so this is
51
+ # what makes it usable nested — `List(Value)`, a field of a struct —
52
+ # and not only as a whole arm.
53
+ function('value', {}, 'Decoder(Value)') {
54
+ Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]
55
+ }
56
+
49
57
  # Structural
50
58
 
51
59
  function(
@@ -88,6 +96,19 @@ module Jade
88
96
  Jade::Decode::Decoder[Jade::Decode::Desc::Lst[decoder.desc]]
89
97
  }
90
98
 
99
+ # Reads the same shape a list does, dropping duplicates. Like
100
+ # `Decode.dict`, it builds the keyed structure directly, so it asks
101
+ # nothing of the element beyond being decodable.
102
+ function(
103
+ 'set',
104
+ { decoder: 'Decoder(a)' },
105
+ 'Decoder(Set(a))',
106
+ ) { |decoder|
107
+ Jade::Decode::Desc::Lst[decoder.desc]
108
+ .then { Jade::Decode::Desc::Map[->(vs) { Jade::Set::Set[vs.to_h { [it, true] }] }, it] }
109
+ .then { Jade::Decode::Decoder[it] }
110
+ }
111
+
91
112
  # Decodes either a Hash (natural Ruby/JSON object form) or a list
92
113
  # of `[k, v]` pairs (what Encode.dict emits — also the only shape
93
114
  # that round-trips non-String key types).
@@ -328,6 +349,7 @@ module Jade
328
349
  implementation('Decodable', 'Basics.Float', 'decoder' => 'float')
329
350
  implementation('Decodable', 'Basics.Bool', 'decoder' => 'bool')
330
351
  implementation('Decodable', 'String.String', 'decoder' => 'string')
352
+ implementation('Decodable', 'Decode.Value', 'decoder' => 'value')
331
353
  end
332
354
  end
333
355
  end
@@ -10,6 +10,7 @@ module Jade
10
10
  import List
11
11
  import Tuple
12
12
  import Dict
13
+ import Set
13
14
 
14
15
  interface(
15
16
  'Encodable',
@@ -25,6 +26,11 @@ module Jade
25
26
  function('bool', { b: 'Bool' }, 'Value') { it }
26
27
  function('null', {}, 'Value') { nil }
27
28
 
29
+ # A `Value` is already encoded, so its encoder is identity. Ports and
30
+ # boundaries take it as the opt-out; the instance is what carries that
31
+ # through `List(Value)` and friends.
32
+ function('value', { v: 'Value' }, 'Value') { it }
33
+
28
34
  # Structural
29
35
 
30
36
  function(
@@ -57,6 +63,16 @@ module Jade
57
63
  dict.hash.map { |k, v| [k_enc.call(k), v_enc.call(v)] }
58
64
  }
59
65
 
66
+ # A set is its elements, in insertion order, with no duplicates —
67
+ # the same shape a list encodes to.
68
+ function(
69
+ 'set',
70
+ { encoder: 'a -> Value', set: 'Set(a)' },
71
+ 'Value',
72
+ ) { |encoder, set|
73
+ set.hash.keys.map { encoder.call(it) }
74
+ }
75
+
60
76
  function(
61
77
  'object',
62
78
  { pairs: 'List(Tuple2(String, Value))' },
@@ -138,6 +154,7 @@ module Jade
138
154
  implementation('Encodable', 'Basics.Float', 'encoder' => 'float')
139
155
  implementation('Encodable', 'Basics.Bool', 'encoder' => 'bool')
140
156
  implementation('Encodable', 'String.String', 'encoder' => 'string')
157
+ implementation('Encodable', 'Decode.Value', 'encoder' => 'value')
141
158
  end
142
159
  end
143
160
  end
@@ -256,6 +256,9 @@ module Jade
256
256
 
257
257
  in 'Encodable'
258
258
  'Encode'
259
+
260
+ in 'Show'
261
+ 'Show'
259
262
  end
260
263
  .then { Symbol.type_ref(it, interface.to_s) }
261
264
  end
@@ -0,0 +1,40 @@
1
+ require 'jade/stdlib/intrinsics'
2
+
3
+ module Jade
4
+ module Stdlib
5
+ module Show
6
+ extend Intrinsics
7
+
8
+ import Basics
9
+ import Char
10
+ import String
11
+
12
+ interface(
13
+ 'Show',
14
+ 'a',
15
+ { 'show' => 'a -> String' },
16
+ )
17
+
18
+ implementation('Show', 'Int', 'show' => 'int_show')
19
+ implementation('Show', 'Float', 'show' => 'float_show')
20
+ implementation('Show', 'Bool', 'show' => 'bool_show')
21
+ implementation('Show', 'String', 'show' => 'str_show')
22
+ implementation('Show', 'Char', 'show' => 'char_show')
23
+
24
+ # Never is uninhabited, so this can only be reached by a compiler bug.
25
+ # The instance exists because the constraint does: without it no
26
+ # `Result(a, Never)` — the shape every port-free task returns — can be
27
+ # shown, and the error surfaces as an unresolved constraint far from
28
+ # its cause.
29
+ implementation('Show', 'Never', 'show' => 'never_show')
30
+
31
+ function('int_show', { n: 'Int' }, 'String')
32
+ function('float_show', { f: 'Float' }, 'String')
33
+ function('bool_show', { b: 'Bool' }, 'String')
34
+ function('str_show', { s: 'String' }, 'String')
35
+ function('char_show', { c: 'Char' }, 'String')
36
+
37
+ function('never_show', { n: 'Never' }, 'String')
38
+ end
39
+ end
40
+ end