jade-lang 0.10.0 → 0.10.1
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 +42 -0
- data/README.md +1 -0
- data/docs/json.md +34 -3
- data/lib/jade/cli/init.rb +86 -0
- data/lib/jade/cli.rb +2 -0
- data/lib/jade/codegen/function_call.rb +15 -0
- data/lib/jade/formatter/type.rb +3 -5
- data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
- data/lib/jade/frontend/type_checking/error.rb +1 -0
- data/lib/jade/frontend/type_checking/inference/record_update.rb +8 -1
- data/lib/jade/parsing/type.rb +1 -1
- data/lib/jade/stdlib/decode.rb +11 -0
- data/lib/jade/symbol/stdlib_function.rb +1 -1
- data/lib/jade/type/function.rb +12 -13
- 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: 9b0d9367dd2a235c282b3e7bbeabc2ee7874fa6899fdbdd746c66d546279cf30
|
|
4
|
+
data.tar.gz: cf83815353b341a4834b5b17712148db973f96a3a914ac6aa2ae019a00ae84df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1551750ffbca65f37071206e6eccc559f2d7ae81b5ecf0545c089bc3b0f8d36ba3cc3d98c4c33ce3b2c572c8a650d494c40c9d926f210a1f449c478257ca084c
|
|
7
|
+
data.tar.gz: 2ec0e124c2057bd34cf99728253b5dbd93765c77694a7f6ca0799be1c56eb1baa6d79e4cbc8268ad56a38fa811877defb5ccbd2f7538ce24e33958754ee90aa6
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.10.1] - 2026-09-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Decode.decoder`, the derived decoder as a value.** `Decode.from_json` has
|
|
12
|
+
always picked a decoder off the return type, but there was no way to name
|
|
13
|
+
that decoder and hand it to a combinator — every project ended up with a
|
|
14
|
+
`uuid_decoder` and a `date_decoder` per module, reimplementing an instance
|
|
15
|
+
that already existed. `Decode.field("on", Decode.decoder)` now resolves the
|
|
16
|
+
`Decodable` instance from the position the decoder stands in, including
|
|
17
|
+
derived ones, and a type without an instance is a compile error naming the
|
|
18
|
+
type.
|
|
19
|
+
|
|
20
|
+
- **`jade init`.** Writes `jade.json`, which is how every tool that runs
|
|
21
|
+
outside the app finds your sources, creates the source directory, and adds
|
|
22
|
+
`.jade/` to a `.gitignore` that has one. It refuses to overwrite an existing
|
|
23
|
+
manifest rather than discarding a source root and extension list that cannot
|
|
24
|
+
be recovered from the directory. Until now the first thing a new project met
|
|
25
|
+
was the error telling it to write that file by hand.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **A function type printed as `(a) -> b`, which means something else.**
|
|
30
|
+
`Type::Function#to_s` parenthesised its parameters unconditionally, so
|
|
31
|
+
`jade q api` printed `List.fold : (List(a), b, (b, a) -> b) -> b`, a
|
|
32
|
+
signature whose outer parentheses read as a tuple argument. It prints the
|
|
33
|
+
bare form now, `List(a), b, (b, a -> b) -> b`, which is what the formatter
|
|
34
|
+
writes and what you can paste back into a file.
|
|
35
|
+
|
|
36
|
+
Parentheses around a comma list are a tuple everywhere, and a parameter
|
|
37
|
+
list is bare. A nested function type keeps parentheses of its own, told
|
|
38
|
+
apart from a tuple by the arrow inside them. `(Int) -> Int` also parses
|
|
39
|
+
now, which it did not: one element is not a tuple.
|
|
40
|
+
|
|
41
|
+
- **A record update that does not fit crashed the compiler.** The unification
|
|
42
|
+
that closes an update was the one call with no error block, and
|
|
43
|
+
`State#unify` calls the block unconditionally when unification fails, so a
|
|
44
|
+
mismatch reached `nil.call` and took the compiler out with a `NoMethodError`
|
|
45
|
+
naming a line in `state.rb`. Changing a field's type is the usual way in;
|
|
46
|
+
anything the surrounding code cannot accept arrives the same way. It now
|
|
47
|
+
reads `This update produces Box(Int), but String was expected`.
|
|
48
|
+
|
|
7
49
|
## [0.10.0] - 2026-09-02
|
|
8
50
|
|
|
9
51
|
### Breaking
|
data/README.md
CHANGED
|
@@ -348,6 +348,7 @@ for us so far.
|
|
|
348
348
|
A single `jade` binary fronts the toolchain:
|
|
349
349
|
|
|
350
350
|
```
|
|
351
|
+
jade init # write jade.json and the source directory
|
|
351
352
|
jade check [file...] # type-check; exits 1 on errors, generates nothing
|
|
352
353
|
jade fmt [-i|-c] [file] # format .jd source (stdin or file)
|
|
353
354
|
jade lsp # language server over stdio (hover, defn, refs, diagnostics)
|
data/docs/json.md
CHANGED
|
@@ -61,9 +61,23 @@ DecodeJson::Internal.user('{"name":"Ada"}')
|
|
|
61
61
|
# => Err(MissingField("age"))
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
The struct decoder is `Decode.succeed(User(_, _))` piped through one
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
The struct decoder is `Decode.succeed(User(_, _))` piped through one step per
|
|
65
|
+
field — the `_` placeholders are the constructor's holes, filled left to right
|
|
66
|
+
as each field decodes. `Decode.required` fails on a missing key;
|
|
67
|
+
`Decode.optional` takes what a missing key stands for instead, which is a
|
|
68
|
+
`Maybe` when that is the field's type:
|
|
69
|
+
|
|
70
|
+
```jade
|
|
71
|
+
Decode.succeed(Note(_, _, _))
|
|
72
|
+
|> Decode.required("body", Decode.nullable(Decode.string))
|
|
73
|
+
|> Decode.optional("kind", Decode.string, "note")
|
|
74
|
+
|> Decode.optional("archived_on", Decode.map(Decode.decoder, Just), Nothing)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A nullable field is a required one: the key has to be there, and `null` is a
|
|
78
|
+
value the decoder admits. Pick the step at the field — once a step has run the
|
|
79
|
+
pipeline holds the constructor's remaining arguments, so nothing downstream
|
|
80
|
+
reaches back into one field.
|
|
67
81
|
|
|
68
82
|
## Encoding
|
|
69
83
|
|
|
@@ -146,6 +160,23 @@ end
|
|
|
146
160
|
Reach for the explicit combinators above when the JSON shape doesn't match the
|
|
147
161
|
struct one-to-one — renamed keys, nested lookups, optional fields.
|
|
148
162
|
|
|
163
|
+
Those combinators still take derived decoders for the leaves. `Decode.decoder`
|
|
164
|
+
is the instance for whatever type is expected of it, so a hand-built shape can
|
|
165
|
+
be filled with types that already know how to read themselves:
|
|
166
|
+
|
|
167
|
+
```jade
|
|
168
|
+
def decoder -> Decoder(Movement)
|
|
169
|
+
Decode.succeed(Movement(_, _, _))
|
|
170
|
+
|> Decode.and_map(Decode.field("from_id", Decode.decoder))
|
|
171
|
+
|> Decode.and_map(Decode.field("to_id", Decode.decoder))
|
|
172
|
+
|> Decode.and_map(Decode.field("occurred_on", Decode.decoder))
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Two of those fields are `Uuid` and one is a `Date`; each resolves from the
|
|
177
|
+
position it stands in. A type with no instance is a compile error naming the
|
|
178
|
+
type, not a decoder that fails at runtime.
|
|
179
|
+
|
|
149
180
|
Derivation reaches through the structural types to their elements, so anything
|
|
150
181
|
built out of encodable parts is itself encodable:
|
|
151
182
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require 'jade'
|
|
5
|
+
|
|
6
|
+
module Jade
|
|
7
|
+
module CLI
|
|
8
|
+
# Writes the manifest every tool outside the app reads. Without one,
|
|
9
|
+
# the CLI and the language server have to guess where the sources are,
|
|
10
|
+
# which is why `Project::NotFound` says to write this file by hand.
|
|
11
|
+
module Init
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
IGNORED = '.jade/'.freeze
|
|
15
|
+
|
|
16
|
+
def run(argv)
|
|
17
|
+
usage if argv.any? { it == '-h' || it == '--help' }
|
|
18
|
+
|
|
19
|
+
source_root(argv)
|
|
20
|
+
.then { [it, File.expand_path(Project::MANIFEST)] }
|
|
21
|
+
.then { |root, manifest| write(root, manifest) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write(root, manifest)
|
|
25
|
+
refuse(manifest) if File.exist?(manifest)
|
|
26
|
+
|
|
27
|
+
File.write(manifest, "#{JSON.pretty_generate(config(root))}\n")
|
|
28
|
+
FileUtils.mkdir_p(root)
|
|
29
|
+
|
|
30
|
+
report(root, manifest)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Only what the defaults do not already say. A manifest naming every
|
|
34
|
+
# setting reads as though each were a decision.
|
|
35
|
+
def config(root)
|
|
36
|
+
{ 'source_roots' => [root], 'extensions' => [] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def source_root(argv)
|
|
40
|
+
argv
|
|
41
|
+
.each_cons(2)
|
|
42
|
+
.find { |flag, _| flag == '--source-root' }
|
|
43
|
+
&.last || Project::DEFAULTS[:source_roots].first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def refuse(manifest)
|
|
47
|
+
warn "jade: #{File.basename(manifest)} already exists in #{File.dirname(manifest)}"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def report(root, manifest)
|
|
52
|
+
puts <<~TXT
|
|
53
|
+
Wrote #{File.basename(manifest)} and #{root}/.
|
|
54
|
+
|
|
55
|
+
Put a module in #{root}/, then:
|
|
56
|
+
|
|
57
|
+
jade check type-check it
|
|
58
|
+
jade fmt #{root}/x.jd#{' ' * [0, 7 - root.length].max} format it
|
|
59
|
+
|
|
60
|
+
#{gitignore_note}
|
|
61
|
+
TXT
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build and cache output, which nobody wants in a diff.
|
|
65
|
+
def gitignore_note
|
|
66
|
+
path = File.expand_path('.gitignore')
|
|
67
|
+
return "Add #{IGNORED} to .gitignore." unless File.exist?(path)
|
|
68
|
+
return "#{IGNORED} is already ignored." if File.read(path).match?(/^\.jade\b/)
|
|
69
|
+
|
|
70
|
+
File.write(path, "#{File.read(path).chomp}\n#{IGNORED}\n")
|
|
71
|
+
"Added #{IGNORED} to .gitignore."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def usage
|
|
75
|
+
warn <<~USAGE
|
|
76
|
+
Usage: jade init [--source-root DIR]
|
|
77
|
+
|
|
78
|
+
Writes jade.json, which is how every tool outside the app finds
|
|
79
|
+
your sources, and creates the source directory. Refuses to
|
|
80
|
+
overwrite an existing manifest.
|
|
81
|
+
USAGE
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/jade/cli.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Jade
|
|
|
6
6
|
'check' => 'Check',
|
|
7
7
|
'eject' => 'Eject',
|
|
8
8
|
'fmt' => 'Fmt',
|
|
9
|
+
'init' => 'Init',
|
|
9
10
|
'lsp' => 'Lsp',
|
|
10
11
|
'q' => 'Q',
|
|
11
12
|
}.freeze
|
|
@@ -40,6 +41,7 @@ module Jade
|
|
|
40
41
|
check Type-check the project (or the given files).
|
|
41
42
|
eject Write the project as Ruby that runs without the gem.
|
|
42
43
|
fmt Format .jd source (stdin or file).
|
|
44
|
+
init Write jade.json and the source directory.
|
|
43
45
|
lsp Run the language server (stdio JSON-RPC).
|
|
44
46
|
q Headless query interface (hover/symbols/defn/refs/api).
|
|
45
47
|
|
|
@@ -16,6 +16,9 @@ module Jade
|
|
|
16
16
|
Inline.try_for(callee, args, dictionaries, registry)
|
|
17
17
|
.then { return it if it }
|
|
18
18
|
|
|
19
|
+
constrained_constant(callee, dictionaries, registry)
|
|
20
|
+
.then { return it if it }
|
|
21
|
+
|
|
19
22
|
return constructor_call(callee, args, registry) if constructor_callee?(callee, registry)
|
|
20
23
|
|
|
21
24
|
[generate_many(args, registry), generate_dict_args(callee, dictionaries, registry)]
|
|
@@ -24,6 +27,18 @@ module Jade
|
|
|
24
27
|
.then { "#{generate_callee(callee, args, registry, dictionaries)}#{invocation_op(callee, registry)}(#{it})" }
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
# A constrained constant — `Decode.decoder` — has no parameters, so its
|
|
31
|
+
# dictionary slot holds the value itself rather than something to call.
|
|
32
|
+
# generate_callee already produced that value; invoking it would be
|
|
33
|
+
# calling a Decoder.
|
|
34
|
+
def constrained_constant(callee, dictionaries, registry)
|
|
35
|
+
symbol = resolve_callee_symbol(callee, registry)
|
|
36
|
+
return nil unless symbol.is_a?(Symbol::StdlibFunction)
|
|
37
|
+
return nil unless symbol.params.empty? && symbol.constraints.any?
|
|
38
|
+
|
|
39
|
+
generate_callee(callee, [], registry, dictionaries)
|
|
40
|
+
end
|
|
41
|
+
|
|
27
42
|
def constructor_call(callee, args, registry)
|
|
28
43
|
resolve_callee_symbol(callee, registry)
|
|
29
44
|
.then { "::#{to_qualified(it.qualified_name)}" }
|
data/lib/jade/formatter/type.rb
CHANGED
|
@@ -26,11 +26,9 @@ module Jade
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
in AST::TypeFunction(params:, return_type:)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"#{params_str} -> #{format_atom(return_type)}"
|
|
29
|
+
params
|
|
30
|
+
.then { it.empty? ? '()' : it.map { |p| format_atom(p) }.join(', ') }
|
|
31
|
+
.then { "#{it} -> #{format_atom(return_type)}" }
|
|
34
32
|
|
|
35
33
|
in AST::TypeRecord(fields:, row_var:)
|
|
36
34
|
fields_str = fields.map { |k, v| "#{k}: #{format(v)}" }.join(", ")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Jade
|
|
2
|
+
module Frontend
|
|
3
|
+
module TypeChecking
|
|
4
|
+
module Error
|
|
5
|
+
# An update produces a record of its own, which then has to be the
|
|
6
|
+
# record the surrounding code wanted. Changing a field's type is the
|
|
7
|
+
# usual way to break that, and any other disagreement lands here too.
|
|
8
|
+
class RecordUpdateTypeMismatch < TypeMismatch
|
|
9
|
+
def message
|
|
10
|
+
"This update produces #{naming.annotated(@actual)}, " \
|
|
11
|
+
"but #{naming.annotated(@expected)} was expected"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def label
|
|
15
|
+
"produces #{naming[@actual]}, expected #{naming[@expected]}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -19,6 +19,7 @@ require 'jade/frontend/type_checking/error/pattern_type_mismatch'
|
|
|
19
19
|
require 'jade/frontend/type_checking/error/empty_range_pattern'
|
|
20
20
|
require 'jade/frontend/type_checking/error/range_pattern_type'
|
|
21
21
|
require 'jade/frontend/type_checking/error/record_access_type_mismatch'
|
|
22
|
+
require 'jade/frontend/type_checking/error/record_update_type_mismatch'
|
|
22
23
|
require 'jade/frontend/type_checking/error/unresolved_constraint'
|
|
23
24
|
require 'jade/frontend/type_checking/error/implementation_type_mismatch'
|
|
24
25
|
require 'jade/frontend/type_checking/error/missing_implementation'
|
|
@@ -28,7 +28,14 @@ module Jade
|
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
after_state.unify_result(result, expected.type)
|
|
31
|
+
after_state.unify_result(result, expected.type) do
|
|
32
|
+
Error::RecordUpdateTypeMismatch.new(
|
|
33
|
+
state.env.entry_name,
|
|
34
|
+
node.range,
|
|
35
|
+
expected: it.expected,
|
|
36
|
+
actual: it.actual,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
32
39
|
end
|
|
33
40
|
end
|
|
34
41
|
end
|
data/lib/jade/parsing/type.rb
CHANGED
data/lib/jade/stdlib/decode.rb
CHANGED
|
@@ -268,6 +268,17 @@ module Jade
|
|
|
268
268
|
|
|
269
269
|
# Constrained helpers — pick the decoder via Decodable.
|
|
270
270
|
|
|
271
|
+
function(
|
|
272
|
+
'decoder',
|
|
273
|
+
{},
|
|
274
|
+
'Decoder(a)',
|
|
275
|
+
constraints: [['Decode.Decodable', 'a']],
|
|
276
|
+
body: Symbol::DerivedFunction.new(
|
|
277
|
+
params: [],
|
|
278
|
+
body: [:impl_arg, 0, 'decoder'],
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
271
282
|
function(
|
|
272
283
|
'from_value',
|
|
273
284
|
{ value: 'Value' },
|
data/lib/jade/type/function.rb
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
module Jade
|
|
2
2
|
module Type
|
|
3
|
-
Function = Data.define(:args, :return_type
|
|
3
|
+
Function = Data.define(:args, :return_type) do
|
|
4
4
|
include Base
|
|
5
|
-
include Displayable
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def identity
|
|
12
|
-
[args, return_type]
|
|
13
|
-
end
|
|
6
|
+
# `Int, Int -> Int`, the spelling the formatter writes. Parentheses
|
|
7
|
+
# around a comma list are a tuple, so a nested function keeps its own.
|
|
8
|
+
def to_s
|
|
9
|
+
params = args.empty? ? '()' : args.map { delimited(it) }.join(', ')
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
args
|
|
17
|
-
.map(&:to_s).join(', ')
|
|
18
|
-
.then { "(#{it})"} + " -> " + return_type.to_s
|
|
11
|
+
"#{params} -> #{delimited(return_type)}"
|
|
19
12
|
end
|
|
20
13
|
|
|
21
14
|
def unbound_vars
|
|
22
15
|
(args.flat_map(&:unbound_vars) + return_type.unbound_vars)
|
|
23
16
|
.to_set.to_a
|
|
24
17
|
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def delimited(type)
|
|
22
|
+
type.is_a?(Function) ? "(#{type})" : type.to_s
|
|
23
|
+
end
|
|
25
24
|
end
|
|
26
25
|
end
|
|
27
26
|
end
|
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.10.
|
|
4
|
+
version: 0.10.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Agustin Cornu
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- lib/jade/cli/check.rb
|
|
58
58
|
- lib/jade/cli/eject.rb
|
|
59
59
|
- lib/jade/cli/fmt.rb
|
|
60
|
+
- lib/jade/cli/init.rb
|
|
60
61
|
- lib/jade/cli/lsp.rb
|
|
61
62
|
- lib/jade/cli/q.rb
|
|
62
63
|
- lib/jade/clock/runtime.rb
|
|
@@ -266,6 +267,7 @@ files:
|
|
|
266
267
|
- lib/jade/frontend/type_checking/error/port_not_encodable.rb
|
|
267
268
|
- lib/jade/frontend/type_checking/error/range_pattern_type.rb
|
|
268
269
|
- lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb
|
|
270
|
+
- lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb
|
|
269
271
|
- lib/jade/frontend/type_checking/error/recursive_derivation.rb
|
|
270
272
|
- lib/jade/frontend/type_checking/error/type_mismatch.rb
|
|
271
273
|
- lib/jade/frontend/type_checking/error/unreachable_branch.rb
|