jade-lang 0.6.0 → 0.8.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/docs/syntax.md +4 -0
  4. data/lib/jade/ast.rb +10 -0
  5. data/lib/jade/codegen/boundary.rb +1 -1
  6. data/lib/jade/codegen/constructor_reference.rb +1 -1
  7. data/lib/jade/codegen/emitter.rb +3 -0
  8. data/lib/jade/codegen/error.rb +8 -0
  9. data/lib/jade/codegen/function_call.rb +6 -4
  10. data/lib/jade/codegen.rb +2 -1
  11. data/lib/jade/formatter/helper.rb +1 -0
  12. data/lib/jade/formatter/leaves.rb +9 -0
  13. data/lib/jade/frontend/desugaring/resolved.rb +35 -0
  14. data/lib/jade/frontend/desugaring.rb +2 -1
  15. data/lib/jade/frontend/fixity_fixer.rb +1 -0
  16. data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +1 -1
  17. data/lib/jade/frontend/semantic_analysis.rb +1 -1
  18. data/lib/jade/frontend/type_checking/constraints/deriving/{sql_mapper.rb → assignable.rb} +46 -6
  19. data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +2 -14
  20. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +2 -2
  21. data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +10 -0
  22. data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +10 -0
  23. data/lib/jade/frontend/type_checking/constraints/deriving.rb +2 -2
  24. data/lib/jade/frontend/type_checking/inference/function_call.rb +33 -5
  25. data/lib/jade/frontend/type_checking/inference/function_declaration.rb +2 -2
  26. data/lib/jade/frontend/type_checking.rb +44 -2
  27. data/lib/jade/lexer.rb +1 -0
  28. data/lib/jade/parsing.rb +6 -1
  29. data/lib/jade/runtime.rb +7 -1
  30. data/lib/jade/version.rb +1 -1
  31. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec12f8f9ca54c6ac59b3d8d521a9ad192eb87dd5a2a5e62620f3abe00b636cee
4
- data.tar.gz: 940c0c3e78b8dcbd013050b298508323e5c0f60f8ddcb4749ede2416b14d9e16
3
+ metadata.gz: 38f4067e761c85016ad9baa6c733e6567aa93eea5cc0ffb755c0ac469a6a0002
4
+ data.tar.gz: 97dd4372b13bcb7e23bdc3ac2dcbe231d067410e9a99634afbbd3bd1a485452e
5
5
  SHA512:
