jade-lang 0.9.0 → 0.10.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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +400 -12
  3. data/docs/interop.md +18 -1
  4. data/docs/stdlib.md +3 -1
  5. data/docs/syntax.md +96 -0
  6. data/lib/jade/api.rb +9 -0
  7. data/lib/jade/ast.rb +37 -1
  8. data/lib/jade/cli/eject.rb +132 -0
  9. data/lib/jade/cli.rb +2 -0
  10. data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
  11. data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
  12. data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
  13. data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
  14. data/lib/jade/codegen/boundary/specialized.rb +13 -5
  15. data/lib/jade/codegen/boundary.rb +12 -2
  16. data/lib/jade/codegen/context.rb +17 -0
  17. data/lib/jade/codegen/error.rb +1 -0
  18. data/lib/jade/codegen/function_call.rb +23 -7
  19. data/lib/jade/codegen/function_declaration.rb +25 -13
  20. data/lib/jade/codegen/helpers.rb +27 -4
  21. data/lib/jade/codegen/implementation.rb +2 -2
  22. data/lib/jade/codegen/inline.rb +1 -1
  23. data/lib/jade/codegen/inlines.rb +6 -1
  24. data/lib/jade/codegen/names.rb +31 -0
  25. data/lib/jade/codegen/pattern/constructor.rb +2 -2
  26. data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
  27. data/lib/jade/codegen.rb +76 -15
  28. data/lib/jade/compiler.rb +2 -0
  29. data/lib/jade/entry.rb +1 -1
  30. data/lib/jade/formatter/calls.rb +68 -11
  31. data/lib/jade/formatter/collections.rb +4 -4
  32. data/lib/jade/formatter/declarations.rb +13 -15
  33. data/lib/jade/formatter/helper.rb +20 -0
  34. data/lib/jade/formatter/infix_application.rb +19 -4
  35. data/lib/jade/formatter/lambda.rb +24 -12
  36. data/lib/jade/formatter/pattern.rb +3 -0
  37. data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
  38. data/lib/jade/frontend/desugaring.rb +29 -9
  39. data/lib/jade/frontend/fixity_fixer.rb +6 -4
  40. data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
  41. data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
  42. data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
  43. data/lib/jade/frontend/forward_declaration/error.rb +3 -0
  44. data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
  45. data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
  46. data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
  47. data/lib/jade/frontend/forward_declaration/module.rb +4 -0
  48. data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
  49. data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
  50. data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
  51. data/lib/jade/frontend/forward_declaration.rb +2 -0
  52. data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
  53. data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
  54. data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
  55. data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
  56. data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
  57. data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
  58. data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
  59. data/lib/jade/frontend/pattern_analysis.rb +5 -38
  60. data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
  61. data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
  62. data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
  63. data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
  64. data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
  65. data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
  66. data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
  67. data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
  68. data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
  69. data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
  70. data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
  71. data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
  72. data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
  73. data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
  74. data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
  75. data/lib/jade/frontend/semantic_analysis.rb +5 -0
  76. data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
  77. data/lib/jade/frontend/type_checking/cascade.rb +90 -0
  78. data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
  79. data/lib/jade/frontend/type_checking/constraints.rb +18 -3
  80. data/lib/jade/frontend/type_checking/error/display.rb +49 -0
  81. data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
  82. data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
  83. data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
  84. data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
  85. data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
  86. data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
  87. data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
  88. data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
  89. data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
  90. data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
  91. data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
  92. data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
  93. data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
  94. data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
  95. data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
  96. data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
  97. data/lib/jade/frontend/type_checking/error.rb +6 -0
  98. data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
  99. data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
  100. data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
  101. data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
  102. data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
  103. data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
  104. data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
  105. data/lib/jade/frontend/type_checking/inference.rb +2 -0
  106. data/lib/jade/frontend/type_checking/loader.rb +11 -0
  107. data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
  108. data/lib/jade/frontend/type_checking/substitution.rb +2 -2
  109. data/lib/jade/frontend/type_checking.rb +3 -1
  110. data/lib/jade/frontend/usage_analysis.rb +5 -1
  111. data/lib/jade/interop/boundary.rb +64 -24
  112. data/lib/jade/interop/error.rb +35 -7
  113. data/lib/jade/interop/lowering.rb +8 -2
  114. data/lib/jade/lsp/converters.rb +83 -2
  115. data/lib/jade/lsp/handlers.rb +122 -25
  116. data/lib/jade/lsp/snippets.rb +12 -0
  117. data/lib/jade/lsp/state.rb +11 -4
  118. data/lib/jade/module_loader/build.rb +36 -0
  119. data/lib/jade/module_loader.rb +1 -0
  120. data/lib/jade/parsing/combinators.rb +78 -2
  121. data/lib/jade/parsing/error.rb +43 -6
  122. data/lib/jade/parsing/type.rb +5 -3
  123. data/lib/jade/parsing.rb +140 -26
  124. data/lib/jade/runtime.rb +32 -13
  125. data/lib/jade/source.rb +6 -2
  126. data/lib/jade/stdlib/basics.rb +22 -3
  127. data/lib/jade/stdlib/char.rb +2 -0
  128. data/lib/jade/stdlib/clock.rb +6 -2
  129. data/lib/jade/stdlib/decimal.rb +51 -14
  130. data/lib/jade/stdlib/intrinsics.rb +59 -6
  131. data/lib/jade/stdlib/list.rb +37 -1
  132. data/lib/jade/stdlib/number.rb +47 -0
  133. data/lib/jade/stdlib/range.rb +211 -0
  134. data/lib/jade/stdlib.rb +11 -4
  135. data/lib/jade/symbol/alias.rb +11 -0
  136. data/lib/jade/symbol/parser.rb +1 -1
  137. data/lib/jade/symbol.rb +13 -1
  138. data/lib/jade/task.rb +0 -2
  139. data/lib/jade/type/anonymous_record.rb +11 -2
  140. data/lib/jade/type/application.rb +11 -2
  141. data/lib/jade/type/base.rb +26 -0
  142. data/lib/jade/type/function.rb +11 -2
  143. data/lib/jade/type.rb +36 -10
  144. data/lib/jade/version.rb +1 -1
  145. metadata +33 -2
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
data/lib/jade/cli.rb CHANGED
@@ -4,6 +4,7 @@ module Jade
4
4
  module CLI
