jade-lang 0.9.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.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +442 -12
  3. data/README.md +1 -0
  4. data/docs/interop.md +18 -1
  5. data/docs/json.md +34 -3
  6. data/docs/stdlib.md +3 -1
  7. data/docs/syntax.md +96 -0
  8. data/lib/jade/api.rb +9 -0
  9. data/lib/jade/ast.rb +37 -1
  10. data/lib/jade/cli/eject.rb +132 -0
  11. data/lib/jade/cli/init.rb +86 -0
  12. data/lib/jade/cli.rb +4 -0
  13. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  14. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  15. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  16. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  17. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  18. data/lib/jade/codegen/boundary.rb +12 -2
  19. data/lib/jade/codegen/context.rb +17 -0
  20. data/lib/jade/codegen/error.rb +1 -0
  21. data/lib/jade/codegen/function_call.rb +38 -7
  22. data/lib/jade/codegen/function_declaration.rb +25 -13
  23. data/lib/jade/codegen/helpers.rb +27 -4
  24. data/lib/jade/codegen/implementation.rb +2 -2
  25. data/lib/jade/codegen/inline.rb +1 -1
  26. data/lib/jade/codegen/inlines.rb +6 -1
  27. data/lib/jade/codegen/names.rb +31 -0
  28. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  29. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  30. data/lib/jade/codegen.rb +76 -15
  31. data/lib/jade/compiler.rb +2 -0
  32. data/lib/jade/entry.rb +1 -1
  33. data/lib/jade/formatter/calls.rb +68 -11
  34. data/lib/jade/formatter/collections.rb +4 -4
  35. data/lib/jade/formatter/declarations.rb +13 -15
  36. data/lib/jade/formatter/helper.rb +20 -0
  37. data/lib/jade/formatter/infix_application.rb +19 -4
  38. data/lib/jade/formatter/lambda.rb +24 -12
  39. data/lib/jade/formatter/pattern.rb +3 -0
  40. data/lib/jade/formatter/type.rb +3 -5
  41. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  42. data/lib/jade/frontend/desugaring.rb +29 -9
  43. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  44. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  45. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  46. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  47. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  48. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  49. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  50. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  51. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  52. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  53. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  54. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  55. data/lib/jade/frontend/forward_declaration.rb +2 -0
  56. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  57. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  58. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  59. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  60. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  61. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  62. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  63. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  64. data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
  65. data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
  66. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
  67. data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
  68. data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
  69. data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
  70. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
  71. data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
  72. data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
  73. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
  74. data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
  75. data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
  76. data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
  77. data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
  78. data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
  79. data/lib/jade/frontend/semantic_analysis.rb +5 -0
  80. data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
  81. data/lib/jade/frontend/type_checking/cascade.rb +90 -0
  82. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
  83. data/lib/jade/frontend/type_checking/constraints.rb +18 -3
  84. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  85. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  86. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  87. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  88. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  89. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  90. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  91. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  92. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  93. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  94. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  95. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  96. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  97. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  98. data/lib/jade/frontend/type_checking/error/record_update_type_mismatch.rb +21 -0
  99. data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
  100. data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
  101. data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
  102. data/lib/jade/frontend/type_checking/error.rb +7 -0
  103. data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
  104. data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
  105. data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
  106. data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
  107. data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
  108. data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
  109. data/lib/jade/frontend/type_checking/inference/record_update.rb +8 -1
  110. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  111. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  112. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  113. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  114. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  115. data/lib/jade/frontend/type_checking.rb +3 -1
  116. data/lib/jade/frontend/usage_analysis.rb +5 -1
  117. data/lib/jade/interop/boundary.rb +64 -24
  118. data/lib/jade/interop/error.rb +35 -7
  119. data/lib/jade/interop/lowering.rb +8 -2
  120. data/lib/jade/lsp/converters.rb +83 -2
  121. data/lib/jade/lsp/handlers.rb +122 -25
  122. data/lib/jade/lsp/snippets.rb +12 -0
  123. data/lib/jade/lsp/state.rb +11 -4
  124. data/lib/jade/module_loader/build.rb +36 -0
  125. data/lib/jade/module_loader.rb +1 -0
  126. data/lib/jade/parsing/combinators.rb +78 -2
  127. data/lib/jade/parsing/error.rb +43 -6
  128. data/lib/jade/parsing/type.rb +6 -4
  129. data/lib/jade/parsing.rb +140 -26
  130. data/lib/jade/runtime.rb +32 -13
  131. data/lib/jade/source.rb +6 -2
  132. data/lib/jade/stdlib/basics.rb +22 -3
  133. data/lib/jade/stdlib/char.rb +2 -0
  134. data/lib/jade/stdlib/clock.rb +6 -2
  135. data/lib/jade/stdlib/decimal.rb +51 -14
  136. data/lib/jade/stdlib/decode.rb +11 -0
  137. data/lib/jade/stdlib/intrinsics.rb +59 -6
  138. data/lib/jade/stdlib/list.rb +37 -1
  139. data/lib/jade/stdlib/number.rb +47 -0
  140. data/lib/jade/stdlib/range.rb +211 -0
  141. data/lib/jade/stdlib.rb +11 -4
  142. data/lib/jade/symbol/alias.rb +11 -0
  143. data/lib/jade/symbol/parser.rb +1 -1
  144. data/lib/jade/symbol/stdlib_function.rb +1 -1
  145. data/lib/jade/symbol.rb +13 -1
  146. data/lib/jade/task.rb +0 -2
  147. data/lib/jade/type/anonymous_record.rb +11 -2
  148. data/lib/jade/type/application.rb +11 -2
  149. data/lib/jade/type/base.rb +26 -0
  150. data/lib/jade/type/function.rb +11 -3
  151. data/lib/jade/type.rb +36 -10
  152. data/lib/jade/version.rb +1 -1
  153. metadata +35 -2
