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.
Files changed (297) hide show
  1. package/README.md +58 -15
  2. package/build/index.d.ts +48 -2
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +57 -8
  5. package/build/src/artifact-path.d.ts +23 -1
  6. package/build/src/artifact-path.d.ts.map +1 -1
  7. package/build/src/artifact-path.js +45 -3
  8. package/build/src/artifacts.d.ts.map +1 -1
  9. package/build/src/artifacts.js +66 -1
  10. package/build/src/backends/c-validation.d.ts.map +1 -1
  11. package/build/src/backends/c-validation.js +7 -3
  12. package/build/src/backends/cpp-validation.d.ts.map +1 -1
  13. package/build/src/backends/cpp-validation.js +7 -3
  14. package/build/src/backends/csharp.js +7 -1
  15. package/build/src/backends/dart-runtime.d.ts +2 -0
  16. package/build/src/backends/dart-runtime.d.ts.map +1 -0
  17. package/build/src/backends/dart-runtime.js +41 -0
  18. package/build/src/backends/dart.d.ts +20 -0
  19. package/build/src/backends/dart.d.ts.map +1 -0
  20. package/build/src/backends/dart.js +321 -0
  21. package/build/src/backends/effects.d.ts +60 -0
  22. package/build/src/backends/effects.d.ts.map +1 -0
  23. package/build/src/backends/effects.js +746 -0
  24. package/build/src/backends/go.js +18 -4
  25. package/build/src/backends/identifiers.d.ts +36 -4
  26. package/build/src/backends/identifiers.d.ts.map +1 -1
  27. package/build/src/backends/identifiers.js +154 -10
  28. package/build/src/backends/index.d.ts.map +1 -1
  29. package/build/src/backends/index.js +27 -4
  30. package/build/src/backends/ios.d.ts +68 -0
  31. package/build/src/backends/ios.d.ts.map +1 -0
  32. package/build/src/backends/ios.js +1503 -0
  33. package/build/src/backends/java.d.ts.map +1 -1
  34. package/build/src/backends/java.js +429 -36
  35. package/build/src/backends/javascript.d.ts +6 -0
  36. package/build/src/backends/javascript.d.ts.map +1 -1
  37. package/build/src/backends/javascript.js +380 -24
  38. package/build/src/backends/kotlin-runtime.d.ts +3 -0
  39. package/build/src/backends/kotlin-runtime.d.ts.map +1 -0
  40. package/build/src/backends/kotlin-runtime.js +29 -0
  41. package/build/src/backends/kotlin.d.ts +23 -0
  42. package/build/src/backends/kotlin.d.ts.map +1 -0
  43. package/build/src/backends/kotlin.js +136 -0
  44. package/build/src/backends/native-validation.d.ts +2 -2
  45. package/build/src/backends/native-validation.d.ts.map +1 -1
  46. package/build/src/backends/native-validation.js +2 -2
  47. package/build/src/backends/ordered-expressions.d.ts.map +1 -1
  48. package/build/src/backends/ordered-expressions.js +39 -7
  49. package/build/src/backends/php-stdlib-support.d.ts +19 -0
  50. package/build/src/backends/php-stdlib-support.d.ts.map +1 -0
  51. package/build/src/backends/php-stdlib-support.js +227 -0
  52. package/build/src/backends/php.d.ts.map +1 -1
  53. package/build/src/backends/php.js +448 -33
  54. package/build/src/backends/program.d.ts +81 -0
  55. package/build/src/backends/program.d.ts.map +1 -0
  56. package/build/src/backends/program.js +1308 -0
  57. package/build/src/backends/python.js +7 -1
  58. package/build/src/backends/ruby.d.ts.map +1 -1
  59. package/build/src/backends/ruby.js +310 -24
  60. package/build/src/backends/rust-runtime.d.ts +7 -0
  61. package/build/src/backends/rust-runtime.d.ts.map +1 -0
  62. package/build/src/backends/rust-runtime.js +32 -0
  63. package/build/src/backends/rust-validation.d.ts +9 -0
  64. package/build/src/backends/rust-validation.d.ts.map +1 -0
  65. package/build/src/backends/rust-validation.js +78 -0
  66. package/build/src/backends/rust.d.ts +24 -0
  67. package/build/src/backends/rust.d.ts.map +1 -0
  68. package/build/src/backends/rust.js +344 -0
  69. package/build/src/backends/scalars.d.ts +14 -6
  70. package/build/src/backends/scalars.d.ts.map +1 -1
  71. package/build/src/backends/scalars.js +167 -17
  72. package/build/src/backends/shared.d.ts +23 -4
  73. package/build/src/backends/shared.d.ts.map +1 -1
  74. package/build/src/backends/shared.js +2158 -62
  75. package/build/src/backends/stdlib-support.d.ts +14 -0
  76. package/build/src/backends/stdlib-support.d.ts.map +1 -0
  77. package/build/src/backends/stdlib-support.js +237 -0
  78. package/build/src/backends/swift-runtime.d.ts +3 -0
  79. package/build/src/backends/swift-runtime.d.ts.map +1 -0
  80. package/build/src/backends/swift-runtime.js +14 -0
  81. package/build/src/backends/swift.d.ts +8 -0
  82. package/build/src/backends/swift.d.ts.map +1 -0
  83. package/build/src/backends/swift.js +127 -0
  84. package/build/src/backends/typescript.d.ts.map +1 -1
  85. package/build/src/backends/typescript.js +344 -24
  86. package/build/src/backends/wasm.d.ts +43 -0
  87. package/build/src/backends/wasm.d.ts.map +1 -0
  88. package/build/src/backends/wasm.js +2164 -0
  89. package/build/src/backends/writer.d.ts +235 -4
  90. package/build/src/backends/writer.d.ts.map +1 -1
  91. package/build/src/backends/writer.js +468 -3
  92. package/build/src/diagnostic.d.ts +4 -3
  93. package/build/src/diagnostic.d.ts.map +1 -1
  94. package/build/src/diagnostic.js +5 -3
  95. package/build/src/frontends/c.d.ts.map +1 -1
  96. package/build/src/frontends/c.js +5 -3
  97. package/build/src/frontends/cpp.d.ts.map +1 -1
  98. package/build/src/frontends/cpp.js +5 -3
  99. package/build/src/frontends/csharp.d.ts.map +1 -1
  100. package/build/src/frontends/csharp.js +7 -3
  101. package/build/src/frontends/dart.d.ts +10 -0
  102. package/build/src/frontends/dart.d.ts.map +1 -0
  103. package/build/src/frontends/dart.js +913 -0
  104. package/build/src/frontends/index.d.ts +3 -1
  105. package/build/src/frontends/index.d.ts.map +1 -1
  106. package/build/src/frontends/index.js +15 -5
  107. package/build/src/frontends/java.d.ts +110 -1
  108. package/build/src/frontends/java.d.ts.map +1 -1
  109. package/build/src/frontends/java.js +1725 -70
  110. package/build/src/frontends/javascript-typescript.d.ts +69 -1
  111. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  112. package/build/src/frontends/javascript-typescript.js +1825 -93
  113. package/build/src/frontends/kotlin.d.ts +10 -0
  114. package/build/src/frontends/kotlin.d.ts.map +1 -0
  115. package/build/src/frontends/kotlin.js +685 -0
  116. package/build/src/frontends/php.d.ts +118 -1
  117. package/build/src/frontends/php.d.ts.map +1 -1
  118. package/build/src/frontends/php.js +1583 -76
  119. package/build/src/frontends/program.d.ts +74 -0
  120. package/build/src/frontends/program.d.ts.map +1 -0
  121. package/build/src/frontends/program.js +989 -0
  122. package/build/src/frontends/ruby.d.ts +68 -1
  123. package/build/src/frontends/ruby.d.ts.map +1 -1
  124. package/build/src/frontends/ruby.js +1446 -101
  125. package/build/src/frontends/rust-parser.d.ts +11 -0
  126. package/build/src/frontends/rust-parser.d.ts.map +1 -0
  127. package/build/src/frontends/rust-parser.js +15 -0
  128. package/build/src/frontends/rust.d.ts +14 -0
  129. package/build/src/frontends/rust.d.ts.map +1 -0
  130. package/build/src/frontends/rust.js +715 -0
  131. package/build/src/frontends/scalars.d.ts +10 -0
  132. package/build/src/frontends/scalars.d.ts.map +1 -1
  133. package/build/src/frontends/scalars.js +28 -2
  134. package/build/src/frontends/swift.d.ts +10 -0
  135. package/build/src/frontends/swift.d.ts.map +1 -0
  136. package/build/src/frontends/swift.js +860 -0
  137. package/build/src/frontends/types.d.ts +186 -0
  138. package/build/src/frontends/types.d.ts.map +1 -0
  139. package/build/src/frontends/types.js +634 -0
  140. package/build/src/language-registry.d.ts +46 -4
  141. package/build/src/language-registry.d.ts.map +1 -1
  142. package/build/src/language-registry.js +167 -13
  143. package/build/src/mapping.d.ts +2 -2
  144. package/build/src/mapping.d.ts.map +1 -1
  145. package/build/src/mapping.js +31 -10
  146. package/build/src/semantic/capabilities.d.ts +66 -0
  147. package/build/src/semantic/capabilities.d.ts.map +1 -0
  148. package/build/src/semantic/capabilities.js +649 -0
  149. package/build/src/semantic/effects.d.ts +26 -0
  150. package/build/src/semantic/effects.d.ts.map +1 -0
  151. package/build/src/semantic/effects.js +764 -0
  152. package/build/src/semantic/generics.d.ts +23 -0
  153. package/build/src/semantic/generics.d.ts.map +1 -0
  154. package/build/src/semantic/generics.js +61 -0
  155. package/build/src/semantic/lifetime.d.ts +37 -0
  156. package/build/src/semantic/lifetime.d.ts.map +1 -0
  157. package/build/src/semantic/lifetime.js +527 -0
  158. package/build/src/semantic/provenance.d.ts +9 -2
  159. package/build/src/semantic/provenance.d.ts.map +1 -1
  160. package/build/src/semantic/provenance.js +462 -20
  161. package/build/src/semantic/scalars.d.ts +11 -0
  162. package/build/src/semantic/scalars.d.ts.map +1 -1
  163. package/build/src/semantic/scalars.js +16 -1
  164. package/build/src/semantic/stdlib.d.ts +254 -0
  165. package/build/src/semantic/stdlib.d.ts.map +1 -0
  166. package/build/src/semantic/stdlib.js +880 -0
  167. package/build/src/semantic/types.d.ts +2952 -202
  168. package/build/src/semantic/types.d.ts.map +1 -1
  169. package/build/src/semantic/types.js +688 -15
  170. package/build/src/semantic/validate.d.ts +85 -5
  171. package/build/src/semantic/validate.d.ts.map +1 -1
  172. package/build/src/semantic/validate.js +2576 -98
  173. package/build/src/stdlib-facades.d.ts +304 -0
  174. package/build/src/stdlib-facades.d.ts.map +1 -0
  175. package/build/src/stdlib-facades.js +1167 -0
  176. package/build/src/stdlib-negotiation.d.ts +69 -0
  177. package/build/src/stdlib-negotiation.d.ts.map +1 -0
  178. package/build/src/stdlib-negotiation.js +403 -0
  179. package/build/src/stdlib-providers.d.ts +72 -0
  180. package/build/src/stdlib-providers.d.ts.map +1 -0
  181. package/build/src/stdlib-providers.js +339 -0
  182. package/build/src/toolchains.d.ts +55 -1
  183. package/build/src/toolchains.d.ts.map +1 -1
  184. package/build/src/toolchains.js +15 -4
  185. package/docs/architecture.md +45 -13
  186. package/docs/dart.md +60 -0
  187. package/docs/goals.md +3 -3
  188. package/docs/ios.md +121 -0
  189. package/docs/kotlin.md +42 -0
  190. package/docs/language-support.md +231 -26
  191. package/docs/multi-file-modules.md +52 -0
  192. package/docs/parser-qualification.md +121 -8
  193. package/docs/plans/2026-09-05-standard-library-portability.md +10 -10
  194. package/docs/rust.md +88 -0
  195. package/docs/source-maps.md +15 -7
  196. package/docs/standard-library-portability.md +39 -8
  197. package/docs/swift.md +44 -0
  198. package/docs/testing.md +366 -4
  199. package/docs/webassembly.md +52 -0
  200. package/index.js +63 -8
  201. package/node_modules/semantifold-tree-sitter-legacy-internal/README.md +2 -2
  202. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/LICENSE +21 -0
  203. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/README.md +45 -0
  204. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/binding.gyp +30 -0
  205. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding.cc +20 -0
  206. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/binding_test.js +9 -0
  207. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.d.ts +28 -0
  208. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/bindings/node/index.js +11 -0
  209. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/grammar.js +1630 -0
  210. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/package.json +64 -0
  211. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-arm64/tree-sitter-rust.node +0 -0
  212. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/darwin-x64/tree-sitter-rust.node +0 -0
  213. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-arm64/tree-sitter-rust.node +0 -0
  214. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/linux-x64/tree-sitter-rust.node +0 -0
  215. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-arm64/tree-sitter-rust.node +0 -0
  216. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/prebuilds/win32-x64/tree-sitter-rust.node +0 -0
  217. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/highlights.scm +159 -0
  218. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/injections.scm +9 -0
  219. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/queries/tags.scm +60 -0
  220. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/grammar.json +9405 -0
  221. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/node-types.json +5399 -0
  222. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/parser.c +192569 -0
  223. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/scanner.c +393 -0
  224. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/alloc.h +54 -0
  225. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/array.h +290 -0
  226. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/src/tree_sitter/parser.h +266 -0
  227. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter-rust.wasm +0 -0
  228. package/node_modules/semantifold-tree-sitter-legacy-internal/node_modules/tree-sitter-rust/tree-sitter.json +53 -0
  229. package/node_modules/semantifold-tree-sitter-legacy-internal/package.json +3 -2
  230. package/node_modules/semantifold-tree-sitter-legacy-internal/src/c.js +5 -4
  231. package/package.json +5 -2
  232. package/src/artifact-path.js +47 -2
  233. package/src/artifacts.js +67 -0
  234. package/src/backends/c-validation.js +6 -1
  235. package/src/backends/cpp-validation.js +6 -1
  236. package/src/backends/csharp.js +3 -0
  237. package/src/backends/dart-runtime.js +41 -0
  238. package/src/backends/dart.js +326 -0
  239. package/src/backends/effects.js +705 -0
  240. package/src/backends/go.js +14 -2
  241. package/src/backends/identifiers.js +160 -9
  242. package/src/backends/index.js +27 -2
  243. package/src/backends/ios.js +1612 -0
  244. package/src/backends/java.js +454 -36
  245. package/src/backends/javascript.js +389 -23
  246. package/src/backends/kotlin-runtime.js +29 -0
  247. package/src/backends/kotlin.js +141 -0
  248. package/src/backends/native-validation.js +1 -1
  249. package/src/backends/ordered-expressions.js +40 -6
  250. package/src/backends/php-stdlib-support.js +239 -0
  251. package/src/backends/php.js +469 -32
  252. package/src/backends/program.js +1407 -0
  253. package/src/backends/python.js +3 -0
  254. package/src/backends/ruby.js +326 -23
  255. package/src/backends/rust-runtime.js +34 -0
  256. package/src/backends/rust-validation.js +75 -0
  257. package/src/backends/rust.js +335 -0
  258. package/src/backends/scalars.js +141 -16
  259. package/src/backends/shared.js +2304 -124
  260. package/src/backends/stdlib-support.js +234 -0
  261. package/src/backends/swift-runtime.js +14 -0
  262. package/src/backends/swift.js +132 -0
  263. package/src/backends/typescript.js +349 -23
  264. package/src/backends/wasm.js +2208 -0
  265. package/src/backends/writer.js +520 -2
  266. package/src/diagnostic.js +4 -2
  267. package/src/frontends/c.js +4 -2
  268. package/src/frontends/cpp.js +4 -2
  269. package/src/frontends/csharp.js +4 -2
  270. package/src/frontends/dart.js +963 -0
  271. package/src/frontends/index.js +14 -4
  272. package/src/frontends/java.js +1977 -69
  273. package/src/frontends/javascript-typescript.js +2046 -102
  274. package/src/frontends/kotlin.js +713 -0
  275. package/src/frontends/php.js +1725 -79
  276. package/src/frontends/program.js +1062 -0
  277. package/src/frontends/ruby.js +1624 -98
  278. package/src/frontends/rust-parser.js +18 -0
  279. package/src/frontends/rust.js +714 -0
  280. package/src/frontends/scalars.js +31 -1
  281. package/src/frontends/swift.js +893 -0
  282. package/src/frontends/types.js +750 -0
  283. package/src/language-registry.js +172 -12
  284. package/src/mapping.js +33 -9
  285. package/src/semantic/capabilities.js +671 -0
  286. package/src/semantic/effects.js +737 -0
  287. package/src/semantic/generics.js +58 -0
  288. package/src/semantic/lifetime.js +517 -0
  289. package/src/semantic/provenance.js +412 -19
  290. package/src/semantic/scalars.js +17 -0
  291. package/src/semantic/stdlib.js +921 -0
  292. package/src/semantic/types.js +765 -14
  293. package/src/semantic/validate.js +2934 -209
  294. package/src/stdlib-facades.js +1211 -0
  295. package/src/stdlib-negotiation.js +446 -0
  296. package/src/stdlib-providers.js +377 -0
  297. package/src/toolchains.js +16 -3