6
- metadata.gz: e2f05e1f66e7b893e547a2ccb4d31a9678705daecd35c4c806a94f65c47444ad8b69b334c14b18a3a67417963870debdc4f28530ac334c0106699ee278c1f199
7
- data.tar.gz: 5711c4a232e7728699e89ddcf168e7dc5b1b86913e34579c7b0b1b2d3df6e069f2f4a12aeb35a800619fdb945169c142a2434f322ce582b1415c524259ab2646
6
+ metadata.gz: 415bcc6027fbefa912960d30c004d3d442000b37d6b3e6a27485b9ef9a12685bad3dc79855f7ccd99378987ea38fb6810fafe8f371dad5c50ce9b605c464f756
7
+ data.tar.gz: 8891cbe3dfe48e80cdb2ae73ec96a13b09a3adc8287adf812e7a30639ca4918b417cc02e45148222bacd2f7b1d412b58ce78d15f783dceed0868d0175e194daf
data/CHANGELOG.md CHANGED
@@ -6,6 +6,84 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0]
10
+
11
+ ### Added
12
+
13
+ - `Assignable` also derives for structs, naming one column per field in
14
+ declaration order. A field renamed to dodge a keyword (`type_`) maps back to
15
+ the column it came from. Generic structs derive at the type they are applied
16
+ to. Inert unless jade-sql is loaded.
17
+
18
+ ### Changed
19
+
20
+ - The derived jade-sql interface is now `Sql.Assignable`, was `Sql.SqlMapper`.
21
+ It is matched by name, so jade-sql must move to the new name in the same
22
+ release.
23
+
24
+ ## [0.7.0]
25
+
26
+ ### Added
27
+
28
+ - **`^Ctor` — the constructor, curried.** `Decode.succeed(Spec(_, _, _, _))`
29
+ opens an applicative decode pipeline, and those underscores are coupled to
30
+ the struct's arity: add a field and every pipeline needs another one,
31
+ reported as a type mismatch at the pipeline rather than at the struct.
32
+ `^Spec` desugars to exactly the same curried lambda, with the arity read off
33
+ the resolved constructor. Works on variants as well as structs.
34
+
35
+ ### Fixed
36
+
37
+ - **A constraint on a compound type holding a free var gets its dictionary.**
38
+ A polymorphic function whose body needed something like
39
+ `Decodable(List(a))`, `Decodable(Maybe(a))`, or `Decodable({ value: a })`
40
+ crashed codegen with `undefined method 'map' for nil` — no span, no message.
41
+ The derived instance depends on `Decodable(a)`, but only *bare* var
42
+ constraints earn a dict param, so the enclosing function took none and the
43
+ dep had nothing to bind to. Those deps are now surfaced as constraints of
44
+ their own, so the function takes the dict and callers pass their witness
45
+ down. Should a dep still find no dict in scope, codegen now fails naming the
46
+ constraint instead of handing `nil` on to the pretty-printer.
47
+ - **The `Encodable` deriver keeps its free-var deps.** The mirror of the above,
48
+ and quieter: `Encodable(List(a))` with `a` free dropped the derivation
49
+ outright, and `Encode.encode(items)` compiled to Ruby referencing an
50
+ `impl_arg` that was never bound — a `NameError` at run time, no compile-time
51
+ complaint. The deriver now falls back to the constraint marker for a free
52
+ var, the way `Decodable` already did.
53
+ - **A witness reaches a callee declared later in the file.** A polymorphic
54
+ function calling a polymorphic function declared below it lost the interface
55
+ witness — the call type-checked, then died at run time missing a dictionary
56
+ argument. Moving the callee above the caller fixed it, which is what made
57
+ this look like it was about lambdas. The collecting pass walks declarations
58
+ in source order, so the callee's constraints weren't known yet; it now
59
+ repeats until the constraint sets stop growing.
60
+ - **Anonymous records of the same shape compare equal across modules.** Each
61
+ module hoisted its own class per record shape, so `{ x: 1 }` built in one
62
+ module compared false against the same record built in another — both have
63
+ type `{ x: Int }`, and it type-checks fine, so nothing warned you. A record
64
+ decoded at a port boundary compared false against either. Shapes now resolve
65
+ through the interned `Jade::Runtime.record` registry, named under
66
+ `Jade::Records` so they don't inspect as belonging to whichever module
67
+ happened to assign one first.
68
+ - **`struct Probe` in `module Probe` loads.** It type-checked clean and died at
69
+ Ruby load with `uninitialized constant Probe::Probe::Probe` — the emitter
70
+ wrote an unrooted `Probe::Probe[n]` from inside `module Probe`, while the
71
+ boundary decoder three lines below already wrote `::Probe::Probe`. Unions
72
+ escaped it because their generated constants are the variants. All three
73
+ unrooted sites are now rooted.
74
+ - **`Eq` on a variant-less union is native equality.** A union with no variants
75
+ represents an opaque native (`Decode.Value`, `List`), and the derived `Eq`
76
+ was one case branch per variant plus a `_ then false` fallthrough — with no
77
+ variants, the fallthrough was the whole function and `x == x` answered false,
78
+ no warning, no type error.
79
+
80
+ ### Changed
81
+
82
+ - Generated output moves for any code that names a constructor or builds an
83
+ anonymous record. A project that CI-checks committed artifacts will see a
84
+ diff.
85
+
86
+
9
87
  ## [0.6.0]
10
88
 
11
89
  ### Fixed
data/docs/syntax.md CHANGED
@@ -266,6 +266,10 @@ Lambdas are `(params) -> { body }` — the one construct that uses braces.
266
266
  A `_` in an argument position curries the call: each `_` becomes a parameter,
267
267
  left to right, so `add(_, 5)` is a one-argument function.
268
268
 
