jade-lang 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -0
- data/docs/syntax.md +4 -0
- data/lib/jade/ast.rb +10 -0
- data/lib/jade/codegen/boundary.rb +1 -1
- data/lib/jade/codegen/constructor_reference.rb +1 -1
- data/lib/jade/codegen/emitter.rb +3 -0
- data/lib/jade/codegen/error.rb +8 -0
- data/lib/jade/codegen/function_call.rb +6 -4
- data/lib/jade/codegen.rb +2 -1
- data/lib/jade/formatter/helper.rb +1 -0
- data/lib/jade/formatter/leaves.rb +9 -0
- data/lib/jade/frontend/desugaring/resolved.rb +35 -0
- data/lib/jade/frontend/desugaring.rb +2 -1
- data/lib/jade/frontend/fixity_fixer.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/constructor_reference.rb +1 -1
- data/lib/jade/frontend/semantic_analysis.rb +1 -1
- data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +2 -14
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +2 -2
- data/lib/jade/frontend/type_checking/constraints/deriving/eq.rb +10 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/helpers.rb +10 -0
- data/lib/jade/frontend/type_checking/inference/function_call.rb +33 -5
- data/lib/jade/frontend/type_checking/inference/function_declaration.rb +2 -2
- data/lib/jade/frontend/type_checking.rb +44 -2
- data/lib/jade/lexer.rb +1 -0
- data/lib/jade/parsing.rb +6 -1
- data/lib/jade/runtime.rb +7 -1
- data/lib/jade/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e1034e93db617cde9f33aad318dc23a4bf4987f9f0243558434f14ed64feca8
|
|
4
|
+
data.tar.gz: f1e067ee109d4db4b7ac9a94664d2d69b92e1df10ccdbf31260958c23827e8d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69fb033248f3950561b72732f70b51e2b7ed0e32aeffdab821c86a112a767e3b01dacccfda561467c312958b0f068be132421798d59551c6a0d28f3ddde06f67
|
|
7
|
+
data.tar.gz: 612d3fa844be6d1fa368679d1af12eb77f59a18175e84a906f29dd47405e7d6ef6002975dde83ffc291973eec3ca1cdad1c061755e89ba158a88eb369e3b8e93
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,68 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`^Ctor` — the constructor, curried.** `Decode.succeed(Spec(_, _, _, _))`
|
|
14
|
+
opens an applicative decode pipeline, and those underscores are coupled to
|
|
15
|
+
the struct's arity: add a field and every pipeline needs another one,
|
|
16
|
+
reported as a type mismatch at the pipeline rather than at the struct.
|
|
17
|
+
`^Spec` desugars to exactly the same curried lambda, with the arity read off
|
|
18
|
+
the resolved constructor. Works on variants as well as structs.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **A constraint on a compound type holding a free var gets its dictionary.**
|
|
23
|
+
A polymorphic function whose body needed something like
|
|
24
|
+
`Decodable(List(a))`, `Decodable(Maybe(a))`, or `Decodable({ value: a })`
|
|
25
|
+
crashed codegen with `undefined method 'map' for nil` — no span, no message.
|
|
26
|
+
The derived instance depends on `Decodable(a)`, but only *bare* var
|
|
27
|
+
constraints earn a dict param, so the enclosing function took none and the
|
|
28
|
+
dep had nothing to bind to. Those deps are now surfaced as constraints of
|
|
29
|
+
their own, so the function takes the dict and callers pass their witness
|
|
30
|
+
down. Should a dep still find no dict in scope, codegen now fails naming the
|
|
31
|
+
constraint instead of handing `nil` on to the pretty-printer.
|
|
32
|
+
- **The `Encodable` deriver keeps its free-var deps.** The mirror of the above,
|
|
33
|
+
and quieter: `Encodable(List(a))` with `a` free dropped the derivation
|
|
34
|
+
outright, and `Encode.encode(items)` compiled to Ruby referencing an
|
|
35
|
+
`impl_arg` that was never bound — a `NameError` at run time, no compile-time
|
|
36
|
+
complaint. The deriver now falls back to the constraint marker for a free
|
|
37
|
+
var, the way `Decodable` already did.
|
|
38
|
+
- **A witness reaches a callee declared later in the file.** A polymorphic
|
|
39
|
+
function calling a polymorphic function declared below it lost the interface
|
|
40
|
+
witness — the call type-checked, then died at run time missing a dictionary
|
|
41
|
+
argument. Moving the callee above the caller fixed it, which is what made
|
|
42
|
+
this look like it was about lambdas. The collecting pass walks declarations
|
|
43
|
+
in source order, so the callee's constraints weren't known yet; it now
|
|
44
|
+
repeats until the constraint sets stop growing.
|
|
45
|
+
- **Anonymous records of the same shape compare equal across modules.** Each
|
|
46
|
+
module hoisted its own class per record shape, so `{ x: 1 }` built in one
|
|
47
|
+
module compared false against the same record built in another — both have
|
|
48
|
+
type `{ x: Int }`, and it type-checks fine, so nothing warned you. A record
|
|
49
|
+
decoded at a port boundary compared false against either. Shapes now resolve
|
|
50
|
+
through the interned `Jade::Runtime.record` registry, named under
|
|
51
|
+
`Jade::Records` so they don't inspect as belonging to whichever module
|
|
52
|
+
happened to assign one first.
|
|
53
|
+
- **`struct Probe` in `module Probe` loads.** It type-checked clean and died at
|
|
54
|
+
Ruby load with `uninitialized constant Probe::Probe::Probe` — the emitter
|
|
55
|
+
wrote an unrooted `Probe::Probe[n]` from inside `module Probe`, while the
|
|
56
|
+
boundary decoder three lines below already wrote `::Probe::Probe`. Unions
|
|
57
|
+
escaped it because their generated constants are the variants. All three
|
|
58
|
+
unrooted sites are now rooted.
|
|
59
|
+
- **`Eq` on a variant-less union is native equality.** A union with no variants
|
|
60
|
+
represents an opaque native (`Decode.Value`, `List`), and the derived `Eq`
|
|
61
|
+
was one case branch per variant plus a `_ then false` fallthrough — with no
|
|
62
|
+
variants, the fallthrough was the whole function and `x == x` answered false,
|
|
63
|
+
no warning, no type error.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Generated output moves for any code that names a constructor or builds an
|
|
68
|
+
anonymous record. A project that CI-checks committed artifacts will see a
|
|
69
|
+
diff.
|
|
70
|
+
|
|
9
71
|
## [0.6.0]
|
|
10
72
|
|
|
11
73
|
### 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
|
data/lib/jade/codegen/emitter.rb
CHANGED
|
@@ -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)} =
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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 {
|
|
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)) }
|
|
@@ -57,7 +57,7 @@ module Jade
|
|
|
57
57
|
.map { st.env.substitution.apply(it) }
|
|
58
58
|
|
|
59
59
|
propagated = (callee_subst + args_subst)
|
|
60
|
-
.
|
|
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
|
|
75
|
-
in Type::
|
|
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
|
|
91
|
-
in Type::
|
|
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 {
|
|
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
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 |
|
|
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
|
|
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
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jade-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Cornu
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
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
|