jade-lang 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -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/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/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/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: 6886eb468004c2c500090b0a919a1a369e31b36039b3a6ef583bb74bc5e7326b
|
|
4
|
+
data.tar.gz: eaa9bb82c455efa0ca5c9e86eefe7d6ae6a56b1a9c8d695ae484500b69c1e049
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a27df2bd32afa406b3205ca40ea047231cfe95c169cc38918b453278675d889ff9038ef93a2d08fd5caeb94683cc47cc5d498c8a5daa0e086a7f98630f733d7
|
|
7
|
+
data.tar.gz: e7fd05fc7622d6268f6ad109e8c6ad2b44df4961f668515d4eafb69767fa437d9b34d13276946723b58486d1393698fb713dba7a484965524753d913e779a9ee
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,63 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0]
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `jade fmt` keeps blank lines between leading comment blocks. A section
|
|
14
|
+
banner separated from the declaration below it stays separated, where it
|
|
15
|
+
used to be pulled into that declaration's doc comment. **Formatted output
|
|
16
|
+
differs from 0.2.0**, so a format check in CI reports diffs on the first
|
|
17
|
+
run after upgrading.
|
|
18
|
+
- `jade q` requires a `jade.json` and reports what to write when there isn't
|
|
19
|
+
one. It previously assumed the source root was the working directory, which
|
|
20
|
+
could not work for a project whose imports come from extension gems — it
|
|
21
|
+
failed later, inside the loader, instead.
|
|
22
|
+
- `Compiler::Config#source_root=` takes one root and refuses a list of more
|
|
23
|
+
than one. A list was accepted before but only its first entry was ever
|
|
24
|
+
read.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `jade.json` manifest, read by `Jade::Project`, giving tools that run
|
|
29
|
+
outside the application — the CLI, an editor's language server, a codegen
|
|
30
|
+
task — the source roots and extension gems that previously existed only as
|
|
31
|
+
Ruby run at boot. `Compiler::Config` seeds from it, and a `Jade.setup`
|
|
32
|
+
block still wins.
|
|
33
|
+
- `Encodable` and `Decodable` derive for unions whose variants take no
|
|
34
|
+
arguments, using the variant name in snake_case as the wire form. A union
|
|
35
|
+
mixing nullary and argument-carrying variants does not derive: adding an
|
|
36
|
+
argument to one variant would otherwise change the encoding of all of them.
|
|
37
|
+
- `SqlMapper` derives for unions whose variants each carry exactly one value,
|
|
38
|
+
naming the column after the variant. Inert unless jade-sql is loaded.
|
|
39
|
+
- Placeholders in keyed calls: `Point(x: _, y: n)` partially applies the way
|
|
40
|
+
`Point(_, n)` already did. Blanks bind in the order the fields are written.
|
|
41
|
+
- `Source` records the root it was loaded from, so an application module and
|
|
42
|
+
one shipped by an extension gem can be told apart without a name list.
|
|
43
|
+
- `UsageAnalysis::Reference` records the declaration each reference was made
|
|
44
|
+
from, including references with no source range and those inside an
|
|
45
|
+
`implements` block.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- An interface method can be referenced as a value where the expected type
|
|
50
|
+
determines the instance. `Decode.field("k", Decode.decoder)` type-checked
|
|
51
|
+
and then failed in codegen, because the method's constraint named a
|
|
52
|
+
variable that appeared in nothing and so could never be bound.
|
|
53
|
+
- A constraint that resolves to a concrete type is discharged rather than
|
|
54
|
+
dropped when a binding is generalized. An interface method referenced
|
|
55
|
+
outside an argument position previously compiled to a bare name and failed
|
|
56
|
+
at runtime.
|
|
57
|
+
- Deriving reports which type it could not derive instead of raising through
|
|
58
|
+
the compiler. A struct with one un-derivable field took the whole run down
|
|
59
|
+
with a Ruby backtrace, which `tolerant: true` did not contain.
|
|
60
|
+
- `jade fmt` no longer drops comments. A comment above the first declaration
|
|
61
|
+
in a module attached to a node nothing renders, so a module's first
|
|
62
|
+
declaration could never carry a doc comment.
|
|
63
|
+
- A type sharing its name with its module no longer generates code that
|
|
64
|
+
resolves the wrong constant.
|
|
65
|
+
|
|
9
66
|
## [0.2.0]
|
|
10
67
|
|
|
11
68
|
### Added
|
|
@@ -42,7 +99,8 @@ Initial release.
|
|
|
42
99
|
- `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
|
|
43
100
|
- Language server and headless query interface for editor/agent tooling.
|
|
44
101
|
|
|
45
|
-
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.
|
|
102
|
+
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.3.0...HEAD
|
|
103
|
+
[0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
|
|
46
104
|
[0.2.0]: https://github.com/agustinrhcp/jade/compare/v0.1.1...v0.2.0
|
|
47
105
|
[0.1.1]: https://github.com/agustinrhcp/jade/compare/v0.1.0...v0.1.1
|
|
48
106
|
[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/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)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
module Constraints
|
|
5
|
+
module Deriving
|
|
6
|
+
# jade-sql's interface, derived here rather than there: the
|
|
7
|
+
# deriving framework is not extensible, and this stays inert
|
|
8
|
+
# without jade-sql, since nothing else names the interface.
|
|
9
|
+
module SqlMapper
|
|
10
|
+
extend self
|
|
11
|
+
include Helpers
|
|
12
|
+
|
|
13
|
+
INTERFACE = 'Sql.SqlMapper'
|
|
14
|
+
ASSIGNMENT = 'Sql.Assignment'
|
|
15
|
+
ASSIGNMENT_FIELDS = %w[col value_sql params].freeze
|
|
16
|
+
|
|
17
|
+
def supports?(interface) = interface == INTERFACE
|
|
18
|
+
|
|
19
|
+
def derive(constraint, registry, entry_name, &lookup)
|
|
20
|
+
return failed(constraint, entry_name) unless assignment_matches?(registry)
|
|
21
|
+
|
|
22
|
+
case constraint.type
|
|
23
|
+
in Type::Application(constructor: Type::Constructor(name:), args: [])
|
|
24
|
+
Symbol
|
|
25
|
+
.type_ref_from_qualified_name(name)
|
|
26
|
+
.then { registry.lookup(it) }
|
|
27
|
+
.then { derive_for(constraint, it, registry, lookup, entry_name) }
|
|
28
|
+
|
|
29
|
+
else
|
|
30
|
+
failed(constraint, entry_name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# The interface is matched by name, so some other module called
|
|
37
|
+
# `Sql` would otherwise be derived against as though it were
|
|
38
|
+
# jade-sql — building its Assignment with the wrong arity, and
|
|
39
|
+
# failing at runtime rather than here.
|
|
40
|
+
def assignment_matches?(registry)
|
|
41
|
+
Symbol
|
|
42
|
+
.type_ref_from_qualified_name(ASSIGNMENT)
|
|
43
|
+
.then { registry.lookup(it) }
|
|
44
|
+
.then do
|
|
45
|
+
it in Symbol::Struct(record_type: { fields: }) and
|
|
46
|
+
fields.keys.map(&:to_s) == ASSIGNMENT_FIELDS
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def derive_for(constraint, symbol, registry, lookup, entry_name)
|
|
51
|
+
case symbol
|
|
52
|
+
in Symbol::Union if single_payload?(symbol, registry)
|
|
53
|
+
derive_union(constraint, symbol, registry, lookup, entry_name)
|
|
54
|
+
|
|
55
|
+
else
|
|
56
|
+
failed(constraint, entry_name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# One column per variant, so each variant carries exactly the
|
|
61
|
+
# value that column is set to.
|
|
62
|
+
def single_payload?(union_sym, registry)
|
|
63
|
+
variants(union_sym, registry)
|
|
64
|
+
.then { it.any? && it.all? { it.args.size == 1 } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def derive_union(constraint, union_sym, registry, lookup, entry_name)
|
|
68
|
+
vs = variants(union_sym, registry)
|
|
69
|
+
|
|
70
|
+
vs
|
|
71
|
+
.map { encodable_dep(it, registry) }
|
|
72
|
+
.map { lookup.call(it) }
|
|
73
|
+
.then { Results.sequence(it) }
|
|
74
|
+
.map { implementation(constraint, union_body(vs), it) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def encodable_dep(variant, registry)
|
|
78
|
+
variant
|
|
79
|
+
.args
|
|
80
|
+
.first
|
|
81
|
+
.then { instantiate(it, {}, registry) }
|
|
82
|
+
.then { Type.constraint('Encode.Encodable', it, nil) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def union_body(variants)
|
|
86
|
+
variants
|
|
87
|
+
.each_with_index
|
|
88
|
+
.map { |v, idx| [[:constructor, v.qualified_name, ['x']], [assignment(v, idx)]] }
|
|
89
|
+
.then { [:case, [:var, 'f'], it] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def assignment(variant, idx)
|
|
93
|
+
[:call,
|
|
94
|
+
[:struct_constructor, ASSIGNMENT, 3],
|
|
95
|
+
[
|
|
96
|
+
wire_name(variant),
|
|
97
|
+
'?',
|
|
98
|
+
[:list, [[:call, [:impl_arg, idx, 'encoder'], [[:var, 'x']]]]],
|
|
99
|
+
],
|
|
100
|
+
]
|
|
101
|
+
.then { [:list, [it]] }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failed(constraint, entry_name)
|
|
105
|
+
Err[
|
|
106
|
+
Error::DerivationFailed.new(
|
|
107
|
+
entry_name, constraint.origin&.range, constraint:, trace: [],
|
|
108
|
+
)
|
|
109
|
+
]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def implementation(constraint, body, deps)
|
|
113
|
+
Symbol::Implementation.new(
|
|
114
|
+
module_name: nil,
|
|
115
|
+
interface: Symbol.type_ref_from_qualified_name(constraint.interface),
|
|
116
|
+
type: constraint.type,
|
|
117
|
+
type_params: [],
|
|
118
|
+
constraints: [],
|
|
119
|
+
functions: { 'to_assigns' => Symbol::DerivedFunction.new(params: ['f'], body:) },
|
|
120
|
+
deps:,
|
|
121
|
+
extends: [],
|
|
122
|
+
decl_span: nil,
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -2,6 +2,7 @@ require_relative './deriving/helpers.rb'
|
|
|
2
2
|
require_relative './deriving/eq.rb'
|
|
3
3
|
require_relative './deriving/decodable.rb'
|
|
4
4
|
require_relative './deriving/encodable.rb'
|
|
5
|
+
require_relative './deriving/sql_mapper.rb'
|
|
5
6
|
|
|
6
7
|
module Jade
|
|
7
8
|
module Frontend
|
|
@@ -10,7 +11,7 @@ module Jade
|
|
|
10
11
|
module Deriving
|
|
11
12
|
extend self
|
|
12
13
|
|
|
13
|
-
DERIVERS = [Eq, Decodable, Encodable]
|
|
14
|
+
DERIVERS = [Eq, Decodable, Encodable, SqlMapper]
|
|
14
15
|
|
|
15
16
|
def derivable?(interface)
|
|
16
17
|
DERIVERS.any? { it.supports?(interface) }
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module Frontend
|
|
3
3
|
module UsageAnalysis
|
|
4
|
-
|
|
4
|
+
# `owner` is the key of the enclosing declaration or implementation,
|
|
5
|
+
# `nil` only at module level. Declaration owners share the
|
|
6
|
+
# `symbol_key` namespace, so a reference's owner can be looked up in
|
|
7
|
+
# the index that produced it.
|
|
8
|
+
Reference = Data.define(:symbol_key, :kind, :range, :owner)
|
|
5
9
|
|
|
6
10
|
ReferenceIndex = Data.define(:references) do
|
|
7
11
|
def initialize(references: {})
|
|
@@ -41,6 +45,8 @@ module Jade
|
|
|
41
45
|
case symbol
|
|
42
46
|
in Symbol::Variable
|
|
43
47
|
[:local, symbol.decl_span]
|
|
48
|
+
in Symbol::Implementation
|
|
49
|
+
[:impl, symbol.interface.qname, symbol.type.qname]
|
|
44
50
|
in Symbol::ValueRef | Symbol::TypeRef
|
|
45
51
|
[symbol.module_name, symbol.name]
|
|
46
52
|
else
|
|
@@ -15,11 +15,14 @@ module Jade
|
|
|
15
15
|
# :type_annotation - type appearing in a signature, variant args,
|
|
16
16
|
# struct fields, interface signatures, etc.
|
|
17
17
|
# :exposed - name listed in `module M exposing (...)`
|
|
18
|
+
#
|
|
19
|
+
# Every reference also carries the `owner` it was made from — see
|
|
20
|
+
# ReferenceIndex. Only `:exposed` has none.
|
|
18
21
|
module UsageAnalysis
|
|
19
22
|
extend self
|
|
20
23
|
|
|
21
24
|
def analyze(entry, _registry)
|
|
22
|
-
walk(entry.ast, :as_value, entry)
|
|
25
|
+
walk(entry.ast, :as_value, entry, nil)
|
|
23
26
|
.group_by(&:symbol_key)
|
|
24
27
|
.freeze
|
|
25
28
|
.then { entry.with(usage_index: ReferenceIndex.new(references: it)) }
|
|
@@ -27,103 +30,123 @@ module Jade
|
|
|
27
30
|
|
|
28
31
|
private
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
# `owner` is the key of the enclosing declaration or implementation,
|
|
34
|
+
# threaded down rather than recovered afterwards by range
|
|
35
|
+
# containment: desugared nodes carry `range == nil`, and references
|
|
36
|
+
# inside an `implements` block sit in no declaration's span, so
|
|
37
|
+
# both are unbucketable.
|
|
38
|
+
def walk(node, ctx, entry, owner)
|
|
31
39
|
case node
|
|
32
40
|
in AST::Module(exposing:, body:)
|
|
33
|
-
walk_exposing(exposing, entry) + walk(body, :as_value, entry)
|
|
41
|
+
walk_exposing(exposing, entry) + walk(body, :as_value, entry, owner)
|
|
34
42
|
|
|
35
43
|
in AST::Body(expressions:)
|
|
36
|
-
expressions.flat_map { walk(it, :as_value, entry) }
|
|
44
|
+
expressions.flat_map { walk(it, :as_value, entry, owner) }
|
|
37
45
|
|
|
38
|
-
in AST::FunctionDeclaration(body:, params:, return_type:)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
in AST::FunctionDeclaration(body:, params:, return_type:, symbol:)
|
|
47
|
+
ReferenceIndex.key_for(symbol).then do |declared|
|
|
48
|
+
walk(body, :as_value, entry, declared) +
|
|
49
|
+
params.flat_map { walk_type(it.type, entry, declared) } +
|
|
50
|
+
walk_type(return_type, entry, declared)
|
|
51
|
+
end
|
|
42
52
|
|
|
43
53
|
in AST::FunctionCall(callee:, args:)
|
|
44
|
-
walk(callee, :called, entry
|
|
54
|
+
walk(callee, :called, entry, owner) +
|
|
55
|
+
args.flat_map { walk(it, :as_value, entry, owner) }
|
|
45
56
|
|
|
46
57
|
in AST::VariableReference(symbol:, range:)
|
|
47
|
-
ref(symbol, ctx, range)
|
|
58
|
+
ref(symbol, ctx, range, owner)
|
|
48
59
|
|
|
49
60
|
in AST::ConstructorReference(symbol:, range:)
|
|
50
|
-
ref(symbol, ctx == :called ? :constructed : :as_value, range)
|
|
61
|
+
ref(symbol, ctx == :called ? :constructed : :as_value, range, owner)
|
|
51
62
|
|
|
52
63
|
in AST::QualifiedAccess(symbol:, range:)
|
|
53
|
-
ref(symbol, ctx, range)
|
|
64
|
+
ref(symbol, ctx, range, owner)
|
|
54
65
|
|
|
55
66
|
in AST::Lambda(body:, params:)
|
|
56
|
-
walk(body, :as_value, entry) +
|
|
57
|
-
params.flat_map { walk(it, :as_value, entry) }
|
|
67
|
+
walk(body, :as_value, entry, owner) +
|
|
68
|
+
params.flat_map { walk(it, :as_value, entry, owner) }
|
|
58
69
|
|
|
59
70
|
in AST::Assign(pattern:, expression:)
|
|
60
|
-
walk(expression, :as_value, entry) +
|
|
71
|
+
walk(expression, :as_value, entry, owner) +
|
|
72
|
+
walk(pattern, :as_value, entry, owner)
|
|
61
73
|
|
|
62
74
|
in AST::IfThenElse(condition:, if_branch:, else_branch:)
|
|
63
|
-
walk(condition, :as_value, entry) +
|
|
64
|
-
walk(if_branch, :as_value, entry) +
|
|
65
|
-
walk(else_branch, :as_value, entry)
|
|
75
|
+
walk(condition, :as_value, entry, owner) +
|
|
76
|
+
walk(if_branch, :as_value, entry, owner) +
|
|
77
|
+
walk(else_branch, :as_value, entry, owner)
|
|
66
78
|
|
|
67
79
|
in AST::CaseOf(expression:, branches:)
|
|
68
|
-
walk(expression, :as_value, entry) +
|
|
69
|
-
branches.flat_map { walk(it, :as_value, entry) }
|
|
80
|
+
walk(expression, :as_value, entry, owner) +
|
|
81
|
+
branches.flat_map { walk(it, :as_value, entry, owner) }
|
|
70
82
|
|
|
71
83
|
in AST::CaseOfBranch(pattern:, body:)
|
|
72
|
-
walk(pattern, :as_value, entry) + walk(body, :as_value, entry)
|
|
84
|
+
walk(pattern, :as_value, entry, owner) + walk(body, :as_value, entry, owner)
|
|
73
85
|
|
|
74
86
|
in AST::Pattern::Constructor(constructor:, patterns:, symbol:)
|
|
75
87
|
# Don't walk `constructor` — it's a bare ConstructorReference
|
|
76
88
|
# and walking it would record a spurious :as_value for every
|
|
77
89
|
# pattern match.
|
|
78
|
-
ref(symbol, :pattern_match, constructor.range) +
|
|
79
|
-
patterns.flat_map { walk(it, :as_value, entry) }
|
|
90
|
+
ref(symbol, :pattern_match, constructor.range, owner) +
|
|
91
|
+
patterns.flat_map { walk(it, :as_value, entry, owner) }
|
|
80
92
|
|
|
81
93
|
in AST::Pattern::List(patterns:, rest:)
|
|
82
|
-
rest_refs = rest ? walk(rest, :as_value, entry) : []
|
|
83
|
-
patterns.flat_map { walk(it, :as_value, entry) } + rest_refs
|
|
94
|
+
rest_refs = rest ? walk(rest, :as_value, entry, owner) : []
|
|
95
|
+
patterns.flat_map { walk(it, :as_value, entry, owner) } + rest_refs
|
|
84
96
|
|
|
85
97
|
in AST::Pattern::Record(fields:)
|
|
86
|
-
fields.flat_map { walk(it.pattern, :as_value, entry) }
|
|
98
|
+
fields.flat_map { walk(it.pattern, :as_value, entry, owner) }
|
|
87
99
|
|
|
88
100
|
in AST::Pattern::Literal | AST::Pattern::Binding | AST::Pattern::Wildcard
|
|
89
101
|
[]
|
|
90
102
|
|
|
91
103
|
in AST::Grouping(expression:)
|
|
92
|
-
walk(expression, ctx, entry)
|
|
104
|
+
walk(expression, ctx, entry, owner)
|
|
93
105
|
|
|
94
106
|
in AST::List(items:)
|
|
95
|
-
items.flat_map { walk(it, :as_value, entry) }
|
|
107
|
+
items.flat_map { walk(it, :as_value, entry, owner) }
|
|
96
108
|
|
|
97
109
|
in AST::RecordLiteral(fields:)
|
|
98
|
-
fields.flat_map { walk(it, :as_value, entry) }
|
|
110
|
+
fields.flat_map { walk(it, :as_value, entry, owner) }
|
|
99
111
|
|
|
100
112
|
in AST::RecordUpdate(base:, fields:)
|
|
101
|
-
walk(base, :as_value, entry
|
|
113
|
+
walk(base, :as_value, entry, owner) +
|
|
114
|
+
fields.flat_map { walk(it, :as_value, entry, owner) }
|
|
102
115
|
|
|
103
116
|
in AST::RecordField(value:)
|
|
104
|
-
walk(value, :as_value, entry)
|
|
117
|
+
walk(value, :as_value, entry, owner)
|
|
105
118
|
|
|
106
119
|
in AST::RecordAccess(target:)
|
|
107
|
-
walk(target, :as_value, entry)
|
|
120
|
+
walk(target, :as_value, entry, owner)
|
|
108
121
|
|
|
109
|
-
in AST::Implementation(applied_type:, functions:)
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
in AST::Implementation(applied_type:, functions:, symbol:)
|
|
123
|
+
# `implements X with f: <lambda>` has no enclosing declaration,
|
|
124
|
+
# so the implementation itself owns what its functions call.
|
|
125
|
+
ReferenceIndex.key_for(symbol).then do |impl|
|
|
126
|
+
walk_type(applied_type, entry, impl) +
|
|
127
|
+
functions.flat_map { walk(it, :as_value, entry, impl) }
|
|
128
|
+
end
|
|
112
129
|
|
|
113
130
|
in AST::ImplementationFunction(fn:)
|
|
114
|
-
walk(fn, :as_value, entry)
|
|
131
|
+
walk(fn, :as_value, entry, owner)
|
|
115
132
|
|
|
116
|
-
in AST::TypeDeclaration(variants:)
|
|
117
|
-
|
|
133
|
+
in AST::TypeDeclaration(variants:, symbol:)
|
|
134
|
+
ReferenceIndex.key_for(symbol).then do |declared|
|
|
135
|
+
variants.flat_map { it.args.flat_map { walk_type(it, entry, declared) } }
|
|
136
|
+
end
|
|
118
137
|
|
|
119
|
-
in AST::StructDeclaration(record_type:)
|
|
120
|
-
walk_type(record_type, entry)
|
|
138
|
+
in AST::StructDeclaration(record_type:, symbol:)
|
|
139
|
+
walk_type(record_type, entry, ReferenceIndex.key_for(symbol))
|
|
121
140
|
|
|
122
|
-
in AST::InterfaceDeclaration(functions:)
|
|
123
|
-
|
|
141
|
+
in AST::InterfaceDeclaration(functions:, symbol:)
|
|
142
|
+
ReferenceIndex.key_for(symbol).then do |declared|
|
|
143
|
+
functions.flat_map { walk_type(it.type, entry, declared) }
|
|
144
|
+
end
|
|
124
145
|
|
|
125
146
|
in AST::InteropImportDeclaration(functions:)
|
|
126
|
-
|
|
147
|
+
# Owned per port, not per `uses` block — the port is what a
|
|
148
|
+
# caller names and what carries the effect boundary.
|
|
149
|
+
functions.flat_map { walk_type(it.type, entry, ReferenceIndex.key_for(it.symbol)) }
|
|
127
150
|
|
|
128
151
|
in AST::ImportDeclaration | AST::VariantDeclaration |
|
|
129
152
|
AST::Literal | AST::CharLiteral |
|
|
@@ -136,26 +159,28 @@ module Jade
|
|
|
136
159
|
end
|
|
137
160
|
end
|
|
138
161
|
|
|
139
|
-
def walk_type(node, entry)
|
|
162
|
+
def walk_type(node, entry, owner)
|
|
140
163
|
case node
|
|
141
164
|
in nil
|
|
142
165
|
[]
|
|
143
166
|
|
|
144
167
|
in AST::TypeName(type:, range:)
|
|
145
168
|
entry.types[type]
|
|
146
|
-
.then { it ?
|
|
169
|
+
.then { it ? ref(it, :type_annotation, range, owner) : [] }
|
|
147
170
|
|
|
148
171
|
in AST::TypeApplication(constructor:, args:)
|
|
149
|
-
walk_type(constructor, entry
|
|
172
|
+
walk_type(constructor, entry, owner) +
|
|
173
|
+
args.flat_map { walk_type(it, entry, owner) }
|
|
150
174
|
|
|
151
175
|
in AST::TypeFunction(params:, return_type:)
|
|
152
|
-
params.flat_map { walk_type(it, entry) } +
|
|
176
|
+
params.flat_map { walk_type(it, entry, owner) } +
|
|
177
|
+
walk_type(return_type, entry, owner)
|
|
153
178
|
|
|
154
179
|
in AST::TypeRecord(fields:)
|
|
155
|
-
fields.values.flat_map { walk_type(it, entry) }
|
|
180
|
+
fields.values.flat_map { walk_type(it, entry, owner) }
|
|
156
181
|
|
|
157
182
|
in AST::TypeTuple(items:)
|
|
158
|
-
items.flat_map { walk_type(it, entry) }
|
|
183
|
+
items.flat_map { walk_type(it, entry, owner) }
|
|
159
184
|
|
|
160
185
|
in AST::TypeVar | AST::TypeUnit | AST::QualifiedTypeName |
|
|
161
186
|
AST::TypeParam
|
|
@@ -183,12 +208,13 @@ module Jade
|
|
|
183
208
|
end
|
|
184
209
|
end
|
|
185
210
|
|
|
211
|
+
# The exposing list is module level, so these have no owner.
|
|
186
212
|
def exposed_ref(symbol, range)
|
|
187
|
-
symbol ?
|
|
213
|
+
symbol ? ref(symbol, :exposed, range, nil) : []
|
|
188
214
|
end
|
|
189
215
|
|
|
190
|
-
def ref(symbol, kind, range)
|
|
191
|
-
[Reference[ReferenceIndex.key_for(symbol), kind, range]]
|
|
216
|
+
def ref(symbol, kind, range, owner)
|
|
217
|
+
[Reference[ReferenceIndex.key_for(symbol), kind, range, owner]]
|
|
192
218
|
end
|
|
193
219
|
end
|
|
194
220
|
end
|
data/lib/jade/parsing.rb
CHANGED
|
@@ -362,11 +362,17 @@ module Jade
|
|
|
362
362
|
parser(:keyed_call_postfix) {
|
|
363
363
|
(
|
|
364
364
|
type(:lparen) >>
|
|
365
|
-
comma_sequence(
|
|
365
|
+
comma_sequence(keyed_call_field) >>
|
|
366
366
|
type(:rparen)
|
|
367
367
|
).map { |(lparen, fields, rparen)| KeyedCallPostfix[lparen, fields, rparen] }
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
# Like a record field, but a value may be `_`, so a keyed call can be
|
|
371
|
+
# partially applied the way a positional one can.
|
|
372
|
+
parser(:keyed_call_field) {
|
|
373
|
+
(identifier >> type(:colon).skip >> function_call_arg).map(&AST.record_field)
|
|
374
|
+
}
|
|
375
|
+
|
|
370
376
|
parser(:function_call_arg) { placeholder | lazy { expression } }
|
|
371
377
|
|
|
372
378
|
parser(:placeholder) { type(:wildcard).map(&AST.placeholder) }
|
data/lib/jade/project.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
|
|
4
|
+
module Jade
|
|
5
|
+
# A project's compiler settings, read from `jade.json` at its root.
|
|
6
|
+
#
|
|
7
|
+
# Without this, project config only exists as Ruby executed at app boot
|
|
8
|
+
# (`Jade.setup`), so every tool that runs outside the app — the CLI, an
|
|
9
|
+
# editor's LSP, a codegen task — is blind to it and has to guess the
|
|
10
|
+
# source root and which extension gems to load.
|
|
11
|
+
#
|
|
12
|
+
# Subclassed rather than `Project = Data.define(...) do ... end` because
|
|
13
|
+
# constants in that block land in `Jade`, not on the class — which would
|
|
14
|
+
# put `Jade::DEFAULTS` in the gem's top namespace.
|
|
15
|
+
class Project < Data.define(
|
|
16
|
+
:root, :source_roots, :build_dir, :cache_dir, :extensions, :entries, :map
|
|
17
|
+
)
|
|
18
|
+
MANIFEST = 'jade.json'.freeze
|
|
19
|
+
|
|
20
|
+
DEFAULTS = {
|
|
21
|
+
source_roots: ['lib'],
|
|
22
|
+
build_dir: '.jade/build',
|
|
23
|
+
cache_dir: '.jade/cache',
|
|
24
|
+
extensions: [],
|
|
25
|
+
entries: [],
|
|
26
|
+
map: {},
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
class NotFound < StandardError
|
|
30
|
+
def initialize(from)
|
|
31
|
+
super(<<~MSG)
|
|
32
|
+
no #{MANIFEST} in #{from} or any parent directory
|
|
33
|
+
|
|
34
|
+
Tools that run outside the app can't see a `Jade.setup` block, so
|
|
35
|
+
they need the manifest to find your sources and load the gems
|
|
36
|
+
that ship the modules you import. Write one at the project root:
|
|
37
|
+
|
|
38
|
+
{ "source_roots": ["lib"], "extensions": ["jade-sql"] }
|
|
39
|
+
MSG
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# nil when there's no manifest — a project predating one is a legitimate
|
|
44
|
+
# state, not a failure. Callers that need it say so with `find!`.
|
|
45
|
+
def self.find(from = Dir.pwd)
|
|
46
|
+
Pathname
|
|
47
|
+
.new(from)
|
|
48
|
+
.expand_path
|
|
49
|
+
.ascend
|
|
50
|
+
.find { (it + MANIFEST).file? }
|
|
51
|
+
&.then { load(it) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.find!(from = Dir.pwd)
|
|
55
|
+
find(from) || fail(NotFound.new(from))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.load(root)
|
|
59
|
+
(root + MANIFEST)
|
|
60
|
+
.read
|
|
61
|
+
.then { JSON.parse(it, symbolize_names: true) }
|
|
62
|
+
.then { new(root: root.to_s, **DEFAULTS.merge(it)) }
|
|
63
|
+
.tap { it.require_extensions }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_root
|
|
67
|
+
File.expand_path(source_roots.first, root)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_path
|
|
71
|
+
File.expand_path(build_dir, root)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cache_path
|
|
75
|
+
File.expand_path(cache_dir, root)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Modules are addressed relative to the source root, but a path typed at
|
|
79
|
+
# a shell or sent by an editor is relative to the project root. Accept
|
|
80
|
+
# either, and say so when it's neither.
|
|
81
|
+
def source_relative(path)
|
|
82
|
+
[source_root, root]
|
|
83
|
+
.map { File.expand_path(path, it) }
|
|
84
|
+
.find { File.file?(it) }
|
|
85
|
+
.then { it || fail("no such file: #{path}") }
|
|
86
|
+
.then { Pathname.new(it).relative_path_from(source_root).to_s }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Extensions register their search root when required, so this is what
|
|
90
|
+
# lets a standalone tool resolve `Sql.Uuid` to the gem that ships it.
|
|
91
|
+
#
|
|
92
|
+
# Plain `require`, not `Kernel.require`: RubyGems overrides the private
|
|
93
|
+
# instance method and leaves the module function alone, so
|
|
94
|
+
# `Kernel.require` finds only what is already on the load path — never
|
|
95
|
+
# an installed gem, which is the whole point here.
|
|
96
|
+
def require_extensions
|
|
97
|
+
extensions.each { require(it) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/jade/source.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
module Jade
|
|
2
|
-
|
|
2
|
+
# `root` is the directory `uri` is relative to — the app's source root
|
|
3
|
+
# for its own modules, an extension gem's for the modules it ships. It
|
|
4
|
+
# is what makes "app or gem?" structural instead of a name list. `nil`
|
|
5
|
+
# for sources that never came off disk: buffers, stdin, the stdlib.
|
|
6
|
+
Source = Data.define(:uri, :text, :line_starts, :root) do
|
|
3
7
|
def self.load(source_root, uri, overlays: {})
|
|
4
8
|
text = overlays[uri] || File.read(File.join(source_root, uri))
|
|
5
|
-
new(uri
|
|
9
|
+
new(uri:, text:, root: source_root)
|
|
6
10
|
end
|
|
7
11
|
|
|
8
12
|
def self.load_from_module_name(source_root, name, overlays: {})
|
|
@@ -26,7 +30,7 @@ module Jade
|
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
def initialize(uri:, text:, line_starts: calculate_line_starts(text))
|
|
33
|
+
def initialize(uri:, text:, line_starts: calculate_line_starts(text), root: nil)
|
|
30
34
|
super
|
|
31
35
|
end
|
|
32
36
|
|
data/lib/jade/stdlib/decode.rb
CHANGED
|
@@ -182,6 +182,24 @@ module Jade
|
|
|
182
182
|
Jade::Decode::Decoder[Jade::Decode::Desc::Fail[msg]]
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
# Backs derived Decodable for unions whose variants take no
|
|
186
|
+
# arguments. `names` and `values` are positionally paired.
|
|
187
|
+
function(
|
|
188
|
+
'string_enum',
|
|
189
|
+
{ names: 'List(String)', values: 'List(a)' },
|
|
190
|
+
'Decoder(a)',
|
|
191
|
+
) { |names, values|
|
|
192
|
+
table = names.zip(values).to_h
|
|
193
|
+
|
|
194
|
+
->(s) {
|
|
195
|
+
table.key?(s) \
|
|
196
|
+
? Jade::Decode::Decoder[Jade::Decode::Desc::Succeed[table[s]]]
|
|
197
|
+
: Jade::Decode::Decoder[Jade::Decode::Desc::Fail["expected one of #{names.join(', ')}, got #{s.inspect}"]]
|
|
198
|
+
}
|
|
199
|
+
.then { Jade::Decode::Desc::AndThen[it, Jade::Decode::Desc::Str[]] }
|
|
200
|
+
.then { Jade::Decode::Decoder[it] }
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
function(
|
|
186
204
|
'from_result',
|
|
187
205
|
{ r: 'Result(a, String)' },
|
data/lib/jade/type.rb
CHANGED
|
@@ -168,12 +168,15 @@ module Jade
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
interface = registry.lookup(symbol.interface)
|
|
171
|
+
|
|
172
|
+
# Keep the map the type param lands in: the return type has to see
|
|
173
|
+
# the same variable, or the constraint ends up on a variable that
|
|
174
|
+
# appears in no type and nothing can ever bind it.
|
|
175
|
+
param_type, _, local_map =
|
|
176
|
+
from_symbol_r(interface.type_param, registry, var_gen, local_map)
|
|
177
|
+
|
|
171
178
|
constraint = Type
|
|
172
|
-
.constraint(
|
|
173
|
-
symbol.interface.qualified_name,
|
|
174
|
-
from_symbol_r(interface.type_param, registry, var_gen, local_map).first,
|
|
175
|
-
nil,
|
|
176
|
-
)
|
|
179
|
+
.constraint(symbol.interface.qualified_name, param_type, nil)
|
|
177
180
|
|
|
178
181
|
from_symbol_r(symbol.return_type, registry, var_gen, local_map)
|
|
179
182
|
.then { |(t, c, _)| [args.empty? ? t : Type.function(args, t), c + arg_cs + [constraint]] }
|
data/lib/jade/version.rb
CHANGED
data/lib/jade.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jade-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Cornu
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- lib/jade/frontend/semantic_analysis/error/nested_task_port.rb
|
|
158
158
|
- lib/jade/frontend/semantic_analysis/error/non_task_port.rb
|
|
159
159
|
- lib/jade/frontend/semantic_analysis/error/orphan_implementation.rb
|
|
160
|
+
- lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb
|
|
160
161
|
- lib/jade/frontend/semantic_analysis/error/predicate_must_return_bool.rb
|
|
161
162
|
- lib/jade/frontend/semantic_analysis/error/predicate_name_not_allowed.rb
|
|
162
163
|
- lib/jade/frontend/semantic_analysis/error/shadowing_error.rb
|
|
@@ -210,6 +211,7 @@ files:
|
|
|
210
211
|
- lib/jade/frontend/type_checking/constraints/deriving/encodable.rb
|
|
211
212
|
- lib/jade/frontend/type_checking/constraints/deriving/eq.rb
|
|
212
213
|
- lib/jade/frontend/type_checking/constraints/deriving/helpers.rb
|
|
214
|
+
- lib/jade/frontend/type_checking/constraints/deriving/sql_mapper.rb
|
|
213
215
|
- lib/jade/frontend/type_checking/definition.rb
|
|
214
216
|
- lib/jade/frontend/type_checking/env.rb
|
|
215
217
|
- lib/jade/frontend/type_checking/error.rb
|
|
@@ -297,6 +299,7 @@ files:
|
|
|
297
299
|
- lib/jade/parsing/token.rb
|
|
298
300
|
- lib/jade/parsing/type.rb
|
|
299
301
|
- lib/jade/port.rb
|
|
302
|
+
- lib/jade/project.rb
|
|
300
303
|
- lib/jade/registry.rb
|
|
301
304
|
- lib/jade/result.rb
|
|
302
305
|
- lib/jade/runtime.rb
|