269
+ `^Ctor` is the constructor with every field curried — `^Point` is
270
+ `Point(_, _)` without counting the fields, and stays right when the struct
271
+ gains one.
272
+
269
273
  ```jade
270
274
  module Curry exposing (add_five, add_one)
271
275
 
data/lib/jade/ast.rb CHANGED
@@ -20,6 +20,7 @@ module Jade
20
20
  define(:Bind, :pattern, :expression)
21
21
  define(:VariableReference, :name)
22
22
  define(:ConstructorReference, :name)
23
+ define(:CurriedConstructor, :name)
23
24
 
24
25
  define(:Module, :name, :exposing, :body)
25
26
  define(:Body, :expressions)
@@ -155,6 +156,15 @@ module Jade
155
156
  end
156
157
  end
157
158
 
159
+ def curried_constructor
160
+ ->(constant) do
161
+ CurriedConstructor[
162
+ constant.value,
163
+ constant.range,
164
+ ]
165
+ end
166
+ end
167
+
158
168
  def body
159
169
  ->(expressions) do
160
170
  if expressions.empty?
@@ -160,7 +160,7 @@ module Jade
160
160
  in Ok[impl] then Codegen::FunctionCall.generate_impl_dispatch(impl, registry)
161
161
  in Err then nil
162
162
  end
163
- rescue NoMethodError, NoMatchingPatternError
163
+ rescue MissingDictionary, NoMethodError, NoMatchingPatternError
164
164
  # Deriver assumes derivability (type-check-time invariant); boundary
165
165
  # synthesis may probe types whose deps fail — treat as ineligible.
166
166
  nil
@@ -10,7 +10,7 @@ module Jade
10
10
  end
11
11
 
12
12
  def from_symbol(symbol)
13
- qualified = to_qualified(symbol.qualified_name)
13
+ qualified = "::#{to_qualified(symbol.qualified_name)}"
14
14
  symbol.args.empty? ? "#{qualified}[]" : "#{qualified}.method(:[])"
15
15
  end
16
16
  end
@@ -17,6 +17,9 @@ module Jade
17
17
  in [:and, expr1, expr2]
18
18
  "#{emit(expr1)} && #{emit(expr2)}"
19
19
 
20
+ in [:==, expr1, expr2]
21
+ "#{emit(expr1)} == #{emit(expr2)}"
22
+
20
23
  in Integer | TrueClass | FalseClass | Float
21
24
  ir.to_s
22
25
 
@@ -0,0 +1,8 @@
1
+ module Jade
2
+ module Codegen
3
+ # Codegen runs on a program the frontend already proved well-typed, so
4
+ # this is a bug in jade, not in the source. RuntimeError so the CLI
5
+ # reports it as a message rather than a backtrace.
6
+ MissingDictionary = Class.new(RuntimeError)
7
+ end
8
+ end
@@ -26,7 +26,7 @@ module Jade
26
26
 
27
27
  def constructor_call(callee, args, registry)
28
28
  resolve_callee_symbol(callee, registry)
29
- .then { to_qualified(it.qualified_name) }
29
+ .then { "::#{to_qualified(it.qualified_name)}" }
30
30
  .then { "#{it}[#{generate_many(args, registry)}]" }
31
31
  end
32
32
 
@@ -89,8 +89,10 @@ module Jade
89
89
  in Symbol::Implementation
90
90
  generate_impl_dispatch(dep, registry)
91
91
 
92
- in Type::Constraint(type: Type::Var)
93
- dispatch_value(dep, registry)
92
+ in Type::Constraint(interface:, type: Type::Var => var)
93
+ dispatch_value(dep, registry) ||
94
+ raise(MissingDictionary, "no dict in scope for #{interface} #{var}" \
95
+ " — this is a jade bug, please report it")
94
96
  end
95
97
  end
96
98
 
@@ -151,7 +153,7 @@ module Jade
151
153
  end
152
154
 
153
155
  def generate_keyed_variant_call(constructor, args, registry)
154
- qualified = to_qualified(constructor.qualified_name)
156
+ qualified = "::#{to_qualified(constructor.qualified_name)}"
155
157
  record_fields = constructor.args[0].fields.keys
156
158
 
157
159
  args[0] => arg
data/lib/jade/codegen.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'jade/codegen/error'
1
2
  require 'jade/codegen/context'
2
3
  require 'jade/codegen/helpers'
3
4
  require 'jade/codegen/pretty'
@@ -91,7 +92,7 @@ module Jade
91
92
  shape_consts = collect_record_shapes(body)
92
93
  .sort
93
94
  .map { |keys|
94
- "#{record_shape_constant(keys)} = Data.define(#{keys.map { ":#{it}" }.join(', ')})"
95
+ "#{record_shape_constant(keys)} = #{record_class(keys)}"
95
96
  }
96
97
 
97
98
  boundary_cache = Boundary::Cache.collect(body, registry)
@@ -59,6 +59,7 @@ module Jade
59
59
  in AST::RecordUpdate then RecordUpdate
60
60
  in AST::VariableReference then VariableReference
61
61
  in AST::ConstructorReference then ConstructorReference
62
+ in AST::CurriedConstructor then CurriedConstructor
62
63
  in AST::CharLiteral then CharLiteral
63
64
  in AST::Literal then Literal
64
65
  end
@@ -21,6 +21,15 @@ module Jade
21
21
  end
22
22
  end
23
23
 
24
+ module CurriedConstructor
25
+ extend self
26
+ extend Helper
27
+
28
+ def format(node, indent:, source:)
29
+ "^#{node.name}".then(&and_indent(indent))
30
+ end
31
+ end
32
+
24
33
  module CharLiteral
25
34
  extend self
26
35
  extend Helper
@@ -2,6 +2,7 @@ module Jade
2
2
  module Frontend
3
3
  module Desugaring
4
4
  extend self
5
+ extend Codegen::Helpers
5
6
 
6
7
  # Runs after SemanticAnalysis (which resolves names and attaches
7
8
  # symbols). Add new post-resolution rewrites as `in` branches
@@ -13,6 +14,9 @@ module Jade
13
14
 
14
15
  def desugar_resolved(node, registry)
15
16
  case node
17
+ in AST::CurriedConstructor
18
+ curry_constructor(node, registry)
19
+
16
20
  in AST::VariableReference | AST::QualifiedAccess
17
21
  zero_arg_fn?(node.symbol, registry) ? wrap_call(node) : node
18
22
 
@@ -23,6 +27,37 @@ module Jade
23
27
 
24
28
  private
25
29
 
30
+ # `^C` is `C(_, _, …)` with the arity filled in — which needs the
31
+ # resolved symbol, so it cannot be done at parse time.
32
+ def curry_constructor(node, registry)
33
+ arity = registry.lookup(node.symbol)&.args&.length || 0
34
+ names = (0...arity).map { param_synthetic_name(it) }
35
+
36
+ AST::FunctionCall
37
+ .new(
38
+ callee: AST::ConstructorReference[node.name, node.range].with(symbol: node.symbol),
39
+ args: names.map { AST::VariableReference[it, node.range].with(symbol: Symbol.var(it, nil)) },
40
+ infix: false,
41
+ dictionaries: [],
42
+ range: node.range,
43
+ symbol: nil,
44
+ id: node.id,
45
+ leading_comments: node.leading_comments,
46
+ trailing_comments: node.trailing_comments,
47
+ dangling_comments: node.dangling_comments,
48
+ trailing_comma: false,
49
+ )
50
+ .then do |call|
51
+ names.reverse.reduce(call) do |body, name|
52
+ AST::Lambda[
53
+ [AST::Pattern::Binding[name, node.range]],
54
+ AST::Body[[body], node.range],
55
+ node.range,
56
+ ]
57
+ end
58
+ end
59
+ end
60
+
26
61
  def map_children(node)
27
62
  node
28
63
  .to_h
@@ -175,7 +175,8 @@ module Jade
175
175
  AST::TypeDeclaration | AST::ImportDeclaration |
176
176
  AST::Pattern::Literal | AST::Pattern::Binding | AST::Pattern::Wildcard |
177
177
  AST::Pattern::Record | AST::InteropImportDeclaration | AST::StructDeclaration |
178
- AST::QualifiedAccess | AST::Placeholder | AST::InterfaceDeclaration
178
+ AST::QualifiedAccess | AST::Placeholder | AST::InterfaceDeclaration |
179
+ AST::CurriedConstructor
179
180
 
180
181
  node
181
182
  end
@@ -147,6 +147,7 @@ module Jade
147
147
  in AST::VariableReference | AST::ConstructorReference | AST::TypeDeclaration |
148
148
  AST::ImportDeclaration | AST::Literal | AST::CharLiteral | AST::RecordAccessSugar | AST::InteropImportDeclaration |
149
149
  AST::StructDeclaration | AST::QualifiedAccess | AST::Placeholder | AST::InterfaceDeclaration |
150
+ AST::CurriedConstructor |
150
151
  AST::RecordUpdateSugar
151
152
 
152
153
  node
@@ -6,7 +6,7 @@ module Jade
6
6
  extend Helper
7
7
 
8
8
  def analyze(node, registry, scope, entry)
9
- node => AST::ConstructorReference(name:)
9
+ name = node.name
10
10
 
11
11
  symbol = scope.lookup(name) || resolve_private_constructor(name, registry)
12
12
 
@@ -76,7 +76,7 @@ module Jade
76
76
  in AST::CharLiteral then CharLiteral
77
77
  in AST::Assign then Assign
78
78
  in AST::VariableReference then VariableReference
79
- in AST::ConstructorReference then ConstructorReference
79
+ in AST::ConstructorReference | AST::CurriedConstructor then ConstructorReference
80
80
  in AST::Body then Body
81
81
  in AST::FunctionDeclaration then FunctionDeclaration
82
82
  in AST::FunctionCall then FunctionCall
@@ -6,11 +6,11 @@ module Jade
6
6
  # jade-sql's interface, derived here rather than there: the
7
7
  # deriving framework is not extensible, and this stays inert
8
8
  # without jade-sql, since nothing else names the interface.
9
- module SqlMapper
9
+ module Assignable
10
10
  extend self
11
11
  include Helpers
12
12
 
13
- INTERFACE = 'Sql.SqlMapper'
13
+ INTERFACE = 'Sql.Assignable'
14
14
  ASSIGNMENT = 'Sql.Assignment'
15
15
  ASSIGNMENT_FIELDS = %w[col value_sql params].freeze
16
16
 
@@ -20,11 +20,11 @@ module Jade
20
20
  return failed(constraint, entry_name) unless assignment_matches?(registry)
21
21
 
22
22
  case constraint.type
23
- in Type::Application(constructor: Type::Constructor(name:), args: [])
23
+ in Type::Application(constructor: Type::Constructor(name:), args:)
24
24
  Symbol
25
25
  .type_ref_from_qualified_name(name)
26
26
  .then { registry.lookup(it) }
27
- .then { derive_for(constraint, it, registry, lookup, entry_name) }
27
+ .then { derive_for(constraint, it, args, registry, lookup, entry_name) }
28
28
 
29
29
  else
30
30
  failed(constraint, entry_name)
@@ -47,11 +47,14 @@ module Jade
47
47
  end
48
48
  end
49
49
 
50
- def derive_for(constraint, symbol, registry, lookup, entry_name)
50
+ def derive_for(constraint, symbol, args, registry, lookup, entry_name)
51
51
  case symbol
52
- in Symbol::Union if single_payload?(symbol, registry)
52
+ in Symbol::Union if args.empty? && single_payload?(symbol, registry)
53
53
  derive_union(constraint, symbol, registry, lookup, entry_name)
54
54
 
55
+ in Symbol::Struct
56
+ derive_struct(constraint, symbol, args, registry, lookup, entry_name)
57
+
55
58
  else
56
59
  failed(constraint, entry_name)
57
60
  end
@@ -74,6 +77,43 @@ module Jade
74
77
  .map { implementation(constraint, union_body(vs), it) }
75
78
  end
76
79
 
80
+ def derive_struct(constraint, struct_sym, args, registry, lookup, entry_name)
81
+ fields = struct_fields(struct_sym, args, registry)
82
+
83
+ fields
84
+ .map { |_, type| Type.constraint('Encode.Encodable', type, nil) }
85
+ .map { lookup.call(it) }
86
+ .then { Results.sequence(it) }
87
+ .map { implementation(constraint, struct_body(fields), it) }
88
+ end
89
+
90
+ def struct_body(fields)
91
+ fields
92
+ .each_with_index
93
+ .map { |(name, _), idx| field_assignment(name, idx) }
94
+ .then { [:list, it] }
95
+ end
96
+
97
+ def field_assignment(name, idx)
98
+ [:call,
99
+ [:struct_constructor, ASSIGNMENT, 3],
100
+ [
101
+ column_name(name),
102
+ '?',
103
+ [:list,
104
+ [[:call, [:impl_arg, idx, 'encoder'], [[:access, [:var, 'f'], name.to_s]]]],
105
+ ],
106
+ ],
107
+ ]
108
+ end
109
+
110
+ def column_name(field)
111
+ field
112
+ .to_s
113
+ .then { it.end_with?('_') ? it.delete_suffix('_') : it }
114
+ .then { Lexer::KEYWORDS.include?(it) ? it : field.to_s }
115
+ end
116
+
77
117
  def encodable_dep(variant, registry)
78
118
  variant
79
119
  .args
@@ -74,7 +74,7 @@ module Jade
74
74
 
75
75
  inner_types
76
76
  .map { Type.constraint(INTERFACE, it, nil) }
77
- .map { resolve_dep(it, lookup, entry_name) }
77
+ .map { resolve_dep(it, lookup) }
78
78
  .then { Results.sequence(it) }
79
79
  .map { implementation(constraint, body, it) }
80
80
  end
@@ -99,7 +99,7 @@ module Jade
99
99
  end
100
100
 
101
101
  field_deps
102
- .map { resolve_dep(it, lookup, entry_name) }
102
+ .map { resolve_dep(it, lookup) }
103
103
  .then { Results.sequence(it) }
104
104
  .map { implementation(constraint, record_body(fields, constructor_ref), it) }
105
105
  end
@@ -131,18 +131,6 @@ module Jade
131
131
  ]
132
132
  end
133
133
 
134
- # Free-var inner constraints fall back to the constraint marker
135
- # itself — codegen later resolves it from the caller's dict env.
136
- # Without this, `Decodable(List(a))` with `a` free would bail
137
- # with UnresolvedConstraint.
138
- def resolve_dep(dep, lookup, entry_name)
139
- lookup
140
- .call(dep)
141
- .on_err(Error::UnresolvedConstraint) {
142
- dep.type.is_a?(Type::Var) ? Ok[dep] : Err[it]
143
- }
144
- end
145
-
146
134
  def implementation(constraint, body, deps)
147
135
  decoder_fn = Symbol::DerivedFunction.new(params: [], body:)
148
136
 
@@ -73,7 +73,7 @@ module Jade
73
73
 
74
74
  inner_types
75
75
  .map { Type.constraint(INTERFACE, it, nil) }
76
- .map { lookup.call(it) }
76
+ .map { resolve_dep(it, lookup) }
77
77
  .then { Results.sequence(it) }
78
78
  .map { implementation(constraint, params: [param], body:, deps: it) }
79
79
  end
@@ -111,7 +111,7 @@ module Jade
111
111
  .then { [:hash, it] }
112
112
 
113
113
  field_deps
114
- .map { lookup.call(it) }
114
+ .map { resolve_dep(it, lookup) }
115
115
  .then { Results.sequence(it) }
116
116
  .map { implementation(constraint, params: ['rec'], body:, deps: it) }
117
117
  end
@@ -22,6 +22,8 @@ module Jade
22
22
  index_map = type_vars.each_with_index.map.to_h
23
23
  variants = symbol.variants.map { registry.lookup(it) }
24
24
 
25
+ return native_eq(constraint, type_vars) if variants.empty?
26
+
25
27
  concrete = variants
26
28
  .flat_map(&:args)
27
29
  .reject { it in Symbol::Variable }
@@ -49,6 +51,14 @@ module Jade
49
51
  end
50
52
  end
51
53
 
54
+ # A variant-less union stands in for an opaque native — `Decode.Value`,
55
+ # `List` — so there is nothing to match on and equality is Ruby's.
56
+ def native_eq(constraint, type_vars)
57
+ Symbol::DerivedFunction
58
+ .new(params: ["one", "other"], body: [:==, [:var, "one"], [:var, "other"]])
59
+ .then { Ok[build_union_impl(constraint, type_vars, [], it, [])] }
60
+ end
61
+
52
62
  def build_union_impl(constraint, type_vars, concrete, eq_fn, deps)
53
63
  return implementation(constraint, { '(==)' => eq_fn }, deps:) if type_vars.empty?
54
64
 
@@ -54,6 +54,16 @@ module Jade
54
54
  )
55
55
  end
56
56
 
57
+ # A free-var inner constraint falls back to the marker itself —
58
+ # codegen resolves it from the caller's dict env.
59
+ def resolve_dep(dep, lookup)
60
+ lookup
61
+ .call(dep)
62
+ .on_err(Error::UnresolvedConstraint) {
63
+ dep.type.is_a?(Type::Var) ? Ok[dep] : Err[it]
64
+ }
65
+ end
66
+
57
67
  def resolve_field_deps(field_types, lookup, origin)
58
68
  field_types
59
69
  .map { lookup.call(Type.constraint(self::INTERFACE, it, origin)) }
@@ -2,7 +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
+ require_relative './deriving/assignable.rb'
6
6
  require_relative './deriving/show.rb'
7
7
 
8
8
  module Jade
@@ -12,7 +12,7 @@ module Jade
12
12
  module Deriving
13
13
  extend self
14
14
 
15
- DERIVERS = [Eq, Show, Decodable, Encodable, SqlMapper]
15
+ DERIVERS = [Eq, Show, Decodable, Encodable, Assignable]
16
16
 
17
17
  def derivable?(interface)
18
18
  DERIVERS.any? { it.supports?(interface) }
@@ -57,7 +57,7 @@ module Jade
57
57
  .map { st.env.substitution.apply(it) }
58
58
 
59
59
  propagated = (callee_subst + args_subst)
60
- .select { it.type.is_a?(Type::Var) }
60
+ .flat_map { propagate(it, registry, st.env.entry_name) }
61
61
 
62
62
  # Pass 1 still needs propagation so constraints from inner calls
63
63
  # bubble up through outer constructor calls and reach the function
@@ -71,10 +71,13 @@ module Jade
71
71
  # "use the enclosing function's local dict".
72
72
  callee_errors = callee_subst
73
73
  .flat_map do |c|
74
- case c.type
75
- in Type::Var
74
+ case c
75
+ in Type::Constraint(index: :unindex) then []
76
+
77
+ in Type::Constraint(type: Type::Var)
76
78
  Constraints.attach_dictionary(c, c)
77
79
  []
80
+
78
81
  else
79
82
  Constraints.solve_at_call_site(c, registry, st.env.entry_name)
80
83
  end
@@ -87,10 +90,13 @@ module Jade
87
90
  # resolve via the enclosing function's dict_env.
88
91
  args_errors = args_subst
89
92
  .flat_map do |c|
90
- case c.type
91
- in Type::Var
93
+ case c
94
+ in Type::Constraint(index: :unindex) then []
95
+
96
+ in Type::Constraint(type: Type::Var)
92
97
  Constraints.attach_dictionary(c, c)
93
98
  []
99
+
94
100
  else
95
101
  Constraints.solve_at_call_site(c, registry, st.env.entry_name)
96
102
  end
@@ -104,6 +110,28 @@ module Jade
104
110
 
105
111
  private
106
112
 
113
+ # Only bare-var constraints earn a dict param, so a constraint that
114
+ # still holds free vars (`Decodable(List(a))`) surfaces its deps as
115
+ # markers of their own — unindexed, since they occupy no slot in
116
+ # this call's dictionary list.
117
+ def propagate(constraint, registry, entry_name)
118
+ return [constraint] if constraint.type.is_a?(Type::Var)
119
+ return [] if constraint.unbound_vars.empty?
120
+
121
+ Constraints
122
+ .resolve(constraint, registry, entry_name)
123
+ .map { var_markers(it) }
124
+ .with_default([])
125
+ end
126
+
127
+ def var_markers(dep)
128
+ case dep
129
+ in Symbol::Implementation(deps:) then deps.flat_map { var_markers(it) }
130
+ in Type::Constraint(type: Type::Var) then [dep.with(origin: nil, index: :unindex)]
131
+ else []
132
+ end
133
+ end
134
+
107
135
  def unify_callee(state, callee_result, args_acc, node, outer)
108
136
  return_type = state.fresh
109
137
  fn_type = Type.function(args_acc.types, return_type)
@@ -42,11 +42,11 @@ module Jade
42
42
  )
43
43
  end
44
44
  .then do |st|
45
- next st if st.env.bindings[symbol.qualified_name].is_a?(Scheme)
45
+ next st if st.env.bindings[symbol.qualified_name].is_a?(Scheme) && !st.skip_constraints
46
46
 
47
47
  updated_constraints = (fn_constraints + body_result.constraints)
48
48
  .map { st.env.substitution.apply(it) }
49
- .uniq
49
+ .uniq { it.type.is_a?(Type::Var) ? [it.interface, it.type] : it }
50
50
 
51
51
  # TODO: for impl function declarations, unresolved constraints here
52
52
  # (e.g. Eq(a) when the body calls == on a field of type a) should
@@ -23,8 +23,7 @@ module Jade
23
23
  def check(entry, registry)
24
24
  Loader
25
25
  .load(entry, registry)
26
- .then { check_node(entry.ast, registry, State.init(it, skip_constraints: true), Expected.infer(it.fresh)) }
27
- .then { Generalizer.generalize(it.first.env) }
26
+ .then { collect_constraints(entry, registry, it) }
28
27
  .then { check_node(entry.ast, registry, State.init(it), Expected.infer(it.fresh)) }
29
28
  .then { finalize(*it, registry) }
30
29
  .map { Canonicalize.run(entry.ast, it, registry) }
@@ -33,6 +32,49 @@ module Jade
33
32
  .and_then { PortResolution.resolve(it, registry) }
34
33
  end
35
34
 
35
+ # A collecting pass walks declarations in source order, so a call to a
36
+ # function declared later sees it before its constraints are known and
37
+ # collects nothing. Each repeat carries them one caller further back, so
38
+ # a chain of forward references needs as many rounds as it is long.
39
+ def collect_constraints(entry, registry, env, rounds = declaration_count(entry))
40
+ return env if rounds.zero?
41
+
42
+ collect_round(entry, registry, env)
43
+ .then do |collected|
44
+ next env if same_constraints?(collected, env)
45
+
46
+ collect_constraints(entry, registry, collected, rounds - 1)
47
+ end
48
+ end
49
+
50
+ def collect_round(entry, registry, env)
51
+ State
52
+ .init(env, skip_constraints: true)
53
+ .then { check_node(entry.ast, registry, it, Expected.infer(env.fresh)) }
54
+ .then { Generalizer.generalize(it.first.env) }
55
+ end
56
+
57
+ def same_constraints?(one, other)
58
+ constraint_fingerprint(one) == constraint_fingerprint(other)
59
+ end
60
+
61
+ def constraint_fingerprint(env)
62
+ env
63
+ .bindings
64
+ .map { |name, binding| [name, binding.constraints.map { [it.interface, it.type.to_s] }.sort] }
65
+ .sort
66
+ end
67
+
68
+ def declaration_count(entry)
69
+ case entry.ast
70
+ in AST::Module(body: AST::Body(expressions:)) then expressions
71
+ in AST::Body(expressions:) then expressions
72
+ else []
73
+ end
74
+ .count { it.is_a?(AST::FunctionDeclaration) }
75
+ .then { it + 1 }
76
+ end
77
+
36
78
  def finalize(state, result, registry)
37
79
  state.env => { bindings:, entry_name: }
38
80
 
data/lib/jade/lexer.rb CHANGED
@@ -66,6 +66,7 @@ module Jade
66
66
  '<|' => :pipe_backward,
67
67
  '<-' => :bind,
68
68
 
69
+ '^' => :caret, # `^Ctor` — the constructor, curried
69
70
  '_' => :wildcard, # also used as placeholder
70
71
  '::' => :coloncolon,
71
72
  # Ternary `cond ? a : b`. Predicate-`?` attaches via the identifier
data/lib/jade/parsing.rb CHANGED
@@ -336,7 +336,8 @@ module Jade
336
336
  }
337
337
 
338
338
  parser(:atom) {
339
- variable_reference | negative_literal | literal | constructor_reference |
339
+ variable_reference | negative_literal | literal |
340
+ curried_constructor | constructor_reference |
340
341
  lambda | tuple | grouping |
341
342
  record_literal | record_update_sugar | record_access_sugar | record_update
342
343
  }
@@ -509,6 +510,10 @@ module Jade
509
510
  # Should refactor to just an Constant node
510
511
  parser(:constructor_reference) { constant.map(&AST.constructor_reference) }
511
512
 
513
+ parser(:curried_constructor) {
514
+ (type(:caret) >> constant).map { AST.curried_constructor.call(it.last) }
515
+ }
516
+
512
517
  parser(:param) {
513
518
  (
514
519
  identifier >> type(:colon).skip >> type_expression
data/lib/jade/runtime.rb CHANGED
@@ -6,6 +6,8 @@ require 'jade/decode'
6
6
  require 'jade/interop/boundary'
7
7
 
8
8
  module Jade
9
+ module Records; end
10
+
9
11
  module Tuple
10
12
  Tuple2 = Data.define(:_1, :_2) do
11
13
  def to_s = "(#{[_1, _2].map(&:to_s).join(', ')})"
@@ -123,8 +125,12 @@ module Jade
123
125
  # `{a: 1, b: 2}` expression evaluated in a hot loop would call
124
126
  # `Data.define(:a, :b)` and allocate a fresh anonymous class, defeating
125
127
  # YJIT's inline cache on every subsequent property access.
128
+ # Named, so the first module to assign it doesn't lend it a name
129
+ # pointing at an unrelated module.
126
130
  def record(*keys)
127
- RECORD_CLASSES[keys] ||= Data.define(*keys)
131
+ RECORD_CLASSES[keys] ||= Data
132
+ .define(*keys)
133
+ .tap { Jade::Records.const_set(:"Record_#{keys.join('_')}", it) }
128
134
  end
129
135
 
130
136
  def register_impl(interface_name, ruby_class, functions)
data/lib/jade/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.6.0'
2
+ VERSION = '0.8.0'
3
3
  end
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.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agustin Cornu
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-13 00:00:00.000000000 Z
10
+ date: 2026-08-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -70,6 +70,7 @@ files:
70
70
  - lib/jade/codegen/constructor_reference.rb
71
71
  - lib/jade/codegen/context.rb
72
72
  - lib/jade/codegen/emitter.rb
73
+ - lib/jade/codegen/error.rb
73
74
  - lib/jade/codegen/function_call.rb
74
75
  - lib/jade/codegen/function_declaration.rb
75
76
  - lib/jade/codegen/helpers.rb
@@ -217,12 +218,12 @@ files:
217
218
  - lib/jade/frontend/type_checking/canonicalize.rb
218
219
  - lib/jade/frontend/type_checking/constraints.rb
219
220
  - lib/jade/frontend/type_checking/constraints/deriving.rb
221
+ - lib/jade/frontend/type_checking/constraints/deriving/assignable.rb
220
222
  - lib/jade/frontend/type_checking/constraints/deriving/decodable.rb
221
223
  - lib/jade/frontend/type_checking/constraints/deriving/encodable.rb
222
224
  - lib/jade/frontend/type_checking/constraints/deriving/eq.rb
223
225
  - lib/jade/frontend/type_checking/constraints/deriving/helpers.rb
224
226
  - lib/jade/frontend/type_checking/constraints/deriving/show.rb
225
- - lib/jade/frontend/type_checking/constraints/deriving/sql_mapper.rb
226
227
  - lib/jade/frontend/type_checking/definition.rb
227
228
  - lib/jade/frontend/type_checking/env.rb
228
229
  - lib/jade/frontend/type_checking/error.rb