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
|
@@ -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) }
|
|
@@ -53,6 +53,9 @@ module Jade
|
|
|
53
53
|
in Placeholder => placeholder
|
|
54
54
|
Scheme[placeholder.free_vars, placeholder.type, placeholder.constraints]
|
|
55
55
|
.then { Instantiation.instantiate(it, var_gen) }
|
|
56
|
+
|
|
57
|
+
in nil
|
|
58
|
+
fail "no type binding for `#{key}`"
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
Result.init(type, constraints)
|
|
@@ -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/module_loader.rb
CHANGED
|
@@ -51,13 +51,37 @@ module Jade
|
|
|
51
51
|
.modules_in_topo_order
|
|
52
52
|
.reject { Stdlib.is_stdlib?(it) }
|
|
53
53
|
.reduce([registry, {}]) do |(acc, digests), entry|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
case broken_dependency(entry, acc)
|
|
55
|
+
in String => dependency
|
|
56
|
+
[acc.update_module(blocked(entry, dependency)), digests]
|
|
57
|
+
|
|
58
|
+
else
|
|
59
|
+
compile_with_cache(entry, acc, digests, cache_dir, tolerant)
|
|
60
|
+
.then { |(compiled, digest)| [acc.update_module(compiled), digests.merge(entry.name => digest)] }
|
|
61
|
+
end
|
|
57
62
|
end
|
|
58
63
|
.first
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
def broken_dependency(entry, registry)
|
|
67
|
+
direct_deps(entry.name, registry)
|
|
68
|
+
.filter_map { registry.modules[it] }
|
|
69
|
+
.find { it.diagnostics.any_errors? }
|
|
70
|
+
&.name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def blocked(entry, dependency)
|
|
74
|
+
Diagnostics::List
|
|
75
|
+
.empty
|
|
76
|
+
.add(
|
|
77
|
+
Diagnostics::Diagnostic.error(
|
|
78
|
+
"Not checked: #{dependency} failed to compile",
|
|
79
|
+
primary: nil,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
.then { entry.with(diagnostics: it) }
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
def compile_with_cache(entry, registry, digests, cache_dir, tolerant)
|
|
62
86
|
return [compile_one(entry, registry, tolerant:), nil] unless cache_dir
|
|
63
87
|
|
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