data/docs/syntax.md CHANGED
@@ -90,6 +90,57 @@ Anonymous records do not coerce into nominal structs: passing
90
90
 
91
91
  → [`examples/records.jd`](../examples/records.jd)
92
92
 
93
+ ## Type aliases
94
+
95
+ A name for another type, with no runtime identity of its own:
96
+
97
+ ```jade
98
+ type alias UserId = Int
99
+ type alias Point = (Float, Float)
100
+ type alias Handler = Int -> Int
101
+ type alias Pair(a) = (a, a)
102
+
103
+ type alias User = {
104
+ name: String,
105
+ age: Int
106
+ }
107
+ ```
108
+
109
+ `UserId` and `Int` are the same type, and a record literal satisfies `User`
110
+ directly:
111
+
112
+ ```jade
113
+ u1 = { name: "Paul", age: 55 }
114
+ u2 = { u1 | age: 56 }
115
+ name = u1.name
116
+ ```
117
+
118
+ An alias never has its own constructors or interface implementations, so
119
+ `implements Show(UserId)` is a compile error — two aliases over the same body
120
+ are the same type, so the implementation would land on `Int` and collide with
121
+ every other one. Reach for `struct` when you want a distinct type, or a
122
+ single-variant `type` when you want a newtype around an inner shape.
123
+
124
+ Interfaces an alias *inherits* work as you would expect, because the alias is
125
+ gone by the time they are resolved: `Encode.encode` on a `UserId` is
126
+ `Encode.encode` on an `Int`, and on a `User` it encodes the record.
127
+
128
+ | | identity | construct with |
129
+ | --- | --- | --- |
130
+ | `struct User = { … }` | nominal, distinct from a same-shaped record | `User("Paul", 55)` |
131
+ | `type User = User({ … })` | nominal newtype wrapping an inner shape | `User({ … })` |
132
+ | `type alias User = { … }` | structural, a readability name | `{ … }` |
133
+
134
+ Recursive aliases are rejected: use a `type` for a recursive shape. A cycle is
135
+ reported once, against the first alias in it that was declared.
136
+
137
+ `alias` is only a keyword directly after `type`, so a field or argument may
138
+ still be called `alias`.
139
+
140
+ The name survives into messages — a mismatch against a `UserId` reads
141
+ `should be UserId (= Int)` — and hovering one in an editor gives its
142
+ declaration.
143
+
93
144
  ## Anonymous records and tuples
