jade-lang 0.5.0 → 0.6.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 +39 -1
- 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/function_declaration.rb +7 -6
- data/lib/jade/interop/boundary.rb +10 -1
- data/lib/jade/interop/error.rb +25 -5
- data/lib/jade/module_loader/module_name.rb +46 -0
- data/lib/jade/module_loader.rb +5 -0
- data/lib/jade/task.rb +1 -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: ec12f8f9ca54c6ac59b3d8d521a9ad192eb87dd5a2a5e62620f3abe00b636cee
|
|
4
|
+
data.tar.gz: 940c0c3e78b8dcbd013050b298508323e5c0f60f8ddcb4749ede2416b14d9e16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2f05e1f66e7b893e547a2ccb4d31a9678705daecd35c4c806a94f65c47444ad8b69b334c14b18a3a67417963870debdc4f28530ac334c0106699ee278c1f199
|
|
7
|
+
data.tar.gz: 5711c4a232e7728699e89ddcf168e7dc5b1b86913e34579c7b0b1b2d3df6e069f2f4a12aeb35a800619fdb945169c142a2434f322ce582b1415c524259ab2646
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0]
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A `Maybe` return encodes its element.** A function returning `Maybe(T)`
|
|
14
|
+
handed Ruby the internal `Data` object whenever `T` was a union, or a struct
|
|
15
|
+
holding one — no error, just an un-encoded value across the boundary. The
|
|
16
|
+
specialized encoder fell back to emitting the raw element when it couldn't
|
|
17
|
+
specialize it, instead of declining so the generic encoder could take over;
|
|
18
|
+
`decode`, five lines above it, already declined correctly. `Maybe(Int)` and
|
|
19
|
+
`Maybe(SomeStruct)` were unaffected, which is why it stayed hidden — identity
|
|
20
|
+
happens to be right for primitives. Both derived and hand-written
|
|
21
|
+
`Encodable` instances are now applied, and a `Maybe` field of a struct
|
|
22
|
+
unwraps the same way a `Maybe` return does.
|
|
23
|
+
- **Boundary decode errors name the right side.** `DecodeError` reported
|
|
24
|
+
"Port returned a value that failed to decode" for every failure, including
|
|
25
|
+
values Ruby passed *into* an exposed function, where no port is involved —
|
|
26
|
+
sending you to look for a `uses` block that may not exist. Argument failures
|
|
27
|
+
now read "Ruby passed a value that failed to decode"; port returns are
|
|
28
|
+
unchanged.
|
|
29
|
+
- **A symbol-keyed Hash says so.** Structs cross the boundary as a Hash with
|
|
30
|
+
string keys, so `Person.birthday(name: "Ada", age: 40)` used to fail with
|
|
31
|
+
`expected String, got null` — each field independently reading `nil`, naming
|
|
32
|
+
neither the cause nor the fix. It is now caught where the whole hash is in
|
|
33
|
+
scope, listing the offending keys and the correction. Hashes mixing string
|
|
34
|
+
and symbol keys are left alone; that is a real shape mismatch, and the
|
|
35
|
+
per-field errors describe it better than a guess would.
|
|
36
|
+
- **A module declared under the wrong file name is now a compile error.** A
|
|
37
|
+
module is keyed by the name its file implies — imports resolve `Shop.Cart`
|
|
38
|
+
to `shop/cart.jd` and back — but nothing checked that against what the file
|
|
39
|
+
declared. `module Cart` in `orders.jd` compiled clean and emitted a `Cart`
|
|
40
|
+
module whose own type references pointed at `Orders`, so the first call
|
|
41
|
+
raised `NameError: uninitialized constant Cart::Internal::Orders` far from
|
|
42
|
+
the cause. It now fails at compile time, pointing at the declared name and
|
|
43
|
+
offering both the rename and the move.
|
|
44
|
+
|
|
9
45
|
## [0.5.0]
|
|
10
46
|
|
|
11
47
|
### Added
|
|
@@ -328,7 +364,9 @@ Initial release.
|
|
|
328
364
|
- `jade` CLI dispatcher: `fmt`, `lsp`, `q`.
|
|
329
365
|
- Language server and headless query interface for editor/agent tooling.
|
|
330
366
|
|
|
331
|
-
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.
|
|
367
|
+
[Unreleased]: https://github.com/agustinrhcp/jade/compare/v0.6.0...HEAD
|
|
368
|
+
[0.6.0]: https://github.com/agustinrhcp/jade/compare/v0.5.0...v0.6.0
|
|
369
|
+
[0.5.0]: https://github.com/agustinrhcp/jade/compare/v0.4.0...v0.5.0
|
|
332
370
|
[0.4.0]: https://github.com/agustinrhcp/jade/compare/v0.3.1...v0.4.0
|
|
333
371
|
[0.3.1]: https://github.com/agustinrhcp/jade/compare/v0.3.0...v0.3.1
|
|
334
372
|
[0.3.0]: https://github.com/agustinrhcp/jade/compare/v0.2.0...v0.3.0
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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/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.6.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-13 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -303,6 +303,7 @@ files:
|
|
|
303
303
|
- lib/jade/module_loader/cache.rb
|
|
304
304
|
- lib/jade/module_loader/dependency_graph.rb
|
|
305
305
|
- lib/jade/module_loader/dependency_resolver.rb
|
|
306
|
+
- lib/jade/module_loader/module_name.rb
|
|
306
307
|
- lib/jade/module_loader/normalize.rb
|
|
307
308
|
- lib/jade/module_loader/topological_sort.rb
|
|
308
309
|
- lib/jade/parsing.rb
|