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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba543580eec5d50cae2249c010a03f7a56a96f276705b3b87df7f25f06ff8eeb
4
- data.tar.gz: 15923caf94302e7326aef6cc8a0e581c349b7b46d54ae4898e6bd8d8529657cf
3
+ metadata.gz: 9b0d9367dd2a235c282b3e7bbeabc2ee7874fa6899fdbdd746c66d546279cf30
4
+ data.tar.gz: cf83815353b341a4834b5b17712148db973f96a3a914ac6aa2ae019a00ae84df
5
5
  SHA512:
6
- metadata.gz: 3906343d3094e2b9142aebc9324722c584db45dfb0951d6546d917794559a8cb9249d9ff4243bb380dc383932e78b93609455743a9e349ec3b60086450693846
7
- data.tar.gz: 841c318dc43aa3e99df702eba6ffda7621b5a8c0f97e9302f2d75ef2149a36bcc19a1287d26a3cef5091c1361508d333a964f9eb70094e30a5791e8c4f8394a9
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
- `Decode.required` per field — the `_` placeholders are the constructor's holes,
66
- filled left to right as each field decodes.
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)}" }
@@ -26,11 +26,9 @@ module Jade
26
26
  end
27
27
 
28
28
  in AST::TypeFunction(params:, return_type:)
29
- params_str = params.empty? ?
30
- "()" :
31
- params.map { format_atom(it) }.join(', ')
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
@@ -10,7 +10,7 @@ module Jade
10
10
  }
11
11
 
12
12
  parser(:type_atom) {
13
- type_application | type_var | type_tuple | grouped(lazy { type_function })
13
+ type_application | type_var | type_tuple | grouped(lazy { type_expression })
14
14
  }
15
15
 
16
16
  parser(:type_tuple) {
@@ -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' },
@@ -8,7 +8,7 @@ module Jade
8
8
  end
9
9
 
10
10
  def constant?
11
- params.empty? && constraints.empty?
11
+ params.empty?
12
12
  end
13
13
  end
14
14
  end
@@ -1,27 +1,26 @@
1
1
  module Jade
2
2
  module Type
3
- Function = Data.define(:args, :return_type, :display) do
3
+ Function = Data.define(:args, :return_type) do
4
4
  include Base
5
- include Displayable
6
5
 
7
- def initialize(args:, return_type:, display: nil)
8
- super
9
- end
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
- def render
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
@@ -1,3 +1,3 @@
1
1
  module Jade
2
- VERSION = '0.10.0'
2
+ VERSION = '0.10.1'
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jade-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
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-03 00:00:00.000000000 Z
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