jade-lang 0.5.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 +101 -1
- data/docs/syntax.md +4 -0
- data/lib/jade/ast.rb +10 -0
- data/lib/jade/codegen/boundary/cache.rb +1 -2
- data/lib/jade/codegen/boundary/specialized/list.rb +9 -4
- data/lib/jade/codegen/boundary/specialized/maybe.rb +15 -3
- data/lib/jade/codegen/boundary/specialized/record.rb +7 -3
- data/lib/jade/codegen/boundary/specialized.rb +7 -3
- 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/function_declaration.rb +7 -6
- 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/interop/boundary.rb +10 -1
- data/lib/jade/interop/error.rb +25 -5
- data/lib/jade/lexer.rb +1 -0
- data/lib/jade/module_loader/module_name.rb +46 -0
- data/lib/jade/module_loader.rb +5 -0
- data/lib/jade/parsing.rb +6 -1
- data/lib/jade/runtime.rb +7 -1
- data/lib/jade/task.rb +1 -1
- data/lib/jade/version.rb +1 -1
- metadata +4 -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,104 @@ 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
|
+
|
|
71
|
+
## [0.6.0]
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- **A `Maybe` return encodes its element.** A function returning `Maybe(T)`
|
|
76
|
+
handed Ruby the internal `Data` object whenever `T` was a union, or a struct
|
|
77
|
+
holding one — no error, just an un-encoded value across the boundary. The
|
|
78
|
+
specialized encoder fell back to emitting the raw element when it couldn't
|
|
79
|
+
specialize it, instead of declining so the generic encoder could take over;
|
|
80
|
+
`decode`, five lines above it, already declined correctly. `Maybe(Int)` and
|
|
81
|
+
`Maybe(SomeStruct)` were unaffected, which is why it stayed hidden — identity
|
|
82
|
+
happens to be right for primitives. Both derived and hand-written
|
|
83
|
+
`Encodable` instances are now applied, and a `Maybe` field of a struct
|
|
84
|
+
unwraps the same way a `Maybe` return does.
|
|
85
|
+
- **Boundary decode errors name the right side.** `DecodeError` reported
|
|
86
|
+
"Port returned a value that failed to decode" for every failure, including
|
|
87
|
+
values Ruby passed *into* an exposed function, where no port is involved —
|
|
88
|
+
sending you to look for a `uses` block that may not exist. Argument failures
|
|
89
|
+
now read "Ruby passed a value that failed to decode"; port returns are
|
|
90
|
+
unchanged.
|
|
91
|
+
- **A symbol-keyed Hash says so.** Structs cross the boundary as a Hash with
|
|
92
|
+
string keys, so `Person.birthday(name: "Ada", age: 40)` used to fail with
|
|
93
|
+
`expected String, got null` — each field independently reading `nil`, naming
|
|
94
|
+
neither the cause nor the fix. It is now caught where the whole hash is in
|
|
95
|
+
scope, listing the offending keys and the correction. Hashes mixing string
|
|
96
|
+
and symbol keys are left alone; that is a real shape mismatch, and the
|
|
97
|
+
per-field errors describe it better than a guess would.
|
|
98
|
+
- **A module declared under the wrong file name is now a compile error.** A
|
|
99
|
+
module is keyed by the name its file implies — imports resolve `Shop.Cart`
|
|
100
|
+
to `shop/cart.jd` and back — but nothing checked that against what the file
|
|
101
|
+
declared. `module Cart` in `orders.jd` compiled clean and emitted a `Cart`
|
|
102
|
+
module whose own type references pointed at `Orders`, so the first call
|
|
103
|
+
raised `NameError: uninitialized constant Cart::Internal::Orders` far from
|
|
104
|
+
the cause. It now fails at compile time, pointing at the declared name and
|
|
105
|
+
offering both the rename and the move.
|
|
106
|
+
|
|
9
107
|
## [0.5.0]
|
|
10
108
|
|
|
11
109
|
### Added
|
|
@@ -328,7 +426,9 @@ Initial release.
|
|
|
328
426
|
- `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
|
|
329
427
|
- Language server and headless query interface for editor/agent tooling.
|
|
330
428
|
|
|
331
|
-
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.
|
|
429
|
+
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.6.0...HEAD
|
|
430
|
+
[0.6.0]: https://github.com/agustinrhcp/jade/compare/v0.5.0...v0.6.0
|
|
431
|
+
[0.5.0]: https://github.com/agustinrhcp/jade/compare/v0.4.0...v0.5.0
|
|
332
432
|
[0.4.0]: https://github.com/agustinrhcp/jade/compare/v0.3.1...v0.4.0
|
|
333
433
|
[0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
|
|
334
434
|
[0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
|
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?
|
|
@@ -36,8 +36,7 @@ module Jade
|
|
|
36
36
|
Boundary::Specialized.decode_expr(t, '_', registry)
|
|
37
37
|
},
|
|
38
38
|
encoders: cache_map(per_fn.flat_map { it[:encoders] }, 'ENC') { |t|
|
|
39
|
-
Boundary::Specialized.
|
|
40
|
-
Boundary::Specialized.encode_expr(t, '_', registry)
|
|
39
|
+
Boundary::Specialized.encode_expr(t, '_', registry)
|
|
41
40
|
},
|
|
42
41
|
}
|
|
43
42
|
end
|
|
@@ -19,11 +19,16 @@ module Jade
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def encode(type, value_expr, registry)
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
inner_of(type)
|
|
23
|
+
&.then { Specialized.encode_expr(it, '_1', registry) }
|
|
24
|
+
&.then { map_expr(it, value_expr) }
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
def map_expr(elem, value_expr)
|
|
28
|
+
case elem
|
|
29
|
+
in ::String then "#{value_expr}.map { #{elem} }"
|
|
30
|
+
in :identity then nil
|
|
31
|
+
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def identity_encoder?(type)
|
|
@@ -17,10 +17,22 @@ module Jade
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def encode(type, value_expr, registry)
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
inner_of(type)
|
|
21
|
+
&.then { element_encoder(it, registry) }
|
|
22
|
+
&.then { unwrap_expr(it, value_expr) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unwrap_expr(elem, value_expr)
|
|
26
|
+
"#{value_expr}.then { it.is_a?(::Jade::Maybe::Just) ? #{elem} : nil }"
|
|
27
|
+
end
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
# Never identity even when the element is: the Just still unwraps.
|
|
30
|
+
def element_encoder(inner, registry)
|
|
31
|
+
case Specialized.encode_expr(inner, 'it._1', registry)
|
|
32
|
+
in :identity then 'it._1'
|
|
33
|
+
in ::String => expr then expr
|
|
34
|
+
in nil then nil
|
|
35
|
+
end
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def specializable?(type, registry, seen)
|
|
@@ -138,7 +138,7 @@ module Jade
|
|
|
138
138
|
|
|
139
139
|
def encode_helper(struct, registry)
|
|
140
140
|
struct.record_type.fields
|
|
141
|
-
.map { |k, t| "#{k.inspect} => #{
|
|
141
|
+
.map { |k, t| "#{k.inspect} => #{field_encode(k, t, "p.#{k}", registry)}" }
|
|
142
142
|
.then { "{ #{it.join(', ')} }" }
|
|
143
143
|
.then { Pretty.block("def self.#{encode_helper_name(struct)}(p)", it) }
|
|
144
144
|
end
|
|
@@ -148,8 +148,12 @@ module Jade
|
|
|
148
148
|
fail("non-specializable field type for #{key}: #{type}")
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
def
|
|
152
|
-
Specialized.encode_expr(type, accessor, registry)
|
|
151
|
+
def field_encode(key, type, accessor, registry)
|
|
152
|
+
case Specialized.encode_expr(type, accessor, registry)
|
|
153
|
+
in :identity then accessor
|
|
154
|
+
in ::String => expr then expr
|
|
155
|
+
in nil then fail("non-encodable field type for #{key}: #{type}")
|
|
156
|
+
end
|
|
153
157
|
end
|
|
154
158
|
|
|
155
159
|
def snake(name)
|
|
@@ -28,10 +28,14 @@ module Jade
|
|
|
28
28
|
Record.decode(type, input, registry)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# How to encode `value_expr` to the wire form:
|
|
32
|
+
#
|
|
33
|
+
# String inline expression
|
|
34
|
+
# :identity the value is already wire-shaped; emit it unchanged
|
|
35
|
+
# nil not specializable; fall back to the cached encoder
|
|
34
36
|
def encode_expr(type, value_expr, registry)
|
|
37
|
+
return :identity if identity_encoder?(type)
|
|
38
|
+
|
|
35
39
|
Record.encode(type, value_expr, registry) ||
|
|
36
40
|
List.encode(type, value_expr, registry) ||
|
|
37
41
|
Maybe.encode(type, value_expr, registry)
|
|
@@ -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
|
|
@@ -80,14 +80,15 @@ module Jade
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def encode_return(return_type, call_expr, registry)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
case Codegen::Boundary::Specialized.encode_expr(return_type, call_expr, registry)
|
|
84
|
+
in :identity then call_expr
|
|
85
|
+
|
|
86
|
+
in ::String => expr then expr
|
|
87
|
+
|
|
88
|
+
in nil
|
|
88
89
|
Codegen::Boundary::Cache
|
|
89
90
|
.encoder_for(return_type, registry)
|
|
90
|
-
.then {
|
|
91
|
+
.then { "#{it}.call(#{call_expr})" }
|
|
91
92
|
end
|
|
92
93
|
end
|
|
93
94
|
|
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
|
|
|
@@ -50,12 +50,21 @@ module Jade
|
|
|
50
50
|
|
|
51
51
|
def hash(label, v)
|
|
52
52
|
case v
|
|
53
|
-
when ::Hash then v
|
|
53
|
+
when ::Hash then string_keyed!(label, v)
|
|
54
54
|
when ::Data then v.to_h.transform_keys(&:to_s)
|
|
55
55
|
else type_error!(label, v)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Mixed keys are a real shape mismatch; the per-field errors say more
|
|
60
|
+
# than a guess about intent would.
|
|
61
|
+
def string_keyed!(label, v)
|
|
62
|
+
return v if v.empty? || v.keys.any?(::String)
|
|
63
|
+
|
|
64
|
+
v.keys.grep(::Symbol)
|
|
65
|
+
.then { it.empty? ? v : raise(Jade::Interop::SymbolKeys.new(label, it)) }
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
def type_error!(label, v)
|
|
60
69
|
raise Jade::Interop::DecodeError.new(
|
|
61
70
|
Jade::Decode::WrongType[label, Jade::Decode.type_name(v)],
|
data/lib/jade/interop/error.rb
CHANGED
|
@@ -40,12 +40,31 @@ module Jade
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Caught where the whole hash is in scope; downstream each field just
|
|
44
|
+
# reads nil and blames its own type.
|
|
45
|
+
class SymbolKeys < Error
|
|
46
|
+
def initialize(label, keys)
|
|
47
|
+
super(
|
|
48
|
+
"#{label} expects a Hash with string keys, got symbol keys " \
|
|
49
|
+
"(#{keys.take(4).map(&:inspect).join(', ')}). " \
|
|
50
|
+
"Values cross the boundary as wire data — " \
|
|
51
|
+
"try #{keys.first.to_s.inspect} rather than #{keys.first.inspect}."
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
class DecodeError < Error
|
|
44
|
-
attr_reader :decode_error, :value
|
|
57
|
+
attr_reader :decode_error, :value, :source
|
|
58
|
+
|
|
59
|
+
SUBJECTS = {
|
|
60
|
+
argument: 'Ruby passed a value that failed to decode',
|
|
61
|
+
port_return: 'Port returned a value that failed to decode',
|
|
62
|
+
}.freeze
|
|
45
63
|
|
|
46
|
-
def initialize(decode_error, value)
|
|
64
|
+
def initialize(decode_error, value, source: :argument)
|
|
47
65
|
@decode_error = decode_error
|
|
48
66
|
@value = value
|
|
67
|
+
@source = source
|
|
49
68
|
super(format(decode_error, value))
|
|
50
69
|
end
|
|
51
70
|
|
|
@@ -54,16 +73,17 @@ module Jade
|
|
|
54
73
|
def format(error, value)
|
|
55
74
|
path, leaf = unwind(error)
|
|
56
75
|
location = path.empty? ? "value" : path.join
|
|
76
|
+
subject = SUBJECTS.fetch(@source)
|
|
57
77
|
|
|
58
78
|
case leaf
|
|
59
79
|
in Jade::Decode::WrongType[expected, got]
|
|
60
|
-
"
|
|
80
|
+
"#{subject} at #{location}: expected #{expected}, got #{got} (#{value.inspect})"
|
|
61
81
|
|
|
62
82
|
in Jade::Decode::MissingField[key]
|
|
63
|
-
"
|
|
83
|
+
"#{subject} at #{location}: missing field `#{key}` (#{value.inspect})"
|
|
64
84
|
|
|
65
85
|
in Jade::Decode::Custom[msg]
|
|
66
|
-
"
|
|
86
|
+
"#{subject} at #{location}: #{msg} (#{value.inspect})"
|
|
67
87
|
|
|
68
88
|
in Jade::Decode::Multiple[errors]
|
|
69
89
|
errors
|
data/lib/jade/lexer.rb
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module ModuleLoader
|
|
3
|
+
# A module is keyed by the name its file implies — imports resolve
|
|
4
|
+
# `Shop.Cart` to `shop/cart.jd` and back. The wrapper takes the declared
|
|
5
|
+
# name while every type reference inside takes the file's, so a mismatch
|
|
6
|
+
# emits Ruby referring to a constant that was never defined.
|
|
7
|
+
module ModuleName
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def check(ast, source)
|
|
11
|
+
return Ok[ast] unless ast.is_a?(AST::Module)
|
|
12
|
+
|
|
13
|
+
source.to_module_name.then do |expected|
|
|
14
|
+
ast.name == expected ? Ok[ast] : Err[mismatch(ast, source, expected)]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def mismatch(ast, source, expected)
|
|
21
|
+
Diagnostics::List
|
|
22
|
+
.empty
|
|
23
|
+
.error(
|
|
24
|
+
"Module is declared as `#{ast.name}` but `#{source.uri}` defines `#{expected}`.",
|
|
25
|
+
source:,
|
|
26
|
+
span: name_span(ast),
|
|
27
|
+
label: "expected `#{expected}`",
|
|
28
|
+
)
|
|
29
|
+
.help("Rename it to `#{expected}`, or move the file to `#{path_for(ast.name)}`.")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `module` is skipped during parsing, so the node starts at the name.
|
|
33
|
+
def name_span(ast)
|
|
34
|
+
ast.range.begin...(ast.range.begin + ast.name.length)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path_for(module_name)
|
|
38
|
+
module_name
|
|
39
|
+
.split('.')
|
|
40
|
+
.map { Source.snake_case(it) }
|
|
41
|
+
.join('/')
|
|
42
|
+
.then { "#{it}.jd" }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/jade/module_loader.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'jade/codegen'
|
|
|
9
9
|
require 'jade/module_loader/cache'
|
|
10
10
|
require 'jade/module_loader/dependency_resolver'
|
|
11
11
|
require 'jade/module_loader/dependency_graph'
|
|
12
|
+
require 'jade/module_loader/module_name'
|
|
12
13
|
require 'jade/module_loader/normalize'
|
|
13
14
|
require 'jade/module_loader/topological_sort'
|
|
14
15
|
require 'jade/diagnostics'
|
|
@@ -132,6 +133,10 @@ module Jade
|
|
|
132
133
|
.then { raise CompilationError.new(it) }
|
|
133
134
|
end => Ok([raw_ast, comments])
|
|
134
135
|
|
|
136
|
+
ModuleName
|
|
137
|
+
.check(raw_ast, source)
|
|
138
|
+
.on_err { raise CompilationError.new(it) }
|
|
139
|
+
|
|
135
140
|
Frontend::CommentAttacher
|
|
136
141
|
.attach(raw_ast, comments, source)
|
|
137
142
|
.then { Registry.entry(source.to_module_name).with(ast: it, source:, entry:) }
|
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/task.rb
CHANGED
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
|
|
@@ -303,6 +304,7 @@ files:
|
|
|
303
304
|
- lib/jade/module_loader/cache.rb
|
|
304
305
|
- lib/jade/module_loader/dependency_graph.rb
|
|
305
306
|
- lib/jade/module_loader/dependency_resolver.rb
|
|
307
|
+
- lib/jade/module_loader/module_name.rb
|
|
306
308
|
- lib/jade/module_loader/normalize.rb
|
|
307
309
|
- lib/jade/module_loader/topological_sort.rb
|
|
308
310
|
- lib/jade/parsing.rb
|