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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6886eb468004c2c500090b0a919a1a369e31b36039b3a6ef583bb74bc5e7326b
4
- data.tar.gz: eaa9bb82c455efa0ca5c9e86eefe7d6ae6a56b1a9c8d695ae484500b69c1e049
3
+ metadata.gz: 1cb9527b610f268fc9ae7d784abcbf41018cf99f91b0bdaea462a655a8f12bfb
4
+ data.tar.gz: d888ed1e08771d41147879b5576c0b894f94795af1e4f849f6ca31044661366b
5
5
  SHA512:
6
- metadata.gz: 5a27df2bd32afa406b3205ca40ea047231cfe95c169cc38918b453278675d889ff9038ef93a2d08fd5caeb94683cc47cc5d498c8a5daa0e086a7f98630f733d7
7
- data.tar.gz: e7fd05fc7622d6268f6ad109e8c6ad2b44df4961f668515d4eafb69767fa437d9b34d13276946723b58486d1393698fb713dba7a484965524753d913e779a9ee
6
+ metadata.gz: 95f7b32d5542bb1d386d18a7e1e8749de7a7f5c6ecfffaacee351dcb0d4205d698269954fe4fc023f2fed3cc7fe9f32c0f6344f9415770916ea6237be3f16e8f
7
+ data.tar.gz: 897851f20e95afe86c92a7c4fe104299b46a4239644f06ea26c5580f5d47660834560e5c218336efc3d569f56e5754b8c68e45b1e68524166d347bdf5d178bb6
data/CHANGELOG.md CHANGED
@@ -6,6 +6,94 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0]
10
+
11
+ ### Changed
12
+
13
+ - **Port arguments are encoded on the way out**, the mirror of the return value
14
+ being decoded on the way back. A port declared `Instant, Int -> Task(Instant,
15
+ Never)` used to hand Ruby a live `Jade::Clock::Instant` while demanding an ISO
16
+ string back; both sides are now the wire form. **Every argument type needs an
17
+ `Encodable` instance** — a port taking one without it is a compile error
18
+ (``Port `shift` cannot encode argument 1 (`Shape`): no Encodable instance``)
19
+ rather than a value leaking Jade's internals into Ruby. Declare the argument
20
+ as `Decode.Value` to opt out and pass it untouched. A port whose arguments are
21
+ primitives is unaffected; one taking a struct, a `Clock`/`Calendar`/`Decimal`
22
+ type, or a type variable now receives encoded data, so its Ruby side needs
23
+ updating.
24
+
25
+ ### Added
26
+
27
+ - `Show`, an interface rendering a value the way Jade writes it: `show(B(2))`
28
+ is `"B(2)"`, `show(Point(3, 4))` is `"Point { x: 3, y: 4 }"`, a list shows
29
+ through its elements. Instances ship for the primitives and derive for
30
+ unions, structs and records — including a variant that mixes a type
31
+ parameter with a concrete type. A function shows as `<function>` rather than
32
+ refusing. `Never` has an instance too, so an annotated `Result(a, Never)` —
33
+ what a port-free task returns — is showable; it raises rather than printing
34
+ a plausible string, since it has no values to render.
35
+ - `Debug.log(label, value)` prints `label: value` to stderr and returns the
36
+ value untouched, so it drops into a pipeline without changing it.
37
+ - `Encodable` and `Decodable` derive for tuples, `Dict` and `Set`, from their
38
+ elements' instances — the same way `List` and `Maybe` already did. The
39
+ combinators (`Encode.tuple`, `Decode.dict`, …) existed all along; nothing
40
+ resolved them, so a `(String, Int)` port argument or a `Task(Dict(k, v), e)`
41
+ arm was rejected for want of an instance. A tuple crosses as a positional
42
+ array, a dict as `[key, value]` pairs, a set as an array that drops
43
+ duplicates on the way back. Adds the two functions the set instances needed,
44
+ `Encode.set` and `Decode.set`.
45
+ - `Encodable` and `Decodable` instances for `Decode.Value`, both the identity,
46
+ plus the `Decode.value` and `Encode.value` functions behind them. `Value` is
47
+ the un-decoded value, and it opted out of conversion only as a whole arm or
48
+ argument: `List(Value)`, `Maybe(Value)`, and a struct with a `Value` field
49
+ had no instance to derive from and were rejected at the boundary.
50
+ - A parse error inside a `uses`, `interface`, or `implements` block where the
51
+ entries are separated by newlines instead of commas says so, instead of only
52
+ reporting the next entry as an unexpected token.
53
+
54
+ ### Fixed
55
+
56
+ - `Eq` derives for a union whose variants carry concrete types. `B(1) == B(1)`
57
+ over `type Box = B(Int) | Empty` took the compiler down with a
58
+ `NoMatchingPatternError`; `Just(1) == Just(1)` only worked because `Maybe`'s
59
+ payload is a type parameter. Variants mixing a parameter with a concrete
60
+ type derive too.
61
+ - Diagnostic source excerpts are sliced by byte offset. Spans and line starts
62
+ are the lexer's byte offsets, but the excerpt was cut and measured in
63
+ characters, so one multi-byte glyph anywhere earlier in the file shifted every
64
+ later excerpt and its caret — `def notify_channel(…)` printed as
65
+ `f notify_channel(…)`.
66
+ - Exhaustiveness checking no longer collapses when the scrutinee is a tuple.
67
+ `case (a, b)` over two `Maybe`s reported `Missing cases: (_, _)` with all four
68
+ arms present, and the `else` that silenced it hid genuinely missing cases from
69
+ then on. Two bugs: a tuple's element types were replaced with fresh type
70
+ variables, and specializing on a constructor dropped the types of every column
71
+ after it. A constructor the first column never mentions is now answered from
72
+ the rows that would have covered it, which is also what keeps a recursive type
73
+ from expanding forever — the recursion guard that used to do that job is gone,
74
+ and with it the false "exhaustive" verdicts it caused on any second column.
75
+ - The missing-constructor diagnostic tells you which `exposing` list is missing
76
+ the `(..)`. When the defining module already exposes `Route(..)` and it's the
77
+ importer's `import Routes exposing (Route)` that brought in the type alone, it
78
+ no longer sends you to the wrong file. It also fires for a variant whose name
79
+ differs from its type's (`Home` of `Route`), which previously got no hint.
80
+
81
+ ## [0.3.1]
82
+
83
+ ### Fixed
84
+
85
+ - A module whose dependency failed to compile is no longer type checked. It
86
+ reached for bindings the broken module never produced and raised through the
87
+ compiler, naming the importer rather than the module at fault, and the
88
+ original error was never reported. Dependents now say which dependency
89
+ failed, and modules broken independently of each other are all still
90
+ reported.
91
+ - The load-path line at the top of every compiled entry module appends the
92
+ application's `lib` instead of prepending it. At position 0 it shadowed, for
93
+ the whole process, any gem sharing a name with a directory under `lib` —
94
+ `sidekiq/`, `sentry/`, `warden/`. The line exists so `uses App::Thing` can
95
+ resolve `lib/app/thing.rb`; it never needed to outrank gems.
96
+
9
97
  ## [0.3.0]