94
145
 
95
146
  ```jade
@@ -184,6 +235,51 @@ def area(shape: Shape) -> Float
184
235
  end
185
236
  ```
186
237
 
238
+ **Ranges** match a span of `Int`, with `..` including both bounds. Either
239
+ bound may be left off to run to infinity:
240
+
241
+ ```jade
242
+ module Bands exposing (band)
243
+
244
+ def band(age: Int) -> String
245
+ case age
246
+ in ..-1 then "unborn"
247
+ in 0..2 then "infant"
248
+ in 3..12 then "child"
249
+ in 13.. then "adult"
250
+ end
251
+ end
252
+ ```
253
+
254
+ Those four arms partition `Int`, so the `case` needs no `else`. A range that
255
+ leaves a gap is reported with the gap named — omitting `in 3..12` above gives
256
+ *Missing cases: `3..12`* — and one already covered by an earlier arm is
257
+ reported as unreachable.
258
+
259
+ There is no exclusive form. Over `Int` every half-open range has an exact
260
+ inclusive spelling (`3...12` is `3..11`), so a second operator would buy
261
+ nothing and cost a lifetime of remembering which is which. Ranges are `Int`
262
+ only for the same reason: `0..2` and `3..12` sit next to each other with
263
+ nothing in between them to be missed, which is not true of `Float`.
264
+
265
+ The same spelling builds a `Range` value in expression position, where the
266
+ bounds are ordinary expressions rather than literals:
267
+
268
+ ```jade
269
+ module Bookings exposing (clashes?)
270
+
271
+ def clashes?(check_in: Date, check_out: Date, other: Range(Date)) -> Bool
272
+ Range.overlaps?(check_in..check_out, other)
273
+ end
274
+ ```
275
+
276
+ `..` binds looser than `+` and `-`, so `0..n - 1` is `0..(n - 1)`. It is
277
+ non-associative — `a..b..c` is an error, not a nesting — and it is infix
278
+ only: the endless forms are `Range.from(a)` and `Range.to(b)`, because a
279
+ token that is prefix, infix and postfix by turns costs more to read than two
280
+ names do. A descending pair is `Range.empty` rather than an error, since
281
+ `0..(n - 1)` with `n = 0` is the ordinary way an empty range arises.
282
+
187
283
  **Lists** match with `[]` and `[head | tail]`; the rest after `|` must be a
188
284
  name or wildcard:
189
285
 
