jade-lang 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -1
- data/lib/jade/ast/node.rb +30 -0
- data/lib/jade/cli/q.rb +9 -5
- data/lib/jade/cli.rb +3 -0
- data/lib/jade/codegen/emitter.rb +2 -2
- data/lib/jade/codegen/inlines.rb +1 -0
- data/lib/jade/codegen.rb +5 -1
- data/lib/jade/compiler.rb +17 -8
- data/lib/jade/formatter/helper.rb +11 -5
- data/lib/jade/frontend/comment_attacher.rb +13 -2
- data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +22 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/keyed_call.rb +55 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +26 -7
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +20 -7
- data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +2 -1
- data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +17 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/sql_mapper.rb +130 -0
- data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
- data/lib/jade/frontend/type_checking/env.rb +3 -0
- data/lib/jade/frontend/type_checking/generalizer.rb +0 -1
- data/lib/jade/frontend/usage_analysis/reference_index.rb +7 -1
- data/lib/jade/frontend/usage_analysis.rb +78 -52
- data/lib/jade/module_loader.rb +27 -3
- data/lib/jade/parsing.rb +7 -1
- data/lib/jade/project.rb +100 -0
- data/lib/jade/source.rb +7 -3
- data/lib/jade/stdlib/decode.rb +18 -0
- data/lib/jade/type.rb +8 -5
- data/lib/jade/version.rb +1 -1
- data/lib/jade.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb2b101369a0d671ca8f2aa020e7113a49f22e700491e5d14f7e574f62ee17ec
|
|
4
|
+
data.tar.gz: 7e017dbe893887f9e7ac3dfcb5c434474085771f5062ee509cf8e9ee9682ddd6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45578bee3ec52655fbba6a0cd7a053a6ad65c9981760edd86e2966fd671958a63fc08fb0120b58fda73298c584773a8e6a4c92eb71b2c11d79228bb88a905e16
|
|
7
|
+
data.tar.gz: 698a60f7e747a8042e32f5da24b8c6c322d25a470e3a65f0ed6dfeb61a89c67cc6394ab993115c8c42e9305e75db9b5445f7b5a42a74974832a2c5cd268fe209
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,79 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1]
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A module whose dependency failed to compile is no longer type checked. It
|
|
14
|
+
reached for bindings the broken module never produced and raised through the
|
|
15
|
+
compiler, naming the importer rather than the module at fault, and the
|
|
16
|
+
original error was never reported. Dependents now say which dependency
|
|
17
|
+
failed, and modules broken independently of each other are all still
|
|
18
|
+
reported.
|
|
19
|
+
- The load-path line at the top of every compiled entry module appends the
|
|
20
|
+
application's `lib` instead of prepending it. At position 0 it shadowed, for
|
|
21
|
+
the whole process, any gem sharing a name with a directory under `lib` —
|
|
22
|
+
`sidekiq/`, `sentry/`, `warden/`. The line exists so `uses App::Thing` can
|
|
23
|
+
resolve `lib/app/thing.rb`; it never needed to outrank gems.
|
|
24
|
+
|
|
25
|
+
## [0.3.0]
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `jade fmt` keeps blank lines between leading comment blocks. A section
|
|
30
|
+
banner separated from the declaration below it stays separated, where it
|
|
31
|
+
used to be pulled into that declaration's doc comment. **Formatted output
|
|
32
|
+
differs from 0.2.0**, so a format check in CI reports diffs on the first
|
|
33
|
+
run after upgrading.
|
|
34
|
+
- `jade q` requires a `jade.json` and reports what to write when there isn't
|
|
35
|
+
one. It previously assumed the source root was the working directory, which
|
|
36
|
+
could not work for a project whose imports come from extension gems — it
|
|
37
|
+
failed later, inside the loader, instead.
|
|
38
|
+
- `Compiler::Config#source_root=` takes one root and refuses a list of more
|
|
39
|
+
than one. A list was accepted before but only its first entry was ever
|
|
40
|
+
read.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- `jade.json` manifest, read by `Jade::Project`, giving tools that run
|
|
45
|
+
outside the application — the CLI, an editor's language server, a codegen
|
|
46
|
+
task — the source roots and extension gems that previously existed only as
|
|
47
|
+
Ruby run at boot. `Compiler::Config` seeds from it, and a `Jade.setup`
|
|
48
|
+
block still wins.
|
|
49
|
+
- `Encodable` and `Decodable` derive for unions whose variants take no
|
|
50
|
+
arguments, using the variant name in snake_case as the wire form. A union
|
|
51
|
+
mixing nullary and argument-carrying variants does not derive: adding an
|
|
52
|
+
argument to one variant would otherwise change the encoding of all of them.
|
|
53
|
+
- `SqlMapper` derives for unions whose variants each carry exactly one value,
|
|
54
|
+
naming the column after the variant. Inert unless jade-sql is loaded.
|
|
55
|
+
- Placeholders in keyed calls: `Point(x: _, y: n)` partially applies the way
|
|
56
|
+
`Point(_, n)` already did. Blanks bind in the order the fields are written.
|
|
57
|
+
- `Source` records the root it was loaded from, so an application module and
|
|
58
|
+
one shipped by an extension gem can be told apart without a name list.
|
|
59
|
+
- `UsageAnalysis::Reference` records the declaration each reference was made
|
|
60
|
+
from, including references with no source range and those inside an
|
|
61
|
+
`implements` block.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- An interface method can be referenced as a value where the expected type
|
|
66
|
+
determines the instance. `Decode.field("k", Decode.decoder)` type-checked
|
|
67
|
+
and then failed in codegen, because the method's constraint named a
|
|
68
|
+
variable that appeared in nothing and so could never be bound.
|
|
69
|
+
- A constraint that resolves to a concrete type is discharged rather than
|
|
70
|
+
dropped when a binding is generalized. An interface method referenced
|
|
71
|
+
outside an argument position previously compiled to a bare name and failed
|
|
72
|
+
at runtime.
|
|
73
|
+
- Deriving reports which type it could not derive instead of raising through
|
|
74
|
+
the compiler. A struct with one un-derivable field took the whole run down
|
|
75
|
+
with a Ruby backtrace, which `tolerant: true` did not contain.
|
|
76
|
+
- `jade fmt` no longer drops comments. A comment above the first declaration
|
|
77
|
+
in a module attached to a node nothing renders, so a module's first
|
|
78
|
+
declaration could never carry a doc comment.
|
|
79
|
+
- A type sharing its name with its module no longer generates code that
|
|
80
|
+
resolves the wrong constant.
|
|
81
|
+
|
|
9
82
|
## [0.2.0]
|
|
10
83
|
|
|
11
84
|
### Added
|
|
@@ -42,7 +115,9 @@ Initial release.
|
|
|
42
115
|
- `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
|
|
43
116
|
- Language server and headless query interface for editor/agent tooling.
|
|
44
117
|
|
|
45
|
-
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.
|
|
118
|
+
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.3.1...HEAD
|
|
119
|
+
[0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
|
|
120
|
+
[0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
|
|
46
121
|
[0.2.0]: https://github.com/agustinrhcp/jade/compare/v0.1.1...v0.2.0
|
|
47
122
|
[0.1.1]: https://github.com/agustinrhcp/jade/compare/v0.1.0...v0.1.1
|
|
48
123
|
[0.1.0]: https://github.com/agustinrhcp/jade/releases/tag/v0.1.0
|
data/lib/jade/ast/node.rb
CHANGED
|
@@ -16,6 +16,22 @@ module Jade
|
|
|
16
16
|
@@_next_id += 1
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# The comment block touching this node. A blank line above it means
|
|
20
|
+
# a section banner, which documents the region and not this node.
|
|
21
|
+
def doc(source)
|
|
22
|
+
leading_comment_groups(source)
|
|
23
|
+
.last
|
|
24
|
+
&.then { follows_directly?(it.last, self, source) ? it : nil }
|
|
25
|
+
&.then { it.map(&:value).join("\n") }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Split wherever a blank line separates one block from the next.
|
|
29
|
+
def leading_comment_groups(source)
|
|
30
|
+
leading_comments
|
|
31
|
+
.slice_when { |a, b| !follows_directly?(a, b, source) }
|
|
32
|
+
.to_a
|
|
33
|
+
end
|
|
34
|
+
|
|
19
35
|
# Deepest descendant whose range covers `offset`, or self if no child
|
|
20
36
|
# matches. Returns nil if this node's range is missing or doesn't
|
|
21
37
|
# cover the offset.
|
|
@@ -39,6 +55,20 @@ module Jade
|
|
|
39
55
|
.first
|
|
40
56
|
.then { [self] + (it || []) }
|
|
41
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def follows_directly?(first, second, source)
|
|
62
|
+
line_of(second.range.begin, source) -
|
|
63
|
+
line_of(first.range.end - 1, source) == 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def line_of(pos, source)
|
|
67
|
+
source
|
|
68
|
+
.line_starts
|
|
69
|
+
.bsearch_index { it > pos }
|
|
70
|
+
.then { it ? it - 1 : source.line_starts.length - 1 }
|
|
71
|
+
end
|
|
42
72
|
end
|
|
43
73
|
end
|
|
44
74
|
end
|
data/lib/jade/cli/q.rb
CHANGED
|
@@ -88,18 +88,22 @@ module Jade
|
|
|
88
88
|
class Context
|
|
89
89
|
attr_reader :file, :source_root, :registry, :entry
|
|
90
90
|
|
|
91
|
+
# jade.json is what says where sources live and which extension gems
|
|
92
|
+
# ship the modules they import. Without it `Sql.Uuid` resolves
|
|
93
|
+
# against cwd and the load dies.
|
|
91
94
|
def initialize(file)
|
|
92
|
-
|
|
93
|
-
@
|
|
95
|
+
project = Jade::Project.find!
|
|
96
|
+
@file = project.source_relative(file)
|
|
97
|
+
@source_root = project.source_root
|
|
94
98
|
@registry = Jade::ModuleLoader.load(
|
|
95
|
-
@source_root, file,
|
|
96
|
-
cache_dir:
|
|
99
|
+
@source_root, @file,
|
|
100
|
+
cache_dir: project.cache_path,
|
|
97
101
|
tolerant: true,
|
|
98
102
|
)
|
|
99
103
|
@entry = @registry
|
|
100
104
|
.modules
|
|
101
105
|
.each_value
|
|
102
|
-
.find { it.source&.uri == file } || fail("no module at #{file}")
|
|
106
|
+
.find { it.source&.uri == @file } || fail("no module at #{@file}")
|
|
103
107
|
end
|
|
104
108
|
|
|
105
109
|
def path_at(line, col)
|
data/lib/jade/cli.rb
CHANGED
data/lib/jade/codegen/emitter.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Jade
|
|
|
48
48
|
"Jade::Runtime.intr(#{name.inspect})"
|
|
49
49
|
|
|
50
50
|
in [:struct_constructor, qualified_name, arity]
|
|
51
|
-
"Jade::Runtime.curry(
|
|
51
|
+
"Jade::Runtime.curry(::#{to_qualified(qualified_name)}.method(:[]), #{arity})"
|
|
52
52
|
|
|
53
53
|
in [:anon_record_constructor, keys]
|
|
54
54
|
"Jade::Runtime.curry(#{data_define(keys)}.method(:[]), #{keys.size})"
|
|
@@ -68,7 +68,7 @@ module Jade
|
|
|
68
68
|
args
|
|
69
69
|
.map { "#{it}" }
|
|
70
70
|
.join(',')
|
|
71
|
-
.then { "
|
|
71
|
+
.then { "::#{to_qualified(name)}(#{it})" }
|
|
72
72
|
|
|
73
73
|
in [:_]
|
|
74
74
|
'_'
|
data/lib/jade/codegen/inlines.rb
CHANGED
data/lib/jade/codegen.rb
CHANGED
|
@@ -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.
|
|
401
|
+
'$LOAD_PATH.push(File.expand_path("lib")).uniq!'
|
|
398
402
|
end
|
|
399
403
|
end
|
|
400
404
|
end
|
data/lib/jade/compiler.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Jade
|
|
|
11
11
|
|
|
12
12
|
if needs_rebuild?(target)
|
|
13
13
|
ModuleLoader
|
|
14
|
-
.load(config.source_root
|
|
14
|
+
.load(config.source_root, path + '.jd', cache_dir: cache_root)
|
|
15
15
|
.tap { render_diagnostics(it) }
|
|
16
16
|
.then { ModuleLoader.emit(it, path: build_root) }
|
|
17
17
|
end
|
|
@@ -47,22 +47,31 @@ module Jade
|
|
|
47
47
|
|
|
48
48
|
target_mtime = File.mtime(target)
|
|
49
49
|
Dir
|
|
50
|
-
.glob(File.join(config.source_root
|
|
50
|
+
.glob(File.join(config.source_root, '**/*.jd'))
|
|
51
51
|
.any? { |src| File.mtime(src) > target_mtime }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
class Config
|
|
55
55
|
attr_accessor :project_root, :source_root, :build_dir, :cache_dir
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@
|
|
57
|
+
# Seeded from jade.json when there is one, so `Jade.setup` needs only
|
|
58
|
+
# what the manifest doesn't say. A setup block still wins — it runs
|
|
59
|
+
# after this.
|
|
60
|
+
def initialize(project = Project.find)
|
|
61
|
+
@project_root = project&.root || Dir.pwd
|
|
62
|
+
@source_root = project&.source_root
|
|
63
|
+
@build_dir = project&.build_dir || ".jade/build"
|
|
64
|
+
@cache_dir = project&.cache_dir || ".jade/cache"
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
# One root. A list was accepted but only ever read at `.first`, so
|
|
68
|
+
# anything past the first was silently dropped — say so instead.
|
|
64
69
|
def source_root=(root)
|
|
65
|
-
|
|
70
|
+
Array(root).then do
|
|
71
|
+
fail ArgumentError, "source_root takes one root, got #{it.size}" if it.size > 1
|
|
72
|
+
|
|
73
|
+
@source_root = it.first
|
|
74
|
+
end
|
|
66
75
|
end
|
|
67
76
|
end
|
|
68
77
|
end
|
|
@@ -13,7 +13,7 @@ module Jade
|
|
|
13
13
|
# walks delegated to their own modules.
|
|
14
14
|
module Helper
|
|
15
15
|
def format_node(node, indent: 0, source: nil)
|
|
16
|
-
leading = format_leading_comments(node, indent)
|
|
16
|
+
leading = format_leading_comments(node, indent, source)
|
|
17
17
|
trailing = format_trailing_comment(node)
|
|
18
18
|
|
|
19
19
|
dispatch_for(node)
|
|
@@ -75,15 +75,21 @@ module Jade
|
|
|
75
75
|
(INDENT * indent).length + line.length > LINE_LIMIT
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
# The blank line between blocks is load-bearing: collapsing it turns
|
|
79
|
+
# a section banner into the next declaration's doc comment.
|
|
80
|
+
def format_leading_comments(node, indent, source = nil)
|
|
79
81
|
return "" if node.leading_comments.empty?
|
|
80
82
|
|
|
81
|
-
node
|
|
82
|
-
.map { |
|
|
83
|
-
.join("\n")
|
|
83
|
+
leading_comment_blocks(node, source)
|
|
84
|
+
.map { |block| block.map { it.value.then(&and_indent(indent)) }.join("\n") }
|
|
85
|
+
.join("\n\n")
|
|
84
86
|
.then { it + "\n" }
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
def leading_comment_blocks(node, source)
|
|
90
|
+
source ? node.leading_comment_groups(source) : [node.leading_comments]
|
|
91
|
+
end
|
|
92
|
+
|
|
87
93
|
def format_trailing_comment(node)
|
|
88
94
|
return "" if node.trailing_comments.empty?
|
|
89
95
|
|
|
@@ -10,7 +10,9 @@ module Jade
|
|
|
10
10
|
|
|
11
11
|
collect_nodes(ast)
|
|
12
12
|
.reject { |n| n.range.nil? }
|
|
13
|
-
|
|
13
|
+
# Outermost first, so `x = f(y)` anchors on the Assign rather
|
|
14
|
+
# than the pattern nested inside it.
|
|
15
|
+
.sort_by { |n| [n.range.begin, -n.range.size] }
|
|
14
16
|
.then { build_map(comments, it, source) }
|
|
15
17
|
.then { reattach(ast, it) }
|
|
16
18
|
end
|
|
@@ -70,7 +72,16 @@ module Jade
|
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def first_node_starting_after(pos, sorted_nodes)
|
|
73
|
-
sorted_nodes
|
|
75
|
+
sorted_nodes
|
|
76
|
+
.bsearch_index { |n| n.range.begin >= pos }
|
|
77
|
+
&.then { |from| (from...sorted_nodes.size).find { renders_own_comments?(sorted_nodes[it]) } }
|
|
78
|
+
&.then { sorted_nodes[it] }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ModuleNode walks `body.expressions` directly, so a Body's own
|
|
82
|
+
# leading comments reach no formatter and would be lost.
|
|
83
|
+
def renders_own_comments?(node)
|
|
84
|
+
!node.is_a?(AST::Body)
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
def smallest_enclosing(pos, sorted_nodes)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module SemanticAnalysis
|
|
4
|
+
module Error
|
|
5
|
+
class PlaceholderNotAllowed < Jade::Error
|
|
6
|
+
def initialize(entry, span, field:)
|
|
7
|
+
@field = field
|
|
8
|
+
super(entry:, span:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"`#{@field}: _` is only allowed when constructing a struct"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def label
|
|
16
|
+
"not allowed here"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -5,6 +5,7 @@ require 'jade/frontend/semantic_analysis/error/constant_not_callable'
|
|
|
5
5
|
require 'jade/frontend/semantic_analysis/error/constructor_not_found'
|
|
6
6
|
require 'jade/frontend/semantic_analysis/error/constructor_pattern_arity_mismatch'
|
|
7
7
|
require 'jade/frontend/semantic_analysis/error/duplicate_field'
|
|
8
|
+
require 'jade/frontend/semantic_analysis/error/placeholder_not_allowed'
|
|
8
9
|
require 'jade/frontend/semantic_analysis/error/duplicate_function_declaration'
|
|
9
10
|
require 'jade/frontend/semantic_analysis/error/duplicate_record_field'
|
|
10
11
|
require 'jade/frontend/semantic_analysis/error/invalid_list_rest_pattern'
|
|
@@ -16,6 +16,12 @@ module Jade
|
|
|
16
16
|
constructor = constructor_symbol(callee_resolved, registry)
|
|
17
17
|
parent = constructor && registry.lookup(constructor.parent)
|
|
18
18
|
|
|
19
|
+
if fields.any? { it.value.is_a?(AST::Placeholder) }
|
|
20
|
+
return partially_applied(
|
|
21
|
+
node, callee, fields, parent, constructor, registry, scope, entry, callee_r
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
fields_r = analyze_in_parallel(fields, registry, scope, entry)
|
|
20
26
|
fields_resolved = fields_r.node
|
|
21
27
|
|
|
@@ -34,6 +40,55 @@ module Jade
|
|
|
34
40
|
|
|
35
41
|
private
|
|
36
42
|
|
|
43
|
+
# Lowered before the fields are analyzed, so the placeholders are
|
|
44
|
+
# still bare AST and `Placeholder.lift` can turn the call into a
|
|
45
|
+
# lambda — the same route a positional partial application takes.
|
|
46
|
+
def partially_applied(node, callee, fields, parent, constructor, registry, scope, entry, callee_r)
|
|
47
|
+
errors = Validation.errors(node, fields, parent, constructor, registry, entry) +
|
|
48
|
+
placeholder_errors(fields, parent, entry)
|
|
49
|
+
|
|
50
|
+
return Result[node, callee_r.errors + errors, scope] if errors.any?
|
|
51
|
+
|
|
52
|
+
named, params = name_placeholders(fields)
|
|
53
|
+
|
|
54
|
+
lower(node, callee, named, parent, constructor, registry)
|
|
55
|
+
.then { wrap_in_lambdas(it, params, node.range) }
|
|
56
|
+
.then { analyze_node(it, registry, scope, entry) }
|
|
57
|
+
.then { Result[it.node, callee_r.errors + it.errors, scope] }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Named in the order the fields were written, not the order the
|
|
61
|
+
# struct declares them, so `Point(y: _, x: _)` takes y first.
|
|
62
|
+
def name_placeholders(fields)
|
|
63
|
+
fields.reduce([[], []]) do |(named, params), field|
|
|
64
|
+
case field.value
|
|
65
|
+
in AST::Placeholder
|
|
66
|
+
Codegen::Helpers.param_synthetic_name(params.size).then do |name|
|
|
67
|
+
[named + [field.with(value: AST::VariableReference[name, nil])], params + [name]]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
else
|
|
71
|
+
[named + [field], params]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def wrap_in_lambdas(call, params, range)
|
|
77
|
+
params
|
|
78
|
+
.reverse
|
|
79
|
+
.reduce(call) do |body, name|
|
|
80
|
+
AST::Lambda[[AST::Pattern::Binding[name, nil]], AST::Body[[body], nil], range]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def placeholder_errors(fields, parent, entry)
|
|
85
|
+
return [] if parent.is_a?(Symbol::Struct)
|
|
86
|
+
|
|
87
|
+
fields
|
|
88
|
+
.select { it.value.is_a?(AST::Placeholder) }
|
|
89
|
+
.map { Error::PlaceholderNotAllowed.new(entry.name, it.range, field: it.key) }
|
|
90
|
+
end
|
|
91
|
+
|
|
37
92
|
def lower(node, callee, fields, parent, constructor, registry)
|
|
38
93
|
case parent
|
|
39
94
|
in Symbol::Struct
|
|
@@ -28,6 +28,9 @@ module Jade
|
|
|
28
28
|
in Symbol::Struct
|
|
29
29
|
derive_struct(constraint, resolved_sym, args, registry, lookup, entry_name)
|
|
30
30
|
|
|
31
|
+
in Symbol::Union if nullary?(resolved_sym, registry)
|
|
32
|
+
derive_nullary_union(constraint, resolved_sym, registry)
|
|
33
|
+
|
|
31
34
|
else
|
|
32
35
|
failed(constraint, entry_name)
|
|
33
36
|
end
|
|
@@ -87,14 +90,32 @@ module Jade
|
|
|
87
90
|
Type.constraint(INTERFACE, field_type, nil)
|
|
88
91
|
end
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
resolve_dep(
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
field_deps
|
|
94
|
+
.map { resolve_dep(it, lookup, entry_name) }
|
|
95
|
+
.then { Results.sequence(it) }
|
|
96
|
+
.map { implementation(constraint, record_body(fields, constructor_ref), it) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def derive_nullary_union(constraint, union_sym, registry)
|
|
100
|
+
variants(union_sym, registry)
|
|
101
|
+
.then { nullary_union_body(it) }
|
|
102
|
+
.then { Ok[implementation(constraint, it, [])] }
|
|
103
|
+
end
|
|
94
104
|
|
|
105
|
+
def nullary_union_body(variants)
|
|
106
|
+
[:call,
|
|
107
|
+
[:stdlib_fn, 'Decode.string_enum'],
|
|
108
|
+
[
|
|
109
|
+
[:list, variants.map { wire_name(it) }],
|
|
110
|
+
[:list, variants.map { [:call, [:struct_constructor, it.qualified_name, 0], []] }],
|
|
111
|
+
],
|
|
112
|
+
]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def record_body(fields, constructor_ref)
|
|
95
116
|
seed = [:call, [:stdlib_fn, 'Decode.succeed'], [constructor_ref]]
|
|
96
117
|
|
|
97
|
-
|
|
118
|
+
fields.each_with_index.reduce(seed) do |acc, ((field_name, _), idx)|
|
|
98
119
|
field_decoder = [:call,
|
|
99
120
|
[:stdlib_fn, 'Decode.field'],
|
|
100
121
|
[
|
|
@@ -105,8 +126,6 @@ module Jade
|
|
|
105
126
|
|
|
106
127
|
[:call, [:stdlib_fn, 'Decode.and_map'], [acc, field_decoder]]
|
|
107
128
|
end
|
|
108
|
-
|
|
109
|
-
Ok[implementation(constraint, body, resolved_deps)]
|
|
110
129
|
end
|
|
111
130
|
|
|
112
131
|
# Free-var inner constraints fall back to the constraint marker
|
|
@@ -30,6 +30,9 @@ module Jade
|
|
|
30
30
|
in Symbol::Struct
|
|
31
31
|
derive_struct(constraint, resolved_sym, args, registry, lookup, entry_name)
|
|
32
32
|
|
|
33
|
+
in Symbol::Union if nullary?(resolved_sym, registry)
|
|
34
|
+
derive_nullary_union(constraint, resolved_sym, registry)
|
|
35
|
+
|
|
33
36
|
else
|
|
34
37
|
failed(constraint, entry_name)
|
|
35
38
|
end
|
|
@@ -84,18 +87,25 @@ module Jade
|
|
|
84
87
|
end
|
|
85
88
|
end
|
|
86
89
|
|
|
90
|
+
def derive_nullary_union(constraint, union_sym, registry)
|
|
91
|
+
body = variants(union_sym, registry)
|
|
92
|
+
.map { |v|
|
|
93
|
+
[
|
|
94
|
+
[:constructor, v.qualified_name, []],
|
|
95
|
+
[[:call, [:stdlib_fn, 'Encode.string'], [wire_name(v)]]],
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
.then { [:case, [:var, 'v'], it] }
|
|
99
|
+
|
|
100
|
+
Ok[implementation(constraint, params: ['v'], body:, deps: [])]
|
|
101
|
+
end
|
|
102
|
+
|
|
87
103
|
def derive_struct(constraint, struct_sym, type_args, registry, lookup, entry_name)
|
|
88
104
|
fields = struct_fields(struct_sym, type_args, registry)
|
|
89
105
|
|
|
90
106
|
field_deps = fields
|
|
91
107
|
.map { |_, field_type| Type.constraint(INTERFACE, field_type, nil) }
|
|
92
108
|
|
|
93
|
-
resolved_deps = field_deps
|
|
94
|
-
.map do |dep|
|
|
95
|
-
lookup.call(dep) => Ok[impl]
|
|
96
|
-
impl
|
|
97
|
-
end
|
|
98
|
-
|
|
99
109
|
pair_irs = fields
|
|
100
110
|
.each_with_index
|
|
101
111
|
.map do |(field_name, _), idx|
|
|
@@ -118,7 +128,10 @@ module Jade
|
|
|
118
128
|
],
|
|
119
129
|
]
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
field_deps
|
|
132
|
+
.map { lookup.call(it) }
|
|
133
|
+
.then { Results.sequence(it) }
|
|
134
|
+
.map { implementation(constraint, params: ['rec'], body:, deps: it) }
|
|
122
135
|
end
|
|
123
136
|
|
|
124
137
|
def implementation(constraint, params:, body:, deps:)
|
|
@@ -34,7 +34,8 @@ module Jade
|
|
|
34
34
|
deps = dependencies_of(impl, args)
|
|
35
35
|
resolved_deps = deps.filter_map { |dep|
|
|
36
36
|
next if dep.type in Type::Var
|
|
37
|
-
lookup.call(dep) => Ok[resolved]
|
|
37
|
+
lookup.call(dep).on_err { return Err[it] } => Ok[resolved]
|
|
38
|
+
resolved
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
Symbol::Implementation.new(
|
|
@@ -6,6 +6,23 @@ module Jade
|
|
|
6
6
|
module Helpers
|
|
7
7
|
extend self
|
|
8
8
|
|
|
9
|
+
# `union :Int` and friends are variant-less unions standing in
|
|
10
|
+
# for native types, so an empty variant list is not an enum.
|
|
11
|
+
def nullary?(union_sym, registry)
|
|
12
|
+
variants(union_sym, registry)
|
|
13
|
+
.then { it.any? && it.all? { it.args.empty? } }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def variants(union_sym, registry)
|
|
17
|
+
union_sym.variants.map { registry.lookup(it) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The wire name a variant carries when nothing says otherwise.
|
|
21
|
+
# Matches what hand-written mappings already use.
|
|
22
|
+
def wire_name(variant)
|
|
23
|
+
Source.snake_case(variant.name)
|
|
24
|
+
end
|
|
25
|
+
|
|
9
26
|
def struct_fields(struct_sym, type_args, registry)
|
|
10
27
|
record = struct_sym.record_type
|
|
11
28
|
type_param_names = struct_sym.type_params.map(&:name)
|