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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15e51e91117f799dec4acda81faa5fd62f8f75190570cebc69d69e0c588481cb
4
- data.tar.gz: 1bd08d32f23d5b0e3f184f04d72aa1b44fd391bea3536ff2630ea93b1480f179
3
+ metadata.gz: 6886eb468004c2c500090b0a919a1a369e31b36039b3a6ef583bb74bc5e7326b
4
+ data.tar.gz: eaa9bb82c455efa0ca5c9e86eefe7d6ae6a56b1a9c8d695ae484500b69c1e049
5
5
  SHA512:
6
- metadata.gz: fcb1ae0703cdcd8c29c618b7aaf36d90c1f5b7eb4b95a1ee5f6dd6e4fd81d172977b3786442c1f1635e0ade323ff94a82fd95ee331a23f53459581966d08a346
7
- data.tar.gz: 4208a71d7852def7334d49142dc7aea3b4f6344941a9e66cbc9d544b8f877d84680c7206518d61aaeb35d6ecd8eec9261f7a1adf5d64fe6346020e775fadaaae
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.2.0...HEAD
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
- @file = file
93
- @source_root = Dir.pwd
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: File.join(@source_root, '.jade/cache'),
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
@@ -26,6 +26,9 @@ module Jade
26
26
  usage($stderr)
27
27
  exit 1
28
28
  end
29
+ rescue Project::NotFound => e
30
+ warn "jade: #{e.message}"
31
+ exit 1
29
32
  end
30
33
 
31
34
  def usage(io = $stdout)
@@ -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(#{to_qualified(qualified_name)}.method(:[]), #{arity})"
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 { "#{to_qualified(name)}(#{it})" }
71
+ .then { "::#{to_qualified(name)}(#{it})" }
72
72
 
73
73
  in [:_]
74
74
  '_'
@@ -192,6 +192,7 @@ module Jade
192
192
  Decode.required
193
193
  Decode.sequence
194
194
  Decode.string
195
+ Decode.string_enum
195
196
  Decode.succeed
196
197
  Decode.tuple
197
198
  Decode.tuple3
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.first, path + '.jd', cache_dir: cache_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.first, '**/*.jd'))
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
- def initialize
58
- @project_root = Dir.pwd
59
- @source_root = []
60
- @build_dir = ".jade/build"
61
- @cache_dir = ".jade/cache"
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
- @source_root = Array(root)
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
- def format_leading_comments(node, indent)
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.leading_comments
82
- .map { |tok| tok.value.then(&and_indent(indent)) }
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
- .sort_by { |n| n.range.begin }
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.bsearch { |n| n.range.begin >= pos }
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
- resolved_deps = field_deps.map do |dep|
91
- resolve_dep(dep, lookup, entry_name) => Ok[impl]
92
- impl
93
- end
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
- body = fields.each_with_index.reduce(seed) do |acc, ((field_name, _), idx)|
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
- Ok[implementation(constraint, params: ['rec'], body:, deps: resolved_deps)]
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]; 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) }
@@ -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
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.0'
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'
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.2.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-06-17 00:00:00.000000000 Z
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