data/lib/jade/api.rb CHANGED
@@ -70,6 +70,7 @@ module Jade
70
70
  KIND_ORDER = {
71
71
  'type' => 0,
72
72
  'struct' => 0,
73
+ 'alias' => 0,
73
74
  'interface' => 1,
74
75
  'constructor' => 2,
75
76
  'function' => 3,
@@ -153,6 +154,7 @@ module Jade
153
154
  case symbol
154
155
  in Symbol::Union then 'type'
155
156
  in Symbol::Struct then 'struct'
157
+ in Symbol::Alias then 'alias'
156
158
  in Symbol::Interface then 'interface'
157
159
  in Symbol::Constructor | Symbol::Variant then 'constructor'
158
160
  else 'function'
@@ -167,6 +169,9 @@ module Jade
167
169
  in Symbol::Struct
168
170
  "struct #{symbol.name}#{type_params(symbol.type_params)}#{fields(symbol)}"
169
171
 
172
+ in Symbol::Alias
173
+ "type alias #{symbol.name}#{type_params(symbol.type_params)} = #{body(symbol)}"
174
+
170
175
  in Symbol::Interface
171
176
  "interface #{symbol.name}(#{symbol.type_param.name})"
172
177
 
@@ -192,6 +197,10 @@ module Jade
192
197
  end
193
198
  end
194
199
 
200
+ def body(symbol)
201
+ declared(symbol.body).then { |(type, _)| type }
202
+ end
203
+
195
204
  def fields(symbol)
196
205
  return '' unless symbol.record_type
197
206
 
data/lib/jade/ast.rb CHANGED
@@ -32,6 +32,7 @@ module Jade
32
32
  define(:TypeParam, :name)
33
33
  define(:ImportDeclaration, :module_name, :as, :exposing)
34
34
  define(:StructDeclaration, :name, :type_params, :record_type)
35
+ define(:TypeAliasDeclaration, :name, :type_params, :body_type)
35
36
 
36
37
  define(:ExposeAll)
37
38
  define(:ExposeNone)
@@ -84,6 +85,7 @@ module Jade
84
85
 
85
86
  define(:Wildcard)
86
87
  define(:Literal, :literal)
88
+ define(:Range, :from, :to)
87
89
  define(:Binding, :name)
88
90
  define(:Constructor, :constructor, :patterns)
89
91
  define(:Record, :fields)
@@ -364,11 +366,17 @@ module Jade
364
366
  module_parts.map(&:value).join('.'),
365
367
  exposing,
366
368
  body,
367
- module_parts.first.range.begin...(body.expressions.last.range.end),
369
+ module_parts.first.range.begin...module_end(module_parts, body),
368
370
  ]
369
371
  end
370
372
  end
371
373
 
374
+ # Tolerant mode drops a declaration it cannot parse, so a module whose
375
+ # only declaration is half-written has an empty body.
376
+ def module_end(module_parts, body)
377
+ body.expressions.last&.range&.end || module_parts.last.range.end
378
+ end
379
+
372
380
  def if_then_else
373
381
  ->((if_token, condition, if_branch, else_branch, end_token)) do