10
98
 
11
99
  ### Changed
@@ -99,7 +187,9 @@ Initial release.
99
187
  - `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
100
188
  - Language server and headless query interface for editor/agent tooling.
101
189
 
102
- [Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.3.0...HEAD
190
+ [Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.4.0...HEAD
191
+ [0.4.0]: https://github.com/agustinrhcp/jade/compare/v0.3.1...v0.4.0
192
+ [0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
103
193
  [0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
104
194
  [0.2.0]: https://github.com/agustinrhcp/jade/compare/v0.1.1...v0.2.0
105
195
  [0.1.1]: https://github.com/agustinrhcp/jade/compare/v0.1.0...v0.1.1
data/README.md CHANGED
@@ -148,7 +148,7 @@ at compile time, no annotation needed. You can define your own, with
148
148
  implementations dispatched by type:
149
149
 
150
150
  ```jade
151
- module Shows exposing (describe)
151
+ module Labels exposing (person_label)
152
152
 
153
153
  struct Person = {
154
154
  name: String,
@@ -156,18 +156,18 @@ struct Person = {
156
156
  }
157
157
 
158
158
 
159
- interface Show(a) with
160
- show : a -> String
159
+ interface Labelled(a) with
160
+ label : a -> String
161
161
  end
162
162
 
163
163
 
164
- implements Show(Person) with
165
- show: (p) -> { p.name ++ " (" ++ String.from_int(p.age) ++ ")" }
164
+ implements Labelled(Person) with
165
+ label: (p) -> { p.name ++ " (" ++ String.from_int(p.age) ++ ")" }
166
166
  end
167
167
 
168
168
 
169
- def describe(p: Person) -> String
170
- show(p)
169
+ def person_label(p: Person) -> String
170
+ label(p)
171
171
  end
172
172
  ```
173
173
 
@@ -357,8 +357,9 @@ pre-commit hook.
357
357
  ## Standard library
358
358
 
359
359
  `Basics`, `String`, `Char`, `List`, `Dict`, `Set`, `Tuple`, `Maybe`, `Result`,
360
- `Task`, `Decode`, `Encode`, `Bytes`, `Calendar`, `Clock`, `Decimal`. Stdlib operations
361
- compile inline rather than through a runtime dispatch layer.
360
+ `Task`, `Decode`, `Encode`, `Bytes`, `Calendar`, `Clock`, `Decimal`, `Show`,
361
+ `Debug`. Stdlib operations compile inline rather than through a runtime
362
+ dispatch layer.
362
363
 
363
364
  ## Docs
364
365
 
@@ -374,8 +375,8 @@ compile inline rather than through a runtime dispatch layer.
374
375
 
375
376
  Early and experimental — being tried out on small projects.
376
377
 
377
- **In progress:** `Comparable` / `Show` derivation for user types, partial
378
- record types in signatures, a stable REPL.
378
+ **In progress:** `Comparable` derivation for user types, partial record types
379
+ in signatures, a stable REPL.
379
380
 
380
381
  **Not great for:** throwaway scripts, libraries you ship to other Ruby
381
382
  projects (they'd inherit the dependency), and performance-critical hot paths
@@ -180,7 +180,7 @@ module Jade
180
180
  in Symbol::InteropFunction
181
181
  registry
182
182
  .lookup(callee.symbol.to_ref)
183
- .then { PortDecoder.task_call(it, registry, dictionaries) }
183
+ .then { PortCodec.task_call(it, registry, dictionaries) }
184
184
 
185
185
  in Symbol::StdlibFunction => symbol if symbol.constraints.any?
186
186
  dictionaries
@@ -16,6 +16,15 @@ module Jade
16
16
  }.freeze
17
17
 
18
18
  INLINES = {
19
+ 'Debug.log' => ->(l, v) { "Jade::Debug.log(#{l}, #{v})" },
20
+ 'Show.int_show' => ->(n) { "#{n}.to_s" },
21
+ 'Show.float_show' => ->(f) { "#{f}.to_s" },
22
+ 'Show.bool_show' => ->(b) { "#{b}.to_s" },
23
+ 'Show.str_show' => ->(s) { "#{s}.inspect" },
24
+ 'Show.char_show' => ->(c) { "#{c}.inspect" },
25
+ # Never is uninhabited, so reaching this is a compiler bug, not a
26
+ # rendering problem — say so rather than print a plausible string.
27
+ 'Show.never_show' => ->(_) { 'fail("Show.never_show: Never has no values")' },
19
28
  'Basics.identity' => ->(a) { a },
20
29
  'Basics.always' => ->(x) { "->(_) { #{x} }" },
21
30
  'Basics.int_add' => ->(a, b) { "(#{a} + #{b})" },
@@ -191,6 +200,7 @@ module Jade
191
200
  Decode.optional_field
192
201
  Decode.required
193
202
  Decode.sequence
203
+ Decode.set
194
204
  Decode.string
195
205
  Decode.string_enum
196
206
  Decode.succeed
@@ -198,6 +208,7 @@ module Jade
198
208
  Decode.tuple3
199
209
  Decode.tuple4
200
210
  Decode.type_
211
+ Decode.value
201
212
  Decode.variant
202
213
  Encode.bool
203
214
  Encode.dict
@@ -209,10 +220,12 @@ module Jade
209
220
  Encode.null
210
221
  Encode.nullable
211
222
  Encode.object
223
+ Encode.set
212
224
  Encode.string
213
225
  Encode.tuple
214
226
  Encode.tuple3
215
227
  Encode.tuple4
228
+ Encode.value
216
229
  Encode.variant
217
230
  Task.and_then
218
231
  Task.fail
@@ -1,18 +1,20 @@
1
1
  module Jade
2
2
  module Codegen
3
- module PortDecoder
3
+ module PortCodec
4
4
  extend self
5
5
 
6
- PASS = "Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]"
6
+ PASS_DECODER = "Jade::Decode::Decoder[Jade::Decode::Desc::Pass[]]"
7
+ PASS_ENCODER = "->(v) { v }"
7
8
 
8
9
  # `dictionaries` is the call site's constraint-attachment list; only
9
- # used when an arm is a Dict marker.
10
+ # used when an arm or a parameter is a Dict marker.
10
11
  def task_call(interop_fn, registry, dictionaries = [])
11
12
  [
12
13
  interop_fn.interop_module_name,
13
14
  ":#{interop_fn.name}",
14
- decoder(interop_fn, :ok, registry, dictionaries),
15
- decoder(interop_fn, :err, registry, dictionaries),
15
+ codec(interop_fn.decoders.fetch(:ok), 'decoder', interop_fn, registry, dictionaries),
16
+ codec(interop_fn.decoders.fetch(:err), 'decoder', interop_fn, registry, dictionaries),
17
+ encoders(interop_fn, registry, dictionaries),
16
18
  ]
17
19
  .join(', ')
18
20
  .then { "Jade::Runtime.task_call(#{it})" }
@@ -20,32 +22,42 @@ module Jade
20
22
 
21
23
  private
22
24
 
23
- # Per-arm decoder. PortResolution stamps one of:
24
- # - :pass — Decode.Value or Never arm, no decoder needed
25
+ def encoders(interop_fn, registry, dictionaries)
26
+ interop_fn
27
+ .encoders
28
+ .map { codec(it, 'encoder', interop_fn, registry, dictionaries) }
29
+ .join(', ')
30
+ .then { "[#{it}]" }
31
+ end
32
+
33
+ # Per-arm, per-argument codec. PortResolution stamps one of:
34
+ # - :pass — Decode.Value or Never, nothing to convert
25
35
  # - Symbol::Implementation — concrete OR partial impl (with marker
26
36
  # deps for free vars). We push a synthetic dict_env mapping the
27
37
  # port's vars to the call-site dictionaries so generate_impl_dispatch
28
38
  # can resolve markers uniformly.
29
- # - Symbol::InteropFunction::Dict — bare-var arm, decoder comes
39
+ # - Symbol::InteropFunction::Dict — bare-var position; the codec comes
30
40
  # from the caller's threaded dictionary at the given constraint index
31
- def decoder(interop_fn, arm, registry, dictionaries)
32
- case interop_fn.decoders.fetch(arm)
41
+ def codec(resolution, kind, interop_fn, registry, dictionaries)
42
+ case resolution
33
43
  in Symbol::InteropFunction::PASS
34
- PASS
44
+ kind == 'decoder' ? PASS_DECODER : PASS_ENCODER
35
45
 
36
46
  in Symbol::Implementation => impl
37
- emit_impl_decoder(impl, interop_fn, dictionaries, registry)
47
+ emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
38
48
 
39
49
  in Symbol::InteropFunction::Dict(constraint_index:)
40
- "#{FunctionCall.dispatch_value(dictionaries.fetch(constraint_index), registry)}[\"decoder\"]"
50
+ FunctionCall
51
+ .dispatch_value(dictionaries.fetch(constraint_index), registry)
52
+ .then { "#{it}[#{kind.inspect}]" }
41
53
  end
42
54
  end
43
55
 
44
- def emit_impl_decoder(impl, interop_fn, dictionaries, registry)
56
+ def emit_impl_codec(impl, kind, interop_fn, dictionaries, registry)
45
57
  synthetic_env = build_synthetic_dict_env(impl, interop_fn, dictionaries, registry)
46
58
 
47
59
  Codegen.with_dict_env(synthetic_env) {
48
- FunctionCall.generate_impl_dispatch(impl, registry).fetch('decoder')
60
+ FunctionCall.generate_impl_dispatch(impl, registry).fetch(kind)
49
61
  }
50
62
  end
51
63
 
data/lib/jade/codegen.rb CHANGED
@@ -19,7 +19,7 @@ require 'jade/codegen/transforms/fold_shape'
19
19
  require 'jade/codegen/function_declaration'
20
20
  require 'jade/codegen/function_call'
21
21
  require 'jade/codegen/implementation'
22
- require 'jade/codegen/port_decoder'
22
+ require 'jade/codegen/port_codec'
23
23
 
24
24
  module Jade
25
25
  module Codegen
@@ -159,7 +159,7 @@ module Jade
159
159
  in Symbol::InteropFunction => sym
160
160
  registry
161
161
  .lookup(sym.to_ref)
162
- .then { PortDecoder.task_call(it, registry) }
162
+ .then { PortCodec.task_call(it, registry) }
163
163
 
164
164
  in Symbol::StdlibFunction(codegen:)
165
165
  codegen
@@ -393,8 +393,12 @@ module Jade
393
393
  node.is_a?(AST::ImportDeclaration) || node.is_a?(AST::InteropImportDeclaration)
394
394
  end
395
395
 
396
+ # Appended, not prepended: this exists so `uses App::Thing` can find
397
+ # `lib/app/thing.rb`, and an application's lib directory routinely holds
398
+ # directories named after the gems it depends on. At position 0 those
399
+ # shadow the real gems for the whole process.
396
400
  def load_path
397
- '$LOAD_PATH.unshift(File.expand_path("lib"))'
401
+ '$LOAD_PATH.push(File.expand_path("lib")).uniq!'
398
402
  end
399
403
  end
400
404
  end
data/lib/jade/debug.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Jade
2
+ module Debug
3
+ extend self
4
+
5
+ MAX_DEPTH = 12
6
+
7
+ def log(label, value)
8
+ warn("#{label}: #{render(value)}")
9
+ value
10
+ end
11
+
12
+ def render(value, depth = 0)
13
+ return '…' if depth > MAX_DEPTH
14
+
15
+ case value
16
+ when ::String then value.inspect
17
+ when ::Symbol then value.to_s
18
+ when nil then 'Nothing'
19
+ when true, false, ::Integer, ::Float then value.to_s
20
+ when ::Array then render_list(value, depth)
21
+ when ::Hash then render_record(value, depth)
22
+ when ::Data then render_variant(value, depth)
23
+ when ::Proc then '<function>'
24
+ else value.inspect
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def render_list(items, depth)
31
+ '[' + items.map { render(it, depth + 1) }.join(', ') + ']'
32
+ end
33
+
34
+ def render_record(fields, depth)
35
+ return '{}' if fields.empty?
36
+
37
+ '{ ' + fields.map { |k, v| "#{k}: #{render(v, depth + 1)}" }.join(', ') + ' }'
38
+ end
39
+
40
+ def render_variant(value, depth)
41
+ name = constructor_name(value)
42
+ args = value.deconstruct
43
+
44
+ return '(' + args.map { render(it, depth + 1) }.join(', ') + ')' if name.match?(/\ATuple\d+\z/)
45
+ return name if args.empty?
46
+ return "#{name}(#{args.map { render(it, depth + 1) }.join(', ')})" if positional?(value)
47
+
48
+ "#{name} " + render_record(value.to_h, depth)
49
+ end
50
+
51
+ def positional?(value)
52
+ value.members.each_with_index.all? { |m, i| m.to_s == "_#{i + 1}" }
53
+ end
54
+
55
+ def constructor_name(value)
56
+ value.class.name.to_s.split('::').last.then { it.empty? ? 'Anonymous' : it }
57
+ end
58
+ end
59
+ end
@@ -49,13 +49,13 @@ module Jade::Diagnostics
49
49
  def span_block(label, severity:)
50
50
  loc = location_of(label.source, label.span.begin)
51
51
  end_offset = [label.span.end - 1, 0].max
52
- end_loc = location_of(label.source, [end_offset, label.source.text.length - 1].min)
52
+ end_loc = location_of(label.source, [end_offset, label.source.text.bytesize - 1].min)
53
53
  multiline = end_loc.line > loc.line
54
54
  gutter_w = (multiline ? end_loc.line : loc.line).to_s.length
55
55
  blank = " " * gutter_w
56
56
  col_offset = loc.col - 1
57
57
  first_line = extract_line(label.source, loc.line).chomp
58
- span_len = [multiline ? first_line.length - col_offset : label.span.size, 1].max
58
+ span_len = [multiline ? first_line.length - col_offset : span_width(label), 1].max
59
59
  caret = severity == :secondary ? "-" : "^"
60
60
  underline = " " * col_offset + caret * span_len
61
61
  ann_text = label.message ? " #{label.message}" : ""
@@ -91,15 +91,26 @@ module Jade::Diagnostics
91
91
 
92
92
  Location = Data.define(:line, :col)
93
93
 
94
+ # Spans and line starts are byte offsets — the lexer's — while everything
95
+ # drawn here is counted in characters: slice the text by byte offset, then
96
+ # measure the result.
94
97
  def location_of(source, offset)
95
98
  (source.line_starts.rindex { _1 <= offset } || 0)
96
- .then { Location[it + 1, offset - source.line_starts[it] + 1] }
99
+ .then { Location[it + 1, column_of(source, source.line_starts[it], offset)] }
100
+ end
101
+
102
+ def column_of(source, line_start, offset)
103
+ source.text.byteslice(line_start, offset - line_start).length + 1
104
+ end
105
+
106
+ def span_width(label)
107
+ label.source.text.byteslice(label.span.begin, label.span.size).length
97
108
  end
98
109
 
99
110
  def extract_line(source, line_number)
100
111
  s = source.line_starts[line_number - 1]
101
- e = source.line_starts[line_number] || source.text.length
102
- source.text[s...e]
112
+ e = source.line_starts[line_number] || source.text.bytesize
113
+ source.text.byteslice(s, e - s)
103
114
  end
104
115
 
105
116
  def color(severity)
@@ -47,8 +47,8 @@ module Jade
47
47
  end
48
48
 
49
49
  def fn_type_to_interop(interop_mod_name, function_node, symbol, entry, registry)
50
- lifted_errors = Interop::Lowering
51
- .validate(symbol.return_type, registry, entry)
50
+ lifted_errors = [symbol.return_type, *symbol.params]
51
+ .flat_map { Interop::Lowering.validate(it, registry, entry) }
52
52
  .map { Error::TypeNotLowerable.new(entry, function_node.range, message: it.message) }
53
53
 
54
54
  Symbol
@@ -57,7 +57,8 @@ module Jade
57
57
  symbol.params,
58
58
  symbol.return_type,
59
59
  interop_mod_name.name,
60
- constraints: implicit_decodable_constraints(symbol.return_type),
60
+ constraints: implicit_decodable_constraints(symbol.return_type) +
61
+ implicit_encodable_constraints(symbol.params),
61
62
  )
62
63
  .then { [it, lifted_errors] }
63
64
  end
@@ -78,6 +79,15 @@ module Jade
78
79
  end
79
80
  end
80
81
 
82
+ # Arguments travel the other way, so a free variable in a parameter
83
+ # needs the caller's Encodable instance rather than its Decodable one.
84
+ def implicit_encodable_constraints(params)
85
+ params
86
+ .flat_map { collect_var_names(it) }
87
+ .uniq
88
+ .map { ['Encode.Encodable', it] }
89
+ end
90
+
81
91
  def collect_var_names(type_sym)
82
92
  case type_sym
83
93
  in Symbol::Variable(name:)
@@ -39,7 +39,7 @@ module Jade
39
39
  with(rows: rows + other.rows)
40
40
  end
41
41
 
42
- def missing_patterns(env, seen_recursive_types = ::Set.new)
42
+ def missing_patterns(env)
43
43
  if types.empty?
44
44
  return rows.empty? ? Matrix[[[]], types] : Matrix.empty
45
45
  end
@@ -49,37 +49,72 @@ module Jade
49
49
  return Matrix.empty if never?(type)
50
50
  return Matrix.wildcard(types) if rows.empty?
51
51
 
52
- if infinite?(type) || type_var?(type) || seen_recursive_types.include?(type)
53
- matrix = default
54
- .missing_patterns(env, seen_recursive_types)
52
+ heads = head_constructors
55
53
 
56
- matrix
57
- .map { [Wildcard[]] + it }
54
+ if infinite?(type) || type_var?(type)
55
+ unmatched_column(env)
58
56
 
59
57
  elsif expandable?(type, env)
60
- expand(env).missing_patterns(env, seen_recursive_types)
58
+ expand(env).missing_patterns(env)
59
+
60
+ elsif heads.empty?
61
+ unmatched_column(env)
61
62
 
62
63
  else
63
- new_seen = seen_recursive_types | ::Set[type]
64
64
  constructors_of(type, env)
65
65
  .reduce(Matrix.empty.with(types:)) do |acc, constructor|
66
- missing = specialize(constructor)
67
- .missing_patterns(env, new_seen)
68
-
69
- missing
70
- .map do |row|
71
- new_args = row.take(constructor.args.size)
72
- tail = row.drop(constructor.args.size)
73
-
74
- [Constructor[constructor.name, new_args]] + tail
75
- end
76
- .then { acc.concat(it) }
66
+ witnesses(constructor, env, heads.include?(constructor.name))
67
+ .then { acc.concat(it) }
77
68
  end
78
69
  end
79
70
  end
80
71
 
81
72
  protected
82
73
 
74
+ # Nothing in this column narrows anything — a type with no constructors
75
+ # to enumerate, or one every row left open. Whatever is missing lives in
76
+ # the columns after it.
77
+ def unmatched_column(env)
78
+ default
79
+ .missing_patterns(env)
80
+ .map { [Wildcard[]] + it }
81
+ end
82
+
83
+ # A constructor the first column matches is explored by specializing on
84
+ # it. One it never matches can only be missing as a whole, so its
85
+ # witnesses come from the rows that would have covered it — the ones
86
+ # headed by a wildcard. Recursing there instead of into the constructor
87
+ # is what keeps a recursive type from expanding forever.
88
+ def witnesses(constructor, env, matched)
89
+ return Matrix.empty if constructor.args.any? { never?(it) }
90
+
91
+ arity = constructor.args.size
92
+
93
+ if matched
94
+ specialize(constructor)
95
+ .missing_patterns(env)
96
+ .map { [Constructor[constructor.name, it.take(arity)]] + it.drop(arity) }
97
+
98
+ else
99
+ default
100
+ .missing_patterns(env)
101
+ .map { [Constructor[constructor.name, Array.new(arity) { Wildcard[] }]] + it }
102
+ end
103
+ end
104
+
105
+ def head_constructors
106
+ rows.filter_map { head_constructor(it.first) }
107
+ end
108
+
109
+ def head_constructor(pattern)
110
+ case pattern
111
+ in Constructor(constructor: name) then name
112
+ in Literal(value: true) then 'Basics.True'
113
+ in Literal(value: false) then 'Basics.False'
114
+ in Literal | Record | Wildcard then nil
115
+ end
116
+ end
117
+
83
118
  def expandable?(type, env)
84
119
  case type
85
120
  in Type::AnonymousRecord
@@ -125,9 +160,8 @@ module Jade
125
160
  TypeChecking::ConstructorDef['Basics.True', 'Basics.Bool', []],
126
161
  TypeChecking::ConstructorDef['Basics.False', 'Basics.Bool', []],
127
162
  ]
128
- in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple([2-4])$/ => name))
129
- n = name[-1].to_i
130
- [TypeChecking::Definition.constructor(name, name, Array.new(n) { env.fresh })]
163
+ in Type::Application(constructor: Type::Constructor(name: /^Tuple\.Tuple[2-4]$/ => name), args:)
164
+ [TypeChecking::Definition.constructor(name, name, args)]
131
165
 
132
166
  in Type::Application(constructor: Type::Constructor(name: 'List.List'), args: [elem_type])
133
167
  [
@@ -181,7 +215,7 @@ module Jade
181
215
  end
182
216
  end
183
217
  .then { with(rows: it) }
184
- .then { it.with(types: constructor.args) }
218
+ .then { it.with(types: constructor.args + types.drop(1)) }
185
219
  end
186
220
 
187
221
  def default
@@ -17,7 +17,7 @@ module Jade
17
17
  entry.name,
18
18
  node.range,
19
19
  name:,
20
- exposed_type_module: exposed_type_origin(name, entry, registry),
20
+ hint: constructor_hint(name, entry, registry),
21
21
  candidates: scope.bindings.keys.select { it.match?(/\A[A-Z]/) },
22
22
  ))
23
23
  .then do
@@ -49,14 +49,40 @@ module Jade
49
49
  &.then { klass.new(entry.name, span, arity: it) }
50
50
  end
51
51
 
52
- def exposed_type_origin(name, entry, registry)
53
- type_ref = entry.imported_types[name]
54
- return nil unless type_ref
52
+ # An imported module owning a constructor by this name is the whole
53
+ # story: either it keeps its constructors private, or it exposes them
54
+ # and the import here asked for the type alone.
55
+ def constructor_hint(name, entry, registry)
56
+ entry
57
+ .imports
58
+ .filter_map { hint_for(it.module_name, name, registry) }
59
+ .first
60
+ end
61
+
62
+ def hint_for(module_name, name, registry)
63
+ target = registry.get(module_name)
64
+ return nil if target.nil? || Stdlib.is_stdlib?(target)
65
+
66
+ type_name = owning_type(target, name)
67
+ return nil unless type_name && target.exposed_type(type_name)
55
68
 
56
- target_entry = registry.get(type_ref.module_name)
57
- return nil if target_entry.nil? || Stdlib.is_stdlib?(target_entry)
69
+ exposes_constructor?(target, type_name, name)
70
+ .then { it ? :import : :expose }
71
+ .then { Error::ConstructorNotFound::Hint[module_name, type_name, it] }
72
+ end
73
+
74
+ def owning_type(target, name)
75
+ target
76
+ .defined_types
77
+ .find { |_, sym| sym.constructor_refs.any? { it.name == name } }
78
+ &.first
79
+ end
58
80
 
59
- type_ref.module_name
81
+ def exposes_constructor?(target, type_name, name)
82
+ target
83
+ .exposed_type_variants(type_name)
84
+ .to_a
85
+ .any? { it.name == name }
60
86
  end
61
87
  end
62
88
  end