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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +400 -12
- data/docs/interop.md +18 -1
- data/docs/stdlib.md +3 -1
- data/docs/syntax.md +96 -0
- data/lib/jade/api.rb +9 -0
- data/lib/jade/ast.rb +37 -1
- data/lib/jade/cli/eject.rb +132 -0
- data/lib/jade/cli.rb +2 -0
- data/lib/jade/codegen/boundary/specialized/list.rb +11 -7
- data/lib/jade/codegen/boundary/specialized/maybe.rb +2 -2
- data/lib/jade/codegen/boundary/specialized/record.rb +19 -4
- data/lib/jade/codegen/boundary/specialized/scalar.rb +2 -2
- data/lib/jade/codegen/boundary/specialized.rb +13 -5
- data/lib/jade/codegen/boundary.rb +12 -2
- data/lib/jade/codegen/context.rb +17 -0
- data/lib/jade/codegen/error.rb +1 -0
- data/lib/jade/codegen/function_call.rb +23 -7
- data/lib/jade/codegen/function_declaration.rb +25 -13
- data/lib/jade/codegen/helpers.rb +27 -4
- data/lib/jade/codegen/implementation.rb +2 -2
- data/lib/jade/codegen/inline.rb +1 -1
- data/lib/jade/codegen/inlines.rb +6 -1
- data/lib/jade/codegen/names.rb +31 -0
- data/lib/jade/codegen/pattern/constructor.rb +2 -2
- data/lib/jade/codegen/transforms/fold_shape.rb +1 -1
- data/lib/jade/codegen.rb +76 -15
- data/lib/jade/compiler.rb +2 -0
- data/lib/jade/entry.rb +1 -1
- data/lib/jade/formatter/calls.rb +68 -11
- data/lib/jade/formatter/collections.rb +4 -4
- data/lib/jade/formatter/declarations.rb +13 -15
- data/lib/jade/formatter/helper.rb +20 -0
- data/lib/jade/formatter/infix_application.rb +19 -4
- data/lib/jade/formatter/lambda.rb +24 -12
- data/lib/jade/formatter/pattern.rb +3 -0
- data/lib/jade/frontend/desugaring/placeholder.rb +40 -17
- data/lib/jade/frontend/desugaring.rb +29 -9
- data/lib/jade/frontend/fixity_fixer.rb +6 -4
- data/lib/jade/frontend/forward_declaration/error/alias_expansion.rb +23 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_constructor_name.rb +45 -0
- data/lib/jade/frontend/forward_declaration/error/duplicate_type_name.rb +42 -0
- data/lib/jade/frontend/forward_declaration/error.rb +3 -0
- data/lib/jade/frontend/forward_declaration/helper.rb +58 -0
- data/lib/jade/frontend/forward_declaration/import_declaration.rb +4 -0
- data/lib/jade/frontend/forward_declaration/interface_declaration.rb +6 -0
- data/lib/jade/frontend/forward_declaration/module.rb +4 -0
- data/lib/jade/frontend/forward_declaration/struct_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration/type_alias_declaration.rb +37 -0
- data/lib/jade/frontend/forward_declaration/type_declaration.rb +10 -1
- data/lib/jade/frontend/forward_declaration.rb +2 -0
- data/lib/jade/frontend/pattern_analysis/exhaustiveness.rb +47 -43
- data/lib/jade/frontend/pattern_analysis/matrix.rb +28 -219
- data/lib/jade/frontend/pattern_analysis/pattern.rb +132 -0
- data/lib/jade/frontend/pattern_analysis/redundancy.rb +24 -0
- data/lib/jade/frontend/pattern_analysis/signature.rb +220 -0
- data/lib/jade/frontend/pattern_analysis/usefulness.rb +72 -0
- data/lib/jade/frontend/pattern_analysis/witnesses.rb +73 -0
- data/lib/jade/frontend/pattern_analysis.rb +5 -38
- data/lib/jade/frontend/semantic_analysis/error/implementation_on_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error/no_base_case.rb +45 -0
- data/lib/jade/frontend/semantic_analysis/error/placeholder_not_allowed.rb +15 -3
- data/lib/jade/frontend/semantic_analysis/error/recursive_type_alias.rb +24 -0
- data/lib/jade/frontend/semantic_analysis/error.rb +3 -0
- data/lib/jade/frontend/semantic_analysis/helper.rb +31 -0
- data/lib/jade/frontend/semantic_analysis/implementation.rb +13 -0
- data/lib/jade/frontend/semantic_analysis/inhabitedness.rb +90 -0
- data/lib/jade/frontend/semantic_analysis/interop_import_declaration.rb +28 -16
- data/lib/jade/frontend/semantic_analysis/keyed_call.rb +21 -5
- data/lib/jade/frontend/semantic_analysis/pattern_range.rb +16 -0
- data/lib/jade/frontend/semantic_analysis/struct_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration/cycle_detection.rb +95 -0
- data/lib/jade/frontend/semantic_analysis/type_alias_declaration.rb +48 -0
- data/lib/jade/frontend/semantic_analysis/type_declaration.rb +1 -0
- data/lib/jade/frontend/semantic_analysis.rb +5 -0
- data/lib/jade/frontend/type_checking/canonicalize.rb +1 -0
- data/lib/jade/frontend/type_checking/cascade.rb +90 -0
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +10 -0
- data/lib/jade/frontend/type_checking/constraints.rb +18 -3
- data/lib/jade/frontend/type_checking/error/display.rb +49 -0
- data/lib/jade/frontend/type_checking/error/division_by_zero.rb +31 -0
- data/lib/jade/frontend/type_checking/error/empty_range_pattern.rb +25 -0
- data/lib/jade/frontend/type_checking/error/function_body_type_mismatch.rb +4 -3
- data/lib/jade/frontend/type_checking/error/function_call_type_mismatch.rb +108 -5
- data/lib/jade/frontend/type_checking/error/if_branch_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_branches_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/if_condition_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/implementation_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/list_item_type_mismatch.rb +1 -1
- data/lib/jade/frontend/type_checking/error/missing_patterns.rb +20 -4
- data/lib/jade/frontend/type_checking/error/pattern_type_mismatch.rb +7 -1
- data/lib/jade/frontend/type_checking/error/range_pattern_type.rb +29 -0
- data/lib/jade/frontend/type_checking/error/record_access_type_mismatch.rb +2 -2
- data/lib/jade/frontend/type_checking/error/recursive_derivation.rb +25 -0
- data/lib/jade/frontend/type_checking/error/type_mismatch.rb +10 -2
- data/lib/jade/frontend/type_checking/error/unreachable_branch.rb +22 -0
- data/lib/jade/frontend/type_checking/error.rb +6 -0
- data/lib/jade/frontend/type_checking/inference/assign.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/case_of.rb +21 -4
- data/lib/jade/frontend/type_checking/inference/division.rb +50 -0
- data/lib/jade/frontend/type_checking/inference/function_call.rb +26 -1
- data/lib/jade/frontend/type_checking/inference/lambda.rb +1 -1
- data/lib/jade/frontend/type_checking/inference/pattern.rb +35 -0
- data/lib/jade/frontend/type_checking/inference/type_alias_declaration.rb +18 -0
- data/lib/jade/frontend/type_checking/inference.rb +2 -0
- data/lib/jade/frontend/type_checking/loader.rb +11 -0
- data/lib/jade/frontend/type_checking/port_resolution.rb +26 -8
- data/lib/jade/frontend/type_checking/substitution.rb +2 -2
- data/lib/jade/frontend/type_checking.rb +3 -1
- data/lib/jade/frontend/usage_analysis.rb +5 -1
- data/lib/jade/interop/boundary.rb +64 -24
- data/lib/jade/interop/error.rb +35 -7
- data/lib/jade/interop/lowering.rb +8 -2
- data/lib/jade/lsp/converters.rb +83 -2
- data/lib/jade/lsp/handlers.rb +122 -25
- data/lib/jade/lsp/snippets.rb +12 -0
- data/lib/jade/lsp/state.rb +11 -4
- data/lib/jade/module_loader/build.rb +36 -0
- data/lib/jade/module_loader.rb +1 -0
- data/lib/jade/parsing/combinators.rb +78 -2
- data/lib/jade/parsing/error.rb +43 -6
- data/lib/jade/parsing/type.rb +5 -3
- data/lib/jade/parsing.rb +140 -26
- data/lib/jade/runtime.rb +32 -13
- data/lib/jade/source.rb +6 -2
- data/lib/jade/stdlib/basics.rb +22 -3
- data/lib/jade/stdlib/char.rb +2 -0
- data/lib/jade/stdlib/clock.rb +6 -2
- data/lib/jade/stdlib/decimal.rb +51 -14
- data/lib/jade/stdlib/intrinsics.rb +59 -6
- data/lib/jade/stdlib/list.rb +37 -1
- data/lib/jade/stdlib/number.rb +47 -0
- data/lib/jade/stdlib/range.rb +211 -0
- data/lib/jade/stdlib.rb +11 -4
- data/lib/jade/symbol/alias.rb +11 -0
- data/lib/jade/symbol/parser.rb +1 -1
- data/lib/jade/symbol.rb +13 -1
- data/lib/jade/task.rb +0 -2
- data/lib/jade/type/anonymous_record.rb +11 -2
- data/lib/jade/type/application.rb +11 -2
- data/lib/jade/type/base.rb +26 -0
- data/lib/jade/type/function.rb +11 -2
- data/lib/jade/type.rb +36 -10
- data/lib/jade/version.rb +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
"#{
|
|
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
|
data/lib/jade/codegen/context.rb
CHANGED
|
@@ -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
|
data/lib/jade/codegen/error.rb
CHANGED
|
@@ -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 { "#{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"#{
|
|
341
|
+
"#{internal(fn.module_name)}.#{fn.name}"
|
|
327
342
|
|
|
328
343
|
in Symbol::Function => fn
|
|
329
|
-
"#{
|
|
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(
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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) }
|
data/lib/jade/codegen/helpers.rb
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|