semantifold 0.3.0 → 0.4.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.
- package/README.md +58 -15
- package/build/index.d.ts +48 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +57 -8
- package/build/src/artifact-path.d.ts +23 -1
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +45 -3
- package/build/src/artifacts.d.ts.map +1 -1
- package/build/src/artifacts.js +66 -1
- package/build/src/backends/c-validation.d.ts.map +1 -1
- package/build/src/backends/c-validation.js +7 -3
- package/build/src/backends/cpp-validation.d.ts.map +1 -1
- package/build/src/backends/cpp-validation.js +7 -3
- package/build/src/backends/csharp.js +7 -1
- package/build/src/backends/dart-runtime.d.ts +2 -0
- package/build/src/backends/dart-runtime.d.ts.map +1 -0
- package/build/src/backends/dart-runtime.js +41 -0
- package/build/src/backends/dart.d.ts +20 -0
- package/build/src/backends/dart.d.ts.map +1 -0
- package/build/src/backends/dart.js +321 -0
- package/build/src/backends/effects.d.ts +60 -0
- package/build/src/backends/effects.d.ts.map +1 -0
- package/build/src/backends/effects.js +746 -0
- package/build/src/backends/go.js +18 -4
- package/build/src/backends/identifiers.d.ts +36 -4
- package/build/src/backends/identifiers.d.ts.map +1 -1
- package/build/src/backends/identifiers.js +154 -10
- package/build/src/backends/index.d.ts.map +1 -1
- package/build/src/backends/index.js +27 -4
- package/build/src/backends/ios.d.ts +68 -0
- package/build/src/backends/ios.d.ts.map +1 -0
- package/build/src/backends/ios.js +1503 -0
- package/build/src/backends/java.d.ts.map +1 -1
- package/build/src/backends/java.js +429 -36
- package/build/src/backends/javascript.d.ts +6 -0
- package/build/src/backends/javascript.d.ts.map +1 -1
- package/build/src/backends/javascript.js +380 -24
- package/build/src/backends/kotlin-runtime.d.ts +3 -0
- package/build/src/backends/kotlin-runtime.d.ts.map +1 -0
- package/build/src/backends/kotlin-runtime.js +29 -0
- package/build/src/backends/kotlin.d.ts +23 -0
- package/build/src/backends/kotlin.d.ts.map +1 -0
- package/build/src/backends/kotlin.js +136 -0
- package/build/src/backends/native-validation.d.ts +2 -2
- package/build/src/backends/native-validation.d.ts.map +1 -1
- package/build/src/backends/native-validation.js +2 -2
- package/build/src/backends/ordered-expressions.d.ts.map +1 -1
- package/build/src/backends/ordered-expressions.js +39 -7
- package/build/src/backends/php-stdlib-support.d.ts +19 -0
- package/build/src/backends/php-stdlib-support.d.ts.map +1 -0
- package/build/src/backends/php-stdlib-support.js +227 -0
- package/build/src/backends/php.d.ts.map +1 -1
- package/build/src/backends/php.js +448 -33
- package/build/src/backends/program.d.ts +81 -0
- package/build/src/backends/program.d.ts.map +1 -0
- package/build/src/backends/program.js +1308 -0
- package/build/src/backends/python.js +7 -1
- package/build/src/backends/ruby.d.ts.map +1 -1
- package/build/src/backends/ruby.js +310 -24
- package/build/src/backends/rust-runtime.d.ts +7 -0
- package/build/src/backends/rust-runtime.d.ts.map +1 -0
- package/build/src/backends/rust-runtime.js +32 -0
- package/build/src/backends/rust-validation.d.ts +9 -0
- package/build/src/backends/rust-validation.d.ts.map +1 -0
- package/build/src/backends/rust-validation.js +78 -0
- package/build/src/backends/rust.d.ts +24 -0
- package/build/src/backends/rust.d.ts.map +1 -0
- package/build/src/backends/rust.js +344 -0
- package/build/src/backends/scalars.d.ts +14 -6
- package/build/src/backends/scalars.d.ts.map +1 -1
- package/build/src/backends/scalars.js +167 -17
- package/build/src/backends/shared.d.ts +23 -4
- package/build/src/backends/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +2158 -62
- package/build/src/backends/stdlib-support.d.ts +14 -0
- package/build/src/backends/stdlib-support.d.ts.map +1 -0
- package/build/src/backends/stdlib-support.js +237 -0
- package/build/src/backends/swift-runtime.d.ts +3 -0
- package/build/src/backends/swift-runtime.d.ts.map +1 -0
- package/build/src/backends/swift-runtime.js +14 -0
- package/build/src/backends/swift.d.ts +8 -0
- package/build/src/backends/swift.d.ts.map +1 -0
- package/build/src/backends/swift.js +127 -0
- package/build/src/backends/typescript.d.ts.map +1 -1
- package/build/src/backends/typescript.js +344 -24
- package/build/src/backends/wasm.d.ts +43 -0
- package/build/src/backends/wasm.d.ts.map +1 -0
- package/build/src/backends/wasm.js +2164 -0
- package/build/src/backends/writer.d.ts +235 -4
- package/build/src/backends/writer.d.ts.map +1 -1
- package/build/src/backends/writer.js +468 -3
- package/build/src/diagnostic.d.ts +4 -3
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +5 -3
- package/build/src/frontends/c.d.ts.map +1 -1
- package/build/src/frontends/c.js +5 -3
- package/build/src/frontends/cpp.d.ts.map +1 -1
- package/build/src/frontends/cpp.js +5 -3
- package/build/src/frontends/csharp.d.ts.map +1 -1
- package/build/src/frontends/csharp.js +7 -3
- package/build/src/frontends/dart.d.ts +10 -0
- package/build/src/frontends/dart.d.ts.map +1 -0
- package/build/src/frontends/dart.js +913 -0
- package/build/src/frontends/index.d.ts +3 -1
- package/build/src/frontends/index.d.ts.map +1 -1
- package/build/src/frontends/index.js +15 -5
- package/build/src/frontends/java.d.ts +110 -1
- package/build/src/frontends/java.d.ts.map +1 -1
- package/build/src/frontends/java.js +1725 -70
- package/build/src/frontends/javascript-typescript.d.ts +69 -1
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
- package/build/src/frontends/javascript-typescript.js +1825 -93
- package/build/src/frontends/kotlin.d.ts +10 -0
- package/build/src/frontends/kotlin.d.ts.map +1 -0
- package/build/src/frontends/kotlin.js +685 -0
- package/build/src/frontends/php.d.ts +118 -1
- package/build/src/frontends/php.d.ts.map +1 -1
- package/build/src/frontends/php.js +1583 -76
- package/build/src/frontends/program.d.ts +74 -0
- package/build/src/frontends/program.d.ts.map +1 -0
- package/build/src/frontends/program.js +989 -0
- package/build/src/frontends/ruby.d.ts +68 -1
- package/build/src/frontends/ruby.d.ts.map +1 -1
- package/build/src/frontends/ruby.js +1446 -101
- package/build/src/frontends/rust-parser.d.ts +11 -0
- package/build/src/frontends/rust-parser.d.ts.map +1 -0
- package/build/src/frontends/rust-parser.js +15 -0
- package/build/src/frontends/rust.d.ts +14 -0
- package/build/src/frontends/rust.d.ts.map +1 -0
- package/build/src/frontends/rust.js +715 -0
- package/build/src/frontends/scalars.d.ts +10 -0
- package/build/src/frontends/scalars.d.ts.map +1 -1
- package/build/src/frontends/scalars.js +28 -2
- package/build/src/frontends/swift.d.ts +10 -0
- package/build/src/frontends/swift.d.ts.map +1 -0
- package/build/src/frontends/swift.js +860 -0
- package/build/src/frontends/types.d.ts +186 -0
- package/build/src/frontends/types.d.ts.map +1 -0
- package/build/src/frontends/types.js +634 -0
- package/build/src/language-registry.d.ts +46 -4
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +167 -13
- package/build/src/mapping.d.ts +2 -2
- package/build/src/mapping.d.ts.map +1 -1
- package/build/src/mapping.js +31 -10
- package/build/src/semantic/capabilities.d.ts +66 -0
- package/build/src/semantic/capabilities.d.ts.map +1 -0
- package/build/src/semantic/capabilities.js +649 -0
- package/build/src/semantic/effects.d.ts +26 -0
- package/build/src/semantic/effects.d.ts.map +1 -0
- package/build/src/semantic/effects.js +764 -0
- package/build/src/semantic/generics.d.ts +23 -0
- package/build/src/semantic/generics.d.ts.map +1 -0
- package/build/src/semantic/generics.js +61 -0
- package/build/src/semantic/lifetime.d.ts +37 -0
- package/build/src/semantic/lifetime.d.ts.map +1 -0
- package/build/src/semantic/lifetime.js +527 -0
- package/build/src/semantic/provenance.d.ts +9 -2
- package/build/src/semantic/provenance.d.ts.map +1 -1
- package/build/src/semantic/provenance.js +462 -20
- package/build/src/semantic/scalars.d.ts +11 -0
- package/build/src/semantic/scalars.d.ts.map +1 -1
- package/build/src/semantic/scalars.js +16 -1
- package/build/src/semantic/stdlib.d.ts +254 -0
- package/build/src/semantic/stdlib.d.ts.map +1 -0
- package/build/src/semantic/stdlib.js +880 -0
- package/build/src/semantic/types.d.ts +2952 -202
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +688 -15
- package/build/src/semantic/validate.d.ts +85 -5
- package/build/src/semantic/validate.d.ts.map +1 -1
- package/build/src/semantic/validate.js +2576 -98
- package/build/src/stdlib-facades.d.ts +304 -0
- package/build/src/stdlib-facades.d.ts.map +1 -0
- package/build/src/stdlib-facades.js +1167 -0
- package/build/src/stdlib-negotiation.d.ts +69 -0
- package/build/src/stdlib-negotiation.d.ts.map +1 -0
- package/build/src/stdlib-negotiation.js +403 -0
- package/build/src/stdlib-providers.d.ts +72 -0
- package/build/src/stdlib-providers.d.ts.map +1 -0
- package/build/src/stdlib-providers.js +339 -0
- package/build/src/toolchains.d.ts +55 -1
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +15 -4
- package/docs/architecture.md +45 -13
- package/docs/dart.md +60 -0
- package/docs/goals.md +3 -3
- package/docs/ios.md +121 -0
- package/docs/kotlin.md +42 -0
- package/docs/language-support.md +231 -26
- package/docs/multi-file-modules.md +52 -0
- package/docs/parser-qualification.md +121 -8
- package/docs/plans/2026-09-05-standard-library-portability.md +10 -10
- package/docs/rust.md +88 -0
- package/docs/source-maps.md +15 -7
- package/docs/standard-library-portability.md +39 -8
- package/docs/swift.md +44 -0
- package/docs/testing.md +366 -4
- package/docs/webassembly.md +52 -0
- package/index.js +63 -8
- package/node_modules/semantifold-tree-sitter-legacy-internal/README.md +2 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/LICENSE +21 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/README.md +45 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/binding.gyp +30 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding.cc +20 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding_test.js +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.d.ts +28 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.js +11 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/grammar.js +1630 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/package.json +64 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-arm64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-x64/tree-sitter-rust.node +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/highlights.scm +159 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/injections.scm +9 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/tags.scm +60 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/grammar.json +9405 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/node-types.json +5399 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/parser.c +192569 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/scanner.c +393 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/alloc.h +54 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/array.h +290 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/parser.h +266 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter-rust.wasm +0 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter.json +53 -0
- package/node_modules/semantifold-tree-sitter-legacy-internal/package.json +3 -2
- package/node_modules/semantifold-tree-sitter-legacy-internal/src/c.js +5 -4
- package/package.json +5 -2
- package/src/artifact-path.js +47 -2
- package/src/artifacts.js +67 -0
- package/src/backends/c-validation.js +6 -1
- package/src/backends/cpp-validation.js +6 -1
- package/src/backends/csharp.js +3 -0
- package/src/backends/dart-runtime.js +41 -0
- package/src/backends/dart.js +326 -0
- package/src/backends/effects.js +705 -0
- package/src/backends/go.js +14 -2
- package/src/backends/identifiers.js +160 -9
- package/src/backends/index.js +27 -2
- package/src/backends/ios.js +1612 -0
- package/src/backends/java.js +454 -36
- package/src/backends/javascript.js +389 -23
- package/src/backends/kotlin-runtime.js +29 -0
- package/src/backends/kotlin.js +141 -0
- package/src/backends/native-validation.js +1 -1
- package/src/backends/ordered-expressions.js +40 -6
- package/src/backends/php-stdlib-support.js +239 -0
- package/src/backends/php.js +469 -32
- package/src/backends/program.js +1407 -0
- package/src/backends/python.js +3 -0
- package/src/backends/ruby.js +326 -23
- package/src/backends/rust-runtime.js +34 -0
- package/src/backends/rust-validation.js +75 -0
- package/src/backends/rust.js +335 -0
- package/src/backends/scalars.js +141 -16
- package/src/backends/shared.js +2304 -124
- package/src/backends/stdlib-support.js +234 -0
- package/src/backends/swift-runtime.js +14 -0
- package/src/backends/swift.js +132 -0
- package/src/backends/typescript.js +349 -23
- package/src/backends/wasm.js +2208 -0
- package/src/backends/writer.js +520 -2
- package/src/diagnostic.js +4 -2
- package/src/frontends/c.js +4 -2
- package/src/frontends/cpp.js +4 -2
- package/src/frontends/csharp.js +4 -2
- package/src/frontends/dart.js +963 -0
- package/src/frontends/index.js +14 -4
- package/src/frontends/java.js +1977 -69
- package/src/frontends/javascript-typescript.js +2046 -102
- package/src/frontends/kotlin.js +713 -0
- package/src/frontends/php.js +1725 -79
- package/src/frontends/program.js +1062 -0
- package/src/frontends/ruby.js +1624 -98
- package/src/frontends/rust-parser.js +18 -0
- package/src/frontends/rust.js +714 -0
- package/src/frontends/scalars.js +31 -1
- package/src/frontends/swift.js +893 -0
- package/src/frontends/types.js +750 -0
- package/src/language-registry.js +172 -12
- package/src/mapping.js +33 -9
- package/src/semantic/capabilities.js +671 -0
- package/src/semantic/effects.js +737 -0
- package/src/semantic/generics.js +58 -0
- package/src/semantic/lifetime.js +517 -0
- package/src/semantic/provenance.js +412 -19
- package/src/semantic/scalars.js +17 -0
- package/src/semantic/stdlib.js +921 -0
- package/src/semantic/types.js +765 -14
- package/src/semantic/validate.js +2934 -209
- package/src/stdlib-facades.js +1211 -0
- package/src/stdlib-negotiation.js +446 -0
- package/src/stdlib-providers.js +377 -0
- package/src/toolchains.js +16 -3
package/src/frontends/ruby.js
CHANGED
|
@@ -2,32 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AndNode,
|
|
5
|
+
ArrayNode,
|
|
6
|
+
AssocNode,
|
|
7
|
+
BlockNode,
|
|
8
|
+
BlockParametersNode,
|
|
9
|
+
BeginNode,
|
|
10
|
+
BreakNode,
|
|
5
11
|
CallNode,
|
|
12
|
+
ClassNode,
|
|
13
|
+
ConstantPathNode,
|
|
14
|
+
ConstantReadNode,
|
|
6
15
|
DefNode,
|
|
7
16
|
ElseNode,
|
|
8
17
|
FalseNode,
|
|
18
|
+
HashNode,
|
|
9
19
|
IfNode,
|
|
10
20
|
IntegerNode,
|
|
21
|
+
InstanceVariableWriteNode,
|
|
22
|
+
InstanceVariableReadNode,
|
|
11
23
|
InterpolatedStringNode,
|
|
12
24
|
LocalVariableReadNode,
|
|
25
|
+
LocalVariableTargetNode,
|
|
13
26
|
LocalVariableWriteNode,
|
|
27
|
+
ModuleNode,
|
|
28
|
+
NextNode,
|
|
29
|
+
NilNode,
|
|
14
30
|
OrNode,
|
|
15
31
|
ParenthesesNode,
|
|
16
32
|
ProgramNode,
|
|
17
33
|
RequiredParameterNode,
|
|
34
|
+
RedoNode,
|
|
35
|
+
RescueNode,
|
|
18
36
|
ReturnNode,
|
|
37
|
+
RetryNode,
|
|
38
|
+
SelfNode,
|
|
19
39
|
StatementsNode,
|
|
20
40
|
StringNode,
|
|
41
|
+
SymbolNode,
|
|
21
42
|
TrueNode,
|
|
43
|
+
UnlessNode,
|
|
44
|
+
WhileNode,
|
|
22
45
|
loadPrism
|
|
23
46
|
} from "@ruby/prism"
|
|
24
47
|
import {missingType, SemantifoldDiagnostic, unsupportedSyntax} from "../diagnostic.js"
|
|
48
|
+
import {blockReferencesProtectedEntry} from "../semantic/effects.js"
|
|
25
49
|
import {locationFromOffsets, moduleLocation, utf8ByteOffsetToUtf16Offset} from "../semantic/location.js"
|
|
26
50
|
import {withAdaptedOperation} from "../semantic/operators.js"
|
|
27
51
|
import {withParserRanges} from "../semantic/provenance.js"
|
|
28
52
|
import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
|
|
29
|
-
import {
|
|
53
|
+
import {isTask034ResourceProbe} from "../semantic/capabilities.js"
|
|
54
|
+
import {hasExactEffectSupport} from "../backends/effects.js"
|
|
55
|
+
import {requireSourceReturnType} from "./scalars.js"
|
|
56
|
+
import {capabilityFunctionSignatures, documentedValueType, instantiatedRecordFieldType, iterationBindingType, iterationOperandType, knownCallReturnType, orderedMapTypeFromMap, preservesGenericOptionalEvidence, recordType, referenceType, sameValueType} from "./types.js"
|
|
30
57
|
const parsePrism = await loadPrism()
|
|
58
|
+
const referenceMethodHooks = new Set([
|
|
59
|
+
"__send__", "extend", "fetch", "instance_eval", "instance_exec", "method", "method_missing", "public_send",
|
|
60
|
+
"respond_to_missing?", "send", "singleton_class", "size"
|
|
61
|
+
])
|
|
62
|
+
/** @typedef {{name: string, nameLocation: import("../semantic/types.js").SourceLocation, parameters: import("../semantic/types.js").Parameter[], returnType: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: import("../semantic/types.js").TypeParameter[], location: import("../semantic/types.js").SourceLocation}} RubyFunctionSignature */
|
|
63
|
+
/** @typedef {{bindings: Map<string, import("../semantic/types.js").SemanticBindingType>, classes: Map<string, import("../semantic/types.js").ClassDeclaration>, currentClass?: import("../semantic/types.js").ClassDeclaration, errorNames: Map<string, import("../semantic/types.js").ErrorDeclaration>, errors: Map<string, import("../semantic/types.js").ErrorDeclaration>, functions: Map<string, RubyFunctionSignature>, orderedMapDeclarations?: Set<object>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, recordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>, loopDepth?: number, returnType?: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: Map<string, import("../semantic/types.js").TypeParameter>}} RubyConversionContext */
|
|
31
64
|
const rubyBinaryOperations = new Map([
|
|
32
65
|
["+", "Add"],
|
|
33
66
|
["-", "Subtract"],
|
|
@@ -86,17 +119,71 @@ function slicePrismSource(source, startOffset, endOffset) {
|
|
|
86
119
|
* @param {import("@ruby/prism").Node} node - Prism node.
|
|
87
120
|
* @param {string} filename - Source filename.
|
|
88
121
|
* @param {string} source - Complete source.
|
|
122
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
123
|
+
* @param {import("../semantic/types.js").SemanticValueType} [expectedType] - Explicit contextual value type.
|
|
124
|
+
* @param {boolean} [preserveOptional] - Whether an optional identifier remains wrapped.
|
|
89
125
|
* @returns {import("../semantic/types.js").Expression} Semantic expression.
|
|
90
126
|
*/
|
|
91
|
-
function convertExpression(node, filename, source) {
|
|
127
|
+
function convertExpression(node, filename, source, context, expectedType, preserveOptional = false) {
|
|
92
128
|
const location = nodeLocation(node, filename, source)
|
|
93
129
|
|
|
130
|
+
if (expectedType?.kind == "OptionalType") {
|
|
131
|
+
if (node instanceof NilNode) {
|
|
132
|
+
return withParserRanges({kind: /** @type {const} */ ("OptionalNone"), location}, {absence: location})
|
|
133
|
+
}
|
|
134
|
+
if (knownExpressionType(node, context)?.kind == "OptionalType") {
|
|
135
|
+
return convertExpression(node, filename, source, context, undefined, true)
|
|
136
|
+
}
|
|
137
|
+
return withParserRanges({
|
|
138
|
+
kind: /** @type {const} */ ("OptionalSome"),
|
|
139
|
+
location,
|
|
140
|
+
value: convertExpression(node, filename, source, context, expectedType.valueType)
|
|
141
|
+
}, {some: location})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (node instanceof NilNode) return unsupportedSyntax("ruby", "nil outside an explicit optional context", location)
|
|
145
|
+
|
|
146
|
+
if (node instanceof CallNode && node.block) {
|
|
147
|
+
return unsupportedSyntax("ruby", "call block", nodeLocation(node.block, filename, source))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (node instanceof CallNode && node.callOperatorLoc && slicePrismSource(
|
|
151
|
+
source,
|
|
152
|
+
node.callOperatorLoc.startOffset,
|
|
153
|
+
node.callOperatorLoc.startOffset + node.callOperatorLoc.length
|
|
154
|
+
) == "&.") {
|
|
155
|
+
return unsupportedSyntax("ruby", node.constructor.name, location)
|
|
156
|
+
}
|
|
157
|
+
|
|
94
158
|
if (node instanceof ParenthesesNode && node.body instanceof StatementsNode && node.body.body.length == 1) {
|
|
95
|
-
return convertExpression(node.body.body[0], filename, source)
|
|
159
|
+
return convertExpression(node.body.body[0], filename, source, context, expectedType, preserveOptional)
|
|
96
160
|
}
|
|
97
161
|
|
|
98
162
|
if (node instanceof LocalVariableReadNode) {
|
|
99
|
-
|
|
163
|
+
const identifier = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: node.name}, {name: location})
|
|
164
|
+
|
|
165
|
+
if (!preserveOptional && context.bindings.get(node.name)?.kind == "OptionalType") {
|
|
166
|
+
return withParserRanges({kind: /** @type {const} */ ("OptionalUnwrap"), location, operand: identifier}, {unwrap: location})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return identifier
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (node instanceof SelfNode) {
|
|
173
|
+
if (!context.currentClass) return unsupportedSyntax("ruby", "self outside a reference class", location)
|
|
174
|
+
return withParserRanges({classId: /** @type {string} */ (context.currentClass.id), kind: /** @type {const} */ ("ReceiverExpression"),
|
|
175
|
+
location}, {receiver: location})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (node instanceof InstanceVariableReadNode) {
|
|
179
|
+
const field = context.currentClass?.fields.find((candidate) => `@${candidate.name}` == node.name)
|
|
180
|
+
|
|
181
|
+
if (!field || !context.currentClass) return unsupportedSyntax("ruby", "private instance field outside its declaring class", location)
|
|
182
|
+
const receiver = withParserRanges({classId: /** @type {string} */ (context.currentClass.id),
|
|
183
|
+
kind: /** @type {const} */ ("ReceiverExpression"), location}, {receiver: location})
|
|
184
|
+
|
|
185
|
+
return withParserRanges({field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldRead"),
|
|
186
|
+
location, receiver}, {member: location})
|
|
100
187
|
}
|
|
101
188
|
|
|
102
189
|
if (node instanceof IntegerNode) {
|
|
@@ -122,6 +209,113 @@ function convertExpression(node, filename, source) {
|
|
|
122
209
|
|
|
123
210
|
if (node instanceof InterpolatedStringNode) return unsupportedSyntax("ruby", "interpolated string", location)
|
|
124
211
|
|
|
212
|
+
if (node instanceof OrNode && node.left instanceof LocalVariableReadNode &&
|
|
213
|
+
context.bindings.get(node.left.name)?.kind == "OptionalType") {
|
|
214
|
+
return unsupportedSyntax("ruby", "optional default with ||", prismLocation(node.operatorLoc, filename, source))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (exactNilTest(node, source)) {
|
|
218
|
+
const call = /** @type {CallNode} */ (node)
|
|
219
|
+
const receiver = /** @type {LocalVariableReadNode} */ (call.receiver)
|
|
220
|
+
const operand = /** @type {import("../semantic/types.js").IdentifierExpression} */ (
|
|
221
|
+
convertExpression(receiver, filename, source, context, undefined, true)
|
|
222
|
+
)
|
|
223
|
+
const present = withParserRanges({kind: /** @type {const} */ ("OptionalIsPresent"), location, operand}, {
|
|
224
|
+
operator: prismLocation(call.messageLoc ?? call.location, filename, source)
|
|
225
|
+
})
|
|
226
|
+
const semantic = withAdaptedOperation(withParserRanges({
|
|
227
|
+
kind: "UnaryExpression",
|
|
228
|
+
location,
|
|
229
|
+
operand: present
|
|
230
|
+
}, {operator: prismLocation(call.messageLoc ?? call.location, filename, source)}), "Not")
|
|
231
|
+
|
|
232
|
+
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (node instanceof ArrayNode) {
|
|
236
|
+
if (!node.openingLoc || !node.closingLoc) return unsupportedSyntax("ruby", "special array literal", location)
|
|
237
|
+
if (node.isContainsSplat()) {
|
|
238
|
+
const splat = node.elements.find((element) => element.constructor.name == "SplatNode")
|
|
239
|
+
|
|
240
|
+
return unsupportedSyntax("ruby", "array splat", splat ? nodeLocation(splat, filename, source) : location)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const elementType = expectedType?.kind == "ListType" ? expectedType.elementType : undefined
|
|
244
|
+
|
|
245
|
+
return withParserRanges({
|
|
246
|
+
elements: node.elements.map((element) => convertExpression(element, filename, source, context, elementType)),
|
|
247
|
+
kind: /** @type {const} */ ("ListLiteral"),
|
|
248
|
+
location
|
|
249
|
+
}, {
|
|
250
|
+
close: prismLocation(node.closingLoc, filename, source),
|
|
251
|
+
open: prismLocation(node.openingLoc, filename, source)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (node instanceof HashNode) {
|
|
256
|
+
const valueType = expectedType?.kind == "MapType" || expectedType?.kind == "OrderedMapType"
|
|
257
|
+
? expectedType.valueType : undefined
|
|
258
|
+
const entries = node.elements.map((element) => {
|
|
259
|
+
if (!(element instanceof AssocNode) || !element.operatorLoc ||
|
|
260
|
+
slicePrismSource(source, element.operatorLoc.startOffset, element.operatorLoc.startOffset + element.operatorLoc.length) != "=>") {
|
|
261
|
+
return unsupportedSyntax("ruby", "hash entry", nodeLocation(element, filename, source))
|
|
262
|
+
}
|
|
263
|
+
if (!(element.key instanceof StringNode)) {
|
|
264
|
+
return unsupportedSyntax("ruby", "map key other than a string literal", nodeLocation(element.key, filename, source))
|
|
265
|
+
}
|
|
266
|
+
const key = convertExpression(element.key, filename, source, context)
|
|
267
|
+
|
|
268
|
+
return withParserRanges({
|
|
269
|
+
key: /** @type {import("../semantic/types.js").StringLiteral} */ (key),
|
|
270
|
+
kind: /** @type {const} */ ("MapEntry"),
|
|
271
|
+
location: nodeLocation(element, filename, source),
|
|
272
|
+
value: convertExpression(element.value, filename, source, context, valueType)
|
|
273
|
+
}, {operator: prismLocation(element.operatorLoc, filename, source)})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return withParserRanges({
|
|
277
|
+
entries,
|
|
278
|
+
kind: expectedType?.kind == "OrderedMapType"
|
|
279
|
+
? /** @type {const} */ ("OrderedMapLiteral")
|
|
280
|
+
: /** @type {const} */ ("MapLiteral"),
|
|
281
|
+
location
|
|
282
|
+
}, {
|
|
283
|
+
close: prismLocation(node.closingLoc, filename, source),
|
|
284
|
+
open: prismLocation(node.openingLoc, filename, source)
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (node instanceof CallNode && (node.receiver instanceof ConstantReadNode || node.receiver instanceof ConstantPathNode) && node.name == "new" &&
|
|
289
|
+
node.callOperatorLoc && !node.block) {
|
|
290
|
+
const recordName = constantPathName(node.receiver)
|
|
291
|
+
const declaration = context.recordNames.get(recordName)
|
|
292
|
+
|
|
293
|
+
if (!declaration) return unsupportedSyntax("ruby", "construction of a non-record class", nodeLocation(node.receiver, filename, source))
|
|
294
|
+
if (declaration.kind == "ClassDeclaration") {
|
|
295
|
+
return withParserRanges({arguments: (node.arguments_?.arguments_ ?? []).map((argument, index) =>
|
|
296
|
+
convertExpression(argument, filename, source, context, declaration.constructor.parameters[index]?.type)),
|
|
297
|
+
kind: /** @type {const} */ ("ReferenceConstruction"), location,
|
|
298
|
+
reference: referenceType(/** @type {string} */ (declaration.id), nodeLocation(node.receiver, filename, source))},
|
|
299
|
+
{class: nodeLocation(node.receiver, filename, source)})
|
|
300
|
+
}
|
|
301
|
+
if (declaration.kind == "EffectResourceDeclaration") {
|
|
302
|
+
return unsupportedSyntax("ruby", "owned resource construction", nodeLocation(node.receiver, filename, source))
|
|
303
|
+
}
|
|
304
|
+
const typeArguments = expectedType?.kind == "RecordType" && expectedType.declarationId == declaration.id
|
|
305
|
+
? expectedType.arguments
|
|
306
|
+
: undefined
|
|
307
|
+
|
|
308
|
+
return withParserRanges({
|
|
309
|
+
arguments: (node.arguments_?.arguments_ ?? []).map((argument, index) =>
|
|
310
|
+
convertExpression(argument, filename, source, context,
|
|
311
|
+
instantiatedRecordFieldType(declaration, typeArguments, index))),
|
|
312
|
+
kind: /** @type {const} */ ("RecordConstruction"),
|
|
313
|
+
location,
|
|
314
|
+
record: recordType(/** @type {string} */ (declaration.id), nodeLocation(node.receiver, filename, source),
|
|
315
|
+
typeArguments)
|
|
316
|
+
}, {record: nodeLocation(node.receiver, filename, source)})
|
|
317
|
+
}
|
|
318
|
+
|
|
125
319
|
if (node instanceof CallNode && node.receiver && node.name == "!") {
|
|
126
320
|
const operatorLocation = prismLocation(node.messageLoc ?? node.location, filename, source)
|
|
127
321
|
|
|
@@ -131,10 +325,15 @@ function convertExpression(node, filename, source) {
|
|
|
131
325
|
}
|
|
132
326
|
if (node.arguments_) return unsupportedSyntax("ruby", "boolean not argument list", operatorLocation)
|
|
133
327
|
|
|
328
|
+
const operand = convertExpression(node.receiver, filename, source, context)
|
|
329
|
+
|
|
330
|
+
if (operand.kind == "UnaryExpression" && operand.operand.kind == "OptionalIsPresent") {
|
|
331
|
+
return operand.operand
|
|
332
|
+
}
|
|
134
333
|
const semantic = withAdaptedOperation(withParserRanges({
|
|
135
334
|
kind: "UnaryExpression",
|
|
136
335
|
location,
|
|
137
|
-
operand
|
|
336
|
+
operand
|
|
138
337
|
}, {operator: operatorLocation}), "Not")
|
|
139
338
|
|
|
140
339
|
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
@@ -149,7 +348,7 @@ function convertExpression(node, filename, source) {
|
|
|
149
348
|
const semantic = withAdaptedOperation(withParserRanges({
|
|
150
349
|
kind: "UnaryExpression",
|
|
151
350
|
location,
|
|
152
|
-
operand: convertExpression(node.receiver, filename, source)
|
|
351
|
+
operand: convertExpression(node.receiver, filename, source, context)
|
|
153
352
|
}, {operator: operatorLocation}), "Negate")
|
|
154
353
|
|
|
155
354
|
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
@@ -165,9 +364,9 @@ function convertExpression(node, filename, source) {
|
|
|
165
364
|
|
|
166
365
|
const semantic = withAdaptedOperation(withParserRanges({
|
|
167
366
|
kind: "BinaryExpression",
|
|
168
|
-
left: convertExpression(node.left, filename, source),
|
|
367
|
+
left: convertExpression(node.left, filename, source, context),
|
|
169
368
|
location,
|
|
170
|
-
right: convertExpression(node.right, filename, source)
|
|
369
|
+
right: convertExpression(node.right, filename, source, context)
|
|
171
370
|
}, {operator: operatorLocation}), node instanceof AndNode ? "And" : "Or")
|
|
172
371
|
|
|
173
372
|
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
@@ -185,42 +384,244 @@ function convertExpression(node, filename, source) {
|
|
|
185
384
|
if (node instanceof CallNode && node.receiver && rubyBinaryOperations.has(node.name) && node.arguments_?.arguments_.length == 1) {
|
|
186
385
|
const semantic = withAdaptedOperation(withParserRanges({
|
|
187
386
|
kind: "BinaryExpression",
|
|
188
|
-
left: convertExpression(node.receiver, filename, source),
|
|
387
|
+
left: convertExpression(node.receiver, filename, source, context),
|
|
189
388
|
location,
|
|
190
|
-
right: convertExpression(node.arguments_.arguments_[0], filename, source)
|
|
389
|
+
right: convertExpression(node.arguments_.arguments_[0], filename, source, context)
|
|
191
390
|
}, {operator: prismLocation(node.messageLoc ?? node.location, filename, source)}), /** @type {import("../semantic/operators.js").AdaptedOperation} */ (rubyBinaryOperations.get(node.name)))
|
|
192
391
|
|
|
193
392
|
return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (semantic))
|
|
194
393
|
}
|
|
195
394
|
|
|
395
|
+
if (node instanceof CallNode && node.receiver && node.name == "[]" && node.arguments_?.arguments_.length == 1 &&
|
|
396
|
+
!node.callOperatorLoc && !node.block) {
|
|
397
|
+
return withParserRanges({
|
|
398
|
+
collection: convertExpression(node.receiver, filename, source, context),
|
|
399
|
+
index: convertExpression(node.arguments_.arguments_[0], filename, source, context),
|
|
400
|
+
kind: /** @type {const} */ ("ListIndexExpression"),
|
|
401
|
+
location,
|
|
402
|
+
totality: /** @type {const} */ ("proven")
|
|
403
|
+
}, {operator: prismLocation(node.openingLoc ?? node.messageLoc ?? node.location, filename, source)})
|
|
404
|
+
}
|
|
405
|
+
if (node instanceof CallNode && node.receiver && node.name == "fetch" && node.arguments_?.arguments_.length == 1 &&
|
|
406
|
+
node.callOperatorLoc && !node.block) {
|
|
407
|
+
return withParserRanges({
|
|
408
|
+
collection: convertExpression(node.receiver, filename, source, context),
|
|
409
|
+
key: convertExpression(node.arguments_.arguments_[0], filename, source, context),
|
|
410
|
+
kind: /** @type {const} */ ("MapLookupExpression"),
|
|
411
|
+
location,
|
|
412
|
+
totality: /** @type {const} */ ("fail-on-absence")
|
|
413
|
+
}, {operator: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
414
|
+
}
|
|
415
|
+
if (node instanceof CallNode && node.receiver instanceof ConstantReadNode && node.callOperatorLoc && !node.block) {
|
|
416
|
+
const callee = `${node.receiver.name}.${node.name}`
|
|
417
|
+
const signature = context.functions.get(callee)
|
|
418
|
+
|
|
419
|
+
if (signature) {
|
|
420
|
+
return withParserRanges({
|
|
421
|
+
arguments: (node.arguments_?.arguments_ ?? []).map((argument, index) =>
|
|
422
|
+
convertExpression(argument, filename, source, context, signature.parameters[index]?.type,
|
|
423
|
+
preservesGenericOptionalEvidence(signature, index))),
|
|
424
|
+
callee,
|
|
425
|
+
kind: /** @type {const} */ ("CallExpression"),
|
|
426
|
+
location
|
|
427
|
+
}, {callee: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (node instanceof CallNode && node.receiver && node.name == "size" && !node.arguments_ && node.callOperatorLoc && !node.block) {
|
|
431
|
+
const receiverType = knownValueExpressionType(node.receiver, context)
|
|
432
|
+
|
|
433
|
+
if (receiverType?.kind == "RecordType") {
|
|
434
|
+
return withParserRanges({
|
|
435
|
+
field: node.name,
|
|
436
|
+
kind: /** @type {const} */ ("MemberRead"),
|
|
437
|
+
location,
|
|
438
|
+
receiver: convertExpression(node.receiver, filename, source, context)
|
|
439
|
+
}, {member: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
440
|
+
}
|
|
441
|
+
return withParserRanges({
|
|
442
|
+
collection: convertExpression(node.receiver, filename, source, context),
|
|
443
|
+
kind: /** @type {const} */ ("CollectionSizeExpression"),
|
|
444
|
+
location
|
|
445
|
+
}, {operator: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
446
|
+
}
|
|
447
|
+
if (node instanceof CallNode && node.receiver instanceof LocalVariableReadNode && node.name == "message" &&
|
|
448
|
+
!node.arguments_ && node.callOperatorLoc && !node.block && context.bindings.get(node.receiver.name)?.kind == "ErrorType") {
|
|
449
|
+
const receiverLocation = nodeLocation(node.receiver, filename, source)
|
|
450
|
+
|
|
451
|
+
return withParserRanges({
|
|
452
|
+
kind: /** @type {const} */ ("ErrorMessageRead"),
|
|
453
|
+
location,
|
|
454
|
+
receiver: withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: receiverLocation, name: node.receiver.name}, {
|
|
455
|
+
name: receiverLocation
|
|
456
|
+
})
|
|
457
|
+
}, {member: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
458
|
+
}
|
|
459
|
+
if (node instanceof CallNode && node.receiver && node.callOperatorLoc && !node.block &&
|
|
460
|
+
knownValueExpressionType(node.receiver, context)?.kind == "ReferenceType") {
|
|
461
|
+
const receiverType = /** @type {import("../semantic/types.js").ReferenceType} */ (knownValueExpressionType(node.receiver, context))
|
|
462
|
+
const method = context.classes.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == node.name)
|
|
463
|
+
|
|
464
|
+
return withParserRanges({arguments: (node.arguments_?.arguments_ ?? []).map((argument, index) =>
|
|
465
|
+
convertExpression(argument, filename, source, context, method?.parameters[index]?.type)),
|
|
466
|
+
kind: /** @type {const} */ ("MethodCallExpression"), location, method: method?.id ?? node.name,
|
|
467
|
+
receiver: convertExpression(node.receiver, filename, source, context)},
|
|
468
|
+
{member: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
469
|
+
}
|
|
470
|
+
if (node instanceof CallNode && node.receiver && node.callOperatorLoc && !node.arguments_ && !node.block) {
|
|
471
|
+
if (["send", "public_send", "__send__", "instance_variable_get", "method"].includes(node.name)) {
|
|
472
|
+
return unsupportedSyntax("ruby", "dynamic or reflective member access", prismLocation(node.messageLoc ?? node.location, filename, source))
|
|
473
|
+
}
|
|
474
|
+
return withParserRanges({
|
|
475
|
+
field: node.name,
|
|
476
|
+
kind: /** @type {const} */ ("MemberRead"),
|
|
477
|
+
location,
|
|
478
|
+
receiver: convertExpression(node.receiver, filename, source, context)
|
|
479
|
+
}, {member: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
480
|
+
}
|
|
481
|
+
|
|
196
482
|
if (node instanceof CallNode && !node.receiver) {
|
|
483
|
+
if (["send", "public_send", "__send__"].includes(node.name)) {
|
|
484
|
+
return unsupportedSyntax("ruby", "dynamic call", prismLocation(node.messageLoc ?? node.location, filename, source))
|
|
485
|
+
}
|
|
486
|
+
const signature = context.functions.get(node.name)
|
|
487
|
+
|
|
197
488
|
return withParserRanges({
|
|
198
|
-
arguments: (node.arguments_?.arguments_ ?? []).map((argument) =>
|
|
489
|
+
arguments: (node.arguments_?.arguments_ ?? []).map((argument, index) =>
|
|
490
|
+
convertExpression(argument, filename, source, context, signature?.parameters[index]?.type,
|
|
491
|
+
preservesGenericOptionalEvidence(signature, index))),
|
|
199
492
|
callee: node.name,
|
|
200
493
|
kind: "CallExpression",
|
|
201
494
|
location
|
|
202
495
|
}, {callee: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
203
496
|
}
|
|
497
|
+
if (node instanceof CallNode && node.receiver) {
|
|
498
|
+
const unsupportedLocation = !node.callOperatorLoc && node.messageLoc && node.arguments_?.arguments_.length == 1
|
|
499
|
+
? prismLocation(node.messageLoc, filename, source)
|
|
500
|
+
: nodeLocation(node.receiver, filename, source)
|
|
501
|
+
|
|
502
|
+
return unsupportedSyntax("ruby", node.constructor.name, unsupportedLocation)
|
|
503
|
+
}
|
|
204
504
|
|
|
205
505
|
return unsupportedSyntax("ruby", node.constructor.name, location)
|
|
206
506
|
}
|
|
207
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Resolves only result types established by explicit signatures and bindings.
|
|
510
|
+
* @param {import("@ruby/prism").Node} node - Parser-owned expression.
|
|
511
|
+
* @param {RubyConversionContext} context - Typed lexical context.
|
|
512
|
+
* @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Known result type.
|
|
513
|
+
*/
|
|
514
|
+
function knownExpressionType(node, context) {
|
|
515
|
+
if (node instanceof ParenthesesNode && node.body instanceof StatementsNode && node.body.body.length == 1) {
|
|
516
|
+
return knownExpressionType(node.body.body[0], context)
|
|
517
|
+
}
|
|
518
|
+
if (node instanceof LocalVariableReadNode) return context.bindings.get(node.name)
|
|
519
|
+
if (node instanceof CallNode && !node.receiver) {
|
|
520
|
+
const signature = context.functions.get(node.name)
|
|
521
|
+
|
|
522
|
+
if (signature) return knownCallReturnType(signature, (node.arguments_?.arguments_ ?? []).map((argument) =>
|
|
523
|
+
knownExpressionType(argument, context)))
|
|
524
|
+
}
|
|
525
|
+
if (node instanceof CallNode && node.receiver instanceof ConstantReadNode) {
|
|
526
|
+
const signature = context.functions.get(`${node.receiver.name}.${node.name}`)
|
|
527
|
+
|
|
528
|
+
if (signature) return knownCallReturnType(signature, (node.arguments_?.arguments_ ?? []).map((argument) =>
|
|
529
|
+
knownExpressionType(argument, context)))
|
|
530
|
+
}
|
|
531
|
+
if (node instanceof CallNode && node.receiver && node.name == "[]") {
|
|
532
|
+
const collectionType = knownValueExpressionType(node.receiver, context)
|
|
533
|
+
|
|
534
|
+
if (collectionType?.kind == "ListType") return collectionType.elementType
|
|
535
|
+
}
|
|
536
|
+
if (node instanceof CallNode && node.receiver && node.name == "fetch") {
|
|
537
|
+
const collectionType = knownValueExpressionType(node.receiver, context)
|
|
538
|
+
|
|
539
|
+
if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") return collectionType.valueType
|
|
540
|
+
}
|
|
541
|
+
if (node instanceof CallNode && (node.receiver instanceof ConstantReadNode || node.receiver instanceof ConstantPathNode) && node.name == "new") {
|
|
542
|
+
const declaration = context.recordNames.get(constantPathName(node.receiver))
|
|
543
|
+
|
|
544
|
+
if (declaration?.id) return declaration.kind == "ClassDeclaration"
|
|
545
|
+
? {declarationId: declaration.id, kind: "ReferenceType"}
|
|
546
|
+
: {declarationId: declaration.id, kind: "RecordType"}
|
|
547
|
+
}
|
|
548
|
+
if (node instanceof SelfNode && context.currentClass?.id) return {declarationId: context.currentClass.id, kind: "ReferenceType"}
|
|
549
|
+
if (node instanceof InstanceVariableReadNode && context.currentClass) {
|
|
550
|
+
return context.currentClass.fields.find((field) => `@${field.name}` == node.name)?.type
|
|
551
|
+
}
|
|
552
|
+
if (node instanceof CallNode && node.receiver && node.callOperatorLoc) {
|
|
553
|
+
const receiver = knownValueExpressionType(node.receiver, context)
|
|
554
|
+
|
|
555
|
+
if (receiver?.kind == "ReferenceType") {
|
|
556
|
+
return context.classes.get(receiver.declarationId)?.methods.find((method) => method.name == node.name)?.returnType
|
|
557
|
+
}
|
|
558
|
+
if (node.arguments_) return undefined
|
|
559
|
+
if (receiver?.kind != "RecordType") return undefined
|
|
560
|
+
const declaration = context.records.get(receiver.declarationId)
|
|
561
|
+
const index = declaration?.fields.findIndex((field) => field.name == node.name) ?? -1
|
|
562
|
+
|
|
563
|
+
return declaration ? instantiatedRecordFieldType(declaration, receiver.arguments, index) : undefined
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return undefined
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Resolves the value type produced by the frontend's implicit optional-binding unwrap.
|
|
571
|
+
* Semantic validation separately proves that the unwrap occurs only on a present path.
|
|
572
|
+
* @param {import("@ruby/prism").Node} node - Parser-owned expression.
|
|
573
|
+
* @param {RubyConversionContext} context - Typed lexical context.
|
|
574
|
+
* @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Converted value type.
|
|
575
|
+
*/
|
|
576
|
+
function knownValueExpressionType(node, context) {
|
|
577
|
+
if (node instanceof ParenthesesNode && node.body instanceof StatementsNode && node.body.body.length == 1) {
|
|
578
|
+
return knownValueExpressionType(node.body.body[0], context)
|
|
579
|
+
}
|
|
580
|
+
const type = knownExpressionType(node, context)
|
|
581
|
+
|
|
582
|
+
return node instanceof LocalVariableReadNode && type?.kind == "OptionalType" ? type.valueType : type
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Recognizes only the parser-backed `identifier.nil?` spelling used by Task 007.
|
|
587
|
+
* @param {import("@ruby/prism").Node} node - Candidate Prism expression.
|
|
588
|
+
* @param {string} source - Complete source.
|
|
589
|
+
* @returns {boolean} Whether this is one exact nil test.
|
|
590
|
+
*/
|
|
591
|
+
function exactNilTest(node, source) {
|
|
592
|
+
if (!(node instanceof CallNode) || !(node.receiver instanceof LocalVariableReadNode) ||
|
|
593
|
+
node.name != "nil?" || !node.callOperatorLoc || node.arguments_ || node.block) return false
|
|
594
|
+
|
|
595
|
+
return slicePrismSource(
|
|
596
|
+
source,
|
|
597
|
+
node.callOperatorLoc.startOffset,
|
|
598
|
+
node.callOperatorLoc.startOffset + node.callOperatorLoc.length
|
|
599
|
+
) == "."
|
|
600
|
+
}
|
|
601
|
+
|
|
208
602
|
/**
|
|
209
603
|
* Converts one explicit Ruby return.
|
|
210
604
|
* @param {import("@ruby/prism").Node} node - Prism node.
|
|
211
605
|
* @param {string} filename - Source filename.
|
|
212
606
|
* @param {string} source - Complete source.
|
|
607
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
213
608
|
* @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
|
|
214
609
|
*/
|
|
215
|
-
function convertReturn(node, filename, source) {
|
|
610
|
+
function convertReturn(node, filename, source, context) {
|
|
216
611
|
const location = nodeLocation(node, filename, source)
|
|
217
612
|
|
|
218
|
-
if (!(node instanceof ReturnNode) || node.arguments_?.arguments_.length
|
|
613
|
+
if (!(node instanceof ReturnNode) || (node.arguments_?.arguments_.length ?? 0) > 1) {
|
|
219
614
|
return unsupportedSyntax("ruby", node.constructor.name, location)
|
|
220
615
|
}
|
|
221
616
|
|
|
222
617
|
return {
|
|
223
|
-
|
|
618
|
+
...(node.arguments_?.arguments_[0] ? {expression: convertExpression(
|
|
619
|
+
node.arguments_.arguments_[0],
|
|
620
|
+
filename,
|
|
621
|
+
source,
|
|
622
|
+
context,
|
|
623
|
+
context.returnType?.kind == "TypeReference" && context.returnType.name == "void" ? undefined : context.returnType
|
|
624
|
+
)} : {}),
|
|
224
625
|
kind: "ReturnStatement",
|
|
225
626
|
location
|
|
226
627
|
}
|
|
@@ -232,26 +633,61 @@ function convertReturn(node, filename, source) {
|
|
|
232
633
|
* @param {{location: import("@ruby/prism").Location}} node - Comment owner.
|
|
233
634
|
* @param {string} filename - Source filename.
|
|
234
635
|
* @param {string} source - Complete source.
|
|
235
|
-
* @
|
|
636
|
+
* @param {string} [ownerId] - Stable declaration identity.
|
|
637
|
+
* @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined, typeParameters?: import("../semantic/types.js").TypeParameter[]}} Declared types.
|
|
236
638
|
*/
|
|
237
|
-
function typeComments(comments, node, filename, source) {
|
|
639
|
+
function typeComments(comments, node, filename, source, ownerId) {
|
|
238
640
|
const preceding = associatedComments(comments, node, source)
|
|
239
641
|
const parameterTypes = new Map()
|
|
240
642
|
/** @type {{location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined} */
|
|
241
643
|
let returnType
|
|
644
|
+
/** @type {import("../semantic/types.js").TypeParameter[]} */
|
|
645
|
+
const typeParameters = []
|
|
242
646
|
|
|
243
647
|
for (const comment of preceding) {
|
|
244
648
|
const words = commentTokens(comment, filename, source)
|
|
245
649
|
|
|
246
|
-
if (words[0]?.text == "@param" && words.length
|
|
247
|
-
parameterTypes.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
650
|
+
if (words[0]?.text == "@param" && words.length >= 3) {
|
|
651
|
+
if (parameterTypes.has(words[1].text)) {
|
|
652
|
+
return unsupportedSyntax("ruby", "duplicate parameter annotation", words[1].location)
|
|
653
|
+
}
|
|
654
|
+
parameterTypes.set(words[1].text, joinedCommentToken(words, 2, source))
|
|
655
|
+
} else if (words[0]?.text == "@return" && words.length >= 2) {
|
|
656
|
+
if (returnType) return unsupportedSyntax("ruby", "duplicate return annotation", words[1].location)
|
|
657
|
+
returnType = joinedCommentToken(words, 1, source)
|
|
658
|
+
} else if (words[0]?.text == "@template" && words.length == 2 && ownerId &&
|
|
659
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/u.test(words[1].text)) {
|
|
660
|
+
typeParameters.push(withParserRanges({
|
|
661
|
+
id: `${ownerId}:type:${typeParameters.length}`,
|
|
662
|
+
kind: /** @type {const} */ ("TypeParameter"),
|
|
663
|
+
location: words[1].location,
|
|
664
|
+
name: words[1].text
|
|
665
|
+
}, {name: words[1].location}))
|
|
666
|
+
} else if (words[0]?.text?.startsWith("@template")) {
|
|
667
|
+
return unsupportedSyntax("ruby", "bounded, defaulted, variant, or malformed type parameter", words[1]?.location ?? nodeLocation(node, filename, source))
|
|
668
|
+
} else if (words[0]?.text?.startsWith("@param") || words[0]?.text?.startsWith("@return")) {
|
|
669
|
+
return unsupportedSyntax("ruby", "malformed function type annotation", nodeLocation(node, filename, source))
|
|
251
670
|
}
|
|
252
671
|
}
|
|
253
672
|
|
|
254
|
-
return {parameters: parameterTypes, returnType}
|
|
673
|
+
return {parameters: parameterTypes, returnType, ...(typeParameters.length > 0 ? {typeParameters} : {})}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Rejoins a bounded metadata suffix while retaining its parser-owned range.
|
|
678
|
+
* @param {{location: import("../semantic/types.js").SourceLocation, text: string}[]} tokens - Comment tokens.
|
|
679
|
+
* @param {number} start - First suffix token.
|
|
680
|
+
* @param {string} source - Complete source.
|
|
681
|
+
* @returns {{location: import("../semantic/types.js").SourceLocation, sourceType: string}} Joined token.
|
|
682
|
+
*/
|
|
683
|
+
function joinedCommentToken(tokens, start, source) {
|
|
684
|
+
const first = tokens[start]
|
|
685
|
+
const last = tokens.at(-1)
|
|
686
|
+
|
|
687
|
+
if (!first || !last) throw new Error("Ruby metadata suffix unexpectedly disappeared.")
|
|
688
|
+
const location = locationFromOffsets(first.location.filename, source, first.location.start.offset, last.location.end.offset)
|
|
689
|
+
|
|
690
|
+
return {location, sourceType: source.slice(location.start.offset, location.end.offset)}
|
|
255
691
|
}
|
|
256
692
|
|
|
257
693
|
/**
|
|
@@ -322,9 +758,11 @@ function associatedComments(comments, node, source) {
|
|
|
322
758
|
* @param {LocalVariableWriteNode} node - Local write.
|
|
323
759
|
* @param {string} filename - Source filename.
|
|
324
760
|
* @param {string} source - Complete source.
|
|
325
|
-
* @
|
|
761
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
762
|
+
* @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
|
|
763
|
+
* @returns {{immutable: boolean, type: import("../semantic/types.js").SemanticValueType} | undefined} Metadata when present.
|
|
326
764
|
*/
|
|
327
|
-
function localMetadata(comments, node, filename, source) {
|
|
765
|
+
function localMetadata(comments, node, filename, source, recordNames, typeParameters = new Map()) {
|
|
328
766
|
const associated = associatedComments(comments, node, source)
|
|
329
767
|
const metadata = associated.map((comment) => commentTokens(comment, filename, source))
|
|
330
768
|
const typeMetadata = metadata.filter(([token]) => token?.text == "@type")
|
|
@@ -337,15 +775,17 @@ function localMetadata(comments, node, filename, source) {
|
|
|
337
775
|
|
|
338
776
|
const location = nodeLocation(node, filename, source)
|
|
339
777
|
|
|
340
|
-
if (typeMetadata.length != 1 || typeMetadata[0].length
|
|
778
|
+
if (typeMetadata.length != 1 || typeMetadata[0].length < 2 ||
|
|
341
779
|
immutableMetadata.length > 1 || immutableMetadata.some((tokens) => tokens.length != 1) ||
|
|
342
780
|
profileMetadata.length != typeMetadata.length + immutableMetadata.length) {
|
|
343
781
|
return unsupportedSyntax("ruby", "malformed local type metadata", location)
|
|
344
782
|
}
|
|
345
783
|
|
|
784
|
+
const declaredType = joinedCommentToken(typeMetadata[0], 1, source)
|
|
785
|
+
|
|
346
786
|
return {
|
|
347
787
|
immutable: immutableMetadata.length == 1,
|
|
348
|
-
type: convertType(
|
|
788
|
+
type: convertType(declaredType.sourceType, `Local '${node.name}'`, location, source, declaredType.location, recordNames, typeParameters)
|
|
349
789
|
}
|
|
350
790
|
}
|
|
351
791
|
|
|
@@ -353,78 +793,346 @@ function localMetadata(comments, node, filename, source) {
|
|
|
353
793
|
* Converts one Ruby local declaration or assignment.
|
|
354
794
|
* @param {import("@ruby/prism").Node} node - Prism node.
|
|
355
795
|
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
356
|
-
* @param {
|
|
796
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
357
797
|
* @param {string} filename - Source filename.
|
|
358
798
|
* @param {string} source - Complete source.
|
|
359
799
|
* @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
|
|
360
800
|
*/
|
|
361
|
-
function convertLocalStatement(node, comments,
|
|
801
|
+
function convertLocalStatement(node, comments, context, filename, source) {
|
|
362
802
|
const location = nodeLocation(node, filename, source)
|
|
363
803
|
|
|
364
804
|
if (!(node instanceof LocalVariableWriteNode)) return unsupportedSyntax("ruby", node.constructor.name, location)
|
|
365
805
|
|
|
366
|
-
const metadata = localMetadata(comments, node, filename, source)
|
|
806
|
+
const metadata = localMetadata(comments, node, filename, source, context.recordNames, context.typeParameters)
|
|
367
807
|
|
|
368
808
|
if (metadata) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
809
|
+
let declaredType = metadata.type
|
|
810
|
+
|
|
811
|
+
if (metadata.immutable && declaredType.kind == "MapType" && context.orderedMapDeclarations?.has(node) &&
|
|
812
|
+
node.value instanceof HashNode) {
|
|
813
|
+
declaredType = orderedMapTypeFromMap(declaredType, location)
|
|
814
|
+
}
|
|
815
|
+
const semantic = withParserRanges({
|
|
816
|
+
initializer: convertExpression(node.value, filename, source, context, declaredType),
|
|
817
|
+
kind: /** @type {const} */ ("LocalDeclaration"),
|
|
373
818
|
location,
|
|
374
819
|
mutable: !metadata.immutable,
|
|
375
820
|
name: node.name,
|
|
376
|
-
type:
|
|
821
|
+
type: declaredType
|
|
377
822
|
}, {name: prismLocation(node.nameLoc, filename, source), operator: prismLocation(node.operatorLoc, filename, source)})
|
|
823
|
+
|
|
824
|
+
context.bindings.set(node.name, declaredType)
|
|
825
|
+
return semantic
|
|
378
826
|
}
|
|
379
827
|
|
|
380
|
-
if (!
|
|
828
|
+
if (!context.bindings.has(node.name)) {
|
|
829
|
+
if (node.value instanceof LocalVariableReadNode && context.bindings.get(node.value.name)?.kind == "OrderedMapType") {
|
|
830
|
+
return unsupportedSyntax("ruby", "ordered map alias escape", location)
|
|
831
|
+
}
|
|
832
|
+
return missingType("ruby", `Local '${node.name}'`, location)
|
|
833
|
+
}
|
|
381
834
|
|
|
382
835
|
const targetLocation = prismLocation(node.nameLoc, filename, source)
|
|
383
836
|
const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: node.name}, {
|
|
384
837
|
name: targetLocation
|
|
385
838
|
})
|
|
386
839
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
840
|
+
const bindingType = context.bindings.get(node.name)
|
|
841
|
+
const semantic = withParserRanges({
|
|
842
|
+
expression: convertExpression(node.value, filename, source, context,
|
|
843
|
+
bindingType?.kind == "ErrorType" ? undefined : bindingType),
|
|
844
|
+
kind: /** @type {const} */ ("AssignmentStatement"),
|
|
390
845
|
location,
|
|
391
846
|
target
|
|
392
847
|
}, {operator: prismLocation(node.operatorLoc, filename, source)})
|
|
848
|
+
|
|
849
|
+
return semantic
|
|
393
850
|
}
|
|
394
851
|
|
|
395
852
|
/**
|
|
396
853
|
* Converts one exhaustive Prism statement.
|
|
397
854
|
* @param {import("@ruby/prism").Node} node - Prism statement.
|
|
398
855
|
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
399
|
-
* @param {
|
|
856
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
400
857
|
* @param {string} filename - Source filename.
|
|
401
858
|
* @param {string} source - Complete source.
|
|
402
859
|
* @returns {import("../semantic/types.js").Statement} Semantic statement.
|
|
403
860
|
*/
|
|
404
|
-
function convertStatement(node, comments,
|
|
405
|
-
if (node instanceof ReturnNode)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
861
|
+
function convertStatement(node, comments, context, filename, source) {
|
|
862
|
+
if (node instanceof ReturnNode) {
|
|
863
|
+
if ((context.loopDepth ?? 0) > 0) return unsupportedSyntax("ruby", "nonlocal return from each block", nodeLocation(node, filename, source))
|
|
864
|
+
return convertReturn(node, filename, source, context)
|
|
865
|
+
}
|
|
866
|
+
if (node instanceof IfNode || node instanceof UnlessNode) return convertIf(node, comments, context, filename, source)
|
|
867
|
+
if (node instanceof WhileNode) return convertWhile(node, comments, context, filename, source)
|
|
868
|
+
if (node instanceof BeginNode) return convertRubyTry(node, comments, context, filename, source)
|
|
869
|
+
if (node instanceof LocalVariableWriteNode) return convertLocalStatement(node, comments, context, filename, source)
|
|
870
|
+
if (node instanceof InstanceVariableWriteNode) {
|
|
871
|
+
const location = nodeLocation(node, filename, source)
|
|
872
|
+
const field = context.currentClass?.fields.find((candidate) => `@${candidate.name}` == node.name)
|
|
873
|
+
|
|
874
|
+
if (!field || !context.currentClass) return unsupportedSyntax("ruby", "private instance field outside its declaring class", location)
|
|
875
|
+
const receiver = withParserRanges({classId: /** @type {string} */ (context.currentClass.id),
|
|
876
|
+
kind: /** @type {const} */ ("ReceiverExpression"), location}, {receiver: location})
|
|
877
|
+
|
|
878
|
+
return withParserRanges({expression: convertExpression(node.value, filename, source, context, field.type),
|
|
879
|
+
field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldWriteStatement"), location, receiver},
|
|
880
|
+
{member: location})
|
|
881
|
+
}
|
|
882
|
+
if (node instanceof BreakNode || node instanceof NextNode) {
|
|
883
|
+
if (node.arguments_) return unsupportedSyntax("ruby", `${node instanceof BreakNode ? "break" : "next"} value`, nodeLocation(node.arguments_, filename, source))
|
|
884
|
+
const location = prismLocation(node.keywordLoc, filename, source)
|
|
885
|
+
|
|
886
|
+
return withParserRanges({
|
|
887
|
+
kind: /** @type {"BreakStatement" | "ContinueStatement"} */ (node instanceof BreakNode ? "BreakStatement" : "ContinueStatement"),
|
|
888
|
+
location
|
|
889
|
+
}, {keyword: location})
|
|
890
|
+
}
|
|
891
|
+
if (node instanceof CallNode) {
|
|
892
|
+
if (!node.receiver && node.name == "raise") return convertRubyRaise(node, context, filename, source)
|
|
893
|
+
if (node.block) return convertForEach(node, comments, context, filename, source)
|
|
894
|
+
if (!node.receiver && node.name == "puts" && !context.functions.has("puts")) return convertPrint(node, filename, source, context)
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
expression: /** @type {import("../semantic/types.js").CallExpression} */ (convertExpression(node, filename, source, context)),
|
|
898
|
+
kind: "ExpressionStatement",
|
|
899
|
+
location: nodeLocation(node, filename, source)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
409
902
|
|
|
410
903
|
return unsupportedSyntax("ruby", node.constructor.name, nodeLocation(node, filename, source))
|
|
411
904
|
}
|
|
412
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Converts exact `raise DeclaredError, message`.
|
|
908
|
+
* @param {CallNode} node - Prism raise call.
|
|
909
|
+
* @param {RubyConversionContext} context - Typed lexical context.
|
|
910
|
+
* @param {string} filename - Source filename.
|
|
911
|
+
* @param {string} source - Complete source.
|
|
912
|
+
* @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
|
|
913
|
+
*/
|
|
914
|
+
function convertRubyRaise(node, context, filename, source) {
|
|
915
|
+
const location = nodeLocation(node, filename, source)
|
|
916
|
+
const arguments_ = node.arguments_?.arguments_ ?? []
|
|
917
|
+
const errorNode = arguments_[0]
|
|
918
|
+
|
|
919
|
+
if (node.block || node.callOperatorLoc || arguments_.length != 2 ||
|
|
920
|
+
!(errorNode instanceof ConstantReadNode) && !(errorNode instanceof ConstantPathNode) ||
|
|
921
|
+
!context.errorNames.has(constantPathName(errorNode))) {
|
|
922
|
+
return unsupportedSyntax("ruby", "raise other than exact declared error and message", location)
|
|
923
|
+
}
|
|
924
|
+
const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(constantPathName(errorNode)))
|
|
925
|
+
const typeLocation = nodeLocation(errorNode, filename, source)
|
|
926
|
+
|
|
927
|
+
return withParserRanges({
|
|
928
|
+
error: withParserRanges({
|
|
929
|
+
error: withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {type: typeLocation}),
|
|
930
|
+
kind: /** @type {const} */ ("ErrorConstruction"),
|
|
931
|
+
location,
|
|
932
|
+
message: convertExpression(arguments_[1], filename, source, context)
|
|
933
|
+
}, {type: typeLocation}),
|
|
934
|
+
kind: /** @type {const} */ ("RaiseStatement"),
|
|
935
|
+
location
|
|
936
|
+
}, {keyword: prismLocation(node.messageLoc ?? node.location, filename, source)})
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Converts one exact typed rescue with no else/ensure.
|
|
941
|
+
* @param {BeginNode} node - Prism begin node.
|
|
942
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
|
|
943
|
+
* @param {RubyConversionContext} context - Typed lexical context.
|
|
944
|
+
* @param {string} filename - Source filename.
|
|
945
|
+
* @param {string} source - Complete source.
|
|
946
|
+
* @returns {import("../semantic/types.js").TryStatement} Semantic exact handler.
|
|
947
|
+
*/
|
|
948
|
+
function convertRubyTry(node, comments, context, filename, source) {
|
|
949
|
+
const location = nodeLocation(node, filename, source)
|
|
950
|
+
const handler = node.rescueClause
|
|
951
|
+
|
|
952
|
+
if (!(handler instanceof RescueNode) || handler.subsequent || node.elseClause || node.ensureClause ||
|
|
953
|
+
handler.exceptions.length != 1 ||
|
|
954
|
+
!(handler.exceptions[0] instanceof ConstantReadNode) && !(handler.exceptions[0] instanceof ConstantPathNode) ||
|
|
955
|
+
!context.errorNames.has(constantPathName(handler.exceptions[0])) || !(handler.reference instanceof LocalVariableTargetNode)) {
|
|
956
|
+
return unsupportedSyntax("ruby", "begin without one exact typed rescue and binding", location)
|
|
957
|
+
}
|
|
958
|
+
const errorNode = handler.exceptions[0]
|
|
959
|
+
const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(constantPathName(errorNode)))
|
|
960
|
+
const caughtLocation = nodeLocation(errorNode, filename, source)
|
|
961
|
+
const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
|
|
962
|
+
type: caughtLocation
|
|
963
|
+
})
|
|
964
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
965
|
+
const catchContext = {...context, bindings: new Map(context.bindings)}
|
|
966
|
+
|
|
967
|
+
catchContext.bindings.set(handler.reference.name, catchType)
|
|
968
|
+
return withParserRanges({
|
|
969
|
+
body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
|
|
970
|
+
catchBinding: withParserRanges({
|
|
971
|
+
kind: /** @type {const} */ ("CatchBinding"),
|
|
972
|
+
location: nodeLocation(handler.reference, filename, source),
|
|
973
|
+
mutable: /** @type {const} */ (false),
|
|
974
|
+
name: handler.reference.name,
|
|
975
|
+
type: catchType
|
|
976
|
+
}, {name: nodeLocation(handler.reference, filename, source)}),
|
|
977
|
+
catchBody: convertBlock(handler.statements, comments, catchContext, filename, source, nodeLocation(handler, filename, source)),
|
|
978
|
+
catchType,
|
|
979
|
+
kind: /** @type {const} */ ("TryStatement"),
|
|
980
|
+
location
|
|
981
|
+
}, {catch: nodeLocation(handler, filename, source), try: location})
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Converts exact resolved-list `.each do |value| ... end` syntax.
|
|
986
|
+
* @param {CallNode} node - Prism call with a block.
|
|
987
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
988
|
+
* @param {RubyConversionContext} context - Typed lexical context.
|
|
989
|
+
* @param {string} filename - Source filename.
|
|
990
|
+
* @param {string} source - Complete source.
|
|
991
|
+
* @returns {import("../semantic/types.js").ForEachStatement | import("../semantic/types.js").ForEachMapStatement} Semantic loop.
|
|
992
|
+
*/
|
|
993
|
+
function convertForEach(node, comments, context, filename, source) {
|
|
994
|
+
const location = nodeLocation(node, filename, source)
|
|
995
|
+
const block = node.block
|
|
996
|
+
|
|
997
|
+
if (!(block instanceof BlockNode)) return unsupportedSyntax("ruby", "call block", block ? nodeLocation(block, filename, source) : location)
|
|
998
|
+
if (!node.receiver || node.name != "each" || node.arguments_ || !node.callOperatorLoc || !node.messageLoc) {
|
|
999
|
+
return unsupportedSyntax("ruby", "arbitrary call block", nodeLocation(block, filename, source))
|
|
1000
|
+
}
|
|
1001
|
+
if (slicePrismSource(source, block.openingLoc.startOffset, block.openingLoc.startOffset + block.openingLoc.length) != "do") {
|
|
1002
|
+
return unsupportedSyntax("ruby", "non-do each block", prismLocation(block.openingLoc, filename, source))
|
|
1003
|
+
}
|
|
1004
|
+
const excluded = findRubyDescendant(block.body, (candidate) =>
|
|
1005
|
+
candidate instanceof RedoNode || candidate instanceof RetryNode || candidate instanceof ReturnNode)
|
|
1006
|
+
|
|
1007
|
+
if (excluded) {
|
|
1008
|
+
const detail = excluded instanceof ReturnNode ? "nonlocal return from each block" :
|
|
1009
|
+
excluded instanceof RedoNode ? "redo in each block" : "retry in each block"
|
|
1010
|
+
|
|
1011
|
+
return unsupportedSyntax("ruby", detail, nodeLocation(excluded, filename, source))
|
|
1012
|
+
}
|
|
1013
|
+
if (!(block.parameters instanceof BlockParametersNode) || block.parameters.locals.length > 0 ||
|
|
1014
|
+
!block.parameters.parameters) {
|
|
1015
|
+
return unsupportedSyntax("ruby", "each block parameters", block.parameters
|
|
1016
|
+
? nodeLocation(block.parameters, filename, source)
|
|
1017
|
+
: nodeLocation(block, filename, source))
|
|
1018
|
+
}
|
|
1019
|
+
const parameters = block.parameters.parameters
|
|
1020
|
+
const supportedRequireds = parameters.requireds.length == 1 || parameters.requireds.length == 2
|
|
1021
|
+
const unsupportedParameter = !supportedRequireds
|
|
1022
|
+
? parameters.requireds[2] ?? parameters.requireds[1] ?? parameters.requireds[0] ?? block.parameters
|
|
1023
|
+
: parameters.requireds.every((parameter) => parameter instanceof RequiredParameterNode) &&
|
|
1024
|
+
parameters.optionals.length == 0 && !parameters.rest &&
|
|
1025
|
+
parameters.posts.length == 0 && parameters.keywords.length == 0 && !parameters.keywordRest && !parameters.block
|
|
1026
|
+
? undefined
|
|
1027
|
+
: parameters.optionals[0] ?? parameters.rest ?? parameters.posts[0] ?? parameters.keywords[0] ??
|
|
1028
|
+
parameters.keywordRest ?? parameters.block ?? parameters.requireds[0]
|
|
1029
|
+
|
|
1030
|
+
if (unsupportedParameter) return unsupportedSyntax("ruby", "each block parameter arity or shape", nodeLocation(unsupportedParameter, filename, source))
|
|
1031
|
+
const collectionType = iterationOperandType(knownExpressionType(node.receiver, context))
|
|
1032
|
+
|
|
1033
|
+
if (parameters.requireds.length == 2) {
|
|
1034
|
+
if (collectionType?.kind == "MapType") {
|
|
1035
|
+
return unsupportedSyntax("ruby", "unordered map iteration", nodeLocation(node.receiver, filename, source))
|
|
1036
|
+
}
|
|
1037
|
+
if (!collectionType || collectionType.kind != "OrderedMapType") {
|
|
1038
|
+
return missingType("ruby", "Ordered map iteration collection", nodeLocation(node.receiver, filename, source))
|
|
1039
|
+
}
|
|
1040
|
+
const [keyParameter, valueParameter] = /** @type {[RequiredParameterNode, RequiredParameterNode]} */ (parameters.requireds)
|
|
1041
|
+
const keyLocation = nodeLocation(keyParameter, filename, source)
|
|
1042
|
+
const valueLocation = nodeLocation(valueParameter, filename, source)
|
|
1043
|
+
const keyType = iterationBindingType(collectionType.keyType, keyLocation)
|
|
1044
|
+
const valueType = iterationBindingType(collectionType.valueType, valueLocation)
|
|
1045
|
+
const keyBinding = withParserRanges({
|
|
1046
|
+
kind: /** @type {const} */ ("ValueBinding"), location: keyLocation, mutable: /** @type {const} */ (false),
|
|
1047
|
+
name: keyParameter.name, type: keyType
|
|
1048
|
+
}, {name: keyLocation})
|
|
1049
|
+
const valueBinding = withParserRanges({
|
|
1050
|
+
kind: /** @type {const} */ ("ValueBinding"), location: valueLocation, mutable: /** @type {const} */ (false),
|
|
1051
|
+
name: valueParameter.name, type: valueType
|
|
1052
|
+
}, {name: valueLocation})
|
|
1053
|
+
const bodyContext = {...context, bindings: new Map(context.bindings), loopDepth: (context.loopDepth ?? 0) + 1}
|
|
1054
|
+
|
|
1055
|
+
bodyContext.bindings.set(keyParameter.name, keyType)
|
|
1056
|
+
bodyContext.bindings.set(valueParameter.name, valueType)
|
|
1057
|
+
return withParserRanges({
|
|
1058
|
+
body: convertBlock(block.body instanceof StatementsNode ? block.body : null, comments, bodyContext, filename,
|
|
1059
|
+
source, nodeLocation(block, filename, source)),
|
|
1060
|
+
keyBinding,
|
|
1061
|
+
kind: /** @type {const} */ ("ForEachMapStatement"),
|
|
1062
|
+
location,
|
|
1063
|
+
map: convertExpression(node.receiver, filename, source, context),
|
|
1064
|
+
valueBinding
|
|
1065
|
+
}, {operator: prismLocation(node.messageLoc, filename, source)})
|
|
1066
|
+
}
|
|
1067
|
+
const parameter = /** @type {RequiredParameterNode} */ (parameters.requireds[0])
|
|
1068
|
+
|
|
1069
|
+
if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") {
|
|
1070
|
+
return unsupportedSyntax("ruby", "map iteration without key/value pair binding", nodeLocation(node.receiver, filename, source))
|
|
1071
|
+
}
|
|
1072
|
+
if (!collectionType || collectionType.kind != "ListType") {
|
|
1073
|
+
return missingType("ruby", "Iteration collection", nodeLocation(node.receiver, filename, source))
|
|
1074
|
+
}
|
|
1075
|
+
const bindingLocation = nodeLocation(parameter, filename, source)
|
|
1076
|
+
const inferredType = collectionType.elementType
|
|
1077
|
+
const bindingType = iterationBindingType(inferredType, bindingLocation)
|
|
1078
|
+
const valueBinding = withParserRanges({
|
|
1079
|
+
kind: /** @type {const} */ ("ValueBinding"),
|
|
1080
|
+
location: bindingLocation,
|
|
1081
|
+
mutable: /** @type {const} */ (false),
|
|
1082
|
+
name: parameter.name,
|
|
1083
|
+
type: bindingType
|
|
1084
|
+
}, {name: bindingLocation})
|
|
1085
|
+
const bodyContext = {...context, bindings: new Map(context.bindings), loopDepth: (context.loopDepth ?? 0) + 1}
|
|
1086
|
+
|
|
1087
|
+
bodyContext.bindings.set(parameter.name, bindingType)
|
|
1088
|
+
return withParserRanges({
|
|
1089
|
+
body: convertBlock(
|
|
1090
|
+
block.body instanceof StatementsNode ? block.body : null,
|
|
1091
|
+
comments,
|
|
1092
|
+
bodyContext,
|
|
1093
|
+
filename,
|
|
1094
|
+
source,
|
|
1095
|
+
nodeLocation(block, filename, source)
|
|
1096
|
+
),
|
|
1097
|
+
kind: /** @type {const} */ ("ForEachStatement"),
|
|
1098
|
+
list: convertExpression(node.receiver, filename, source, context),
|
|
1099
|
+
location,
|
|
1100
|
+
valueBinding
|
|
1101
|
+
}, {operator: prismLocation(node.messageLoc, filename, source)})
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Finds the first parser descendant matching one excluded block construct.
|
|
1106
|
+
* @param {import("@ruby/prism").Node | null} node - Parser subtree.
|
|
1107
|
+
* @param {(node: import("@ruby/prism").Node) => boolean} predicate - Exclusion predicate.
|
|
1108
|
+
* @returns {import("@ruby/prism").Node | undefined} First source-ordered match.
|
|
1109
|
+
*/
|
|
1110
|
+
function findRubyDescendant(node, predicate) {
|
|
1111
|
+
if (!node) return undefined
|
|
1112
|
+
if (predicate(node)) return node
|
|
1113
|
+
for (const child of node.compactChildNodes()) {
|
|
1114
|
+
const match = findRubyDescendant(child, predicate)
|
|
1115
|
+
|
|
1116
|
+
if (match) return match
|
|
1117
|
+
}
|
|
1118
|
+
return undefined
|
|
1119
|
+
}
|
|
1120
|
+
|
|
413
1121
|
/**
|
|
414
1122
|
* Converts one ordered Prism statement list.
|
|
415
1123
|
* @param {StatementsNode | null} node - Prism statements, or an empty source block.
|
|
416
1124
|
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
417
|
-
* @param {
|
|
1125
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
418
1126
|
* @param {string} filename - Source filename.
|
|
419
1127
|
* @param {string} source - Complete source.
|
|
420
1128
|
* @param {import("../semantic/types.js").SourceLocation} fallbackLocation - Empty block location.
|
|
421
1129
|
* @returns {import("../semantic/types.js").Block} Semantic block.
|
|
422
1130
|
*/
|
|
423
|
-
function convertBlock(node, comments,
|
|
1131
|
+
function convertBlock(node, comments, context, filename, source, fallbackLocation) {
|
|
424
1132
|
return {
|
|
425
1133
|
kind: "Block",
|
|
426
1134
|
location: node ? nodeLocation(node, filename, source) : fallbackLocation,
|
|
427
|
-
statements: node ? node.body.map((statement) => convertStatement(statement, comments,
|
|
1135
|
+
statements: node ? node.body.map((statement) => convertStatement(statement, comments, context, filename, source)) : []
|
|
428
1136
|
}
|
|
429
1137
|
}
|
|
430
1138
|
|
|
@@ -448,36 +1156,59 @@ function isImmediateCommentGap(gap) {
|
|
|
448
1156
|
}
|
|
449
1157
|
|
|
450
1158
|
/**
|
|
451
|
-
* Requires an exact supported Ruby
|
|
1159
|
+
* Requires an exact supported Ruby value-type spelling.
|
|
452
1160
|
* @param {string | undefined} sourceType - Ruby type comment value.
|
|
453
1161
|
* @param {string} subject - Typed subject.
|
|
454
1162
|
* @param {import("../semantic/types.js").SourceLocation} location - Source location.
|
|
1163
|
+
* @param {string} source - Complete parser input.
|
|
455
1164
|
* @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type comment token location.
|
|
456
|
-
* @
|
|
1165
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
|
|
1166
|
+
* @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
|
|
1167
|
+
* @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
|
|
457
1168
|
*/
|
|
458
|
-
function convertType(sourceType, subject, location, typeLocation = location) {
|
|
459
|
-
return
|
|
1169
|
+
function convertType(sourceType, subject, location, source, typeLocation = location, recordNames = new Map(), typeParameters = new Map()) {
|
|
1170
|
+
return documentedValueType({language: "ruby", location: typeLocation, ownerLocation: location, records: recordNames, source, sourceType, subject, typeParameters})
|
|
460
1171
|
}
|
|
461
1172
|
|
|
462
1173
|
/**
|
|
463
|
-
* Converts a Ruby definition.
|
|
1174
|
+
* Converts a Ruby definition signature before body expressions.
|
|
464
1175
|
* @param {DefNode} node - Prism definition.
|
|
465
1176
|
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
466
1177
|
* @param {string} filename - Source filename.
|
|
467
1178
|
* @param {string} source - Complete source.
|
|
468
|
-
* @
|
|
1179
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
1180
|
+
* @param {string} ownerId - Stable function identity.
|
|
1181
|
+
* @param {boolean} [allowBuiltinOutput] - Whether compiler authority admits the exact output facade function.
|
|
1182
|
+
* @returns {RubyFunctionSignature} Semantic signature.
|
|
469
1183
|
*/
|
|
470
|
-
function
|
|
1184
|
+
function convertFunctionSignature(node, comments, filename, source, recordNames, ownerId, allowBuiltinOutput = false) {
|
|
471
1185
|
const location = nodeLocation(node, filename, source)
|
|
1186
|
+
const parameterList = node.parameters
|
|
1187
|
+
|
|
1188
|
+
if (node.receiver) return unsupportedSyntax("ruby", "singleton method", nodeLocation(node.receiver, filename, source))
|
|
1189
|
+
const nameLocation = prismLocation(node.nameLoc, filename, source)
|
|
472
1190
|
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1191
|
+
if (node.name == "puts" && !allowBuiltinOutput) {
|
|
1192
|
+
return unsupportedSyntax("ruby", "function 'puts' captures built-in printing", nameLocation)
|
|
1193
|
+
}
|
|
1194
|
+
const unsupportedParameter = parameterList && [
|
|
1195
|
+
parameterList.optionals[0], parameterList.rest, parameterList.posts[0], parameterList.keywords[0],
|
|
1196
|
+
parameterList.keywordRest, parameterList.block,
|
|
1197
|
+
parameterList.requireds.find((parameter) => !(parameter instanceof RequiredParameterNode))
|
|
1198
|
+
].find(Boolean)
|
|
1199
|
+
|
|
1200
|
+
if (unsupportedParameter) {
|
|
1201
|
+
return unsupportedSyntax("ruby", "parameters", nodeLocation(unsupportedParameter, filename, source))
|
|
477
1202
|
}
|
|
478
1203
|
|
|
479
|
-
const declaredTypes = typeComments(comments, node, filename, source)
|
|
480
|
-
const
|
|
1204
|
+
const declaredTypes = typeComments(comments, node, filename, source, ownerId)
|
|
1205
|
+
const typeParameterNames = new Map((declaredTypes.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1206
|
+
const requiredParameters = parameterList?.requireds ?? []
|
|
1207
|
+
const extraAnnotation = [...declaredTypes.parameters.keys()].find((name) =>
|
|
1208
|
+
!requiredParameters.some((parameter) => parameter instanceof RequiredParameterNode && parameter.name == name))
|
|
1209
|
+
|
|
1210
|
+
if (extraAnnotation) return unsupportedSyntax("ruby", `annotation for unknown parameter '${extraAnnotation}'`, location)
|
|
1211
|
+
const parameters = requiredParameters.map((parameter) => {
|
|
481
1212
|
if (!(parameter instanceof RequiredParameterNode)) return unsupportedSyntax("ruby", parameter.constructor.name, location)
|
|
482
1213
|
|
|
483
1214
|
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
@@ -488,82 +1219,467 @@ function convertFunction(node, comments, filename, source) {
|
|
|
488
1219
|
kind: /** @type {const} */ ("Parameter"),
|
|
489
1220
|
location: parameterLocation,
|
|
490
1221
|
name: parameter.name,
|
|
491
|
-
type: convertType(declaredType?.sourceType, `Parameter '${parameter.name}'`, parameterLocation,
|
|
1222
|
+
type: convertType(declaredType?.sourceType, `Parameter '${parameter.name}'`, parameterLocation, source,
|
|
1223
|
+
declaredType?.location, recordNames, typeParameterNames)
|
|
492
1224
|
}, {name: parameterLocation})
|
|
493
1225
|
})
|
|
494
|
-
|
|
495
|
-
|
|
1226
|
+
return {
|
|
1227
|
+
location,
|
|
1228
|
+
name: node.name,
|
|
1229
|
+
nameLocation,
|
|
1230
|
+
parameters,
|
|
1231
|
+
returnType: declaredTypes.returnType?.sourceType == "[void]"
|
|
1232
|
+
? requireSourceReturnType("ruby", "[void]", `Function '${node.name}' return`, location, declaredTypes.returnType.location)
|
|
1233
|
+
: convertType(declaredTypes.returnType?.sourceType, `Function '${node.name}' return`, location, source,
|
|
1234
|
+
declaredTypes.returnType?.location, recordNames, typeParameterNames),
|
|
1235
|
+
...(declaredTypes.typeParameters ? {typeParameters: declaredTypes.typeParameters} : {})
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Converts a Ruby definition body using all already-proved signatures.
|
|
1241
|
+
* @param {DefNode} node - Prism definition.
|
|
1242
|
+
* @param {RubyFunctionSignature} signature - Converted signature.
|
|
1243
|
+
* @param {Map<string, RubyFunctionSignature>} functions - Module signatures.
|
|
1244
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
|
|
1245
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
|
|
1246
|
+
* @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
|
|
1247
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
|
|
1248
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
|
|
1249
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
1250
|
+
* @param {string} filename - Source filename.
|
|
1251
|
+
* @param {string} source - Complete source.
|
|
1252
|
+
* @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
|
|
1253
|
+
*/
|
|
1254
|
+
function convertFunction(node, signature, functions, recordNames, records, classes, errorNames, errors, comments, filename, source) {
|
|
1255
|
+
const context = {
|
|
1256
|
+
bindings: new Map(signature.parameters.map((parameter) => [parameter.name, parameter.type])),
|
|
1257
|
+
classes,
|
|
1258
|
+
errorNames,
|
|
1259
|
+
errors,
|
|
1260
|
+
functions,
|
|
1261
|
+
orderedMapDeclarations: orderedMapDeclarationsForRuby(node.body instanceof StatementsNode ? node.body.body : []),
|
|
1262
|
+
recordNames,
|
|
1263
|
+
records,
|
|
1264
|
+
returnType: signature.returnType,
|
|
1265
|
+
typeParameters: new Map((signature.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1266
|
+
}
|
|
1267
|
+
const body = convertBlock(
|
|
1268
|
+
node.body instanceof StatementsNode ? node.body : null,
|
|
1269
|
+
comments,
|
|
1270
|
+
context,
|
|
1271
|
+
filename,
|
|
1272
|
+
source,
|
|
1273
|
+
signature.location
|
|
1274
|
+
)
|
|
496
1275
|
|
|
497
1276
|
return withParserRanges({
|
|
498
1277
|
body,
|
|
499
1278
|
kind: "FunctionDeclaration",
|
|
500
|
-
location,
|
|
501
|
-
name:
|
|
502
|
-
parameters,
|
|
503
|
-
returnType:
|
|
504
|
-
|
|
1279
|
+
location: signature.location,
|
|
1280
|
+
name: signature.name,
|
|
1281
|
+
parameters: signature.parameters,
|
|
1282
|
+
returnType: signature.returnType,
|
|
1283
|
+
...(signature.typeParameters ? {typeParameters: signature.typeParameters} : {})
|
|
1284
|
+
}, {name: signature.nameLocation})
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Resolves parser-owned pair blocks to exact declarations inside one lexical owner.
|
|
1289
|
+
* Ambiguous same-name declarations produce no evidence and fail during loop conversion.
|
|
1290
|
+
* @param {import("@ruby/prism").Node[]} nodes - Function body or entry-point roots.
|
|
1291
|
+
* @returns {Set<object>} Exact pair-iterated parser declarations.
|
|
1292
|
+
*/
|
|
1293
|
+
function orderedMapDeclarationsForRuby(nodes) {
|
|
1294
|
+
/** @type {{declaration: LocalVariableWriteNode, name: string, offset: number}[]} */
|
|
1295
|
+
const declarations = []
|
|
1296
|
+
/** @type {{name: string, offset: number}[]} */
|
|
1297
|
+
const receivers = []
|
|
1298
|
+
/**
|
|
1299
|
+
* Visits one parser candidate.
|
|
1300
|
+
* @param {import("@ruby/prism").Node} node - Parser candidate.
|
|
1301
|
+
*/
|
|
1302
|
+
const visit = (node) => {
|
|
1303
|
+
if (node instanceof DefNode || node instanceof ClassNode) return
|
|
1304
|
+
if (node instanceof LocalVariableWriteNode) declarations.push({
|
|
1305
|
+
declaration: node,
|
|
1306
|
+
name: node.name,
|
|
1307
|
+
offset: node.location.startOffset
|
|
1308
|
+
})
|
|
1309
|
+
if (node instanceof CallNode && node.receiver instanceof LocalVariableReadNode && node.name == "each" &&
|
|
1310
|
+
node.block instanceof BlockNode && node.block.parameters instanceof BlockParametersNode &&
|
|
1311
|
+
node.block.parameters.parameters?.requireds.length == 2) receivers.push({
|
|
1312
|
+
name: node.receiver.name,
|
|
1313
|
+
offset: node.location.startOffset
|
|
1314
|
+
})
|
|
1315
|
+
for (const child of node.compactChildNodes()) visit(child)
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
for (const node of nodes) visit(node)
|
|
1319
|
+
const selected = new Set()
|
|
1320
|
+
for (const receiver of receivers) {
|
|
1321
|
+
const matches = declarations.filter(({name, offset}) => name == receiver.name && offset < receiver.offset)
|
|
1322
|
+
|
|
1323
|
+
if (matches.length == 1) selected.add(matches[0].declaration)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return selected
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Converts one exact empty StandardError subclass.
|
|
1331
|
+
* @param {ClassNode} node - Prism class declaration.
|
|
1332
|
+
* @param {import("../semantic/types.js").ErrorDeclaration} declaration - Predeclared semantic error.
|
|
1333
|
+
* @param {string} filename - Source filename.
|
|
1334
|
+
* @param {string} source - Complete source.
|
|
1335
|
+
* @returns {import("../semantic/types.js").ErrorDeclaration} Semantic error declaration.
|
|
1336
|
+
*/
|
|
1337
|
+
function convertRubyError(node, declaration, filename, source) {
|
|
1338
|
+
const location = nodeLocation(node, filename, source)
|
|
1339
|
+
|
|
1340
|
+
if (!(node.constantPath instanceof ConstantReadNode) || !(node.superclass instanceof ConstantReadNode) ||
|
|
1341
|
+
node.superclass.name != "StandardError" || !node.inheritanceOperatorLoc || node.body) {
|
|
1342
|
+
return unsupportedSyntax("ruby", "noncanonical typed error declaration", location)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return withParserRanges(declaration, {
|
|
1346
|
+
name: nodeLocation(node.constantPath, filename, source),
|
|
1347
|
+
type: nodeLocation(node.superclass, filename, source)
|
|
1348
|
+
})
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Converts the bounded Ruby reference-class profile.
|
|
1353
|
+
* @param {ClassNode} node - Prism class declaration.
|
|
1354
|
+
* @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
|
|
1355
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
|
|
1356
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
|
|
1357
|
+
* @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
|
|
1358
|
+
* @param {Map<string, RubyFunctionSignature>} functions - Top-level signatures.
|
|
1359
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by name.
|
|
1360
|
+
* @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Errors by identity.
|
|
1361
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
1362
|
+
* @param {string} filename - Source filename.
|
|
1363
|
+
* @param {string} source - Complete source.
|
|
1364
|
+
* @returns {import("../semantic/types.js").ClassDeclaration} Semantic class.
|
|
1365
|
+
*/
|
|
1366
|
+
function convertRubyReferenceClass(node, declaration, nominalNames, records, classes, functions, errorNames, errors,
|
|
1367
|
+
comments, filename, source) {
|
|
1368
|
+
const location = nodeLocation(node, filename, source)
|
|
1369
|
+
|
|
1370
|
+
if (!(node.constantPath instanceof ConstantReadNode) || node.superclass || node.inheritanceOperatorLoc ||
|
|
1371
|
+
!(node.body instanceof StatementsNode)) {
|
|
1372
|
+
return unsupportedSyntax("ruby", "reference class inheritance, qualified name, or empty body", location)
|
|
1373
|
+
}
|
|
1374
|
+
const members = node.body.body
|
|
1375
|
+
const initializers = members.filter((member) => member instanceof DefNode && member.name == "initialize")
|
|
1376
|
+
const methodNodes = /** @type {DefNode[]} */ (members.filter((member) => member instanceof DefNode && member.name != "initialize"))
|
|
1377
|
+
const reservedMethod = methodNodes.find((method) => referenceMethodHooks.has(method.name))
|
|
1378
|
+
|
|
1379
|
+
if (initializers.length != 1 || members.some((member) => !(member instanceof DefNode)) || reservedMethod) {
|
|
1380
|
+
return unsupportedSyntax("ruby", "reference class requires one initializer and ordinary methods", location)
|
|
1381
|
+
}
|
|
1382
|
+
const initializer = /** @type {DefNode} */ (initializers[0])
|
|
1383
|
+
const parameters = initializer.parameters?.requireds ?? []
|
|
1384
|
+
const invalidParameter = initializer.parameters && [initializer.parameters.optionals[0], initializer.parameters.rest,
|
|
1385
|
+
initializer.parameters.posts[0], initializer.parameters.keywords[0], initializer.parameters.keywordRest,
|
|
1386
|
+
initializer.parameters.block].find(Boolean)
|
|
1387
|
+
const statements = initializer.body instanceof StatementsNode ? initializer.body.body : []
|
|
1388
|
+
const annotations = typeComments(comments, initializer, filename, source)
|
|
1389
|
+
const acquisition = statements.length == 1 && statements[0] instanceof InstanceVariableWriteNode &&
|
|
1390
|
+
statements[0].name == "@resource" && statements[0].value instanceof CallNode && !statements[0].value.receiver &&
|
|
1391
|
+
!statements[0].value.block && statements[0].value.name == "v1_connect" &&
|
|
1392
|
+
statements[0].value.arguments_?.arguments_.length == 2 && parameters.length == 2 &&
|
|
1393
|
+
parameters[0] instanceof RequiredParameterNode && parameters[0].name == "host" &&
|
|
1394
|
+
parameters[1] instanceof RequiredParameterNode && parameters[1].name == "port" &&
|
|
1395
|
+
statements[0].value.arguments_.arguments_[0] instanceof LocalVariableReadNode &&
|
|
1396
|
+
statements[0].value.arguments_.arguments_[0].name == "host" &&
|
|
1397
|
+
statements[0].value.arguments_.arguments_[1] instanceof LocalVariableReadNode &&
|
|
1398
|
+
statements[0].value.arguments_.arguments_[1].name == "port" && declaration.name == "TCPSocket" &&
|
|
1399
|
+
functions.get("v1_connect")?.returnType.kind == "OwnedResourceType"
|
|
1400
|
+
|
|
1401
|
+
if (initializer.receiver || invalidParameter || annotations.returnType || annotations.parameters.size != parameters.length ||
|
|
1402
|
+
!acquisition && statements.length != parameters.length) {
|
|
1403
|
+
return unsupportedSyntax("ruby", "noncanonical reference class initializer", nodeLocation(initializer, filename, source))
|
|
1404
|
+
}
|
|
1405
|
+
declaration.fields = acquisition ? [withParserRanges({
|
|
1406
|
+
id: `${declaration.id}:field:0`,
|
|
1407
|
+
kind: /** @type {const} */ ("PrivateField"),
|
|
1408
|
+
location: nodeLocation(/** @type {InstanceVariableWriteNode} */ (statements[0]), filename, source),
|
|
1409
|
+
name: "resource",
|
|
1410
|
+
type: /** @type {import("../semantic/types.js").OwnedResourceType} */ (structuredClone(functions.get("v1_connect")?.returnType))
|
|
1411
|
+
}, {name: nodeLocation(/** @type {InstanceVariableWriteNode} */ (statements[0]), filename, source)})] : parameters.map((candidate, index) => {
|
|
1412
|
+
const parameter = /** @type {RequiredParameterNode} */ (candidate)
|
|
1413
|
+
const assignment = statements[index]
|
|
1414
|
+
const annotated = annotations.parameters.get(parameter.name)
|
|
1415
|
+
|
|
1416
|
+
if (!(parameter instanceof RequiredParameterNode) || !(assignment instanceof InstanceVariableWriteNode) ||
|
|
1417
|
+
assignment.name != `@${parameter.name}` || !(assignment.value instanceof LocalVariableReadNode) ||
|
|
1418
|
+
assignment.value.name != parameter.name || !annotated) {
|
|
1419
|
+
return unsupportedSyntax("ruby", "private field not initialized exactly once in parameter order",
|
|
1420
|
+
nodeLocation(assignment ?? initializer, filename, source))
|
|
1421
|
+
}
|
|
1422
|
+
return withParserRanges({id: `${declaration.id}:field:${index}`, kind: /** @type {const} */ ("PrivateField"),
|
|
1423
|
+
location: nodeLocation(assignment, filename, source), name: parameter.name,
|
|
1424
|
+
type: convertType(annotated.sourceType, `Private field '${parameter.name}'`, nodeLocation(parameter, filename, source),
|
|
1425
|
+
source, annotated.location, nominalNames)}, {name: nodeLocation(assignment, filename, source)})
|
|
1426
|
+
})
|
|
1427
|
+
const constructorParameters = parameters.map((candidate, index) => {
|
|
1428
|
+
const parameter = /** @type {RequiredParameterNode} */ (candidate)
|
|
1429
|
+
const parameterLocation = nodeLocation(parameter, filename, source)
|
|
1430
|
+
const annotated = annotations.parameters.get(parameter.name)
|
|
1431
|
+
|
|
1432
|
+
return withParserRanges({kind: /** @type {const} */ ("Parameter"), location: parameterLocation, name: parameter.name,
|
|
1433
|
+
type: acquisition
|
|
1434
|
+
? convertType(annotated?.sourceType, `Constructor parameter '${parameter.name}'`, parameterLocation, source,
|
|
1435
|
+
annotated?.location, nominalNames)
|
|
1436
|
+
: declaration.fields[index].type}, {name: parameterLocation})
|
|
1437
|
+
})
|
|
1438
|
+
declaration.constructor = withParserRanges({body: /** @type {import("../semantic/types.js").Block} */ ({}),
|
|
1439
|
+
id: `${declaration.id}:constructor`, kind: /** @type {const} */ ("ConstructorDeclaration"), location: nodeLocation(initializer, filename, source),
|
|
1440
|
+
parameters: constructorParameters}, {constructor: prismLocation(initializer.nameLoc, filename, source)})
|
|
1441
|
+
const methodSignatures = methodNodes.map((method, index) =>
|
|
1442
|
+
convertFunctionSignature(method, comments, filename, source, nominalNames, `${declaration.id}:method:${index}`))
|
|
1443
|
+
declaration.methods = methodSignatures.map((signature, index) => withParserRanges({
|
|
1444
|
+
body: /** @type {import("../semantic/types.js").Block} */ ({}), id: `${declaration.id}:method:${index}`,
|
|
1445
|
+
kind: /** @type {const} */ ("MethodDeclaration"), location: signature.location, name: signature.name,
|
|
1446
|
+
parameters: signature.parameters, returnType: signature.returnType
|
|
1447
|
+
}, {name: signature.nameLocation}))
|
|
1448
|
+
const base = {bindings: new Map(), classes, currentClass: declaration, errorNames, errors, functions,
|
|
1449
|
+
recordNames: nominalNames, records}
|
|
1450
|
+
|
|
1451
|
+
declaration.constructor.body = convertBlock(initializer.body instanceof StatementsNode ? initializer.body : null,
|
|
1452
|
+
comments, {...base, bindings: new Map(constructorParameters.map((parameter) => [parameter.name, parameter.type])),
|
|
1453
|
+
returnType: {kind: /** @type {const} */ ("TypeReference"), name: /** @type {const} */ ("void")}},
|
|
1454
|
+
filename, source, declaration.constructor.location)
|
|
1455
|
+
for (let index = 0; index < methodNodes.length; index += 1) {
|
|
1456
|
+
const method = declaration.methods[index]
|
|
1457
|
+
const methodBody = methodNodes[index].body
|
|
1458
|
+
|
|
1459
|
+
method.body = convertBlock(methodBody instanceof StatementsNode ? methodBody : null,
|
|
1460
|
+
comments, {...base, bindings: new Map(method.parameters.map((parameter) => [parameter.name, parameter.type])),
|
|
1461
|
+
returnType: method.returnType}, filename, source, method.location)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return withParserRanges(declaration, {name: nodeLocation(node.constantPath, filename, source)})
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Converts one exact typed-reader/initializer/freeze Ruby record profile.
|
|
1469
|
+
* @param {ClassNode} node - Prism class declaration.
|
|
1470
|
+
* @param {import("../semantic/types.js").RecordDeclaration} declaration - Predeclared nominal identity.
|
|
1471
|
+
* @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by name.
|
|
1472
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
|
|
1473
|
+
* @param {string} filename - Source filename.
|
|
1474
|
+
* @param {string} source - Complete source.
|
|
1475
|
+
* @returns {import("../semantic/types.js").RecordDeclaration} Semantic record.
|
|
1476
|
+
*/
|
|
1477
|
+
function convertRubyRecord(node, declaration, recordNames, comments, filename, source) {
|
|
1478
|
+
const location = nodeLocation(node, filename, source)
|
|
1479
|
+
const typeParameterNames = new Map((declaration.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
|
|
1480
|
+
|
|
1481
|
+
if (!(node.constantPath instanceof ConstantReadNode) || node.superclass || node.inheritanceOperatorLoc) {
|
|
1482
|
+
return unsupportedSyntax("ruby", "record inheritance or qualified name", location)
|
|
1483
|
+
}
|
|
1484
|
+
const members = node.body instanceof StatementsNode ? node.body.body : []
|
|
1485
|
+
const initializerIndex = members.findIndex((member) => member instanceof DefNode && member.name == "initialize")
|
|
1486
|
+
|
|
1487
|
+
if (initializerIndex < 0 || initializerIndex != members.length - 1 || members.slice(0, initializerIndex).some((member) =>
|
|
1488
|
+
!(member instanceof CallNode) || member.receiver || member.name != "attr_reader" || member.block ||
|
|
1489
|
+
member.arguments_?.arguments_.length != 1 || !(member.arguments_.arguments_[0] instanceof SymbolNode))) {
|
|
1490
|
+
return unsupportedSyntax("ruby", "record class body outside typed readers and one initializer", nodeLocation(members.find((member, index) =>
|
|
1491
|
+
index >= initializerIndex + 1 || !(member instanceof CallNode) || member.name != "attr_reader") ?? node, filename, source))
|
|
1492
|
+
}
|
|
1493
|
+
const readerNodes = /** @type {CallNode[]} */ (members.slice(0, initializerIndex))
|
|
1494
|
+
declaration.fields = readerNodes.map((reader) => {
|
|
1495
|
+
const symbol = /** @type {SymbolNode} */ (reader.arguments_?.arguments_[0])
|
|
1496
|
+
const metadata = associatedComments(comments, reader, source).map((comment) => commentTokens(comment, filename, source))
|
|
1497
|
+
const typeMetadata = metadata.filter(([token]) => token?.text == "@type")
|
|
1498
|
+
const fieldLocation = nodeLocation(reader, filename, source)
|
|
1499
|
+
|
|
1500
|
+
if (typeMetadata.length != 1 || typeMetadata[0].length < 2 || metadata.some(([token]) => token?.text?.startsWith("@type") && token.text != "@type")) {
|
|
1501
|
+
return unsupportedSyntax("ruby", "missing or malformed record reader type", fieldLocation)
|
|
1502
|
+
}
|
|
1503
|
+
const declaredType = joinedCommentToken(typeMetadata[0], 1, source)
|
|
1504
|
+
const field = {
|
|
1505
|
+
kind: /** @type {const} */ ("RecordField"),
|
|
1506
|
+
location: fieldLocation,
|
|
1507
|
+
name: symbol.unescaped.value,
|
|
1508
|
+
type: convertType(declaredType.sourceType, `Record field '${symbol.unescaped.value}'`, fieldLocation, source,
|
|
1509
|
+
declaredType.location, recordNames, typeParameterNames)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return withParserRanges(field, {name: symbol.valueLoc ? prismLocation(symbol.valueLoc, filename, source) : fieldLocation})
|
|
1513
|
+
})
|
|
1514
|
+
const initializer = /** @type {DefNode} */ (members[initializerIndex])
|
|
1515
|
+
const parameterList = initializer.parameters
|
|
1516
|
+
const parameters = parameterList?.requireds ?? []
|
|
1517
|
+
const invalidParameter = parameterList && [parameterList.optionals[0], parameterList.rest, parameterList.posts[0],
|
|
1518
|
+
parameterList.keywords[0], parameterList.keywordRest, parameterList.block].find(Boolean)
|
|
1519
|
+
const initializerStatements = initializer.body instanceof StatementsNode ? initializer.body.body : []
|
|
1520
|
+
|
|
1521
|
+
if (initializer.receiver || invalidParameter || parameters.length != declaration.fields.length ||
|
|
1522
|
+
parameters.some((parameter, index) => !(parameter instanceof RequiredParameterNode) || parameter.name != declaration.fields[index].name) ||
|
|
1523
|
+
initializerStatements.length != declaration.fields.length + 1) {
|
|
1524
|
+
return unsupportedSyntax("ruby", "noncanonical record initializer", nodeLocation(initializer, filename, source))
|
|
1525
|
+
}
|
|
1526
|
+
const annotations = typeComments(comments, initializer, filename, source)
|
|
1527
|
+
|
|
1528
|
+
if (annotations.returnType || annotations.parameters.size != declaration.fields.length) {
|
|
1529
|
+
return unsupportedSyntax("ruby", "incomplete record initializer annotations", nodeLocation(initializer, filename, source))
|
|
1530
|
+
}
|
|
1531
|
+
for (let index = 0; index < declaration.fields.length; index += 1) {
|
|
1532
|
+
const field = declaration.fields[index]
|
|
1533
|
+
const parameter = /** @type {RequiredParameterNode} */ (parameters[index])
|
|
1534
|
+
const assignment = initializerStatements[index]
|
|
1535
|
+
const annotated = annotations.parameters.get(parameter.name)
|
|
1536
|
+
|
|
1537
|
+
if (!(assignment instanceof InstanceVariableWriteNode) || assignment.name != `@${field.name}` ||
|
|
1538
|
+
!(assignment.value instanceof LocalVariableReadNode) || assignment.value.name != field.name || !annotated) {
|
|
1539
|
+
return unsupportedSyntax("ruby", "record field not initialized exactly once", nodeLocation(assignment ?? initializer, filename, source))
|
|
1540
|
+
}
|
|
1541
|
+
const parameterType = convertType(annotated.sourceType, `Record initializer parameter '${parameter.name}'`,
|
|
1542
|
+
nodeLocation(parameter, filename, source), source, annotated.location, recordNames, typeParameterNames)
|
|
1543
|
+
|
|
1544
|
+
if (!sameValueType(parameterType, field.type)) {
|
|
1545
|
+
return unsupportedSyntax("ruby", "record reader/initializer type mismatch", annotated.location)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const freeze = initializerStatements.at(-1)
|
|
1549
|
+
|
|
1550
|
+
if (!isRubyRecordFreeze(freeze)) {
|
|
1551
|
+
return unsupportedSyntax("ruby", "record initializer without final freeze", nodeLocation(freeze ?? initializer, filename, source))
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return withParserRanges(declaration, {name: nodeLocation(node.constantPath, filename, source)})
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Recognizes canonical source freezing or the generated root-owned Kernel implementation binding.
|
|
1559
|
+
* @param {import("@ruby/prism").Node | undefined} node - Final initializer statement.
|
|
1560
|
+
* @returns {boolean} Whether the statement owns the native record-freeze operation.
|
|
1561
|
+
*/
|
|
1562
|
+
function isRubyRecordFreeze(node) {
|
|
1563
|
+
if (!(node instanceof CallNode) || node.block) return false
|
|
1564
|
+
if (!node.receiver) return node.name == "freeze" && !node.arguments_
|
|
1565
|
+
if (node.name != "bind_call" || !node.callOperatorLoc || node.arguments_?.arguments_.length != 1 ||
|
|
1566
|
+
!(node.arguments_.arguments_[0] instanceof SelfNode) || !(node.receiver instanceof CallNode)) return false
|
|
1567
|
+
const method = node.receiver
|
|
1568
|
+
|
|
1569
|
+
if (method.name != "instance_method" || !method.callOperatorLoc || method.block ||
|
|
1570
|
+
method.arguments_?.arguments_.length != 1 || !(method.arguments_.arguments_[0] instanceof SymbolNode) ||
|
|
1571
|
+
!(method.receiver instanceof ConstantPathNode)) return false
|
|
1572
|
+
|
|
1573
|
+
return method.receiver.parent == null && method.receiver.delimiterLoc != null && method.receiver.name == "Kernel" &&
|
|
1574
|
+
method.arguments_.arguments_[0].unescaped.value == "freeze"
|
|
505
1575
|
}
|
|
506
1576
|
|
|
507
1577
|
/**
|
|
508
1578
|
* Converts the existing Ruby if/else terminal with restricted branch prefixes.
|
|
509
|
-
* @param {IfNode} node - Prism
|
|
1579
|
+
* @param {IfNode | UnlessNode} node - Prism conditional node.
|
|
510
1580
|
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
511
|
-
* @param {
|
|
1581
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
512
1582
|
* @param {string} filename - Source filename.
|
|
513
1583
|
* @param {string} source - Complete source.
|
|
514
1584
|
* @returns {import("../semantic/types.js").IfStatement} Semantic branch.
|
|
515
1585
|
*/
|
|
516
|
-
function convertIf(node, comments,
|
|
1586
|
+
function convertIf(node, comments, context, filename, source) {
|
|
517
1587
|
const location = nodeLocation(node, filename, source)
|
|
518
1588
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1589
|
+
const keywordLocation = node instanceof UnlessNode ? node.keywordLoc : node.ifKeywordLoc
|
|
1590
|
+
|
|
1591
|
+
if (!keywordLocation || keywordLocation.startOffset != node.location.startOffset) {
|
|
1592
|
+
return unsupportedSyntax("ruby", "modifier conditional", keywordLocation
|
|
1593
|
+
? prismLocation(keywordLocation, filename, source)
|
|
522
1594
|
: location)
|
|
523
1595
|
}
|
|
524
1596
|
|
|
525
|
-
|
|
526
|
-
|
|
1597
|
+
if (node.predicate instanceof LocalVariableReadNode &&
|
|
1598
|
+
context.bindings.get(node.predicate.name)?.kind == "OptionalType") {
|
|
1599
|
+
return unsupportedSyntax("ruby", "optional truthiness condition", nodeLocation(node.predicate, filename, source))
|
|
1600
|
+
}
|
|
527
1601
|
|
|
528
|
-
if (node.
|
|
529
|
-
|
|
1602
|
+
if (node instanceof UnlessNode && !exactNilTest(node.predicate, source)) {
|
|
1603
|
+
return unsupportedSyntax("ruby", "unless condition other than an exact nil? presence guard", nodeLocation(node.predicate, filename, source))
|
|
1604
|
+
}
|
|
530
1605
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1606
|
+
const predicate = convertExpression(node.predicate, filename, source, context)
|
|
1607
|
+
const condition = node instanceof UnlessNode && predicate.kind == "UnaryExpression" &&
|
|
1608
|
+
predicate.operand.kind == "OptionalIsPresent"
|
|
1609
|
+
? predicate.operand
|
|
1610
|
+
: predicate
|
|
1611
|
+
if (node instanceof UnlessNode && condition.kind != "OptionalIsPresent") {
|
|
1612
|
+
return unsupportedSyntax("ruby", "unless condition other than an exact nil? presence guard", nodeLocation(node.predicate, filename, source))
|
|
1613
|
+
}
|
|
1614
|
+
const consequentContext = {...context, bindings: new Map(context.bindings)}
|
|
1615
|
+
const alternateContext = {...context, bindings: new Map(context.bindings)}
|
|
1616
|
+
let alternate
|
|
1617
|
+
const subsequent = node instanceof UnlessNode ? node.elseClause : node.subsequent
|
|
1618
|
+
|
|
1619
|
+
if (subsequent) {
|
|
1620
|
+
if (subsequent instanceof ElseNode) {
|
|
1621
|
+
alternate = convertBlock(subsequent.statements, comments, alternateContext, filename, source,
|
|
1622
|
+
nodeLocation(subsequent, filename, source))
|
|
1623
|
+
} else if (subsequent instanceof IfNode) {
|
|
535
1624
|
alternate = {
|
|
536
1625
|
kind: /** @type {const} */ ("Block"),
|
|
537
|
-
location: nodeLocation(
|
|
538
|
-
statements: [convertIf(
|
|
1626
|
+
location: nodeLocation(subsequent, filename, source),
|
|
1627
|
+
statements: [convertIf(subsequent, comments, alternateContext, filename, source)]
|
|
539
1628
|
}
|
|
540
|
-
} else return unsupportedSyntax("ruby",
|
|
1629
|
+
} else return unsupportedSyntax("ruby", subsequent.constructor.name, nodeLocation(subsequent, filename, source))
|
|
541
1630
|
}
|
|
542
1631
|
|
|
543
1632
|
return {
|
|
544
1633
|
...(alternate ? {alternate} : {}),
|
|
545
|
-
condition
|
|
546
|
-
consequent: convertBlock(node.statements, comments,
|
|
1634
|
+
condition,
|
|
1635
|
+
consequent: convertBlock(node.statements, comments, consequentContext, filename, source, location),
|
|
547
1636
|
kind: "IfStatement",
|
|
548
1637
|
location
|
|
549
1638
|
}
|
|
550
1639
|
}
|
|
551
1640
|
|
|
1641
|
+
/**
|
|
1642
|
+
* Converts one ordinary block-form Ruby pre-condition loop.
|
|
1643
|
+
* @param {WhileNode} node - Prism while node.
|
|
1644
|
+
* @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
|
|
1645
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
1646
|
+
* @param {string} filename - Source filename.
|
|
1647
|
+
* @param {string} source - Complete source.
|
|
1648
|
+
* @returns {import("../semantic/types.js").WhileStatement} Semantic loop.
|
|
1649
|
+
*/
|
|
1650
|
+
function convertWhile(node, comments, context, filename, source) {
|
|
1651
|
+
const location = nodeLocation(node, filename, source)
|
|
1652
|
+
const keywordLocation = prismLocation(node.keywordLoc, filename, source)
|
|
1653
|
+
|
|
1654
|
+
if (node.isBeginModifier() || !node.closingLoc || node.keywordLoc.startOffset != node.location.startOffset) {
|
|
1655
|
+
return unsupportedSyntax("ruby", "modifier while", keywordLocation)
|
|
1656
|
+
}
|
|
1657
|
+
const bodyContext = {...context, bindings: new Map(context.bindings)}
|
|
1658
|
+
|
|
1659
|
+
return withParserRanges({
|
|
1660
|
+
body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
|
|
1661
|
+
condition: convertExpression(node.predicate, filename, source, context, undefined, true),
|
|
1662
|
+
kind: /** @type {const} */ ("WhileStatement"),
|
|
1663
|
+
location
|
|
1664
|
+
}, {keyword: keywordLocation})
|
|
1665
|
+
}
|
|
1666
|
+
|
|
552
1667
|
/**
|
|
553
1668
|
* Converts the supported Ruby puts entry point.
|
|
554
1669
|
* @param {import("@ruby/prism").Node} node - Prism node.
|
|
555
1670
|
* @param {string} filename - Source filename.
|
|
556
1671
|
* @param {string} source - Complete source.
|
|
1672
|
+
* @param {RubyConversionContext} context - Typed lexical conversion context.
|
|
557
1673
|
* @returns {import("../semantic/types.js").PrintStatement} Print statement.
|
|
558
1674
|
*/
|
|
559
|
-
function convertPrint(node, filename, source) {
|
|
1675
|
+
function convertPrint(node, filename, source, context) {
|
|
560
1676
|
const location = nodeLocation(node, filename, source)
|
|
561
1677
|
|
|
562
1678
|
if (!(node instanceof CallNode) || node.receiver || node.name != "puts" || node.arguments_?.arguments_.length != 1) {
|
|
563
1679
|
return unsupportedSyntax("ruby", node.constructor.name, location)
|
|
564
1680
|
}
|
|
565
1681
|
|
|
566
|
-
return {expression: convertExpression(node.arguments_.arguments_[0], filename, source), kind: "PrintStatement", location}
|
|
1682
|
+
return {expression: convertExpression(node.arguments_.arguments_[0], filename, source, context), kind: "PrintStatement", location}
|
|
567
1683
|
}
|
|
568
1684
|
|
|
569
1685
|
/**
|
|
@@ -571,9 +1687,11 @@ function convertPrint(node, filename, source) {
|
|
|
571
1687
|
* @param {object} input - Parser input.
|
|
572
1688
|
* @param {string} input.filename - Source filename.
|
|
573
1689
|
* @param {string} input.source - Source text.
|
|
1690
|
+
* @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} [input.capabilities] - Compiler-authorized declarations.
|
|
1691
|
+
* @param {{isEntry: boolean, classes?: Map<string, import("../semantic/types.js").ClassDeclaration>, functions: Map<string, import("../semantic/types.js").FunctionDeclaration>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, errors?: Map<string, import("../semantic/types.js").ErrorDeclaration>}} [input.program] - Resolved program imports and entry role.
|
|
574
1692
|
* @returns {import("../semantic/types.js").SemanticModule} Semantic module.
|
|
575
1693
|
*/
|
|
576
|
-
export function parseRuby({filename, source}) {
|
|
1694
|
+
export function parseRuby({capabilities = [], filename, source, program}) {
|
|
577
1695
|
const result = parsePrism(source, {filepath: filename})
|
|
578
1696
|
|
|
579
1697
|
if (result.errors.length > 0) {
|
|
@@ -585,13 +1703,105 @@ export function parseRuby({filename, source}) {
|
|
|
585
1703
|
|
|
586
1704
|
if (!(result.value instanceof ProgramNode)) throw new Error("Prism returned a non-program root.")
|
|
587
1705
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1706
|
+
let body = task034RubyBody(result.value.statements.body, result.comments, capabilities, filename, source)
|
|
1707
|
+
|
|
1708
|
+
if (program) {
|
|
1709
|
+
inspectRubyModule({filename, source})
|
|
1710
|
+
const moduleNode = /** @type {ModuleNode} */ (body.find((node) => node instanceof ModuleNode))
|
|
1711
|
+
|
|
1712
|
+
body = moduleNode.body instanceof StatementsNode ? moduleNode.body.body : []
|
|
1713
|
+
const moduleFunctionNodes = body.filter(isModuleFunctionMarker)
|
|
1714
|
+
const definitions = body.filter((node) => node instanceof DefNode)
|
|
1715
|
+
|
|
1716
|
+
if (definitions.length > 0 && (moduleFunctionNodes.length != 1 || body.indexOf(moduleFunctionNodes[0]) > body.indexOf(definitions[0]))) {
|
|
1717
|
+
return unsupportedSyntax("ruby", "module functions outside one leading module_function profile",
|
|
1718
|
+
nodeLocation(moduleFunctionNodes[1] ?? definitions[0], filename, source))
|
|
1719
|
+
}
|
|
1720
|
+
body = body.filter((node) => !isModuleFunctionMarker(node) && !rubyVisibilityMarker(node))
|
|
1721
|
+
}
|
|
1722
|
+
const classNodes = body.filter((node) => node instanceof ClassNode)
|
|
1723
|
+
const errorNodes = classNodes.filter((node) => node.superclass instanceof ConstantReadNode && node.superclass.name == "StandardError")
|
|
1724
|
+
const referenceClassNodes = classNodes.filter((node) => !errorNodes.includes(node) && node.body instanceof StatementsNode &&
|
|
1725
|
+
node.body.body.length > 0 && node.body.body.every((member) => member instanceof DefNode))
|
|
1726
|
+
const recordNodes = classNodes.filter((node) => !errorNodes.includes(node) && !referenceClassNodes.includes(node))
|
|
1727
|
+
const errorDeclarations = errorNodes.map((node, index) => ({
|
|
1728
|
+
id: `error:${index}`,
|
|
1729
|
+
kind: /** @type {const} */ ("ErrorDeclaration"),
|
|
1730
|
+
location: nodeLocation(node, filename, source),
|
|
1731
|
+
name: node.name
|
|
1732
|
+
}))
|
|
1733
|
+
const recordDeclarations = recordNodes.map((node, index) => {
|
|
1734
|
+
const id = `record:${index}`
|
|
1735
|
+
const documented = typeComments(result.comments, node, filename, source, id)
|
|
1736
|
+
|
|
1737
|
+
if (documented.parameters.size || documented.returnType) {
|
|
1738
|
+
return unsupportedSyntax("ruby", "function type annotation on record declaration", nodeLocation(node, filename, source))
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
return {
|
|
1742
|
+
fields: [],
|
|
1743
|
+
id,
|
|
1744
|
+
kind: /** @type {const} */ ("RecordDeclaration"),
|
|
1745
|
+
location: nodeLocation(node, filename, source),
|
|
1746
|
+
name: node.name,
|
|
1747
|
+
...(documented.typeParameters ? {typeParameters: documented.typeParameters} : {})
|
|
1748
|
+
}
|
|
1749
|
+
})
|
|
1750
|
+
const classDeclarations = referenceClassNodes.map((node, index) => ({
|
|
1751
|
+
constructor: /** @type {import("../semantic/types.js").ConstructorDeclaration} */ ({}), fields: [], id: `class:${index}`,
|
|
1752
|
+
kind: /** @type {const} */ ("ClassDeclaration"), location: nodeLocation(node, filename, source), methods: [], name: node.name
|
|
1753
|
+
}))
|
|
1754
|
+
const reopened = classDeclarations.find((declaration, index) =>
|
|
1755
|
+
classDeclarations.findIndex((candidate) => candidate.name == declaration.name) != index)
|
|
1756
|
+
|
|
1757
|
+
if (reopened) return unsupportedSyntax("ruby", `reference class reopening '${reopened.name}'`, reopened.location)
|
|
1758
|
+
const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} */ (
|
|
1759
|
+
new Map(program?.records ?? []))
|
|
1760
|
+
for (const [name, declaration] of program?.classes ?? []) recordNames.set(name, declaration)
|
|
1761
|
+
for (const resource of capabilities.flatMap(({resources}) => resources)) recordNames.set(resource.name, resource)
|
|
1762
|
+
const errorNames = new Map(program?.errors ?? [])
|
|
1763
|
+
for (const failure of capabilities.flatMap(({failures}) => failures)) {
|
|
1764
|
+
errorNames.set(failure.name, /** @type {import("../semantic/types.js").ErrorDeclaration} */ (/** @type {unknown} */ (failure)))
|
|
1765
|
+
}
|
|
1766
|
+
for (const declaration of errorDeclarations) errorNames.set(declaration.name, declaration)
|
|
1767
|
+
const errorsById = new Map([...errorNames.values()].map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
|
|
1768
|
+
const errors = errorNodes.map((node, index) => convertRubyError(node, errorDeclarations[index], filename, source))
|
|
1769
|
+
for (const declaration of recordDeclarations) recordNames.set(declaration.name, declaration)
|
|
1770
|
+
for (const declaration of classDeclarations) recordNames.set(declaration.name, declaration)
|
|
1771
|
+
const recordsById = new Map(recordDeclarations.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
|
|
1772
|
+
const classesById = new Map([
|
|
1773
|
+
...[...program?.classes?.values() ?? []].map((declaration) => /** @type {[string, import("../semantic/types.js").ClassDeclaration]} */ (
|
|
1774
|
+
[/** @type {string} */ (declaration.id), declaration])),
|
|
1775
|
+
...classDeclarations.map((declaration) => /** @type {[string, import("../semantic/types.js").ClassDeclaration]} */ (
|
|
1776
|
+
[/** @type {string} */ (declaration.id), declaration]))
|
|
1777
|
+
])
|
|
1778
|
+
const functionNodes = body.filter((node) => node instanceof DefNode)
|
|
1779
|
+
const allowsBuiltinOutput = capabilities.some(({operations}) => operations.some(({name}) => name == "v1_write_line"))
|
|
1780
|
+
const signatures = functionNodes.map((node, index) =>
|
|
1781
|
+
convertFunctionSignature(node, result.comments, filename, source, recordNames, `function:${index}`, allowsBuiltinOutput))
|
|
1782
|
+
const functionSignatures = new Map([...capabilityFunctionSignatures(capabilities, moduleLocation(filename, source)),
|
|
1783
|
+
...(program?.functions ?? [])].map(([localName, declaration]) => [localName, {
|
|
1784
|
+
location: declaration.location,
|
|
1785
|
+
name: localName,
|
|
1786
|
+
nameLocation: declaration.location,
|
|
1787
|
+
parameters: declaration.parameters,
|
|
1788
|
+
returnType: declaration.returnType,
|
|
1789
|
+
...(declaration.typeParameters ? {typeParameters: declaration.typeParameters} : {})
|
|
1790
|
+
}]))
|
|
1791
|
+
for (const signature of signatures) functionSignatures.set(signature.name, signature)
|
|
1792
|
+
const records = recordNodes.map((node, index) =>
|
|
1793
|
+
convertRubyRecord(node, recordDeclarations[index], recordNames, result.comments, filename, source))
|
|
1794
|
+
const classes = referenceClassNodes.map((node, index) => convertRubyReferenceClass(node, classDeclarations[index],
|
|
1795
|
+
recordNames, recordsById, classesById, functionSignatures, errorNames, errorsById, result.comments, filename, source))
|
|
1796
|
+
const functions = functionNodes.map((node, index) =>
|
|
1797
|
+
convertFunction(node, signatures[index], functionSignatures, recordNames, recordsById, classesById, errorNames, errorsById,
|
|
1798
|
+
result.comments, filename, source))
|
|
1799
|
+
const entryNodes = body.filter((node) => !(node instanceof DefNode) && !(node instanceof ClassNode))
|
|
592
1800
|
const location = moduleLocation(filename, source)
|
|
593
1801
|
|
|
594
|
-
if (
|
|
1802
|
+
if (program && !program.isEntry && entryNodes.length > 0) {
|
|
1803
|
+
return unsupportedSyntax("ruby", "top-level side effect outside the selected entry module", nodeLocation(entryNodes[0], filename, source))
|
|
1804
|
+
}
|
|
595
1805
|
const firstEntry = entryNodes[0]
|
|
596
1806
|
const entryLocation = firstEntry ? locationFromOffsets(
|
|
597
1807
|
filename,
|
|
@@ -602,13 +1812,17 @@ export function parseRuby({filename, source}) {
|
|
|
602
1812
|
(entryNodes.at(-1)?.location.startOffset ?? 0) + (entryNodes.at(-1)?.location.length ?? 0)
|
|
603
1813
|
)
|
|
604
1814
|
) : location
|
|
605
|
-
const
|
|
1815
|
+
const entryContext = {bindings: new Map(), classes: classesById, errorNames, errors: errorsById, functions: functionSignatures,
|
|
1816
|
+
orderedMapDeclarations: orderedMapDeclarationsForRuby(entryNodes), recordNames, records: recordsById}
|
|
606
1817
|
const entryBlock = {
|
|
607
1818
|
kind: /** @type {const} */ ("Block"),
|
|
608
1819
|
location: entryLocation,
|
|
609
|
-
statements: entryNodes.map((statement) => convertStatement(statement, result.comments,
|
|
1820
|
+
statements: entryNodes.map((statement) => convertStatement(statement, result.comments, entryContext, filename, source))
|
|
610
1821
|
}
|
|
611
1822
|
|
|
1823
|
+
if (!program && functions.length == 0 && capabilities.length == 0 && !blockReferencesProtectedEntry(entryBlock)) {
|
|
1824
|
+
return unsupportedSyntax("ruby", "module without a function", location)
|
|
1825
|
+
}
|
|
612
1826
|
return {
|
|
613
1827
|
entryPoint: {
|
|
614
1828
|
body: entryBlock,
|
|
@@ -617,6 +1831,318 @@ export function parseRuby({filename, source}) {
|
|
|
617
1831
|
},
|
|
618
1832
|
functions,
|
|
619
1833
|
kind: "Module",
|
|
620
|
-
location
|
|
1834
|
+
location,
|
|
1835
|
+
...(classes.length > 0 ? {classes} : {}),
|
|
1836
|
+
...(errors.length > 0 ? {errors} : {}),
|
|
1837
|
+
...(records.length > 0 ? {records} : {})
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Removes exact protected Ruby probe support before portable conversion.
|
|
1843
|
+
* @param {import("@ruby/prism").Node[]} body - Parsed top-level nodes.
|
|
1844
|
+
* @param {{location: {startOffset: number, length: number}}[]} comments - Parser comments.
|
|
1845
|
+
* @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} capabilities - Authorized declarations.
|
|
1846
|
+
* @param {string} filename - Source filename.
|
|
1847
|
+
* @param {string} source - Complete source.
|
|
1848
|
+
* @returns {import("@ruby/prism").Node[]} Portable nodes.
|
|
1849
|
+
*/
|
|
1850
|
+
function task034RubyBody(body, comments, capabilities, filename, source) {
|
|
1851
|
+
if (!isTask034ResourceProbe(capabilities)) return body
|
|
1852
|
+
const marker = comments.find((comment) =>
|
|
1853
|
+
source.slice(comment.location.startOffset, comment.location.startOffset + comment.location.length) ==
|
|
1854
|
+
"# semantifold-task034-support")
|
|
1855
|
+
|
|
1856
|
+
if (!marker) return body
|
|
1857
|
+
if (!hasExactEffectSupport(source, "ruby")) {
|
|
1858
|
+
return unsupportedSyntax("ruby", "malformed protected Task 034 support", nodeLocation(body[0], filename, source))
|
|
1859
|
+
}
|
|
1860
|
+
const expected = [
|
|
1861
|
+
["ClassNode", "ProbeOperationFailure"], ["ClassNode", "ProbeAcquireFailure"], ["ClassNode", "ProbeReadFailure"],
|
|
1862
|
+
["ClassNode", "ProbeCloseFailure"], ["ClassNode", "ProbeResourceClosed"], ["ClassNode", "ProbeResource"],
|
|
1863
|
+
["GlobalVariableWriteNode", "$__semantifold_task034_trace"], ["DefNode", "probeEffect"], ["DefNode", "probeAcquire"],
|
|
1864
|
+
["DefNode", "probeRead"], ["DefNode", "probeClose"], ["DefNode", "probeTrace"]
|
|
1865
|
+
]
|
|
1866
|
+
const matches = expected.every(([kind, name], index) => body[index]?.constructor.name == kind && Reflect.get(body[index], "name") == name)
|
|
1867
|
+
const end = comments.find((comment) =>
|
|
1868
|
+
source.slice(comment.location.startOffset, comment.location.startOffset + comment.location.length) ==
|
|
1869
|
+
"# semantifold-task034-support-end")
|
|
1870
|
+
|
|
1871
|
+
if (!matches || !end || end.location.startOffset < body[expected.length - 1].location.startOffset + body[expected.length - 1].location.length) {
|
|
1872
|
+
return unsupportedSyntax("ruby", "malformed protected Task 034 support", nodeLocation(body[0], filename, source))
|
|
1873
|
+
}
|
|
1874
|
+
return body.slice(expected.length)
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Reads the canonical parser-owned Ruby module and literal require-relative edges.
|
|
1879
|
+
* @param {object} input - Parser input.
|
|
1880
|
+
* @param {string} input.filename - Source filename.
|
|
1881
|
+
* @param {string} input.source - Source text.
|
|
1882
|
+
* @returns {{imports: {importedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, namespace: true, pathLocation: import("../semantic/types.js").SourceLocation, specifier: string, stdlibCandidate?: true, typeOnly: false}[], exports: {exportedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, typeOnly: false}[], nativeName: string, nativeReferences: {form: "constructor-call" | "receiver-call" | "unqualified-call", location: import("../semantic/types.js").SourceLocation, nativeModule: string, symbol: string}[]}} Parser-owned Ruby module header.
|
|
1883
|
+
*/
|
|
1884
|
+
export function inspectRubyModule({filename, source}) {
|
|
1885
|
+
const result = parsePrism(source, {filepath: filename})
|
|
1886
|
+
|
|
1887
|
+
if (result.errors.length > 0) {
|
|
1888
|
+
const error = result.errors[0]
|
|
1889
|
+
const location = locationFromOffsets(filename, source, error.location.startOffset, error.location.startOffset + error.location.length)
|
|
1890
|
+
|
|
1891
|
+
throw new SemantifoldDiagnostic({code: "PARSE_ERROR", language: "ruby", location, message: error.message})
|
|
621
1892
|
}
|
|
1893
|
+
if (!(result.value instanceof ProgramNode)) throw new Error("Prism returned a non-program root.")
|
|
1894
|
+
const body = result.value.statements.body
|
|
1895
|
+
const modules = body.filter((node) => node instanceof ModuleNode)
|
|
1896
|
+
const dynamicRequire = body.find(isDynamicStdlibRequire)
|
|
1897
|
+
|
|
1898
|
+
if (dynamicRequire) {
|
|
1899
|
+
throw new SemantifoldDiagnostic({
|
|
1900
|
+
code: "STDLIB_FACADE_DYNAMIC_REFERENCE",
|
|
1901
|
+
language: "ruby",
|
|
1902
|
+
location: nodeLocation(dynamicRequire, filename, source),
|
|
1903
|
+
message: "Dynamic require cannot prove a standard-library facade identity."
|
|
1904
|
+
})
|
|
1905
|
+
}
|
|
1906
|
+
validateTask037NativeAttempts(body, filename, source)
|
|
1907
|
+
const invalid = body.find((node) => !(node instanceof ModuleNode) && !isRequireRelative(node) && !isStdlibRequire(node))
|
|
1908
|
+
|
|
1909
|
+
if (modules.length != 1 || !(modules[0].constantPath instanceof ConstantReadNode) || invalid) {
|
|
1910
|
+
return unsupportedSyntax("ruby", "one simple module with literal require_relative declarations",
|
|
1911
|
+
nodeLocation(invalid ?? modules[1] ?? modules[0] ?? result.value, filename, source))
|
|
1912
|
+
}
|
|
1913
|
+
const imports = body.filter((node) => isRequireRelative(node) || isStdlibRequire(node)).map((node) => {
|
|
1914
|
+
const call = /** @type {CallNode} */ (node)
|
|
1915
|
+
const argument = /** @type {StringNode} */ (call.arguments_?.arguments_[0])
|
|
1916
|
+
const stdlibCandidate = isStdlibRequire(call)
|
|
1917
|
+
|
|
1918
|
+
if (!argument.unescaped.validEncoding || argument.unescaped.encoding != "utf-8" || argument.isForcedBinaryEncoding()) {
|
|
1919
|
+
return unsupportedSyntax("ruby", "non-UTF-8 require_relative path", nodeLocation(argument, filename, source))
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
return {
|
|
1923
|
+
importedName: "*",
|
|
1924
|
+
kind: /** @type {const} */ ("Import"),
|
|
1925
|
+
localName: "",
|
|
1926
|
+
location: nodeLocation(call, filename, source),
|
|
1927
|
+
namespace: /** @type {const} */ (true),
|
|
1928
|
+
pathLocation: nodeLocation(argument, filename, source),
|
|
1929
|
+
specifier: argument.unescaped.value,
|
|
1930
|
+
...(stdlibCandidate ? {stdlibCandidate: /** @type {const} */ (true)} : {}),
|
|
1931
|
+
typeOnly: /** @type {const} */ (false)
|
|
1932
|
+
}
|
|
1933
|
+
})
|
|
1934
|
+
const moduleBody = modules[0].body instanceof StatementsNode ? modules[0].body.body : []
|
|
1935
|
+
const visibility = new Map()
|
|
1936
|
+
|
|
1937
|
+
for (const node of moduleBody) {
|
|
1938
|
+
if (!(node instanceof CallNode) || !["private_class_method", "private_constant"].includes(node.name)) continue
|
|
1939
|
+
const marker = rubyVisibilityMarker(node)
|
|
1940
|
+
|
|
1941
|
+
if (!marker) return unsupportedSyntax("ruby", `malformed ${node.name}`, nodeLocation(node, filename, source))
|
|
1942
|
+
if (visibility.has(marker.name)) return unsupportedSyntax("ruby", `duplicate visibility marker for '${marker.name}'`, nodeLocation(node, filename, source))
|
|
1943
|
+
const declaration = moduleBody.find((candidate) => marker.kind == "function"
|
|
1944
|
+
? candidate instanceof DefNode && candidate.name == marker.name
|
|
1945
|
+
: candidate instanceof ClassNode && candidate.name == marker.name)
|
|
1946
|
+
|
|
1947
|
+
if (!declaration || moduleBody.indexOf(declaration) > moduleBody.indexOf(node)) {
|
|
1948
|
+
return unsupportedSyntax("ruby", `visibility marker without an earlier ${marker.kind}`, nodeLocation(node, filename, source))
|
|
1949
|
+
}
|
|
1950
|
+
visibility.set(marker.name, marker.kind)
|
|
1951
|
+
}
|
|
1952
|
+
const exports = moduleBody.flatMap((node) => {
|
|
1953
|
+
if (node instanceof DefNode || node instanceof ClassNode) {
|
|
1954
|
+
const kind = node instanceof DefNode ? "function" : node.body instanceof StatementsNode && node.body.body.length > 0 &&
|
|
1955
|
+
node.body.body.every((member) => member instanceof DefNode) ? "class" : "record"
|
|
1956
|
+
|
|
1957
|
+
if (visibility.get(node.name) == kind) return []
|
|
1958
|
+
return [{
|
|
1959
|
+
exportedName: node.name,
|
|
1960
|
+
localName: node.name,
|
|
1961
|
+
location: nodeLocation(node instanceof ClassNode ? node.constantPath : node, filename, source),
|
|
1962
|
+
symbolKind: /** @type {import("../semantic/types.js").SemanticDeclarationKind} */ (kind),
|
|
1963
|
+
typeOnly: /** @type {const} */ (false)
|
|
1964
|
+
}]
|
|
1965
|
+
}
|
|
1966
|
+
return []
|
|
1967
|
+
})
|
|
1968
|
+
const nativeReferences = rubyNativeReferences(modules[0], filename, source)
|
|
1969
|
+
|
|
1970
|
+
return {exports, imports, nativeName: modules[0].name, nativeReferences}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/**
|
|
1974
|
+
* Rejects parser-proved attempts to use or mutate the bounded socket facade outside its exact surface.
|
|
1975
|
+
* @param {import("@ruby/prism").Node[]} body - Prism program body.
|
|
1976
|
+
* @param {string} filename - Source filename.
|
|
1977
|
+
* @param {string} source - Complete source.
|
|
1978
|
+
* @returns {void}
|
|
1979
|
+
*/
|
|
1980
|
+
function validateTask037NativeAttempts(body, filename, source) {
|
|
1981
|
+
const active = body.some((node) => isStdlibRequire(node) && node instanceof CallNode &&
|
|
1982
|
+
node.arguments_?.arguments_[0] instanceof StringNode && node.arguments_.arguments_[0].unescaped.value == "socket")
|
|
1983
|
+
const reopened = body.find((node) => node instanceof ClassNode && node.name == "TCPSocket")
|
|
1984
|
+
|
|
1985
|
+
if (active && reopened) throw new SemantifoldDiagnostic({
|
|
1986
|
+
code: "STDLIB_FACADE_REOPENED",
|
|
1987
|
+
language: "ruby",
|
|
1988
|
+
location: nodeLocation(reopened, filename, source),
|
|
1989
|
+
message: "Caller source reopens the compiler-owned TCPSocket facade identity."
|
|
1990
|
+
})
|
|
1991
|
+
const modules = body.filter((node) => node instanceof ModuleNode)
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Visits one parser node for bounded native attempts.
|
|
1995
|
+
* @param {import("@ruby/prism").Node} node - Candidate Prism node.
|
|
1996
|
+
*/
|
|
1997
|
+
function visit(node) {
|
|
1998
|
+
if (node instanceof CallNode) {
|
|
1999
|
+
const receiver = node.receiver
|
|
2000
|
+
const tcpReceiver = receiver instanceof ConstantReadNode && receiver.name == "TCPSocket"
|
|
2001
|
+
|
|
2002
|
+
if (tcpReceiver) {
|
|
2003
|
+
if (!active) throw new SemantifoldDiagnostic({
|
|
2004
|
+
code: "STDLIB_FACADE_IDENTITY_UNPROVED",
|
|
2005
|
+
language: "ruby",
|
|
2006
|
+
location: nodeLocation(/** @type {ConstantReadNode} */ (receiver), filename, source),
|
|
2007
|
+
message: "TCPSocket requires the exact parser-proved require \"socket\" identity."
|
|
2008
|
+
})
|
|
2009
|
+
if (node.name != "new" || node.block || node.arguments_?.arguments_.length != 2) {
|
|
2010
|
+
throw new SemantifoldDiagnostic({
|
|
2011
|
+
code: "STDLIB_FACADE_MEMBER_UNSUPPORTED",
|
|
2012
|
+
language: "ruby",
|
|
2013
|
+
location: nodeLocation(node, filename, source),
|
|
2014
|
+
message: "TCPSocket supports only exact two-argument new without a block."
|
|
2015
|
+
})
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const kernelPuts = !node.receiver && node.name == "puts" ||
|
|
2019
|
+
node.receiver instanceof ConstantReadNode && node.receiver.name == "Kernel" && node.name == "puts"
|
|
2020
|
+
|
|
2021
|
+
if (active && kernelPuts && (node.block || node.arguments_?.arguments_.length != 1)) {
|
|
2022
|
+
throw new SemantifoldDiagnostic({
|
|
2023
|
+
code: "STDLIB_FACADE_MEMBER_UNSUPPORTED",
|
|
2024
|
+
language: "ruby",
|
|
2025
|
+
location: nodeLocation(node, filename, source),
|
|
2026
|
+
message: "The Task 037 Output facade supports exactly one string argument without a block."
|
|
2027
|
+
})
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
for (const child of node.compactChildNodes()) visit(child)
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
for (const module of modules) visit(module)
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* Collects exact Task 037 native identities from Prism nodes in source order.
|
|
2038
|
+
* @param {ModuleNode} module - Parsed application module.
|
|
2039
|
+
* @param {string} filename - Source filename.
|
|
2040
|
+
* @param {string} source - Complete source.
|
|
2041
|
+
* @returns {{form: "constructor-call" | "receiver-call" | "unqualified-call", location: import("../semantic/types.js").SourceLocation, nativeModule: string, symbol: string}[]} Parser-proved native references.
|
|
2042
|
+
*/
|
|
2043
|
+
function rubyNativeReferences(module, filename, source) {
|
|
2044
|
+
/** @type {{form: "constructor-call" | "receiver-call" | "unqualified-call", location: import("../semantic/types.js").SourceLocation, nativeModule: string, offset: number, symbol: string}[]} */
|
|
2045
|
+
const references = []
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* Visits one parser node without interpreting source text.
|
|
2049
|
+
* @param {import("@ruby/prism").Node} node - Parser node.
|
|
2050
|
+
* @returns {void}
|
|
2051
|
+
*/
|
|
2052
|
+
function visit(node) {
|
|
2053
|
+
if (node instanceof CallNode && !node.block && node.arguments_?.arguments_.length == 1 &&
|
|
2054
|
+
!node.receiver && node.name == "puts") {
|
|
2055
|
+
references.push({form: "unqualified-call", location: nodeLocation(node, filename, source), nativeModule: "ruby:Kernel",
|
|
2056
|
+
offset: node.location.startOffset, symbol: "puts"})
|
|
2057
|
+
}
|
|
2058
|
+
if (node instanceof CallNode && !node.block && node.arguments_?.arguments_.length == 1 && node.name == "puts" &&
|
|
2059
|
+
node.callOperatorLoc && node.receiver instanceof ConstantReadNode && node.receiver.name == "Kernel") {
|
|
2060
|
+
references.push({form: "receiver-call", location: nodeLocation(node, filename, source), nativeModule: "ruby:Kernel",
|
|
2061
|
+
offset: node.location.startOffset, symbol: "puts"})
|
|
2062
|
+
}
|
|
2063
|
+
if (node instanceof CallNode && !node.block && node.arguments_?.arguments_.length == 2 && node.name == "new" &&
|
|
2064
|
+
node.callOperatorLoc && node.receiver instanceof ConstantReadNode && node.receiver.name == "TCPSocket") {
|
|
2065
|
+
references.push({form: "constructor-call", location: nodeLocation(node, filename, source), nativeModule: "socket",
|
|
2066
|
+
offset: node.location.startOffset, symbol: "TCPSocket"})
|
|
2067
|
+
}
|
|
2068
|
+
for (const child of node.compactChildNodes()) visit(child)
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
visit(module)
|
|
2072
|
+
return references.sort((left, right) => left.offset - right.offset).map(({offset: _offset, ...reference}) => reference)
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Reports whether a Ruby node is one canonical literal relative load edge.
|
|
2077
|
+
* @param {import("@ruby/prism").Node} node - Candidate call.
|
|
2078
|
+
* @returns {boolean} Whether the node is a supported require-relative call.
|
|
2079
|
+
*/
|
|
2080
|
+
function isRequireRelative(node) {
|
|
2081
|
+
return node instanceof CallNode && !node.receiver && !node.block && node.name == "require_relative" &&
|
|
2082
|
+
node.arguments_?.arguments_.length == 1 && node.arguments_.arguments_[0] instanceof StringNode
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Reports whether a Ruby node is one parser-proved literal facade require candidate.
|
|
2087
|
+
* Other load-path requires retain the Task 010 unsupported profile.
|
|
2088
|
+
* @param {import("@ruby/prism").Node} node - Candidate call.
|
|
2089
|
+
* @returns {boolean} Whether the node is a literal Semantifold facade require.
|
|
2090
|
+
*/
|
|
2091
|
+
function isStdlibRequire(node) {
|
|
2092
|
+
return node instanceof CallNode && !node.receiver && !node.block && node.name == "require" &&
|
|
2093
|
+
node.arguments_?.arguments_.length == 1 && node.arguments_.arguments_[0] instanceof StringNode &&
|
|
2094
|
+
(node.arguments_.arguments_[0].unescaped.value == "socket" ||
|
|
2095
|
+
node.arguments_.arguments_[0].unescaped.value.startsWith("semantifold/"))
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Reports whether a Ruby node dynamically constructs a facade require identity.
|
|
2100
|
+
* @param {import("@ruby/prism").Node} node - Candidate call.
|
|
2101
|
+
* @returns {boolean} Whether the parser exposes a nonliteral require.
|
|
2102
|
+
*/
|
|
2103
|
+
function isDynamicStdlibRequire(node) {
|
|
2104
|
+
if (!(node instanceof CallNode) || node.receiver || node.block || node.name != "require" ||
|
|
2105
|
+
node.arguments_?.arguments_.length != 1) return false
|
|
2106
|
+
return !(node.arguments_.arguments_[0] instanceof StringNode)
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* Reports whether a Ruby node is the canonical module-function marker.
|
|
2111
|
+
* @param {import("@ruby/prism").Node} node - Candidate marker.
|
|
2112
|
+
* @returns {boolean} Whether the node is a bare module-function call.
|
|
2113
|
+
*/
|
|
2114
|
+
function isModuleFunctionMarker(node) {
|
|
2115
|
+
return node instanceof CallNode && !node.receiver && !node.block && node.name == "module_function" && !node.arguments_
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* Reads one exact generated Ruby visibility marker.
|
|
2120
|
+
* @param {import("@ruby/prism").Node} node - Candidate module-body node.
|
|
2121
|
+
* @returns {{kind: import("../semantic/types.js").SemanticDeclarationKind, name: string} | undefined} Marker meaning.
|
|
2122
|
+
*/
|
|
2123
|
+
function rubyVisibilityMarker(node) {
|
|
2124
|
+
if (!(node instanceof CallNode) || node.receiver || node.block ||
|
|
2125
|
+
!["private_class_method", "private_constant"].includes(node.name) ||
|
|
2126
|
+
node.arguments_?.arguments_.length != 1 || !(node.arguments_.arguments_[0] instanceof SymbolNode)) return undefined
|
|
2127
|
+
|
|
2128
|
+
return {
|
|
2129
|
+
kind: node.name == "private_class_method" ? "function" : "record",
|
|
2130
|
+
name: node.arguments_.arguments_[0].unescaped.value
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
/**
|
|
2135
|
+
* Builds a parser-owned simple Ruby constant path without inspecting source text.
|
|
2136
|
+
* @param {ConstantReadNode | ConstantPathNode} node - Constant path.
|
|
2137
|
+
* @returns {string} Qualified constant name.
|
|
2138
|
+
*/
|
|
2139
|
+
function constantPathName(node) {
|
|
2140
|
+
if (node instanceof ConstantReadNode) return node.name
|
|
2141
|
+
if (typeof node.name != "string") throw new Error("Prism returned a constant path without a name.")
|
|
2142
|
+
if (!node.parent) return node.name
|
|
2143
|
+
if (!(node.parent instanceof ConstantReadNode) && !(node.parent instanceof ConstantPathNode)) {
|
|
2144
|
+
throw new Error("Prism returned an unsupported constant-path parent.")
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
return `${constantPathName(node.parent)}::${node.name}`
|
|
622
2148
|
}
|