374
382
  IfThenElse[
@@ -442,6 +450,22 @@ module Jade
442
450
  end
443
451
  end
444
452
 
453
+ def range_pattern
454
+ ->((from, dotdot, to)) do
455
+ Pattern::Range[
456
+ from,
457
+ to,
458
+ from.range.begin...(to&.range&.end || dotdot.range.end),
459
+ ]
460
+ end
461
+ end
462
+
463
+ def unbounded_below_pattern
464
+ ->((dotdot, to)) do
465
+ Pattern::Range[nil, to, dotdot.range.begin...to.range.end]
466
+ end
467
+ end
468
+
445
469
  def binding_pattern
446
470
  ->(identifier) do
447
471
  Pattern::Binding[identifier.value, identifier.range]
@@ -676,6 +700,18 @@ module Jade
676
700
  end
677
701
  end
678
702
 
703
+ def type_alias_declaration
704
+ ->((type_token, name, type_params_list, body_type)) do
705
+ TypeAliasDeclaration.new(
706
+ name: name.value,
707
+ type_params: type_params_list.items,
708
+ body_type:,
709
+ trailing_comma: type_params_list.trailing_comma,
710
+ range: type_token.range.begin...body_type.range.end,
711
+ )
712
+ end
713
+ end
714
+
679
715
  def implementation
680
716
  ->((implements_token, interface, applied_type, extends, functions, end_token)) do
681
717
  Implementation[
@@ -0,0 +1,132 @@
1
+ require 'fileutils'
2
+ require 'rbconfig'
3
+
4
+ require 'jade'
5
+ require 'jade/module_loader'
6
+
7
+ module Jade
8
+ module CLI
9
+ # Writes the project as Ruby that runs without the gem: the compiled
10
+ # modules, the runtime they call, and requires that point at each other
11
+ # rather than at a load path.
12
+ module Eject
13
+ module_function
14
+
15
+ DEFAULT_OUT = 'ejected'.freeze
16
+
17
+ def run(argv)
18
+ usage if argv.any? { it == '-h' || it == '--help' }
19
+
20
+ Project
21
+ .find!
22
+ .then { [it, out_dir(argv, it)] }
23
+ .then { |project, out| eject(project, out) }
24
+ end
25
+
26
+ def eject(project, out)
27
+ FileUtils.rm_rf(out)
28
+
29
+ compile_all(project, out)
30
+ vendor_runtime(out)
31
+ rewrite_requires(out)
32
+
33
+ report(out)
34
+ end
35
+
36
+ # Every module, not only the ones something imports: an ejected tree
37
+ # that drops a file is not the project.
38
+ def compile_all(project, out)
39
+ sources(project).each do |entry|
40
+ ModuleLoader
41
+ .load(project.source_root, entry, cache_dir: project.cache_path)
42
+ .then { ModuleLoader.emit(it, path: out) }
43
+ end
44
+ end
45
+
46
+ def sources(project)
47
+ Dir
48
+ .glob(File.join(project.source_root, '**', '*.jd'))
49
+ .map { Pathname.new(it).relative_path_from(Pathname.new(project.source_root)).to_s }
50
+ .sort
51
+ end
52
+
53
+ # Whatever a booted runtime loads is what the ejected code needs, so
54
+ # ask one rather than keeping a list here.
55
+ def vendor_runtime(out)
56
+ runtime_files.each do |file|
57
+ File.join(out, file)
58
+ .tap { FileUtils.mkdir_p(File.dirname(it)) }
59
+ .then { FileUtils.cp(File.join(gem_lib, file), it) }
60
+ end
61
+ end
62
+
63
+ def runtime_files
64
+ [RbConfig.ruby, '-I', gem_lib, '-e', BOOT]
65
+ .then { IO.popen(it, err: [:child, :out], &:read) }
66
+ .lines(chomp: true)
67
+ .select { it.start_with?("#{gem_lib}/") }
68
+ .map { it.delete_prefix("#{gem_lib}/") }
69
+ end
70
+
71
+ BOOT = "require 'jade/runtime'; Jade::Runtime.boot!; puts $LOADED_FEATURES".freeze
72
+
73
+ def gem_lib
74
+ File.expand_path('../../..', __dir__).then { File.join(it, 'lib') }
75
+ end
76
+
77
+ # `require 'jade/x'` finds the gem; the point is not to have one.
78
+ def rewrite_requires(out)
79
+ Dir
80
+ .glob(File.join(out, '**', '*.rb'))
81
+ .each { rewrite(it, out) }
82
+ end
83
+
84
+ def rewrite(path, out)
85
+ File
86
+ .read(path)
87
+ .gsub(/^\$LOAD_PATH\.push\(File\.expand_path\("lib"\)\)\.uniq!\n/, '')
88
+ .gsub(/require ['"](jade\/[a-z_\/]+)['"]/) { "require_relative '#{up_to(path, out)}#{$1}'" }
89
+ .then { File.write(path, it) }
90
+ end
91
+
92
+ def up_to(path, out)
93
+ Pathname
94
+ .new(out)
95
+ .expand_path
96
+ .relative_path_from(Pathname.new(File.dirname(File.expand_path(path))))
97
+ .then { it.to_s == '.' ? '' : "#{it}/" }
98
+ end
99
+
100
+ def report(out)
101
+ puts <<~TXT
102
+ Ejected to #{out}/ (#{Dir.glob(File.join(out, '**', '*.rb')).size} files).
103
+
104
+ Nothing under #{out}/ requires the jade gem. To leave:
105
+
106
+ 1. point your app at #{out}/ instead of the build directory
107
+ 2. run your suite
108
+ 3. drop jade-lang from your Gemfile and delete the .jd sources
109
+ TXT
110
+ end
111
+
112
+ def out_dir(argv, project)
113
+ argv
114
+ .each_cons(2)
115
+ .find { |flag, _| flag == '--out' }
116
+ &.last
117
+ .then { File.expand_path(it || DEFAULT_OUT, project.root) }
118
+ end
119
+
120
+ def usage
121
+ warn <<~USAGE
122
+ Usage: jade eject [--out DIR]
123
+
124
+ Compiles the project and writes it, plus the runtime it calls, as
125
+ Ruby that does not require the jade gem. Writes to ./ejected by
126
+ default. The directory is replaced if it exists.
127
+ USAGE
128
+ exit 1
129
+ end
130
+ end
131
+ end
132
+ end
@@ -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
@@ -4,7 +4,9 @@ module Jade
4
4
  module CLI
5
5
  SUBCOMMANDS = {
6
6
  'check' => 'Check',
7
+ 'eject' => 'Eject',
7
8
  'fmt' => 'Fmt',
9
+ 'init' => 'Init',
8
10
  'lsp' => 'Lsp',
9
11
  'q' => 'Q',
10
12
  }.freeze
@@ -37,7 +39,9 @@ module Jade
37
39
  Usage: jade COMMAND [ARGS]
38
40
 
39
41
  check Type-check the project (or the given files).
42
+ eject Write the project as Ruby that runs without the gem.
40
43
  fmt Format .jd source (stdin or file).
44
+ init Write jade.json and the source directory.
41
45
  lsp Run the language server (stdio JSON-RPC).
42
46
  q Headless query interface (hover/symbols/defn/refs/api).
43
47
 
@@ -11,11 +11,11 @@ module Jade
11
11
  module List
12
12
  extend self
13
13
 
14
- def decode(type, input, registry)
14
+ def decode(type, input, registry, where = nil)
15
15
  inner = inner_of(type) or return nil
16
16
 
17
- scalar_optimized(inner, input) ||
18
- generic_decode(type, inner, input, registry)
17
+ scalar_optimized(inner, input, where) ||
18
+ generic_decode(type, inner, input, registry, where)
19
19
  end
20
20
 
21
21
  def encode(type, value_expr, registry)
@@ -51,17 +51,21 @@ module Jade
51
51
 
52
52
  # `List(scalar)` fast path: validate elements with a single
53
53
  # C-loop `all?` check, no per-element decoder call.
54
- def scalar_optimized(inner, input)
54
+ def scalar_optimized(inner, input, where)
55
55
  qname = Scalar.qname_for(inner) or return nil
56
56
  klass = Scalar::LIST_ELEM_CLASS[qname]
57
57
  label = "List(#{Scalar::LABEL[qname]})".inspect
58
- "Jade::Interop::Boundary.list_of(#{klass}, #{label}, #{input})"
58
+ "Jade::Interop::Boundary.list_of(#{klass}, #{label}, #{input}#{Specialized.where_arg(where)})"
59
59
  end
60
60
 
61
- def generic_decode(type, inner, input, registry)
61
+ # The index comes from `elements`, so the element decoder itself
62
+ # carries no path of its own.
63
+ def generic_decode(type, inner, input, registry, where)
62
64
  elem = Specialized.decode_expr(inner, '_1', registry) or return nil
63
65
  label = "List(#{Specialized.type_label(inner)})".inspect
64
- "Jade::Interop::Boundary.array(#{label}, #{input}).map { #{elem} }"
66
+ at = Specialized.where_arg(where)
67
+ array = "Jade::Interop::Boundary.array(#{label}, #{input}#{at})"
68
+ "Jade::Interop::Boundary.elements(#{array}#{at}) { #{elem} }"
65
69
  end
66
70
  end
67
71
  end
@@ -9,9 +9,9 @@ module Jade
9
9
  module Maybe
10
10
  extend self
11
11
 
12
- def decode(type, input, registry)
12
+ def decode(type, input, registry, where = nil)
13
13
  inner = inner_of(type) or return nil
14
- elem = Specialized.decode_expr(inner, 'it', registry) or return nil
14
+ elem = Specialized.decode_expr(inner, 'it', registry, where) or return nil
15
15
 
16
16
  "#{input}.then { it.nil? ? Jade::Maybe::Nothing[] : Jade::Maybe::Just[#{elem}] }"
17
17
  end
@@ -16,9 +16,13 @@ module Jade
16
16
  extend self
17
17
  extend Helpers
18
18
 
19
- def decode(type, input, registry)
19
+ # The helper is shared across call sites, so its fields can only
20
+ # know where they sit inside the struct: the segment that leads
21
+ # to the struct itself goes on at the call site.
22
+ def decode(type, input, registry, where = nil)
20
23
  struct = struct_for(type, registry) or return nil
21
- "#{decode_helper_name(struct)}(#{input})"
24
+ call = "#{decode_helper_name(struct)}(#{input})"
25
+ where ? "Jade::Interop::Boundary.at(#{where.inspect}) { #{call} }" : call
22
26
  end
23
27
 
24
28
  def encode(type, value_expr, registry)
@@ -132,7 +136,7 @@ module Jade
132
136
  struct.record_type.fields
133
137
  .map { |k, t| field_decode(k, t, registry) }
134
138
  .then { Pretty.call(ctor, it, open: '[', close: ']') }
135
- .then { Pretty.block("#{hash_call}.then do |h|", it) }
139
+ .then { Pretty.block("#{hash_call}.then do |h|", it, blaming_the_key(struct)) }
136
140
  .then { Pretty.block("def self.#{decode_helper_name(struct)}(value)", it) }
137
141
  end
138
142
 
@@ -144,7 +148,7 @@ module Jade
144
148
  end
145
149
 
146
150
  def field_decode(key, type, registry)
147
- Specialized.decode_expr(type, "h[#{key.to_s.inspect}]", registry) ||
151
+ Specialized.decode_expr(type, "h[#{key.to_s.inspect}]", registry, ".#{key}") ||
148
152
  fail("non-specializable field type for #{key}: #{type}")
149
153
  end
150
154
 
@@ -156,6 +160,17 @@ module Jade
156
160
  end
157
161
  end
158
162
 
163
+ # A field the hash never had reads as nil to its decoder, which
164
+ # blames the type. The key it sat under is right here, and so is
165
+ # the hash that was supposed to hold it.
166
+ def blaming_the_key(struct)
167
+ [
168
+ 'rescue Jade::Interop::DecodeError => e',
169
+ " raise Jade::Interop::Boundary.blame(e, h, #{struct.name.inspect})",
170
+ 'end',
171
+ ].join("\n")
172
+ end
173
+
159
174
  def snake(name)
160
175
  name
161
176
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -31,10 +31,10 @@ module Jade
31
31
  'String.String' => '::String',
32
32
  }.freeze
33
33
 
34
- def decode(type, input)
34
+ def decode(type, input, where = nil)
35
35
  qname = qname_for(type) or return nil
36
36
  label = LABEL[qname].inspect
37
- "Jade::Interop::Boundary.#{HELPER[qname]}(#{label}, #{input})"
37
+ "Jade::Interop::Boundary.#{HELPER[qname]}(#{label}, #{input}#{Specialized.where_arg(where)})"
38
38
  end
39
39
 
40
40
  # Scalar encoders are identity (Ruby int IS the JSON int, etc.),
@@ -21,11 +21,13 @@ module Jade
21
21
 
22
22
  # Ruby expression that validates `input` and yields the decoded
23
23
  # value, or `nil` if `type` isn't specializable.
24
- def decode_expr(type, input, registry)
25
- Scalar.decode(type, input) ||
26
- List.decode(type, input, registry) ||
27
- Maybe.decode(type, input, registry) ||
28
- Record.decode(type, input, registry)
24
+ # `where` is the path segment this value sits at, `".cents"`,
25
+ # baked into the emitted call so a failure can name it.
26
+ def decode_expr(type, input, registry, where = nil)
27
+ Scalar.decode(type, input, where) ||
28
+ List.decode(type, input, registry, where) ||
29
+ Maybe.decode(type, input, registry, where) ||
30
+ Record.decode(type, input, registry, where)
29
31
  end
30
32
 
31
33
  # How to encode `value_expr` to the wire form:
@@ -66,6 +68,12 @@ module Jade
66
68
  Record.emit_helpers(structs, registry)
67
69
  end
68
70
 
71
+ # Trailing argument for the runtime helpers, which all take the
72
+ # path segment last and default it to nil.
73
+ def where_arg(where)
74
+ where ? ", #{where.inspect}" : ''
75
+ end
76
+
69
77
  # --- type-shape helpers shared across shape modules ---
70
78
 
71
79
  # Both `Type::Application` (from inferred boundary types) and
@@ -18,6 +18,9 @@ module Jade
18
18
  in Type::Application(constructor: Type::Constructor(name:), args:)
19
19
  decode_app(name, args, registry)
20
20
 
21
+ in Type::AnonymousRecord
22
+ derived_for(DECODABLE, type, registry)&.dig('decoder')
23
+
21
24
  else
22
25
  nil
23
26
  end
@@ -28,6 +31,9 @@ module Jade
28
31
  in Type::Application(constructor: Type::Constructor(name:), args:)
29
32
  encode_app(name, args, registry)
30
33
 
34
+ in Type::AnonymousRecord
35
+ derived_for(ENCODABLE, type, registry)&.dig('encoder')
36
+
31
37
  else
32
38
  nil
33
39
  end
@@ -153,7 +159,11 @@ module Jade
153
159
  end
154
160
 
155
161
  def derived_dispatch(interface_qname, type_qname, args, registry)
156
- type = Type.constructor(type_qname).apply(args)
162
+ type = Type.constructor(type_qname).apply(args)
163
+ derived_for(interface_qname, type, registry)
164
+ end
165
+
166
+ def derived_for(interface_qname, type, registry)
157
167
  constraint = Type.constraint(interface_qname, type, nil)
158
168
 
159
169
  case Frontend::TypeChecking::Constraints.resolve(constraint, registry, nil)
@@ -175,7 +185,7 @@ module Jade
175
185
 
176
186
  case fn_sym
177
187
  in Symbol::Function
178
- "#{to_qualified(fn_sym.module_name)}::Internal.method(:#{fn_sym.name})"
188
+ "#{internal(fn_sym.module_name)}.method(:#{fn_sym.name})"
179
189
 
180
190
  in Symbol::StdlibFunction(codegen:) if codegen.is_a?(String)
181
191
  codegen
@@ -22,6 +22,23 @@ module Jade
22
22
  @dict_env = prev
23
23
  end
24
24
 
25
+ # Dictionary source => constant name, filled while a module emits.
26
+ # A dictionary built entirely from concrete implementations depends
27
+ # on nothing at the call site, so it is built once at load instead
28
+ # of on every call. Nil outside a module: bare expressions have no
29
+ # constants to reference.
30
+ def dict_consts
31
+ @dict_consts
32
+ end
33
+
34
+ def with_dict_consts(table)
35
+ prev = @dict_consts
36
+ @dict_consts = table
37
+ yield
38
+ ensure
39
+ @dict_consts = prev
40
+ end
41
+
25
42
  # When set, references with this name emit as `self` (and field accesses
26
43
  # on them as bare method calls). Used to rewrite operator-impl lambda
27
44
  # bodies — `(a, b) -> { a.amount == b.amount }` becomes
@@ -4,5 +4,6 @@ module Jade
4
4
  # this is a bug in jade, not in the source. RuntimeError so the CLI
5
5
  # reports it as a message rather than a backtrace.
6
6
  MissingDictionary = Class.new(RuntimeError)
7
+ UnrepresentableType = Class.new(RuntimeError)
7
8
  end
8
9
  end