@@ -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 {requireSourceScalarType} from "./scalars.js"
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
- return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: node.name}, {name: location})
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: convertExpression(node.receiver, filename, source)
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) => convertExpression(argument, filename, source)),
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 != 1) {
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
- expression: convertExpression(node.arguments_.arguments_[0], filename, source),
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
- * @returns {{parameters: Map<string, {location: import("../semantic/types.js").SourceLocation, sourceType: string}>, returnType: {location: import("../semantic/types.js").SourceLocation, sourceType: string} | undefined}} Declared types.
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 == 3) {
247
- parameterTypes.set(words[1].text, {location: words[2].location, sourceType: words[2].text})
248
- }
249
- if (words[0]?.text == "@return" && words.length == 2) {
250
- returnType = {location: words[1].location, sourceType: words[1].text}
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
- * @returns {{immutable: boolean, type: import("../semantic/types.js").TypeReference} | undefined} Metadata when present.
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 != 2 ||
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(typeMetadata[0][1].text, `Local '${node.name}'`, location, typeMetadata[0][1].location)
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 {Set<string>} visible - Names visible during adaptation.
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, visible, filename, source) {
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
- visible.add(node.name)
370
- return withParserRanges({
371
- initializer: convertExpression(node.value, filename, source),
372
- kind: "LocalDeclaration",
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: metadata.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 (!visible.has(node.name)) return missingType("ruby", `Local '${node.name}'`, location)
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
- return withParserRanges({
388
- expression: convertExpression(node.value, filename, source),
389
- kind: "AssignmentStatement",
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 {Set<string>} visible - Names visible during adaptation.
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, visible, filename, source) {
405
- if (node instanceof ReturnNode) return convertReturn(node, filename, source)
406
- if (node instanceof IfNode) return convertIf(node, comments, visible, filename, source)
407
- if (node instanceof LocalVariableWriteNode) return convertLocalStatement(node, comments, visible, filename, source)
408
- if (node instanceof CallNode) return convertPrint(node, filename, source)
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 {Set<string>} visible - Names visible on block entry.
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, visible, filename, source, fallbackLocation) {
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, visible, filename, source)) : []
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 scalar spelling.
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
- * @returns {import("../semantic/types.js").TypeReference} Semantic type.
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 requireSourceScalarType("ruby", sourceType, subject, location, typeLocation)
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
- * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
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 convertFunction(node, comments, filename, source) {
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 (!node.parameters || node.parameters.optionals.length > 0 || node.parameters.rest || node.parameters.posts.length > 0 ||
474
- node.parameters.keywords.length > 0 || node.parameters.keywordRest || node.parameters.block ||
475
- node.parameters.requireds.some((parameter) => !(parameter instanceof RequiredParameterNode))) {
476
- return unsupportedSyntax("ruby", "parameters", location)
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 parameters = node.parameters.requireds.map((parameter) => {
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, declaredType?.location)
1222
+ type: convertType(declaredType?.sourceType, `Parameter '${parameter.name}'`, parameterLocation, source,
1223
+ declaredType?.location, recordNames, typeParameterNames)
492
1224
  }, {name: parameterLocation})
493
1225
  })
494
- const visible = new Set(parameters.map((parameter) => parameter.name))
495
- const body = convertBlock(node.body instanceof StatementsNode ? node.body : null, comments, visible, filename, source, location)
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: node.name,
502
- parameters,
503
- returnType: convertType(declaredTypes.returnType?.sourceType, `Function '${node.name}' return`, location, declaredTypes.returnType?.location)
504
- }, {name: prismLocation(node.nameLoc, filename, source)})
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 if node.
1579
+ * @param {IfNode | UnlessNode} node - Prism conditional node.
510
1580
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
511
- * @param {Set<string>} visible - Enclosing visible names.
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, visible, filename, source) {
1586
+ function convertIf(node, comments, context, filename, source) {
517
1587
  const location = nodeLocation(node, filename, source)
518
1588
 
519
- if (!node.ifKeywordLoc || node.ifKeywordLoc.startOffset != node.location.startOffset) {
520
- return unsupportedSyntax("ruby", "modifier conditional", node.ifKeywordLoc
521
- ? prismLocation(node.ifKeywordLoc, filename, source)
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
- const consequentVisible = new Set(visible)
526
- let alternate
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.subsequent) {
529
- const alternateVisible = new Set(visible)
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
- if (node.subsequent instanceof ElseNode) {
532
- alternate = convertBlock(node.subsequent.statements, comments, alternateVisible, filename, source,
533
- nodeLocation(node.subsequent, filename, source))
534
- } else if (node.subsequent instanceof IfNode) {
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(node.subsequent, filename, source),
538
- statements: [convertIf(node.subsequent, comments, alternateVisible, filename, source)]
1626
+ location: nodeLocation(subsequent, filename, source),
1627
+ statements: [convertIf(subsequent, comments, alternateContext, filename, source)]
539
1628
  }
540
- } else return unsupportedSyntax("ruby", node.subsequent.constructor.name, nodeLocation(node.subsequent, filename, source))
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: convertExpression(node.predicate, filename, source),
546
- consequent: convertBlock(node.statements, comments, consequentVisible, filename, source, location),
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
- const body = result.value.statements.body
589
- const functions = body.filter((node) => node instanceof DefNode)
590
- .map((node) => convertFunction(node, result.comments, filename, source))
591
- const entryNodes = body.filter((node) => !(node instanceof DefNode))
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 (functions.length == 0) return unsupportedSyntax("ruby", "module without a function", location)
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 entryVisible = new Set()
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, entryVisible, filename, source))
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
  }