5
5
  SUBCOMMANDS = {
6
6
  'check' => 'Check',
7
+ 'eject' => 'Eject',
7
8
  'fmt' => 'Fmt',
8
9
  'lsp' => 'Lsp',
9
10
  'q' => 'Q',
@@ -37,6 +38,7 @@ module Jade
37
38
  Usage: jade COMMAND [ARGS]
38
39
 
39
40
  check Type-check the project (or the given files).
41
+ eject Write the project as Ruby that runs without the gem.
40
42
  fmt Format .jd source (stdin or file).
41
43
  lsp Run the language server (stdio JSON-RPC).
42
44
  q Headless query interface (hover/symbols/defn/refs/api).
@@ -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
@@ -102,10 +102,25 @@ module Jade
102
102
  Codegen.dict_env[[interface, id]]
103
103
 
104
104
  in Symbol::Implementation
105
- Pretty.hash(generate_impl_dispatch(entry, registry))
105
+ hoisted(Pretty.hash(generate_impl_dispatch(entry, registry)))
106
106
  end
107
107
  end
108
108
 
109
+ # A dictionary built only from concrete implementations is the same
110
+ # object on every call, so it is worth building once. One that names
111
+ # a dict parameter is not: that slot is the caller's, and differs per
112
+ # call. Outside a module there is nowhere to put a constant, so the
113
+ # literal stands.
114
+ #
115
+ # Keyed by source, which dedupes the same dictionary across every
116
+ # call site in the module.
117
+ def hoisted(source)
118
+ table = Codegen.dict_consts or return source
119
+ return source if Codegen.dict_env.values.any? { source.include?(it) }
120
+
121
+ table[source] ||= "DICT_#{table.size}"
122
+ end
123
+
109
124
  # Polymorphic fn referenced as a value (not called). Wraps the fn with
110
125
  # its dispatched dictionaries so the result is a monomorphic callable
111
126
  # matching the type at the use site. Returns nil when the symbol
@@ -127,13 +142,13 @@ module Jade
127
142
  .each_with_index
128
143
  .filter_map { |c, i| dispatch_value(dictionaries[i], registry) if c.type.is_a?(Type::Var) }
129
144
  .then { (param_names + it).join(', ') }
130
- .then { "#{to_qualified(fn.module_name)}::Internal.#{fn_target_name(fn, registry)}(#{it})" }
145
+ .then { "#{internal(fn.module_name)}.#{fn_target_name(fn, registry)}(#{it})" }
131
146
  .then { Pretty.lambda(param_names.join(', '), it) }
132
147
 
133
148
  in Symbol::InterfaceFunction => fn
134
- dispatch_value(dictionaries.first, registry)
135
- &.then { "#{it}[#{fn.name.inspect}]" } ||
149
+ dispatch_lookup(dictionaries.first, fn.name, registry) {
136
150
  fail("no dict in scope to reference interface method `#{fn.qualified_name}` as a value")
151
+ }
137
152
 
138
153
  else
139
154
  nil
@@ -199,7 +214,7 @@ module Jade
199
214
  generate_node(callee, registry)
200
215
 
201
216
  in Symbol::Function => fn_sym
202
- to_qualified(fn_sym.module_name) + "::Internal." + fn_target_name(fn_sym, registry)
217
+ internal(fn_sym.module_name) + "." + fn_target_name(fn_sym, registry)
203
218
 
204
219
  in Symbol::StdlibImplementation => symbol
205
220
  dictionaries
@@ -323,10 +338,10 @@ module Jade
323
338
  # 0-arg fn: Ruby auto-invokes on bare reference. Result is the
324
339
  # dispatch-slot value (decoder, encoder template, ...) ready to use.
325
340
  in Symbol::Function => fn if fn.constant?
326
- "#{to_qualified(fn.module_name)}::Internal.#{fn.name}"
341
+ "#{internal(fn.module_name)}.#{fn.name}"
327
342
 
328
343
  in Symbol::Function => fn
329
- "#{to_qualified(fn.module_name)}::Internal.method(:#{fn.name})"
344
+ "#{internal(fn.module_name)}.method(:#{fn.name})"
330
345
  end
331
346
  end
332
347
 
@@ -334,6 +349,7 @@ module Jade
334
349
  dep_dispatches
335
350
  .map { it.is_a?(String) ? it : Pretty.hash(it) }
336
351
  .then { Pretty.array(it) }
352
+ .then { hoisted(it) }
337
353
  end
338
354
 
339
355
  # Stdlib intrinsics implementation language.
@@ -20,12 +20,12 @@ module Jade
20
20
  end
21
21
 
22
22
  args, return_type = Type.signature(fn_type)
23
- param_names = params.map(&:name)
23
+ param_names = params.map { Names.local(it.name) }
24
24
 
25
25
  if task_return?(return_type)
26
- task_wrapper_pair(name, args, param_names, return_type, registry)
26
+ task_wrapper_pair(name, args, param_names, return_type, registry, symbol)
27
27
  else
28
- eligible_wrapper(name, args, param_names, return_type, registry)
28
+ eligible_wrapper(name, args, param_names, return_type, registry, symbol)
29
29
  end
30
30
  end
31
31
 
@@ -58,8 +58,8 @@ module Jade
58
58
  end
59
59
  end
60
60
 
61
- def eligible_wrapper(name, args, param_names, return_type, registry)
62
- decoded_args(args, param_names, registry)
61
+ def eligible_wrapper(name, args, param_names, return_type, registry, symbol)
62
+ decoded_args(args, param_names, registry, name, symbol)
63
63
  .then { Pretty.call("Internal.#{name}", it) }
64
64
  .then { encode_return(return_type, it, registry) }
65
65
  .then { Pretty.block(boundary_def_header(name, param_names), it) }
@@ -69,14 +69,26 @@ module Jade
69
69
  "def self.#{name}(#{param_names.join(', ')})"
70
70
  end
71
71
 
72
- def decoded_args(args, param_names, registry)
73
- args.zip(param_names).map { |t, pname| decode_call(t, pname, registry) }
72
+ def decoded_args(args, param_names, registry, name, symbol)
73
+ args
74
+ .zip(param_names)
75
+ .map do |t, pname|
76
+ decode_call(t, pname, registry, "#{to_qualified(symbol.module_name)}.#{name}(#{pname})")
77
+ end
78
+ end
79
+
80
+ def named(expr, where)
81
+ "Jade::Interop::Boundary.arg(#{where.inspect}) { #{expr} }"
74
82
  end
75
83
 
76
- def decode_call(arg_type, pname, registry)
77
- Codegen::Boundary::Specialized.decode_expr(arg_type, pname, registry) ||
84
+ # A specialized decoder carries the argument name itself, in a
85
+ # constant it was going to pass anyway. Only the generic path needs a
86
+ # frame around it to say where the value came from.
87
+ def decode_call(arg_type, pname, registry, where)
88
+ Codegen::Boundary::Specialized.decode_expr(arg_type, pname, registry, where) ||
78
89
  Codegen::Boundary::Cache.decoder_for(arg_type, registry)
79
90
  .then { "Jade::Interop::Boundary.decode_or_raise(#{it}, #{pname})" }
91
+ .then { named(it, where) }
80
92
  end
81
93
 
82
94
  def encode_return(return_type, call_expr, registry)
@@ -92,17 +104,17 @@ module Jade
92
104
  end
93
105
  end
94
106
 
95
- def task_wrapper_pair(name, args, param_names, task_return, registry)
107
+ def task_wrapper_pair(name, args, param_names, task_return, registry, symbol)
96
108
  ok_enc, err_enc = Codegen::Boundary::Cache.task_arms(task_return, registry)
97
109
 
98
110
  [
99
- task_run_def(name, args, param_names, ok_enc, err_enc, registry),
111
+ task_run_def(name, args, param_names, ok_enc, err_enc, registry, symbol),
100
112
  task_bang_def(name, param_names),
101
113
  ].join(Pretty.newline(2))
102
114
  end
103
115
 
104
- def task_run_def(name, args, param_names, ok_enc, err_enc, registry)
105
- decoded_args(args, param_names, registry)
116
+ def task_run_def(name, args, param_names, ok_enc, err_enc, registry, symbol)
117
+ decoded_args(args, param_names, registry, name, symbol)
106
118
  .then { Pretty.call("Internal.#{name}", it) }
107
119
  .then { task_run_body(it, ok_enc, err_enc) }
108
120
  .then { Pretty.block(boundary_def_header(name, param_names), it) }
@@ -16,8 +16,19 @@ module Jade
16
16
  Stdlib.stdlib_name?(module_name) ? "Jade::#{base}" : base
17
17
  end
18
18
 
19
+ # Inside `module Plan`, a bare `Plan::` resolves to whatever `Plan`
20
+ # names there, which may be a type. `module X` is the only name that
21
+ # cannot be rooted.
22
+ def rooted(module_name)
23
+ "::#{to_qualified(module_name)}"
24
+ end
25
+
26
+ def internal(module_name)
27
+ "#{rooted(module_name)}::Internal"
28
+ end
29
+
19
30
  def data_define(fields)
20
- return "Data.define" if fields.empty?
31
+ return "Jade.nullary" if fields.empty?
21
32
 
22
33
  "Data.define(#{fields.map { ":#{it}" }.join(', ')})"
23
34
  end
@@ -44,8 +55,14 @@ module Jade
44
55
  end
45
56
 
46
57
  def impl_synthetic_name(interface, type_name, fn_name)
47
- sanitized = fn_name.gsub(/[^a-zA-Z0-9_]/) { |c| "x#{c.ord.to_s(16)}" }
48
- "__impl_#{interface}_#{type_name}_#{sanitized}__"
58
+ sanitized_name(fn_name)
59
+ .then { "__impl_#{interface}_#{type_name}_#{it}__" }
60
+ end
61
+
62
+ # `contains?` is a fine Jade name and not a fine Ruby one anywhere but
63
+ # the end of a method name, which is not where these put it.
64
+ def sanitized_name(name)
65
+ name.gsub(/[^a-zA-Z0-9_]/) { |c| "x#{c.ord.to_s(16)}" }
49
66
  end
50
67
 
51
68
  def dict_synthetic_name(index)
@@ -53,7 +70,8 @@ module Jade
53
70
  end
54
71
 
55
72
  def fn_impl_synthetic_name(name)
56
- "__#{name}__impl__"
73
+ sanitized_name(name)
74
+ .then { "__#{it}__impl__" }
57
75
  end
58
76
 
59
77
  def fn_constraints(fn_symbol, registry)
@@ -101,6 +119,11 @@ module Jade
101
119
 
102
120
  in Symbol::Interface
103
121
  []
122
+
123
+ in other
124
+ raise(UnrepresentableType, "`#{qname}` has no runtime form " \
125
+ "(#{other.class}); this is a jade bug, " \
126
+ 'please report it')
104
127
  end
105
128
  end
106
129