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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -1
  3. data/lib/jade/ast/node.rb +30 -0
  4. data/lib/jade/cli/q.rb +9 -5
  5. data/lib/jade/cli.rb +3 -0
  6. data/lib/jade/codegen/emitter.rb +2 -2
  7. data/lib/jade/codegen/inlines.rb +1 -0
  8. data/lib/jade/codegen.rb +5 -1
  9. data/lib/jade/compiler.rb +17 -8
  10. data/lib/jade/formatter/helper.rb +11 -5
  11. data/lib/jade/frontend/comment_attacher.rb +13 -2
  12. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +22 -0
  13. data/lib/jade/frontend/semantic_analysis/error.rb +1 -0
  14. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +55 -0
  15. data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +26 -7
  16. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +20 -7
  17. data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +2 -1
  18. data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +17 -0
  19. data/lib/jade/frontend/type_checking/constraints/deriving/sql_mapper.rb +130 -0
  20. data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -1
  21. data/lib/jade/frontend/type_checking/env.rb +3 -0
  22. data/lib/jade/frontend/type_checking/generalizer.rb +0 -1
  23. data/lib/jade/frontend/usage_analysis/reference_index.rb +7 -1
  24. data/lib/jade/frontend/usage_analysis.rb +78 -52
  25. data/lib/jade/module_loader.rb +27 -3
  26. data/lib/jade/parsing.rb +7 -1
  27. data/lib/jade/project.rb +100 -0
  28. data/lib/jade/source.rb +7 -3
  29. data/lib/jade/stdlib/decode.rb +18 -0
  30. data/lib/jade/type.rb +8 -5
  31. data/lib/jade/version.rb +1 -1
  32. data/lib/jade.rb +1 -0
  33. 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)
@@ -14,7 +14,6 @@ module Jade
14
14
  unbound_cs = constraints
15
15
  .map { e.substitution.apply(it) }
16
16
  .uniq
17
- .select { it.unbound_vars.any? }
18
17
 
19
18
  Generalization.
20
19
  generalize(
@@ -1,7 +1,11 @@
1
1
  module Jade
2
2
  module Frontend
3
3
  module UsageAnalysis
4
- Reference = Data.define(:symbol_key, :kind, :range)
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
- def walk(node, ctx, entry)
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
- walk(body, :as_value, entry) +
40
- params.flat_map { walk_type(it.type, entry) } +
41
- walk_type(return_type, entry)
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) + args.flat_map { walk(it, :as_value, 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) + walk(pattern, :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) + fields.flat_map { walk(it, :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
- walk_type(applied_type, entry) +
111
- functions.flat_map { walk(it, :as_value, entry) }
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
- variants.flat_map { it.args.flat_map { walk_type(it, entry) } }
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
- functions.flat_map { walk_type(it.type, entry) }
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
- functions.flat_map { walk_type(it.type, entry) }
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 ? [Reference[ReferenceIndex.key_for(it), :type_annotation, range]] : [] }
169
+ .then { it ? ref(it, :type_annotation, range, owner) : [] }
147
170
 
148
171
  in AST::TypeApplication(constructor:, args:)
149
- walk_type(constructor, entry) + args.flat_map { walk_type(it, 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) } + walk_type(return_type, 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 ? [Reference[ReferenceIndex.key_for(symbol), :exposed, range]] : []
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
@@ -51,13 +51,37 @@ module Jade
51
51
  .modules_in_topo_order
52
52
  .reject { Stdlib.is_stdlib?(it) }
53
53
  .reduce([registry, {}]) do |(acc, digests), entry|
54
- compiled, digest = compile_with_cache(entry, acc, digests, cache_dir, tolerant)
55
-
56
- [acc.update_module(compiled), digests.merge(entry.name => digest)]
54
+ case broken_dependency(entry, acc)
55
+ in String => dependency
56
+ [acc.update_module(blocked(entry, dependency)), digests]
57
+
58
+ else
59
+ compile_with_cache(entry, acc, digests, cache_dir, tolerant)
60
+ .then { |(compiled, digest)| [acc.update_module(compiled), digests.merge(entry.name => digest)] }
61
+ end
57
62
  end
58
63
  .first
59
64
  end
60
65
 
66
+ def broken_dependency(entry, registry)
67
+ direct_deps(entry.name, registry)
68
+ .filter_map { registry.modules[it] }
69
+ .find { it.diagnostics.any_errors? }
70
+ &.name
71
+ end
72
+
73
+ def blocked(entry, dependency)
74
+ Diagnostics::List
75
+ .empty
76
+ .add(
77
+ Diagnostics::Diagnostic.error(
78
+ "Not checked: #{dependency} failed to compile",
79
+ primary: nil,
80
+ ),
81
+ )
82
+ .then { entry.with(diagnostics: it) }
83
+ end
84
+
61
85
  def compile_with_cache(entry, registry, digests, cache_dir, tolerant)
62
86
  return [compile_one(entry, registry, tolerant:), nil] unless cache_dir
63
87
 
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(record_field) >>
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) }
@@ -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
- Source = Data.define(:uri, :text, :line_starts) do
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, text)
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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.1'
3
3
  end
data/lib/jade.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'jade/version'
2
+ require 'jade/project'
2
3
  require 'jade/did_you_mean'
3
4
  require 'jade/symbol'
4
5
  require 'jade/registry'