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
@@ -3,11 +3,16 @@
3
3
  import PhpParser from "php-parser"
4
4
  import {parse as parseComment} from "comment-parser"
5
5
  import {missingType, parseFailure, unsupportedSyntax} from "../diagnostic.js"
6
+ import {blockReferencesProtectedEntry} from "../semantic/effects.js"
6
7
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
7
8
  import {withAdaptedOperation} from "../semantic/operators.js"
8
9
  import {withParserRanges} from "../semantic/provenance.js"
9
- import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
10
- import {requireSourceScalarType} from "./scalars.js"
10
+ import {isTask034ResourceProbe} from "../semantic/capabilities.js"
11
+ import {hasExactEffectSupport} from "../backends/effects.js"
12
+ import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
13
+ import {typeContainsAnyVariable} from "../semantic/generics.js"
14
+ import {requireSourceReturnType, requireSourceScalarType, sourceScalarType} from "./scalars.js"
15
+ import {capabilityFunctionSignatures, documentedValueType, instantiatedRecordFieldType, iterationBindingType, iterationOperandType, knownCallParameterTypes, knownCallReturnType, optionalType, orderedMapTypeFromMap, ownedResourceType, preservesGenericOptionalEvidence, recordType, referenceType, sameValueType} from "./types.js"
11
16
  const parser = new PhpParser.Engine({
12
17
  ast: {withPositions: true},
13
18
  parser: {extractDoc: true, suppressErrors: false}
@@ -31,6 +36,31 @@ const phpBinaryOperations = new Map([
31
36
  [".", "StringConcat"]
32
37
  ])
33
38
 
39
+ /**
40
+ * @typedef PhpConversionContext
41
+ * @property {Map<string, import("../semantic/types.js").SemanticBindingType>} bindings - Explicitly typed visible bindings.
42
+ * @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
43
+ * @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
44
+ * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
45
+ * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
46
+ * @property {Map<string, PhpFunctionSignature>} functions - Explicit module function signatures.
47
+ * @property {Set<object>} [orderedMapDeclarations] - Exact local declarations selected by parser-owned pair iteration.
48
+ * @property {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
49
+ * @property {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
50
+ * @property {import("../semantic/types.js").SemanticFunctionReturnType | undefined} returnType - Enclosing return type.
51
+ * @property {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
52
+ */
53
+
54
+ /**
55
+ * @typedef PhpFunctionSignature
56
+ * @property {import("../semantic/types.js").SourceLocation} location - Complete declaration location.
57
+ * @property {import("../semantic/types.js").SourceLocation} nameLocation - Parser-owned name location.
58
+ * @property {string} name - Function name.
59
+ * @property {import("../semantic/types.js").Parameter[]} parameters - Semantic parameters.
60
+ * @property {import("../semantic/types.js").SemanticFunctionReturnType} returnType - Semantic return type.
61
+ * @property {import("../semantic/types.js").TypeParameter[]} [typeParameters] - Declaration-scoped parameters.
62
+ */
63
+
34
64
  /**
35
65
  * Returns a normalized PHP node location.
36
66
  * @param {import("php-parser").Node} node - PHP parser node.
@@ -112,17 +142,47 @@ function tokenLocation(tokenText, startOffset, endOffset, filename, source) {
112
142
  * @param {import("php-parser").Expression} node - PHP expression.
113
143
  * @param {string} filename - Source filename.
114
144
  * @param {string} source - Complete source.
145
+ * @param {PhpConversionContext} context - Typed conversion context.
146
+ * @param {import("../semantic/types.js").SemanticValueType} [expectedType] - Contextual type for empty PHP arrays.
147
+ * @param {boolean} [preserveOptional] - Whether an optional identifier remains wrapped.
115
148
  * @returns {import("../semantic/types.js").Expression} Semantic expression.
116
149
  */
117
- function convertExpression(node, filename, source) {
150
+ function convertExpression(node, filename, source, context, expectedType, preserveOptional = false) {
118
151
  const location = nodeLocation(node, filename, source)
119
152
 
153
+ if (expectedType?.kind == "OptionalType") {
154
+ if (node.kind == "nullkeyword") {
155
+ return withParserRanges({kind: /** @type {const} */ ("OptionalNone"), location}, {absence: location})
156
+ }
157
+ if (knownExpressionType(node, context)?.kind == "OptionalType") {
158
+ return convertExpression(node, filename, source, context, undefined, true)
159
+ }
160
+ return withParserRanges({
161
+ kind: /** @type {const} */ ("OptionalSome"),
162
+ location,
163
+ value: convertExpression(node, filename, source, context, expectedType.valueType)
164
+ }, {some: location})
165
+ }
166
+
167
+ if (node.kind == "nullkeyword") return unsupportedSyntax("php", "null outside an explicit optional context", location)
168
+
120
169
  if (node.kind == "variable") {
121
170
  const variable = /** @type {import("php-parser").Variable} */ (node)
122
171
 
123
172
  if (typeof variable.name != "string") return unsupportedSyntax("php", "dynamic variable", location)
173
+ if (variable.name == "this") {
174
+ if (!context.currentClass) return unsupportedSyntax("php", "receiver outside a reference class", location)
175
+ return withParserRanges({classId: /** @type {string} */ (context.currentClass.id),
176
+ kind: /** @type {const} */ ("ReceiverExpression"), location}, {receiver: location})
177
+ }
178
+
179
+ const identifier = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: variable.name}, {name: location})
124
180
 
125
- return withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location, name: variable.name}, {name: location})
181
+ if (!preserveOptional && context.bindings.get(variable.name)?.kind == "OptionalType") {
182
+ return withParserRanges({kind: /** @type {const} */ ("OptionalUnwrap"), location, operand: identifier}, {unwrap: location})
183
+ }
184
+
185
+ return identifier
126
186
  }
127
187
 
128
188
  if (node.kind == "number") {
@@ -169,6 +229,175 @@ function convertExpression(node, filename, source) {
169
229
 
170
230
  if (node.kind == "nowdoc") return unsupportedSyntax("php", "nowdoc string", location)
171
231
 
232
+ if (node.kind == "array") {
233
+ const literal = /** @type {import("php-parser").Array} */ (node)
234
+ const invalidEntry = literal.items.find((entry) => {
235
+ if (entry.kind != "entry") return true
236
+ const candidate = /** @type {import("php-parser").Entry} */ (entry)
237
+
238
+ return candidate.byRef || candidate.unpack
239
+ })
240
+
241
+ if (invalidEntry) return unsupportedSyntax("php", "array reference or unpacking", nodeLocation(invalidEntry, filename, source))
242
+ const entries = /** @type {import("php-parser").Entry[]} */ (literal.items)
243
+ const hasKeys = entries.some((entry) => entry.key != null)
244
+ const hasImplicit = entries.some((entry) => entry.key == null)
245
+ const collectionKind = entries.length == 0 ? expectedType?.kind : hasKeys && !hasImplicit
246
+ ? expectedType?.kind == "OrderedMapType" ? "OrderedMapType" : "MapType"
247
+ : !hasKeys ? "ListType" : "mixed"
248
+
249
+ if (collectionKind == "mixed") return unsupportedSyntax("php", "mixed list/map array literal", location)
250
+ if (collectionKind == "ListType") {
251
+ const elementType = expectedType?.kind == "ListType" ? expectedType.elementType : undefined
252
+
253
+ return withParserRanges({
254
+ elements: entries.map((entry) => convertExpression(entry.value, filename, source, context, elementType)),
255
+ kind: /** @type {const} */ ("ListLiteral"),
256
+ location
257
+ }, {literal: location})
258
+ }
259
+ if (collectionKind == "MapType" || collectionKind == "OrderedMapType") {
260
+ const valueType = expectedType?.kind == "MapType" || expectedType?.kind == "OrderedMapType"
261
+ ? expectedType.valueType : undefined
262
+ const semanticEntries = entries.map((entry) => {
263
+ if (!entry.key || entry.key.kind != "string") {
264
+ return unsupportedSyntax("php", "map key other than a literal string", entry.key ? nodeLocation(entry.key, filename, source) : nodeLocation(entry, filename, source))
265
+ }
266
+ const key = convertExpression(entry.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(entry, filename, source),
272
+ value: convertExpression(entry.value, filename, source, context, valueType)
273
+ }, {operator: tokenLocation("=>", entry.key.loc?.end.offset ?? 0, entry.value.loc?.start.offset ?? source.length, filename, source)})
274
+ })
275
+
276
+ return withParserRanges({
277
+ entries: semanticEntries,
278
+ kind: collectionKind == "OrderedMapType"
279
+ ? /** @type {const} */ ("OrderedMapLiteral")
280
+ : /** @type {const} */ ("MapLiteral"),
281
+ location
282
+ }, {literal: location})
283
+ }
284
+
285
+ return unsupportedSyntax("php", "empty array without a list or map type", location)
286
+ }
287
+
288
+ if (node.kind == "new") {
289
+ const construction = /** @type {import("php-parser").New} */ (node)
290
+ const name = construction.what.kind == "name" ? /** @type {import("php-parser").Name} */ (construction.what) : undefined
291
+
292
+ if (!name || name.resolution != "uqn" || typeof name.name != "string") {
293
+ return unsupportedSyntax("php", "dynamic or qualified record construction", nodeLocation(construction.what, filename, source))
294
+ }
295
+ const declaration = context.recordNames.get(name.name)
296
+
297
+ if (!declaration) return unsupportedSyntax("php", "construction of an unknown nominal class", nodeLocation(construction.what, filename, source))
298
+ if (declaration.kind == "ClassDeclaration") {
299
+ const unsupportedArgument = construction.arguments.find((argument) =>
300
+ argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
301
+
302
+ if (unsupportedArgument) return unsupportedSyntax("php", "non-positional reference constructor argument",
303
+ nodeLocation(unsupportedArgument, filename, source))
304
+ const classLocation = nodeLocation(construction.what, filename, source)
305
+
306
+ return withParserRanges({arguments: construction.arguments.map((argument, index) =>
307
+ convertExpression(argument, filename, source, context, declaration.constructor.parameters[index]?.type)),
308
+ kind: /** @type {const} */ ("ReferenceConstruction"), location,
309
+ reference: referenceType(/** @type {string} */ (declaration.id), classLocation)}, {class: classLocation})
310
+ }
311
+ if (declaration.kind == "EffectResourceDeclaration") {
312
+ return unsupportedSyntax("php", "owned resource construction", nodeLocation(construction, filename, source))
313
+ }
314
+ const contextualTypeArguments = expectedType?.kind == "RecordType" && expectedType.declarationId == declaration.id
315
+ ? expectedType.arguments
316
+ : undefined
317
+ const visibleTypeParameterIds = new Set([...context.typeParameters?.values() ?? []]
318
+ .map((parameter) => /** @type {string} */ (parameter.id)))
319
+ const typeArguments = contextualTypeArguments &&
320
+ !contextualTypeArguments.some((argument) => typeContainsAnyVariable(argument, visibleTypeParameterIds))
321
+ ? contextualTypeArguments
322
+ : undefined
323
+
324
+ return withParserRanges({
325
+ arguments: construction.arguments.map((argument, index) =>
326
+ convertExpression(argument, filename, source, context,
327
+ instantiatedRecordFieldType(declaration, typeArguments, index))),
328
+ kind: /** @type {const} */ ("RecordConstruction"),
329
+ location,
330
+ record: recordType(/** @type {string} */ (declaration.id), nodeLocation(construction.what, filename, source),
331
+ typeArguments)
332
+ }, {record: nodeLocation(construction.what, filename, source)})
333
+ }
334
+
335
+ if (node.kind == "propertylookup") {
336
+ const read = /** @type {import("php-parser").PropertyLookup} */ (node)
337
+ const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
338
+
339
+ if (!offset || typeof offset.name != "string") {
340
+ return unsupportedSyntax("php", "dynamic or computed member access", nodeLocation(read.offset, filename, source))
341
+ }
342
+ if (read.what.kind == "variable" && /** @type {import("php-parser").Variable} */ (read.what).name == "this" &&
343
+ context.currentClass) {
344
+ const field = context.currentClass.fields.find((candidate) => candidate.name == offset.name)
345
+
346
+ if (!field) return unsupportedSyntax("php", `unknown private field '${offset.name}'`, nodeLocation(offset, filename, source))
347
+ return withParserRanges({field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldRead"), location,
348
+ receiver: /** @type {import("../semantic/types.js").ReceiverExpression} */ (
349
+ convertExpression(read.what, filename, source, context))}, {member: nodeLocation(offset, filename, source)})
350
+ }
351
+ const receiverType = knownExpressionType(read.what, context)
352
+ if (receiverType?.kind == "ReferenceType") {
353
+ return unsupportedSyntax("php", "private reference-class field access", location)
354
+ }
355
+ const receiverRecord = receiverType?.kind == "OptionalType" ? receiverType.valueType : receiverType
356
+ const declaration = receiverRecord?.kind == "RecordType" ? context.records.get(receiverRecord.declarationId) : undefined
357
+
358
+ if ((declaration?.typeParameters?.length ?? 0) > 0) {
359
+ return unsupportedSyntax("php", "generic record property access", nodeLocation(read.offset, filename, source))
360
+ }
361
+ return withParserRanges({
362
+ field: offset.name,
363
+ kind: /** @type {const} */ ("MemberRead"),
364
+ location,
365
+ receiver: convertExpression(read.what, filename, source, context)
366
+ }, {member: nodeLocation(read.offset, filename, source)})
367
+ }
368
+
369
+ if (node.kind == "offsetlookup") {
370
+ const lookup = /** @type {import("php-parser").OffsetLookup} */ (node)
371
+
372
+ if (!lookup.offset) return unsupportedSyntax("php", "append or missing array index", location)
373
+ const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
374
+ const receiverType = receiver && typeof receiver.name == "string" ? context.bindings.get(receiver.name) : undefined
375
+ const accessKind = receiverType?.kind == "ListType" ? "list" :
376
+ receiverType?.kind == "MapType" || receiverType?.kind == "OrderedMapType" ? "map" :
377
+ lookup.offset.kind == "number" ? "list" : lookup.offset.kind == "string" ? "map" : undefined
378
+
379
+ if (accessKind == "list" && ["number", "string", "variable"].includes(lookup.offset.kind)) {
380
+ return withParserRanges({
381
+ collection: convertExpression(lookup.what, filename, source, context),
382
+ index: convertExpression(lookup.offset, filename, source, context),
383
+ kind: /** @type {const} */ ("ListIndexExpression"),
384
+ location,
385
+ totality: /** @type {const} */ ("proven")
386
+ }, {operator: tokenLocation("[", lookup.what.loc?.end.offset ?? 0, lookup.offset.loc?.start.offset ?? source.length, filename, source)})
387
+ }
388
+ if (accessKind == "map" && ["number", "string", "variable"].includes(lookup.offset.kind)) {
389
+ return withParserRanges({
390
+ collection: convertExpression(lookup.what, filename, source, context),
391
+ key: convertExpression(lookup.offset, filename, source, context),
392
+ kind: /** @type {const} */ ("MapLookupExpression"),
393
+ location,
394
+ totality: /** @type {const} */ ("proven")
395
+ }, {operator: tokenLocation("[", lookup.what.loc?.end.offset ?? 0, lookup.offset.loc?.start.offset ?? source.length, filename, source)})
396
+ }
397
+
398
+ return unsupportedSyntax("php", "nonliteral array access", nodeLocation(lookup.offset, filename, source))
399
+ }
400
+
172
401
  if (node.kind == "unary") {
173
402
  const unary = /** @type {import("php-parser").Unary} */ (node)
174
403
 
@@ -177,7 +406,7 @@ function convertExpression(node, filename, source) {
177
406
  const semantic = withAdaptedOperation(withParserRanges({
178
407
  kind: "UnaryExpression",
179
408
  location,
180
- operand: convertExpression(unary.what, filename, source)
409
+ operand: convertExpression(unary.what, filename, source, context)
181
410
  }, {
182
411
  operator: tokenLocation(unary.type, unary.loc?.start.offset ?? 0, unary.what.loc?.start.offset ?? unary.loc?.end.offset ?? source.length, filename, source)
183
412
  }), unary.type == "!" ? "Not" : "Negate")
@@ -187,15 +416,24 @@ function convertExpression(node, filename, source) {
187
416
 
188
417
  if (node.kind == "bin") {
189
418
  const binary = /** @type {import("php-parser").Bin} */ (node)
419
+ if (binary.type == "!==" && binary.left.kind == "variable" && binary.right.kind == "nullkeyword") {
420
+ const operand = /** @type {import("../semantic/types.js").IdentifierExpression} */ (
421
+ convertExpression(binary.left, filename, source, context, undefined, true)
422
+ )
423
+
424
+ return withParserRanges({kind: /** @type {const} */ ("OptionalIsPresent"), location, operand}, {
425
+ operator: tokenLocation(binary.type, binary.left.loc?.end.offset ?? 0, binary.right.loc?.start.offset ?? source.length, filename, source)
426
+ })
427
+ }
190
428
  if (["==", "!="].includes(binary.type)) return unsupportedSyntax("php", `coercive equality ${binary.type}`, location)
191
429
  if (["and", "or"].includes(binary.type)) return unsupportedSyntax("php", `precedence-sensitive boolean ${binary.type}`, location)
192
430
  if (!phpBinaryOperations.has(binary.type)) return unsupportedSyntax("php", `binary ${binary.type}`, location)
193
431
 
194
432
  const semantic = withAdaptedOperation({
195
433
  kind: /** @type {const} */ ("BinaryExpression"),
196
- left: convertExpression(binary.left, filename, source),
434
+ left: convertExpression(binary.left, filename, source, context),
197
435
  location,
198
- right: convertExpression(binary.right, filename, source)
436
+ right: convertExpression(binary.right, filename, source, context)
199
437
  }, /** @type {import("../semantic/operators.js").AdaptedOperation} */ (phpBinaryOperations.get(binary.type)))
200
438
 
201
439
  return /** @type {import("../semantic/types.js").Expression} */ (/** @type {unknown} */ (withParserRanges(semantic, {
@@ -206,14 +444,88 @@ function convertExpression(node, filename, source) {
206
444
  if (node.kind == "call") {
207
445
  const call = /** @type {import("php-parser").Call} */ (node)
208
446
 
209
- if (call.what.kind != "name" && call.what.kind != "identifier") {
447
+ if (call.what.kind == "propertylookup") {
448
+ const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
449
+ const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
450
+ const receiverType = knownExpressionType(lookup.what, context)
451
+
452
+ if (member && typeof member.name == "string" && receiverType?.kind == "ReferenceType") {
453
+ const method = context.classes.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == member.name)
454
+ const unsupportedArgument = call.arguments.find((argument) =>
455
+ argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
456
+
457
+ if (unsupportedArgument) return unsupportedSyntax("php", "non-positional method argument",
458
+ nodeLocation(unsupportedArgument, filename, source))
459
+ return withParserRanges({arguments: call.arguments.map((argument, index) =>
460
+ convertExpression(argument, filename, source, context, method?.parameters[index]?.type)),
461
+ kind: /** @type {const} */ ("MethodCallExpression"), location, method: method?.id ?? member.name,
462
+ receiver: convertExpression(lookup.what, filename, source, context)}, {member: nodeLocation(member, filename, source)})
463
+ }
464
+ }
465
+
466
+ if (call.what.kind == "propertylookup" && call.arguments.length == 0) {
467
+ const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
468
+ const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
469
+ const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
470
+
471
+ if (receiver && typeof receiver.name == "string" && member?.name == "getMessage" && context.bindings.get(receiver.name)?.kind == "ErrorType") {
472
+ const receiverLocation = nodeLocation(receiver, filename, source)
473
+
474
+ return withParserRanges({
475
+ kind: /** @type {const} */ ("ErrorMessageRead"),
476
+ location,
477
+ receiver: withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: receiverLocation, name: receiver.name}, {
478
+ name: receiverLocation
479
+ })
480
+ }, {member: nodeLocation(member, filename, source)})
481
+ }
482
+ if (member && typeof member.name == "string" && knownGenericRecordAccessorType(lookup, context)) {
483
+ return withParserRanges({
484
+ field: member.name,
485
+ kind: /** @type {const} */ ("MemberRead"),
486
+ location,
487
+ receiver: convertExpression(lookup.what, filename, source, context)
488
+ }, {member: nodeLocation(member, filename, source)})
489
+ }
490
+ }
491
+
492
+ if (call.what.kind == "name" && call.what.name == "count" && call.arguments.length == 1) {
493
+ return withParserRanges({
494
+ collection: convertExpression(call.arguments[0], filename, source, context),
495
+ kind: /** @type {const} */ ("CollectionSizeExpression"),
496
+ location
497
+ }, {operator: nodeLocation(call.what, filename, source)})
498
+ }
499
+
500
+ if (call.what.kind != "name") {
210
501
  return unsupportedSyntax("php", "dynamic call", location)
211
502
  }
503
+ const calledName = /** @type {import("php-parser").Name} */ (call.what)
212
504
 
213
- const callee = /** @type {import("php-parser").Name | import("php-parser").Identifier} */ (call.what).name
505
+ if (calledName.resolution != "uqn" || calledName.name.includes("\\")) {
506
+ return unsupportedSyntax("php", "qualified call", nodeLocation(call.what, filename, source))
507
+ }
508
+ const unsupportedArgument = call.arguments.find((argument) =>
509
+ argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
510
+
511
+ if (unsupportedArgument) {
512
+ const detail = unsupportedArgument.kind == "namedargument" ? "named argument" :
513
+ unsupportedArgument.kind == "variadic" ? "unpacked argument" : "by-reference argument"
514
+
515
+ return unsupportedSyntax("php", detail, nodeLocation(unsupportedArgument, filename, source))
516
+ }
517
+
518
+ const callee = calledName.name
519
+
520
+ const signature = context.functions.get(callee)
521
+ const knownArguments = call.arguments.map((argument) => knownCallArgumentType(argument, context))
522
+ const parameterTypes = signature ? knownCallParameterTypes(signature, knownArguments) : undefined
214
523
 
215
524
  return withParserRanges({
216
- arguments: call.arguments.map((argument) => convertExpression(argument, filename, source)),
525
+ arguments: call.arguments.map((argument, index) =>
526
+ convertExpression(argument, filename, source, context,
527
+ parameterTypes?.[index] ?? signature?.parameters[index]?.type,
528
+ preservesGenericOptionalEvidence(signature, index))),
217
529
  callee,
218
530
  kind: "CallExpression",
219
531
  location
@@ -223,23 +535,158 @@ function convertExpression(node, filename, source) {
223
535
  return unsupportedSyntax("php", node.kind, location)
224
536
  }
225
537
 
538
+ /**
539
+ * Resolves only result types established by explicit signatures and bindings.
540
+ * @param {import("php-parser").Expression} node - Parser-owned expression.
541
+ * @param {PhpConversionContext} context - Typed conversion context.
542
+ * @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Known result type.
543
+ */
544
+ function knownExpressionType(node, context) {
545
+ if (node.kind == "variable") {
546
+ const variable = /** @type {import("php-parser").Variable} */ (node)
547
+
548
+ if (variable.name == "this" && context.currentClass?.id) {
549
+ return {declarationId: context.currentClass.id, kind: "ReferenceType"}
550
+ }
551
+ if (typeof variable.name == "string") return context.bindings.get(variable.name)
552
+ }
553
+ if (node.kind == "call") {
554
+ const call = /** @type {import("php-parser").Call} */ (node)
555
+ const name = call.what.kind == "name" && typeof call.what.name == "string" ? call.what.name : undefined
556
+
557
+ if (name) {
558
+ const signature = context.functions.get(name)
559
+
560
+ if (signature) return knownCallReturnType(signature, call.arguments.map((argument) => knownExpressionType(argument, context)))
561
+ }
562
+ if (call.what.kind == "propertylookup" && call.arguments.length == 0) {
563
+ return knownGenericRecordAccessorType(
564
+ /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what)), context)
565
+ }
566
+ if (call.what.kind == "propertylookup") {
567
+ const lookup = /** @type {import("php-parser").PropertyLookup} */ (/** @type {unknown} */ (call.what))
568
+ const receiver = knownExpressionType(lookup.what, context)
569
+ const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
570
+
571
+ if (receiver?.kind == "ReferenceType" && member && typeof member.name == "string") {
572
+ return context.classes.get(receiver.declarationId)?.methods.find((method) => method.name == member.name)?.returnType
573
+ }
574
+ }
575
+ }
576
+ if (node.kind == "offsetlookup") {
577
+ const lookup = /** @type {import("php-parser").OffsetLookup} */ (node)
578
+ const collectionType = knownExpressionType(lookup.what, context)
579
+
580
+ if (collectionType?.kind == "ListType") return collectionType.elementType
581
+ if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") return collectionType.valueType
582
+ }
583
+ if (node.kind == "new") {
584
+ const construction = /** @type {import("php-parser").New} */ (node)
585
+ const name = construction.what.kind == "name" ? /** @type {import("php-parser").Name} */ (construction.what) : undefined
586
+ const declaration = name && typeof name.name == "string" ? context.recordNames.get(name.name) : undefined
587
+
588
+ if (declaration?.id) return declaration.kind == "ClassDeclaration"
589
+ ? {declarationId: declaration.id, kind: "ReferenceType"}
590
+ : {declarationId: declaration.id, kind: "RecordType"}
591
+ }
592
+ if (node.kind == "propertylookup") {
593
+ const read = /** @type {import("php-parser").PropertyLookup} */ (node)
594
+ const receiver = knownExpressionType(read.what, context)
595
+
596
+ if (read.what.kind == "variable" && /** @type {import("php-parser").Variable} */ (read.what).name == "this" &&
597
+ context.currentClass) {
598
+ const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
599
+
600
+ return context.currentClass.fields.find((field) => field.name == offset?.name)?.type
601
+ }
602
+
603
+ if (receiver?.kind != "RecordType") return undefined
604
+ const declaration = context.records.get(receiver.declarationId)
605
+ const offset = read.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (read.offset) : undefined
606
+
607
+ const memberName = offset && typeof offset.name == "string" ? offset.name : undefined
608
+
609
+ if (memberName && declaration) {
610
+ const index = declaration.fields.findIndex((field) => field.name == memberName)
611
+
612
+ return instantiatedRecordFieldType(declaration, receiver.arguments, index)
613
+ }
614
+ }
615
+
616
+ return undefined
617
+ }
618
+
619
+ /**
620
+ * Resolves exact parser-known argument evidence without changing the established
621
+ * expression-result classifier used for optional conversion.
622
+ * @param {import("php-parser").Expression} node - Parser-owned call argument.
623
+ * @param {PhpConversionContext} context - Typed conversion context.
624
+ * @returns {import("../semantic/types.js").SemanticFunctionReturnType | import("../semantic/types.js").ErrorType | undefined} Known argument type.
625
+ */
626
+ function knownCallArgumentType(node, context) {
627
+ const known = knownExpressionType(node, context)
628
+
629
+ if (known) return known
630
+ if (node.kind == "number" && Number.isSafeInteger(Number(/** @type {import("php-parser").Number} */ (node).value))) {
631
+ return scalarType("integer")
632
+ }
633
+ if (node.kind == "boolean" && typeof /** @type {import("php-parser").Boolean} */ (node).value == "boolean") {
634
+ return scalarType("boolean")
635
+ }
636
+ if (node.kind == "string" && typeof /** @type {import("php-parser").String} */ (node).value == "string" &&
637
+ hasOnlyUnicodeScalars(/** @type {import("php-parser").String} */ (node).value)) {
638
+ return scalarType("string")
639
+ }
640
+
641
+ return undefined
642
+ }
643
+
644
+ /**
645
+ * Resolves PHP's exact zero-argument accessor carrier against its receiver's
646
+ * generic record declaration.
647
+ * @param {import("php-parser").PropertyLookup} lookup - Parser-owned method lookup.
648
+ * @param {PhpConversionContext} context - Typed conversion context.
649
+ * @returns {import("../semantic/types.js").SemanticValueType | undefined} Instantiated field type.
650
+ */
651
+ function knownGenericRecordAccessorType(lookup, context) {
652
+ const receiver = knownExpressionType(lookup.what, context)
653
+ const receiverRecord = receiver?.kind == "OptionalType" ? receiver.valueType : receiver
654
+
655
+ if (receiverRecord?.kind != "RecordType") return undefined
656
+ const declaration = context.records.get(receiverRecord.declarationId)
657
+ const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
658
+ const index = member && typeof member.name == "string"
659
+ ? declaration?.fields.findIndex((field) => field.name == member.name) ?? -1
660
+ : -1
661
+
662
+ if (!declaration || (declaration.typeParameters?.length ?? 0) == 0 || index < 0) return undefined
663
+
664
+ return instantiatedRecordFieldType(declaration, receiverRecord.arguments, index)
665
+ }
666
+
226
667
  /**
227
668
  * Converts one explicit PHP return.
228
669
  * @param {import("php-parser").Node} node - PHP node.
229
670
  * @param {string} filename - Source filename.
230
671
  * @param {string} source - Complete source.
672
+ * @param {PhpConversionContext} context - Typed conversion context.
231
673
  * @returns {import("../semantic/types.js").ReturnStatement} Semantic return.
232
674
  */
233
- function convertReturn(node, filename, source) {
675
+ function convertReturn(node, filename, source, context) {
234
676
  const location = nodeLocation(node, filename, source)
235
677
 
236
678
  if (node.kind != "return") return unsupportedSyntax("php", node.kind, location)
237
679
 
238
680
  const returnNode = /** @type {import("php-parser").Return} */ (node)
681
+ const expectedType = context.returnType?.kind == "TypeReference" && context.returnType.name == "void"
682
+ ? undefined
683
+ : /** @type {import("../semantic/types.js").SemanticValueType | undefined} */ (context.returnType)
239
684
 
240
- if (!returnNode.expr) return unsupportedSyntax("php", "empty return", location)
241
-
242
- return {expression: convertExpression(returnNode.expr, filename, source), kind: "ReturnStatement", location}
685
+ return {
686
+ ...(returnNode.expr ? {expression: convertExpression(returnNode.expr, filename, source, context, expectedType)} : {}),
687
+ kind: "ReturnStatement",
688
+ location
689
+ }
243
690
  }
244
691
 
245
692
  /**
@@ -248,9 +695,11 @@ function convertReturn(node, filename, source) {
248
695
  * @param {string} name - Local name.
249
696
  * @param {string} filename - Source filename.
250
697
  * @param {string} source - Complete source.
251
- * @returns {{immutable: boolean, type: import("../semantic/types.js").TypeReference} | undefined} Metadata when present.
698
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
699
+ * @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
700
+ * @returns {{immutable: boolean, type: import("../semantic/types.js").SemanticValueType} | undefined} Metadata when present.
252
701
  */
253
- function localMetadata(node, name, filename, source) {
702
+ function localMetadata(node, name, filename, source, recordNames, typeParameters = new Map()) {
254
703
  const comments = node.leadingComments ?? []
255
704
  const comment = comments.at(-1)
256
705
 
@@ -278,25 +727,28 @@ function localMetadata(node, name, filename, source) {
278
727
 
279
728
  return {
280
729
  immutable: immutable.length == 1,
281
- type: requireSourceScalarType(
282
- "php",
283
- variables[0].name,
284
- `Local '${name}'`,
285
- location,
286
- commentTagLocation(comment, variables[0], "name", filename, source)
287
- )
730
+ type: documentedValueType({
731
+ language: "php",
732
+ location: commentTagLocation(comment, variables[0], "name", filename, source),
733
+ ownerLocation: location,
734
+ records: recordNames,
735
+ source,
736
+ sourceType: variables[0].name,
737
+ subject: `Local '${name}'`,
738
+ typeParameters
739
+ })
288
740
  }
289
741
  }
290
742
 
291
743
  /**
292
744
  * Converts one PHP local declaration or assignment.
293
745
  * @param {import("php-parser").Node} node - PHP statement.
294
- * @param {Set<string>} visible - Names visible during adaptation.
295
746
  * @param {string} filename - Source filename.
296
747
  * @param {string} source - Complete source.
297
- * @returns {import("../semantic/types.js").LocalStatement} Semantic local statement.
748
+ * @param {PhpConversionContext} context - Typed conversion context.
749
+ * @returns {import("../semantic/types.js").LocalStatement | import("../semantic/types.js").PrivateFieldWriteStatement} Semantic local or private-field statement.
298
750
  */
299
- function convertLocalStatement(node, visible, filename, source) {
751
+ function convertLocalStatement(node, filename, source, context) {
300
752
  const location = nodeLocation(node, filename, source)
301
753
 
302
754
  if (node.kind != "expressionstatement") return unsupportedSyntax("php", node.kind, location)
@@ -308,38 +760,90 @@ function convertLocalStatement(node, visible, filename, source) {
308
760
  const assignment = /** @type {import("php-parser").Assign} */ (expression)
309
761
 
310
762
  if (assignment.operator != "=") return unsupportedSyntax("php", `assignment ${assignment.operator}`, location)
763
+ if (assignment.left.kind == "propertylookup" && context.currentClass) {
764
+ const lookup = /** @type {import("php-parser").PropertyLookup} */ (assignment.left)
765
+ const receiver = lookup.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
766
+ const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
767
+ const field = receiver?.name == "this" && member && typeof member.name == "string"
768
+ ? context.currentClass.fields.find((candidate) => candidate.name == member.name)
769
+ : undefined
770
+
771
+ if (!field) return unsupportedSyntax("php", "private field write outside the declaring receiver", nodeLocation(assignment.left, filename, source))
772
+ return withParserRanges({expression: convertExpression(assignment.right, filename, source, context, field.type),
773
+ field: /** @type {string} */ (field.id), kind: /** @type {const} */ ("PrivateFieldWriteStatement"), location,
774
+ receiver: /** @type {import("../semantic/types.js").ReceiverExpression} */ (
775
+ convertExpression(lookup.what, filename, source, context))},
776
+ {member: nodeLocation(/** @type {import("php-parser").Identifier} */ (member), filename, source), operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0,
777
+ assignment.right.loc?.start.offset ?? source.length, filename, source)})
778
+ }
311
779
  if (assignment.left.kind != "variable") return unsupportedSyntax("php", assignment.left.kind, nodeLocation(assignment.left, filename, source))
780
+ if (assignment.right.kind == "throw") {
781
+ return unsupportedSyntax("php", "throw expression in a value context", nodeLocation(assignment.right, filename, source))
782
+ }
312
783
 
313
784
  const variable = /** @type {import("php-parser").Variable} */ (assignment.left)
314
785
  const targetLocation = nodeLocation(variable, filename, source)
315
786
 
316
787
  if (typeof variable.name != "string" || variable.curly) return unsupportedSyntax("php", "dynamic variable", targetLocation)
317
788
 
318
- const metadata = localMetadata(node, variable.name, filename, source)
789
+ const metadata = localMetadata(node, variable.name, filename, source, context.recordNames, context.typeParameters)
319
790
 
320
791
  if (metadata) {
321
- visible.add(variable.name)
792
+ let declaredType = metadata.type
793
+ let ordered = false
794
+
795
+ if (declaredType.kind == "MapType" && context.orderedMapDeclarations?.has(assignment) && assignment.right.kind == "array") {
796
+ declaredType = orderedMapTypeFromMap(declaredType, location)
797
+ ordered = true
798
+ }
799
+ const initializer = convertExpression(assignment.right, filename, source, context, declaredType)
800
+
801
+ context.bindings.set(variable.name, declaredType)
322
802
  return withParserRanges({
323
- initializer: convertExpression(assignment.right, filename, source),
803
+ initializer,
324
804
  kind: "LocalDeclaration",
325
805
  location,
326
- mutable: !metadata.immutable,
806
+ mutable: ordered ? false : !metadata.immutable,
807
+ name: variable.name,
808
+ type: declaredType
809
+ }, {
810
+ name: targetLocation,
811
+ operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
812
+ })
813
+ }
814
+
815
+ const inferredType = knownExpressionType(assignment.right, context)
816
+
817
+ if (inferredType?.kind == "RecordType" || inferredType?.kind == "ReferenceType" ||
818
+ inferredType?.kind == "OwnedResourceType") {
819
+ const initializer = convertExpression(assignment.right, filename, source, context, inferredType)
820
+
821
+ context.bindings.set(variable.name, inferredType)
822
+ return withParserRanges({
823
+ initializer,
824
+ kind: /** @type {const} */ ("LocalDeclaration"),
825
+ location,
826
+ mutable: false,
327
827
  name: variable.name,
328
- type: metadata.type
828
+ type: inferredType
329
829
  }, {
330
830
  name: targetLocation,
331
831
  operator: tokenLocation("=", assignment.left.loc?.end.offset ?? 0, assignment.right.loc?.start.offset ?? source.length, filename, source)
332
832
  })
333
833
  }
334
834
 
335
- if (!visible.has(variable.name)) return missingType("php", `Local '${variable.name}'`, location)
835
+ const bindingType = context.bindings.get(variable.name)
836
+
837
+ if (!bindingType) return missingType("php", `Local '${variable.name}'`, location)
336
838
 
337
839
  const target = withParserRanges({kind: /** @type {const} */ ("IdentifierExpression"), location: targetLocation, name: variable.name}, {
338
840
  name: targetLocation
339
841
  })
842
+ const value = convertExpression(assignment.right, filename, source, context,
843
+ bindingType.kind == "ErrorType" ? undefined : bindingType)
340
844
 
341
845
  return withParserRanges({
342
- expression: convertExpression(assignment.right, filename, source),
846
+ expression: value,
343
847
  kind: "AssignmentStatement",
344
848
  location,
345
849
  target
@@ -351,33 +855,236 @@ function convertLocalStatement(node, visible, filename, source) {
351
855
  /**
352
856
  * Converts one exhaustive PHP statement.
353
857
  * @param {import("php-parser").Node} node - PHP statement.
354
- * @param {Set<string>} visible - Names visible during adaptation.
355
858
  * @param {string} filename - Source filename.
356
859
  * @param {string} source - Complete source.
860
+ * @param {PhpConversionContext} context - Typed conversion context.
357
861
  * @returns {import("../semantic/types.js").Statement} Semantic statement.
358
862
  */
359
- function convertStatement(node, visible, filename, source) {
360
- if (node.kind == "return") return convertReturn(node, filename, source)
361
- if (node.kind == "if") return convertIf(/** @type {import("php-parser").If} */ (node), visible, filename, source)
362
- if (node.kind == "echo") return convertPrint(/** @type {import("php-parser").Echo} */ (node), filename, source)
363
- if (node.kind == "expressionstatement") return convertLocalStatement(node, visible, filename, source)
863
+ function convertStatement(node, filename, source, context) {
864
+ if (node.kind == "return") return convertReturn(node, filename, source, context)
865
+ if (node.kind == "if") return convertIf(/** @type {import("php-parser").If} */ (node), filename, source, context)
866
+ if (node.kind == "throw") return convertPhpRaise(node, filename, source, context)
867
+ if (node.kind == "try") return convertPhpTry(node, filename, source, context)
868
+ if (node.kind == "while") return convertWhile(/** @type {import("php-parser").While} */ (node), filename, source, context)
869
+ if (node.kind == "foreach") return convertForEach(/** @type {import("php-parser").Foreach} */ (node), filename, source, context)
870
+ if (node.kind == "break" || node.kind == "continue") {
871
+ const control = /** @type {import("php-parser").Break | import("php-parser").Continue} */ (node)
872
+
873
+ if (control.level) return unsupportedSyntax("php", `${node.kind} level`, nodeLocation(node, filename, source))
874
+ const location = tokenLocation(node.kind, node.loc?.start.offset ?? 0, node.loc?.end.offset ?? source.length, filename, source)
875
+
876
+ return withParserRanges({
877
+ kind: /** @type {"BreakStatement" | "ContinueStatement"} */ (node.kind == "break" ? "BreakStatement" : "ContinueStatement"),
878
+ location
879
+ }, {keyword: location})
880
+ }
881
+ if (node.kind == "echo") return convertPrint(/** @type {import("php-parser").Echo} */ (node), filename, source, context)
882
+ if (node.kind == "expressionstatement") {
883
+ const expression = /** @type {import("php-parser").ExpressionStatement} */ (node).expression
884
+
885
+ if (expression.kind == "call") {
886
+ return {
887
+ expression: /** @type {import("../semantic/types.js").CallExpression | import("../semantic/types.js").MethodCallExpression} */ (
888
+ convertExpression(expression, filename, source, context)
889
+ ),
890
+ kind: "ExpressionStatement",
891
+ location: nodeLocation(node, filename, source)
892
+ }
893
+ }
894
+
895
+ return convertLocalStatement(node, filename, source, context)
896
+ }
364
897
 
365
898
  return unsupportedSyntax("php", node.kind, nodeLocation(node, filename, source))
366
899
  }
367
900
 
901
+ /**
902
+ * Converts exact `throw new DeclaredError(message)`.
903
+ * @param {import("php-parser").Node} node - PHP throw node.
904
+ * @param {string} filename - Source filename.
905
+ * @param {string} source - Complete source.
906
+ * @param {PhpConversionContext} context - Typed lexical context.
907
+ * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
908
+ */
909
+ function convertPhpRaise(node, filename, source, context) {
910
+ const location = nodeLocation(node, filename, source)
911
+ const construction = Reflect.get(node, "what")
912
+
913
+ if (!construction || construction.kind != "new" || construction.what?.kind != "name" ||
914
+ construction.what.resolution != "uqn" || !context.errorNames.has(construction.what.name) ||
915
+ !Array.isArray(construction.arguments) || construction.arguments.length != 1 ||
916
+ construction.arguments[0].kind == "namedargument" || construction.arguments[0].kind == "variadic") {
917
+ return unsupportedSyntax("php", "throw other than exact declared error construction", location)
918
+ }
919
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(construction.what.name))
920
+ const typeLocation = nodeLocation(construction.what, filename, source)
921
+
922
+ return withParserRanges({
923
+ error: withParserRanges({
924
+ error: withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {type: typeLocation}),
925
+ kind: /** @type {const} */ ("ErrorConstruction"),
926
+ location: nodeLocation(construction, filename, source),
927
+ message: convertExpression(construction.arguments[0], filename, source, context)
928
+ }, {type: typeLocation}),
929
+ kind: /** @type {const} */ ("RaiseStatement"),
930
+ location
931
+ }, {keyword: tokenLocation("throw", node.loc?.start.offset ?? 0, construction.loc?.start.offset ?? source.length, filename, source)})
932
+ }
933
+
934
+ /**
935
+ * Converts one exact typed catch with no finally.
936
+ * @param {import("php-parser").Node} node - PHP try node.
937
+ * @param {string} filename - Source filename.
938
+ * @param {string} source - Complete source.
939
+ * @param {PhpConversionContext} context - Typed lexical context.
940
+ * @returns {import("../semantic/types.js").TryStatement} Semantic exact handler.
941
+ */
942
+ function convertPhpTry(node, filename, source, context) {
943
+ const location = nodeLocation(node, filename, source)
944
+ const catches = Reflect.get(node, "catches")
945
+ const body = Reflect.get(node, "body")
946
+
947
+ if (Reflect.get(node, "always") || !Array.isArray(catches) || catches.length != 1 || body?.kind != "block") {
948
+ return unsupportedSyntax("php", "try without one typed catch and no finally", location)
949
+ }
950
+ const handler = catches[0]
951
+ const caught = handler.what?.[0]
952
+ const binding = handler.variable
953
+
954
+ if (handler.what?.length != 1 || caught?.kind != "name" || caught.resolution != "uqn" ||
955
+ !context.errorNames.has(caught.name) || binding?.kind != "variable" || typeof binding.name != "string" ||
956
+ binding.curly || handler.body?.kind != "block") {
957
+ return unsupportedSyntax("php", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
958
+ }
959
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(caught.name))
960
+ const caughtLocation = nodeLocation(caught, 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(binding.name, catchType)
968
+ return withParserRanges({
969
+ body: convertBlock(body, filename, source, bodyContext),
970
+ catchBinding: withParserRanges({
971
+ kind: /** @type {const} */ ("CatchBinding"),
972
+ location: nodeLocation(binding, filename, source),
973
+ mutable: /** @type {const} */ (false),
974
+ name: binding.name,
975
+ type: catchType
976
+ }, {name: nodeLocation(binding, filename, source)}),
977
+ catchBody: convertBlock(handler.body, filename, source, catchContext),
978
+ catchType,
979
+ kind: /** @type {const} */ ("TryStatement"),
980
+ location
981
+ }, {catch: nodeLocation(handler, filename, source), try: tokenLocation("try", node.loc?.start.offset ?? 0, body.loc?.start.offset ?? source.length, filename, source)})
982
+ }
983
+
984
+ /**
985
+ * Converts exact block-form PHP `foreach ($list as $value)`.
986
+ * @param {import("php-parser").Foreach} node - PHP foreach node.
987
+ * @param {string} filename - Source filename.
988
+ * @param {string} source - Complete source.
989
+ * @param {PhpConversionContext} context - Typed conversion context.
990
+ * @returns {import("../semantic/types.js").ForEachStatement | import("../semantic/types.js").ForEachMapStatement} Semantic loop.
991
+ */
992
+ function convertForEach(node, filename, source, context) {
993
+ const location = nodeLocation(node, filename, source)
994
+
995
+ if (node.shortForm) return unsupportedSyntax("php", "alternative foreach syntax", location)
996
+ if (node.value.kind != "variable") return unsupportedSyntax("php", "foreach destructuring", nodeLocation(node.value, filename, source))
997
+ const variable = /** @type {import("php-parser").Variable} */ (node.value)
998
+
999
+ if (Reflect.get(variable, "byref") === true) return unsupportedSyntax("php", "by-reference foreach", nodeLocation(variable, filename, source))
1000
+ if (typeof variable.name != "string" || variable.curly) return unsupportedSyntax("php", "dynamic foreach binding", nodeLocation(variable, filename, source))
1001
+ if (!node.body || node.body.kind != "block") {
1002
+ return unsupportedSyntax("php", "foreach without block body", node.body ? nodeLocation(node.body, filename, source) : location)
1003
+ }
1004
+ const body = /** @type {import("php-parser").Block} */ (node.body)
1005
+ const collectionType = iterationOperandType(knownExpressionType(node.source, context))
1006
+
1007
+ if (node.key) {
1008
+ if (node.key.kind != "variable") return unsupportedSyntax("php", "foreach key binding shape", nodeLocation(node.key, filename, source))
1009
+ const keyVariable = /** @type {import("php-parser").Variable} */ (node.key)
1010
+
1011
+ if (Reflect.get(keyVariable, "byref") === true || typeof keyVariable.name != "string" || keyVariable.curly) {
1012
+ return unsupportedSyntax("php", "invalid foreach key binding", nodeLocation(keyVariable, filename, source))
1013
+ }
1014
+ if (collectionType?.kind == "MapType") {
1015
+ return unsupportedSyntax("php", "unordered map iteration", nodeLocation(node.source, filename, source))
1016
+ }
1017
+ if (!collectionType || collectionType.kind != "OrderedMapType") {
1018
+ return missingType("php", "Ordered map iteration collection", nodeLocation(node.source, filename, source))
1019
+ }
1020
+ const keyLocation = nodeLocation(keyVariable, filename, source)
1021
+ const valueLocation = nodeLocation(variable, filename, source)
1022
+ const keyType = iterationBindingType(collectionType.keyType, keyLocation)
1023
+ const valueType = iterationBindingType(collectionType.valueType, valueLocation)
1024
+ const keyBinding = withParserRanges({
1025
+ kind: /** @type {const} */ ("ValueBinding"), location: keyLocation, mutable: /** @type {const} */ (false),
1026
+ name: keyVariable.name, type: keyType
1027
+ }, {name: keyLocation})
1028
+ const valueBinding = withParserRanges({
1029
+ kind: /** @type {const} */ ("ValueBinding"), location: valueLocation, mutable: /** @type {const} */ (false),
1030
+ name: variable.name, type: valueType
1031
+ }, {name: valueLocation})
1032
+ const bodyContext = {...context, bindings: new Map(context.bindings)}
1033
+
1034
+ bodyContext.bindings.set(keyVariable.name, keyType)
1035
+ bodyContext.bindings.set(variable.name, valueType)
1036
+ return withParserRanges({
1037
+ body: convertBlock(body, filename, source, bodyContext),
1038
+ keyBinding,
1039
+ kind: /** @type {const} */ ("ForEachMapStatement"),
1040
+ location,
1041
+ map: convertExpression(node.source, filename, source, context),
1042
+ valueBinding
1043
+ }, {operator: tokenLocation("foreach", node.loc?.start.offset ?? 0, node.source.loc?.start.offset ?? source.length,
1044
+ filename, source)})
1045
+ }
1046
+
1047
+ if (collectionType?.kind == "MapType" || collectionType?.kind == "OrderedMapType") {
1048
+ return unsupportedSyntax("php", "map iteration without key/value pair binding", nodeLocation(node.source, filename, source))
1049
+ }
1050
+ if (!collectionType || collectionType.kind != "ListType") {
1051
+ return missingType("php", "Iteration collection", nodeLocation(node.source, filename, source))
1052
+ }
1053
+ const bindingLocation = nodeLocation(variable, filename, source)
1054
+ const inferredType = collectionType.elementType
1055
+ const bindingType = iterationBindingType(inferredType, bindingLocation)
1056
+ const valueBinding = withParserRanges({
1057
+ kind: /** @type {const} */ ("ValueBinding"),
1058
+ location: bindingLocation,
1059
+ mutable: /** @type {const} */ (false),
1060
+ name: variable.name,
1061
+ type: bindingType
1062
+ }, {name: bindingLocation})
1063
+ const bodyContext = {...context, bindings: new Map(context.bindings)}
1064
+
1065
+ bodyContext.bindings.set(variable.name, bindingType)
1066
+ return withParserRanges({
1067
+ body: convertBlock(body, filename, source, bodyContext),
1068
+ kind: /** @type {const} */ ("ForEachStatement"),
1069
+ list: convertExpression(node.source, filename, source, context),
1070
+ location,
1071
+ valueBinding
1072
+ }, {operator: tokenLocation("foreach", node.loc?.start.offset ?? 0, node.source.loc?.start.offset ?? source.length, filename, source)})
1073
+ }
1074
+
368
1075
  /**
369
1076
  * Converts an ordered PHP block with its own adaptation scope.
370
1077
  * @param {import("php-parser").Block} node - PHP parser block.
371
- * @param {Set<string>} visible - Names visible on block entry.
372
1078
  * @param {string} filename - Source filename.
373
1079
  * @param {string} source - Complete source.
1080
+ * @param {PhpConversionContext} context - Typed conversion context.
374
1081
  * @returns {import("../semantic/types.js").Block} Semantic block.
375
1082
  */
376
- function convertBlock(node, visible, filename, source) {
1083
+ function convertBlock(node, filename, source, context) {
377
1084
  return {
378
1085
  kind: "Block",
379
1086
  location: nodeLocation(node, filename, source),
380
- statements: node.children.map((statement) => convertStatement(statement, visible, filename, source))
1087
+ statements: node.children.map((statement) => convertStatement(statement, filename, source, context))
381
1088
  }
382
1089
  }
383
1090
 
@@ -388,9 +1095,10 @@ function convertBlock(node, visible, filename, source) {
388
1095
  * @param {import("../semantic/types.js").SourceLocation} location - Source location.
389
1096
  * @param {string} filename - Source filename.
390
1097
  * @param {string} source - Complete source.
391
- * @returns {import("../semantic/types.js").TypeReference} Semantic type.
1098
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
1099
+ * @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
392
1100
  */
393
- function convertType(sourceType, subject, location, filename, source) {
1101
+ function convertType(sourceType, subject, location, filename, source, recordNames = new Map()) {
394
1102
  if (!sourceType) return requireSourceScalarType("php", undefined, subject, location)
395
1103
  if (sourceType.kind != "typereference" && sourceType.kind != "name" && sourceType.kind != "identifier") {
396
1104
  return unsupportedSyntax("php", "unsupported scalar type", nodeLocation(sourceType, filename, source))
@@ -398,17 +1106,161 @@ function convertType(sourceType, subject, location, filename, source) {
398
1106
 
399
1107
  const typeName = /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (sourceType).name
400
1108
 
401
- return requireSourceScalarType("php", typeName, subject, location, nodeLocation(sourceType, filename, source))
1109
+ const typeLocation = nodeLocation(sourceType, filename, source)
1110
+ const declaration = recordNames.get(typeName)
1111
+
1112
+ if (declaration?.id) return declaration.kind == "ClassDeclaration"
1113
+ ? referenceType(declaration.id, typeLocation)
1114
+ : declaration.kind == "EffectResourceDeclaration"
1115
+ ? ownedResourceType(declaration.id, typeLocation)
1116
+ : recordType(declaration.id, typeLocation)
1117
+
1118
+ return requireSourceScalarType("php", typeName, subject, location, typeLocation)
1119
+ }
1120
+
1121
+ /**
1122
+ * Reads the exact PHPDoc container signature attached to one function.
1123
+ * @param {import("php-parser").Function} node - Function declaration.
1124
+ * @param {string} filename - Source filename.
1125
+ * @param {string} source - Complete source.
1126
+ * @param {string} [ownerId] - Stable declaration identity.
1127
+ * @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[]}} Documented types.
1128
+ */
1129
+ function functionDocumentedTypes(node, filename, source, ownerId) {
1130
+ const comment = node.leadingComments?.at(-1)
1131
+
1132
+ if (!comment || comment.kind != "commentblock" || !comment.loc || !node.loc ||
1133
+ !/^\s*$/u.test(source.slice(comment.loc.end.offset, node.loc.start.offset))) return {parameters: new Map(), returnType: undefined}
1134
+ const block = parseComment(comment.value)[0]
1135
+ const tags = block?.tags ?? []
1136
+ const parameterTags = tags.filter((tag) => tag.tag == "param")
1137
+ const returnTags = tags.filter((tag) => tag.tag == "return" || tag.tag == "returns")
1138
+ const templateTags = tags.filter((tag) => tag.tag == "template")
1139
+ const malformedTemplate = tags.find((tag) => tag.tag.startsWith("template") && tag.tag != "template")
1140
+ const malformed = tags.some((tag) => !["param", "return", "returns", "template"].includes(tag.tag)) || returnTags.length > 1 ||
1141
+ parameterTags.some((tag) => tag.type != "" || !/^\$[A-Za-z_][A-Za-z0-9_]*$/u.test(tag.description)) ||
1142
+ returnTags.some((tag) => tag.type != "" || tag.description != "") || malformedTemplate ||
1143
+ templateTags.some((tag) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(tag.name) || tag.type != "" || tag.description != "") ||
1144
+ templateTags.length > 0 && !ownerId
1145
+
1146
+ if (malformed) return unsupportedSyntax("php", "malformed function type annotation", nodeLocation(node, filename, source))
1147
+ const parameters = new Map(parameterTags.map((tag) => [tag.description.slice(1), {
1148
+ location: commentTagLocation(comment, tag, "name", filename, source),
1149
+ sourceType: tag.name
1150
+ }]))
1151
+
1152
+ if (parameters.size != parameterTags.length) {
1153
+ return unsupportedSyntax("php", "duplicate parameter annotation", nodeLocation(node, filename, source))
1154
+ }
1155
+ const returnTag = returnTags[0]
1156
+ const typeParameters = templateTags.map((tag, index) => {
1157
+ const location = commentTagLocation(comment, tag, "name", filename, source)
1158
+
1159
+ return withParserRanges({id: `${ownerId}:type:${index}`, kind: /** @type {const} */ ("TypeParameter"), location, name: tag.name}, {name: location})
1160
+ })
1161
+
1162
+ return {
1163
+ parameters,
1164
+ returnType: returnTag ? {
1165
+ location: commentTagLocation(comment, returnTag, "name", filename, source),
1166
+ sourceType: returnTag.name
1167
+ } : undefined,
1168
+ ...(typeParameters.length > 0 ? {typeParameters} : {})
1169
+ }
1170
+ }
1171
+
1172
+ /**
1173
+ * Reads an optional exact class-level PHPDoc template list.
1174
+ * @param {import("php-parser").Class} node - Class declaration.
1175
+ * @param {string} ownerId - Stable record identity.
1176
+ * @param {string} filename - Source filename.
1177
+ * @param {string} source - Complete source.
1178
+ * @returns {import("../semantic/types.js").TypeParameter[] | undefined} Ordered parameters.
1179
+ */
1180
+ function classDocumentedTypeParameters(node, ownerId, filename, source) {
1181
+ const documented = functionDocumentedTypes(
1182
+ /** @type {import("php-parser").Function} */ (/** @type {unknown} */ (node)), filename, source, ownerId)
1183
+
1184
+ if (documented.parameters.size || documented.returnType) {
1185
+ return unsupportedSyntax("php", "function type annotation on record declaration", nodeLocation(node, filename, source))
1186
+ }
1187
+
1188
+ return documented.typeParameters
1189
+ }
1190
+
1191
+ /**
1192
+ * Returns a native PHP type name from one supported parser node.
1193
+ * @param {import("php-parser").Node | null} type - Native type node.
1194
+ * @returns {string | undefined} Type name.
1195
+ */
1196
+ function phpTypeName(type) {
1197
+ if (!type || !["typereference", "name", "identifier"].includes(type.kind)) return undefined
1198
+
1199
+ return /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (type).name
1200
+ }
1201
+
1202
+ /**
1203
+ * Wraps one parser-declared canonical PHP nullable type.
1204
+ * @param {import("../semantic/types.js").SemanticValueType} valueType - Present-value semantic type.
1205
+ * @param {import("php-parser").Node} sourceType - Native parser type node.
1206
+ * @param {number} ownerStart - Earliest owning parser offset.
1207
+ * @param {string} filename - Source filename.
1208
+ * @param {string} source - Complete source.
1209
+ * @returns {import("../semantic/types.js").OptionalType} Optional semantic type.
1210
+ */
1211
+ function nullableType(valueType, sourceType, ownerStart, filename, source) {
1212
+ if (!sourceType.loc) throw new Error("PHP parser omitted nullable type location.")
1213
+ const valueLocation = nodeLocation(sourceType, filename, source)
1214
+ const marker = tokenLocation("?", ownerStart, sourceType.loc.start.offset, filename, source)
1215
+
1216
+ if (marker.end.offset != valueLocation.start.offset) {
1217
+ return unsupportedSyntax("php", "noncanonical nullable type", marker)
1218
+ }
1219
+
1220
+ return optionalType(
1221
+ valueType,
1222
+ locationFromOffsets(filename, source, marker.start.offset, valueLocation.end.offset),
1223
+ valueLocation
1224
+ )
1225
+ }
1226
+
1227
+ /**
1228
+ * Converts one explicit PHP function return type.
1229
+ * @param {import("php-parser").Node | null} sourceType - PHP return type node.
1230
+ * @param {string} subject - Typed function return.
1231
+ * @param {import("../semantic/types.js").SourceLocation} location - Function location.
1232
+ * @param {string} filename - Source filename.
1233
+ * @param {string} source - Complete source.
1234
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
1235
+ * @returns {import("../semantic/types.js").SemanticFunctionReturnType} Semantic return type.
1236
+ */
1237
+ function convertReturnType(sourceType, subject, location, filename, source, recordNames = new Map()) {
1238
+ if (!sourceType) return requireSourceReturnType("php", undefined, subject, location)
1239
+ if (sourceType.kind != "typereference" && sourceType.kind != "name" && sourceType.kind != "identifier") {
1240
+ return unsupportedSyntax("php", "unsupported return type", nodeLocation(sourceType, filename, source))
1241
+ }
1242
+ const typeName = /** @type {import("php-parser").TypeReference | import("php-parser").Name | import("php-parser").Identifier} */ (sourceType).name
1243
+ const declaration = recordNames.get(typeName)
1244
+
1245
+ if (declaration?.id) return declaration.kind == "ClassDeclaration"
1246
+ ? referenceType(declaration.id, nodeLocation(sourceType, filename, source))
1247
+ : declaration.kind == "EffectResourceDeclaration"
1248
+ ? ownedResourceType(declaration.id, nodeLocation(sourceType, filename, source))
1249
+ : recordType(declaration.id, nodeLocation(sourceType, filename, source))
1250
+
1251
+ return requireSourceReturnType("php", typeName, subject, location, nodeLocation(sourceType, filename, source))
402
1252
  }
403
1253
 
404
1254
  /**
405
- * Converts a PHP function.
1255
+ * Converts the explicit type signature of one PHP function before any body expressions.
406
1256
  * @param {import("php-parser").Function} node - PHP function node.
407
1257
  * @param {string} filename - Source filename.
408
1258
  * @param {string} source - Complete source.
409
- * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
1259
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1260
+ * @param {string} ownerId - Stable function identity.
1261
+ * @returns {PhpFunctionSignature} Typed function signature.
410
1262
  */
411
- function convertFunction(node, filename, source) {
1263
+ function convertFunctionSignature(node, filename, source, recordNames, ownerId) {
412
1264
  const location = nodeLocation(node, filename, source)
413
1265
  const name = typeof node.name == "string" ? node.name : node.name.name
414
1266
 
@@ -416,88 +1268,615 @@ function convertFunction(node, filename, source) {
416
1268
  return unsupportedSyntax("php", "function body", location)
417
1269
  }
418
1270
 
1271
+ const documented = functionDocumentedTypes(node, filename, source, ownerId)
1272
+ const typeParameterNames = new Map((documented.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
419
1273
  const parameters = node.arguments.map((parameter) => {
420
1274
  const parameterLocation = nodeLocation(parameter, filename, source)
421
1275
  const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
422
1276
 
423
- if (parameter.nullable) return unsupportedSyntax("php", "unsupported scalar type", parameterLocation)
424
-
1277
+ if (parameter.byref) return unsupportedSyntax("php", "by-reference parameter", parameterLocation)
1278
+ if (parameter.variadic) return unsupportedSyntax("php", "variadic parameter", parameterLocation)
1279
+ if (parameter.value) return unsupportedSyntax("php", "default parameter", parameterLocation)
425
1280
  const parameterNode = typeof parameter.name == "string" ? parameter : parameter.name
1281
+ const documentedType = documented.parameters.get(parameterName)
1282
+ const nativeType = phpTypeName(parameter.type)
1283
+
1284
+ let parameterType = documentedType
1285
+ ? documentedValueType({language: "php", location: documentedType.location, ownerLocation: parameterLocation, source,
1286
+ records: recordNames, sourceType: documentedType.sourceType, subject: `Parameter '${parameterName}'`, typeParameters: typeParameterNames})
1287
+ : convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source, recordNames)
1288
+
1289
+ if (nativeType == "array" && !documentedType) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
1290
+ if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, parameterType, recordNames)) {
1291
+ return unsupportedSyntax("php", "documented parameter type incompatible with native type", documentedType.location)
1292
+ }
1293
+
1294
+ if (parameter.nullable && parameterType.kind != "OptionalType") {
1295
+ if (!parameter.type) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
1296
+ parameterType = nullableType(parameterType, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
1297
+ } else if (!parameter.nullable && parameterType.kind == "OptionalType" &&
1298
+ parameterType.valueType.kind != "TypeVariableReference") {
1299
+ return unsupportedSyntax("php", "optional annotation without nullable native type", documentedType?.location ?? parameterLocation)
1300
+ }
426
1301
  const semanticParameter = {
427
1302
  kind: /** @type {const} */ ("Parameter"),
428
1303
  location: parameterLocation,
429
1304
  name: parameterName,
430
- type: convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source)
1305
+ type: parameterType
431
1306
  }
432
1307
 
433
1308
  return withParserRanges(semanticParameter, {name: nodeLocation(parameterNode, filename, source)})
434
1309
  })
435
- if (node.nullable) return unsupportedSyntax("php", "unsupported scalar type", location)
436
- const visible = new Set(parameters.map((parameter) => parameter.name))
437
- const body = convertBlock(node.body, visible, filename, source)
1310
+ const extraDocumentedParameter = [...documented.parameters.keys()].find((name) =>
1311
+ !node.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == name))
438
1312
 
1313
+ if (extraDocumentedParameter) return unsupportedSyntax("php", `annotation for unknown parameter '${extraDocumentedParameter}'`, location)
1314
+ if (node.byref) return unsupportedSyntax("php", "by-reference return", location)
439
1315
  const nameNode = typeof node.name == "string" ? node : node.name
1316
+ const nativeReturnType = phpTypeName(node.type)
1317
+ let returnType
1318
+
1319
+ if (documented.returnType) {
1320
+ returnType = documentedValueType({language: "php", location: documented.returnType.location, ownerLocation: location, source,
1321
+ records: recordNames, sourceType: documented.returnType.sourceType, subject: `Function '${name}' return`, typeParameters: typeParameterNames})
1322
+ if (!phpDocumentedNativeTypeMatches(nativeReturnType, node.nullable, returnType, recordNames)) {
1323
+ return unsupportedSyntax("php", "documented return type incompatible with native type", documented.returnType.location)
1324
+ }
1325
+ } else {
1326
+ if (nativeReturnType == "array") return missingType("php", `Function '${name}' return`, location)
1327
+ returnType = convertReturnType(node.type, `Function '${name}' return`, location, filename, source, recordNames)
1328
+ }
1329
+ if (node.nullable && returnType.kind != "OptionalType") {
1330
+ if (!node.type) return missingType("php", `Function '${name}' return`, location)
1331
+ const typeStart = node.type.loc?.start.offset ?? 0
1332
+
1333
+ returnType = nullableType(
1334
+ /** @type {import("../semantic/types.js").SemanticValueType} */ (returnType),
1335
+ node.type,
1336
+ Math.max(node.loc?.start.offset ?? 0, typeStart - 2),
1337
+ filename,
1338
+ source
1339
+ )
1340
+ } else if (!node.nullable && returnType.kind == "OptionalType" &&
1341
+ returnType.valueType.kind != "TypeVariableReference") {
1342
+ return unsupportedSyntax("php", "optional annotation without nullable native return type", documented.returnType?.location ?? location)
1343
+ }
440
1344
 
441
- return withParserRanges({
442
- body,
443
- kind: "FunctionDeclaration",
1345
+ return {
444
1346
  location,
445
1347
  name,
1348
+ nameLocation: nodeLocation(nameNode, filename, source),
446
1349
  parameters,
447
- returnType: convertType(node.type, `Function '${name}' return`, location, filename, source)
448
- }, {name: nodeLocation(nameNode, filename, source)})
1350
+ returnType,
1351
+ ...(documented.typeParameters ? {typeParameters: documented.typeParameters} : {})
1352
+ }
1353
+ }
1354
+
1355
+ /**
1356
+ * Checks the exact runtime carrier permitted beside a precise PHPDoc type.
1357
+ * @param {string | undefined} nativeType - Native PHP type name.
1358
+ * @param {boolean} nativeNullable - Whether the native carrier has PHP's nullable marker.
1359
+ * @param {import("../semantic/types.js").SemanticValueType} documentedType - Exact semantic document type.
1360
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} records - Visible nominal declarations by name.
1361
+ * @returns {boolean} Whether the native carrier agrees without changing meaning.
1362
+ */
1363
+ function phpDocumentedNativeTypeMatches(nativeType, nativeNullable, documentedType, records) {
1364
+ if (documentedType.kind == "OptionalType") {
1365
+ if (documentedType.valueType.kind == "TypeVariableReference") {
1366
+ return nativeType === undefined && !nativeNullable
1367
+ }
1368
+
1369
+ return nativeNullable && phpDocumentedNativeTypeMatches(nativeType, false, documentedType.valueType, records)
1370
+ }
1371
+ if (nativeNullable) return false
1372
+ if (documentedType.kind == "TypeVariableReference") return nativeType === undefined
1373
+ if (documentedType.kind == "TypeReference") {
1374
+ return nativeType !== undefined && sourceScalarType("php", nativeType)?.name == documentedType.name
1375
+ }
1376
+ if (documentedType.kind == "ListType" || documentedType.kind == "MapType" || documentedType.kind == "OrderedMapType") {
1377
+ return nativeType == "array"
1378
+ }
1379
+ if (documentedType.kind == "RecordType") {
1380
+ return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
1381
+ }
1382
+ if (documentedType.kind == "ReferenceType") {
1383
+ return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
1384
+ }
1385
+
1386
+ return false
1387
+ }
1388
+
1389
+ /**
1390
+ * Converts a PHP function body using the already-proved module signatures.
1391
+ * @param {import("php-parser").Function} node - PHP function node.
1392
+ * @param {PhpFunctionSignature} signature - Preconverted explicit signature.
1393
+ * @param {Map<string, PhpFunctionSignature>} functions - Module function signatures.
1394
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1395
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
1396
+ * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
1397
+ * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
1398
+ * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
1399
+ * @param {string} filename - Source filename.
1400
+ * @param {string} source - Complete source.
1401
+ * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
1402
+ */
1403
+ function convertFunction(node, signature, functions, recordNames, records, classes, errorNames, errors, filename, source) {
1404
+ if (!node.body) return unsupportedSyntax("php", "function body", signature.location)
1405
+ const context = {
1406
+ bindings: new Map(signature.parameters.map((parameter) => [parameter.name, parameter.type])),
1407
+ classes,
1408
+ errorNames,
1409
+ errors,
1410
+ functions,
1411
+ orderedMapDeclarations: orderedMapDeclarationsForPhp(node.body),
1412
+ recordNames,
1413
+ records,
1414
+ returnType: signature.returnType,
1415
+ typeParameters: new Map((signature.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
1416
+ }
1417
+
1418
+ return withParserRanges({
1419
+ body: convertBlock(node.body, filename, source, context),
1420
+ kind: "FunctionDeclaration",
1421
+ location: signature.location,
1422
+ name: signature.name,
1423
+ parameters: signature.parameters,
1424
+ returnType: signature.returnType,
1425
+ ...(signature.typeParameters ? {typeParameters: signature.typeParameters} : {})
1426
+ }, {name: signature.nameLocation})
1427
+ }
1428
+
1429
+ /**
1430
+ * Resolves parser-owned key/value loops to exact declarations inside one lexical owner.
1431
+ * Ambiguous same-name declarations produce no evidence and fail during loop conversion.
1432
+ * @param {unknown} value - Function body or entry-point subtree.
1433
+ * @returns {Set<object>} Exact pair-iterated parser declarations.
1434
+ */
1435
+ function orderedMapDeclarationsForPhp(value) {
1436
+ /** @type {{declaration: object, name: string, offset: number}[]} */
1437
+ const declarations = []
1438
+ /** @type {{name: string, offset: number}[]} */
1439
+ const receivers = []
1440
+ const seen = new WeakSet()
1441
+ /**
1442
+ * Visits one parser candidate.
1443
+ * @param {unknown} candidate - Parser candidate.
1444
+ */
1445
+ const visit = (candidate) => {
1446
+ if (!candidate || typeof candidate != "object" || seen.has(candidate)) return
1447
+ seen.add(candidate)
1448
+ const kind = Reflect.get(candidate, "kind")
1449
+
1450
+ if (kind == "function" || kind == "class") return
1451
+ if (kind == "assign") {
1452
+ const variable = Reflect.get(candidate, "left")
1453
+
1454
+ if (Reflect.get(variable, "kind") == "variable" && typeof Reflect.get(variable, "name") == "string") {
1455
+ declarations.push({
1456
+ declaration: candidate,
1457
+ name: Reflect.get(variable, "name"),
1458
+ offset: Reflect.get(variable, "loc")?.start.offset
1459
+ })
1460
+ }
1461
+ } else if (kind == "foreach") {
1462
+ const receiver = Reflect.get(candidate, "source")
1463
+
1464
+ if (Reflect.get(receiver, "kind") == "variable" && typeof Reflect.get(receiver, "name") == "string" &&
1465
+ Reflect.get(candidate, "key")) receivers.push({
1466
+ name: Reflect.get(receiver, "name"),
1467
+ offset: Reflect.get(candidate, "loc")?.start.offset
1468
+ })
1469
+ }
1470
+ for (const [key, child] of Object.entries(candidate)) {
1471
+ if (["loc", "leadingComments", "trailingComments"].includes(key)) continue
1472
+ if (Array.isArray(child)) {
1473
+ for (const item of child) visit(item)
1474
+ } else visit(child)
1475
+ }
1476
+ }
1477
+
1478
+ visit(value)
1479
+ const selected = new Set()
1480
+ for (const receiver of receivers) {
1481
+ const matches = declarations.filter(({name, offset}) => name == receiver.name && offset < receiver.offset)
1482
+
1483
+ if (matches.length == 1) selected.add(matches[0].declaration)
1484
+ }
1485
+
1486
+ return selected
1487
+ }
1488
+
1489
+ /**
1490
+ * Converts one exact final RuntimeException subclass.
1491
+ * @param {import("php-parser").Class} node - PHP class declaration.
1492
+ * @param {import("../semantic/types.js").ErrorDeclaration} declaration - Predeclared semantic error.
1493
+ * @param {string} filename - Source filename.
1494
+ * @param {string} source - Complete source.
1495
+ * @returns {import("../semantic/types.js").ErrorDeclaration} Semantic error declaration.
1496
+ */
1497
+ function convertPhpError(node, declaration, filename, source) {
1498
+ const location = nodeLocation(node, filename, source)
1499
+ const parent = node.extends
1500
+ const parentName = parent?.kind == "name" ? /** @type {import("php-parser").Name} */ (/** @type {unknown} */ (parent)) : undefined
1501
+
1502
+ if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.implements?.length ||
1503
+ node.attrGroups.length || node.body.length != 0 || !parentName ||
1504
+ !(parentName.name == "RuntimeException" && parentName.resolution == "uqn" ||
1505
+ parentName.name == "\\RuntimeException" && parentName.resolution == "fqn")) {
1506
+ return unsupportedSyntax("php", "noncanonical typed error declaration", location)
1507
+ }
1508
+ const nameNode = typeof node.name == "string" ? node : node.name
1509
+
1510
+ return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source), type: nodeLocation(parentName, filename, source)})
1511
+ }
1512
+
1513
+ /**
1514
+ * Reads one exact public PHP constructor or instance-method signature.
1515
+ * @param {import("php-parser").Method} node - Parser-owned method.
1516
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
1517
+ * @param {string} ownerId - Stable callable identity.
1518
+ * @param {boolean} constructor - Whether this is the sole constructor.
1519
+ * @param {string} filename - Source filename.
1520
+ * @param {string} source - Complete source.
1521
+ * @returns {{location: import("../semantic/types.js").SourceLocation, name: string, nameLocation: import("../semantic/types.js").SourceLocation, parameters: import("../semantic/types.js").Parameter[], returnType?: import("../semantic/types.js").SemanticFunctionReturnType}} Exact signature.
1522
+ */
1523
+ function convertPhpClassCallableSignature(node, nominalNames, ownerId, constructor, filename, source) {
1524
+ const location = nodeLocation(node, filename, source)
1525
+ const name = typeof node.name == "string" ? node.name : node.name.name
1526
+
1527
+ if (!constructor && /^__/u.test(name)) return unsupportedSyntax("php", `reserved reference method '${name}'`, location)
1528
+ if (node.visibility != "public" || node.isStatic || node.isAbstract || node.isFinal || Reflect.get(node, "isReadonly") || node.byref ||
1529
+ node.attrGroups.length || !node.body || constructor != (name == "__construct")) {
1530
+ return unsupportedSyntax("php", "noncanonical reference class callable", location)
1531
+ }
1532
+ const documented = functionDocumentedTypes(/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (node)),
1533
+ filename, source, ownerId)
1534
+ if (documented.typeParameters?.length) return unsupportedSyntax("php", "generic reference class callable", location)
1535
+ if (constructor && (node.type || node.nullable || documented.returnType)) {
1536
+ return unsupportedSyntax("php", "constructor return annotation", location)
1537
+ }
1538
+ const parameters = node.arguments.map((parameter) => {
1539
+ const parameterLocation = nodeLocation(parameter, filename, source)
1540
+ const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
1541
+
1542
+ if (parameter.byref || parameter.variadic || parameter.value || parameter.flags || parameter.readonly ||
1543
+ parameter.hooks.length || parameter.attrGroups.length) {
1544
+ return unsupportedSyntax("php", "noncanonical reference class parameter", parameterLocation)
1545
+ }
1546
+ const documentedType = documented.parameters.get(parameterName)
1547
+ const nativeType = phpTypeName(parameter.type)
1548
+ let type = documentedType
1549
+ ? documentedValueType({language: "php", location: documentedType.location, ownerLocation: parameterLocation,
1550
+ records: nominalNames, source, sourceType: documentedType.sourceType, subject: `Parameter '${parameterName}'`})
1551
+ : convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source, nominalNames)
1552
+
1553
+ if (nativeType == "array" && !documentedType) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
1554
+ if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, type, nominalNames)) {
1555
+ return unsupportedSyntax("php", "documented parameter type incompatible with native type", documentedType.location)
1556
+ }
1557
+ if (parameter.nullable && type.kind != "OptionalType") {
1558
+ if (!parameter.type) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
1559
+ type = nullableType(type, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
1560
+ } else if (!parameter.nullable && type.kind == "OptionalType") {
1561
+ return unsupportedSyntax("php", "optional annotation without nullable native type", documentedType?.location ?? parameterLocation)
1562
+ }
1563
+ const nameNode = typeof parameter.name == "string" ? parameter : parameter.name
1564
+
1565
+ return withParserRanges({kind: /** @type {const} */ ("Parameter"), location: parameterLocation,
1566
+ name: parameterName, type}, {name: nodeLocation(nameNode, filename, source)})
1567
+ })
1568
+ if ([...documented.parameters.keys()].some((parameterName) =>
1569
+ !node.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == parameterName))) {
1570
+ return unsupportedSyntax("php", "method PHPDoc parameter mismatch", location)
1571
+ }
1572
+ const nameNode = typeof node.name == "string" ? node : node.name
1573
+
1574
+ if (constructor) return {location, name: "constructor", nameLocation: nodeLocation(nameNode, filename, source), parameters}
1575
+ let returnType
1576
+ const nativeReturnType = phpTypeName(node.type)
1577
+
1578
+ if (documented.returnType) {
1579
+ returnType = documentedValueType({language: "php", location: documented.returnType.location, ownerLocation: location,
1580
+ records: nominalNames, source, sourceType: documented.returnType.sourceType, subject: `Method '${name}' return`})
1581
+ if (!phpDocumentedNativeTypeMatches(nativeReturnType, node.nullable, returnType, nominalNames)) {
1582
+ return unsupportedSyntax("php", "documented method return type incompatible with native type", documented.returnType.location)
1583
+ }
1584
+ } else {
1585
+ if (nativeReturnType == "array") return missingType("php", `Method '${name}' return`, location)
1586
+ returnType = convertReturnType(node.type, `Method '${name}' return`, location, filename, source, nominalNames)
1587
+ }
1588
+ if (node.nullable && returnType.kind != "OptionalType") {
1589
+ if (!node.type) return missingType("php", `Method '${name}' return`, location)
1590
+ returnType = nullableType(/** @type {import("../semantic/types.js").SemanticValueType} */ (returnType), node.type,
1591
+ node.type.loc?.start.offset ?? 0, filename, source)
1592
+ } else if (!node.nullable && returnType.kind == "OptionalType") {
1593
+ return unsupportedSyntax("php", "optional annotation without nullable native return type", documented.returnType?.location ?? location)
1594
+ }
1595
+
1596
+ return {location, name, nameLocation: nodeLocation(nameNode, filename, source), parameters, returnType}
1597
+ }
1598
+
1599
+ /**
1600
+ * Converts the bounded final/private-field PHP reference-class profile.
1601
+ * @param {import("php-parser").Class} node - Parser class.
1602
+ * @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
1603
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
1604
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
1605
+ * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
1606
+ * @param {Map<string, PhpFunctionSignature>} functions - Top-level signatures.
1607
+ * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by name.
1608
+ * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Errors by identity.
1609
+ * @param {string} filename - Source filename.
1610
+ * @param {string} source - Complete source.
1611
+ * @returns {import("../semantic/types.js").ClassDeclaration} Semantic class.
1612
+ */
1613
+ function convertPhpReferenceClass(node, declaration, nominalNames, records, classes, functions, errorNames, errors,
1614
+ filename, source) {
1615
+ const location = nodeLocation(node, filename, source)
1616
+
1617
+ if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.extends || node.implements?.length ||
1618
+ node.attrGroups.length) return unsupportedSyntax("php", "reference class inheritance or noncanonical modifier", location)
1619
+ const fieldStatements = node.body.filter((member) => member.kind == "propertystatement")
1620
+ const methodNodes = /** @type {import("php-parser").Method[]} */ (node.body.filter((member) => member.kind == "method"))
1621
+ const constructors = methodNodes.filter((method) => (typeof method.name == "string" ? method.name : method.name.name) == "__construct")
1622
+ const ordinaryMethods = methodNodes.filter((method) => !constructors.includes(method))
1623
+
1624
+ if (constructors.length != 1 || fieldStatements.length == 0 || fieldStatements.length + methodNodes.length != node.body.length) {
1625
+ return unsupportedSyntax("php", "reference class requires private fields, one constructor, and ordinary methods", location)
1626
+ }
1627
+ declaration.fields = fieldStatements.map((statement, index) => {
1628
+ const fieldLocation = nodeLocation(statement, filename, source)
1629
+ const properties = Reflect.get(statement, "properties")
1630
+ const property = Array.isArray(properties) && properties.length == 1 ? properties[0] : undefined
1631
+ const nameNode = property?.name
1632
+ const name = typeof nameNode == "string" ? nameNode : nameNode?.name
1633
+
1634
+ if (Reflect.get(statement, "visibility") != "private" || Reflect.get(statement, "isStatic") ||
1635
+ Reflect.get(statement, "isAbstract") || Reflect.get(statement, "isFinal") || !property ||
1636
+ typeof name != "string" || property.value || property.readonly || property.nullable || property.attrGroups?.length ||
1637
+ property.hooks?.length) return unsupportedSyntax("php", "noncanonical private instance field", fieldLocation)
1638
+ const type = convertType(property.type, `Private field '${name}'`, fieldLocation, filename, source, nominalNames)
1639
+
1640
+ return withParserRanges({id: `${declaration.id}:field:${index}`, kind: /** @type {const} */ ("PrivateField"),
1641
+ location: fieldLocation, name, type}, {name: nodeLocation(typeof nameNode == "string" ? property : nameNode, filename, source)})
1642
+ })
1643
+ const constructorNode = constructors[0]
1644
+ const constructorSignature = convertPhpClassCallableSignature(constructorNode, nominalNames,
1645
+ `${declaration.id}:constructor`, true, filename, source)
1646
+
1647
+ declaration.constructor = withParserRanges({body: /** @type {import("../semantic/types.js").Block} */ ({}),
1648
+ id: `${declaration.id}:constructor`, kind: /** @type {const} */ ("ConstructorDeclaration"),
1649
+ location: constructorSignature.location, parameters: constructorSignature.parameters}, {constructor: constructorSignature.nameLocation})
1650
+ const methodSignatures = ordinaryMethods.map((method, index) => convertPhpClassCallableSignature(method, nominalNames,
1651
+ `${declaration.id}:method:${index}`, false, filename, source))
1652
+
1653
+ declaration.methods = methodSignatures.map((signature, index) => withParserRanges({
1654
+ body: /** @type {import("../semantic/types.js").Block} */ ({}), id: `${declaration.id}:method:${index}`,
1655
+ kind: /** @type {const} */ ("MethodDeclaration"), location: signature.location, name: signature.name,
1656
+ parameters: signature.parameters,
1657
+ returnType: /** @type {import("../semantic/types.js").SemanticFunctionReturnType} */ (signature.returnType)
1658
+ }, {name: signature.nameLocation}))
1659
+ const base = {bindings: new Map(), classes, currentClass: declaration, errorNames, errors, functions,
1660
+ recordNames: nominalNames, records}
1661
+
1662
+ declaration.constructor.body = convertBlock(/** @type {import("php-parser").Block} */ (constructorNode.body), filename, source,
1663
+ {...base, bindings: new Map(declaration.constructor.parameters.map((parameter) => [parameter.name, parameter.type])),
1664
+ returnType: {kind: /** @type {const} */ ("TypeReference"), name: /** @type {const} */ ("void")}})
1665
+ for (let index = 0; index < ordinaryMethods.length; index += 1) {
1666
+ const method = declaration.methods[index]
1667
+
1668
+ method.body = convertBlock(/** @type {import("php-parser").Block} */ (ordinaryMethods[index].body), filename, source,
1669
+ {...base, bindings: new Map(method.parameters.map((parameter) => [parameter.name, parameter.type])), returnType: method.returnType})
1670
+ }
1671
+ const nameNode = typeof node.name == "string" ? node : node.name
1672
+
1673
+ return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source)})
1674
+ }
1675
+
1676
+ /**
1677
+ * Converts one exact final readonly promoted-property PHP record.
1678
+ * @param {import("php-parser").Class} node - PHP class declaration.
1679
+ * @param {import("../semantic/types.js").RecordDeclaration} declaration - Predeclared nominal identity.
1680
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} recordNames - Record declarations by name.
1681
+ * @param {string} filename - Source filename.
1682
+ * @param {string} source - Complete source.
1683
+ * @returns {import("../semantic/types.js").RecordDeclaration} Semantic record.
1684
+ */
1685
+ function convertPhpRecord(node, declaration, recordNames, filename, source) {
1686
+ const location = nodeLocation(node, filename, source)
1687
+ const generic = (declaration.typeParameters?.length ?? 0) > 0
1688
+
1689
+ if (node.isAnonymous || !node.isFinal || node.isReadonly == generic || node.isAbstract || node.extends || node.implements?.length ||
1690
+ node.attrGroups.length) return unsupportedSyntax("php", "noncanonical record class modifiers", location)
1691
+ if (node.body.length < 1 || node.body[0].kind != "method" || !generic && node.body.length != 1) {
1692
+ return unsupportedSyntax("php", "record class body outside one constructor", nodeLocation(node.body[1] ?? node.body[0] ?? node, filename, source))
1693
+ }
1694
+ const constructor = /** @type {import("php-parser").Method} */ (node.body[0])
1695
+ const typeParameterNames = new Map((declaration.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
1696
+ const constructorName = typeof constructor.name == "string" ? constructor.name : constructor.name.name
1697
+
1698
+ if (constructorName != "__construct" || constructor.visibility != "public" || constructor.isStatic || constructor.isAbstract ||
1699
+ constructor.isFinal || Reflect.get(constructor, "isReadonly") || constructor.byref || constructor.type || constructor.nullable ||
1700
+ constructor.attrGroups.length || !constructor.body || constructor.body.children.length != 0) {
1701
+ return unsupportedSyntax("php", "noncanonical record constructor", nodeLocation(constructor, filename, source))
1702
+ }
1703
+ const documented = functionDocumentedTypes(/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (constructor)), filename, source)
1704
+
1705
+ declaration.fields = constructor.arguments.map((parameter) => {
1706
+ const fieldLocation = nodeLocation(parameter, filename, source)
1707
+ const name = typeof parameter.name == "string" ? parameter.name : parameter.name.name
1708
+ const documentedType = documented.parameters.get(name)
1709
+ const nativeType = phpTypeName(parameter.type)
1710
+
1711
+ if (parameter.flags != (generic ? 4 : 1) || parameter.byref || parameter.variadic || parameter.value || parameter.readonly ||
1712
+ parameter.hooks.length || parameter.attrGroups.length) {
1713
+ return unsupportedSyntax("php", "record field outside public promoted-property profile", fieldLocation)
1714
+ }
1715
+ if (nativeType == "array" && !documentedType) return missingType("php", `Record field '${name}'`, fieldLocation)
1716
+ let type = documentedType
1717
+ ? documentedValueType({language: "php", location: documentedType.location, ownerLocation: fieldLocation, records: recordNames,
1718
+ source, sourceType: documentedType.sourceType, subject: `Record field '${name}'`, typeParameters: typeParameterNames})
1719
+ : convertType(parameter.type, `Record field '${name}'`, fieldLocation, filename, source, recordNames)
1720
+
1721
+ if (documentedType && !phpDocumentedNativeTypeMatches(nativeType, parameter.nullable, type, recordNames)) {
1722
+ return unsupportedSyntax("php", "documented record field type incompatible with native type", documentedType.location)
1723
+ }
1724
+
1725
+ if (parameter.nullable && type.kind != "OptionalType") {
1726
+ if (!parameter.type) return missingType("php", `Record field '${name}'`, fieldLocation)
1727
+ type = nullableType(type, parameter.type, parameter.loc?.start.offset ?? 0, filename, source)
1728
+ } else if (!parameter.nullable && type.kind == "OptionalType" && type.valueType.kind != "TypeVariableReference") {
1729
+ return unsupportedSyntax("php", "optional annotation without nullable native record field", documentedType?.location ?? fieldLocation)
1730
+ }
1731
+ const field = {kind: /** @type {const} */ ("RecordField"), location: fieldLocation, name, type}
1732
+ const nameNode = typeof parameter.name == "string" ? parameter : parameter.name
1733
+
1734
+ return withParserRanges(field, {name: nodeLocation(nameNode, filename, source)})
1735
+ })
1736
+ const extraDocumented = [...documented.parameters.keys()].find((name) =>
1737
+ !constructor.arguments.some((parameter) => (typeof parameter.name == "string" ? parameter.name : parameter.name.name) == name))
1738
+
1739
+ if (extraDocumented || documented.returnType) return unsupportedSyntax("php", "invalid record constructor annotation", location)
1740
+ if (generic) validatePhpGenericRecordGetters(node, declaration, recordNames, filename, source)
1741
+ const nameNode = typeof node.name == "string" ? node : node.name
1742
+
1743
+ return withParserRanges(declaration, {name: nodeLocation(nameNode, filename, source)})
1744
+ }
1745
+
1746
+ /**
1747
+ * Validates the exact accessor half of PHP's documented generic-record profile.
1748
+ * @param {import("php-parser").Class} node - PHP class declaration.
1749
+ * @param {import("../semantic/types.js").RecordDeclaration} declaration - Converted record.
1750
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} recordNames - Visible records.
1751
+ * @param {string} filename - Source filename.
1752
+ * @param {string} source - Complete source.
1753
+ * @returns {void}
1754
+ */
1755
+ function validatePhpGenericRecordGetters(node, declaration, recordNames, filename, source) {
1756
+ const getters = node.body.slice(1)
1757
+ const typeParameters = new Map((declaration.typeParameters ?? []).map((parameter) => [parameter.name, parameter]))
1758
+
1759
+ if (getters.length != declaration.fields.length) {
1760
+ return unsupportedSyntax("php", "generic record getter count", nodeLocation(getters[declaration.fields.length] ?? node, filename, source))
1761
+ }
1762
+ getters.forEach((candidate, index) => {
1763
+ const field = declaration.fields[index]
1764
+
1765
+ if (candidate.kind != "method") return unsupportedSyntax("php", "noncanonical generic record getter", nodeLocation(candidate, filename, source))
1766
+ const getter = /** @type {import("php-parser").Method} */ (candidate)
1767
+ const name = typeof getter.name == "string" ? getter.name : getter.name.name
1768
+ const statement = getter.body?.children[0]
1769
+ const returned = statement?.kind == "return" ? /** @type {import("php-parser").Return} */ (statement).expr : undefined
1770
+ const lookup = returned?.kind == "propertylookup" ? /** @type {import("php-parser").PropertyLookup} */ (returned) : undefined
1771
+ const receiver = lookup?.what.kind == "variable" ? /** @type {import("php-parser").Variable} */ (lookup.what) : undefined
1772
+ const member = lookup?.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
1773
+
1774
+ if (name != field.name || getter.visibility != "public" || getter.isStatic || getter.isAbstract || getter.isFinal ||
1775
+ Reflect.get(getter, "isReadonly") || getter.byref || getter.arguments.length || getter.attrGroups.length ||
1776
+ !getter.body || getter.body.children.length != 1 || receiver?.name != "this" || member?.name != field.name) {
1777
+ return unsupportedSyntax("php", "noncanonical generic record getter", nodeLocation(getter, filename, source))
1778
+ }
1779
+ const documented = functionDocumentedTypes(
1780
+ /** @type {import("php-parser").Function} */ (/** @type {unknown} */ (getter)), filename, source)
1781
+
1782
+ if (documented.parameters.size || documented.typeParameters || !documented.returnType) {
1783
+ return unsupportedSyntax("php", "missing or invalid generic record getter type", nodeLocation(getter, filename, source))
1784
+ }
1785
+ const returnType = documentedValueType({language: "php", location: documented.returnType.location,
1786
+ ownerLocation: nodeLocation(getter, filename, source), records: recordNames, source,
1787
+ sourceType: documented.returnType.sourceType, subject: `Record getter '${name}'`, typeParameters})
1788
+ const nativeType = phpTypeName(getter.type)
1789
+
1790
+ if (!phpDocumentedNativeTypeMatches(nativeType, getter.nullable, returnType, recordNames) ||
1791
+ !sameValueType(returnType, field.type)) {
1792
+ return unsupportedSyntax("php", "generic record getter type mismatch", documented.returnType.location)
1793
+ }
1794
+ })
449
1795
  }
450
1796
 
451
1797
  /**
452
1798
  * Converts the existing PHP if/else terminal with restricted branch prefixes.
453
1799
  * @param {import("php-parser").If} node - PHP if node.
454
- * @param {Set<string>} visible - Enclosing visible names.
455
1800
  * @param {string} filename - Source filename.
456
1801
  * @param {string} source - Complete source.
1802
+ * @param {PhpConversionContext} context - Typed conversion context.
457
1803
  * @returns {import("../semantic/types.js").IfStatement} Semantic branch.
458
1804
  */
459
- function convertIf(node, visible, filename, source) {
1805
+ function convertIf(node, filename, source, context) {
460
1806
  const location = nodeLocation(node, filename, source)
461
1807
 
462
1808
  if (node.shortForm) return unsupportedSyntax("php", "alternative if syntax", location)
463
1809
  if (node.body.kind != "block") return unsupportedSyntax("php", "if without block consequent", location)
1810
+ if (node.test.kind == "variable") {
1811
+ const tested = /** @type {import("php-parser").Variable} */ (node.test)
1812
+
1813
+ if (typeof tested.name == "string" && context.bindings.get(tested.name)?.kind == "OptionalType") {
1814
+ return unsupportedSyntax("php", "optional truthiness condition", nodeLocation(tested, filename, source))
1815
+ }
1816
+ }
464
1817
 
465
- const consequentVisible = new Set(visible)
1818
+ const condition = convertExpression(node.test, filename, source, context)
1819
+ const consequentContext = {...context, bindings: new Map(context.bindings)}
1820
+ const alternateContext = {...context, bindings: new Map(context.bindings)}
466
1821
  let alternate
467
1822
 
468
1823
  if (node.alternate) {
469
- const alternateVisible = new Set(visible)
470
-
471
1824
  if (node.alternate.kind == "block") {
472
- alternate = convertBlock(/** @type {import("php-parser").Block} */ (node.alternate), alternateVisible, filename, source)
1825
+ alternate = convertBlock(/** @type {import("php-parser").Block} */ (node.alternate), filename, source, alternateContext)
473
1826
  } else if (node.alternate.kind == "if") {
474
1827
  const nested = /** @type {import("php-parser").If} */ (node.alternate)
475
1828
 
476
1829
  alternate = {
477
1830
  kind: /** @type {const} */ ("Block"),
478
1831
  location: nodeLocation(nested, filename, source),
479
- statements: [convertIf(nested, alternateVisible, filename, source)]
1832
+ statements: [convertIf(nested, filename, source, alternateContext)]
480
1833
  }
481
1834
  } else return unsupportedSyntax("php", `if alternate ${node.alternate.kind}`, nodeLocation(node.alternate, filename, source))
482
1835
  }
483
1836
 
484
1837
  return {
485
1838
  ...(alternate ? {alternate} : {}),
486
- condition: convertExpression(node.test, filename, source),
487
- consequent: convertBlock(node.body, consequentVisible, filename, source),
1839
+ condition,
1840
+ consequent: convertBlock(node.body, filename, source, consequentContext),
488
1841
  kind: "IfStatement",
489
1842
  location
490
1843
  }
491
1844
  }
492
1845
 
1846
+ /**
1847
+ * Converts exact braced PHP `while ($condition) { ... }` syntax.
1848
+ * @param {import("php-parser").While} node - PHP while node.
1849
+ * @param {string} filename - Source filename.
1850
+ * @param {string} source - Complete source.
1851
+ * @param {PhpConversionContext} context - Typed conversion context.
1852
+ * @returns {import("../semantic/types.js").WhileStatement} Semantic loop.
1853
+ */
1854
+ function convertWhile(node, filename, source, context) {
1855
+ const location = nodeLocation(node, filename, source)
1856
+
1857
+ if (node.shortForm) return unsupportedSyntax("php", "alternative while syntax", location)
1858
+ if (!node.body || node.body.kind != "block") {
1859
+ return unsupportedSyntax("php", "while without block body", node.body ? nodeLocation(node.body, filename, source) : location)
1860
+ }
1861
+ const bodyContext = {...context, bindings: new Map(context.bindings)}
1862
+
1863
+ return withParserRanges({
1864
+ body: convertBlock(/** @type {import("php-parser").Block} */ (node.body), filename, source, bodyContext),
1865
+ condition: convertExpression(node.test, filename, source, context, undefined, true),
1866
+ kind: /** @type {const} */ ("WhileStatement"),
1867
+ location
1868
+ }, {keyword: tokenLocation("while", node.loc?.start.offset ?? 0, node.test.loc?.start.offset ?? source.length, filename, source)})
1869
+ }
1870
+
493
1871
  /**
494
1872
  * Converts a PHP echo entry point.
495
1873
  * @param {import("php-parser").Echo} node - PHP echo node.
496
1874
  * @param {string} filename - Source filename.
497
1875
  * @param {string} source - Complete source.
1876
+ * @param {PhpConversionContext} context - Typed conversion context.
498
1877
  * @returns {import("../semantic/types.js").PrintStatement} Print statement.
499
1878
  */
500
- function convertPrint(node, filename, source) {
1879
+ function convertPrint(node, filename, source, context) {
501
1880
  const location = nodeLocation(node, filename, source)
502
1881
 
503
1882
  if (node.expressions.length != 2 || node.expressions[1].kind != "name" ||
@@ -505,7 +1884,7 @@ function convertPrint(node, filename, source) {
505
1884
  return unsupportedSyntax("php", "echo without one expression and PHP_EOL", location)
506
1885
  }
507
1886
 
508
- return {expression: convertExpression(node.expressions[0], filename, source), kind: "PrintStatement", location}
1887
+ return {expression: convertExpression(node.expressions[0], filename, source, context), kind: "PrintStatement", location}
509
1888
  }
510
1889
 
511
1890
  /**
@@ -513,29 +1892,118 @@ function convertPrint(node, filename, source) {
513
1892
  * @param {object} input - Parser input.
514
1893
  * @param {string} input.filename - Source filename.
515
1894
  * @param {string} input.source - Source text.
1895
+ * @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} [input.capabilities] - Compiler-authorized declarations.
1896
+ * @param {{isEntry: boolean, 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.
516
1897
  * @returns {import("../semantic/types.js").SemanticModule} Semantic module.
517
1898
  */
518
- export function parsePhp({filename, source}) {
1899
+ export function parsePhp({capabilities = [], filename, source, program: programContext}) {
519
1900
  const program = parsePhpProgram(filename, source)
520
1901
  validateDeclareNodes(program.children, filename, source)
521
- const functions = program.children.filter((node) => node.kind == "function")
522
- .map((node) => convertFunction(/** @type {import("php-parser").Function} */ (node), filename, source))
523
- const executableNodes = program.children.filter((node) => node.kind != "function" && node.kind != "declare" && node.kind != "noop")
1902
+ let body = task034PhpBody(program.children, capabilities, filename, source)
1903
+
1904
+ if (programContext) {
1905
+ inspectPhpModule({filename, source})
1906
+ const namespace = /** @type {import("php-parser").Namespace} */ (body.find((node) => node.kind == "namespace"))
1907
+
1908
+ body = namespace.children.filter((node) => node.kind != "usegroup" && !isPhpRequireOnce(node))
1909
+ }
1910
+ const classNodes = /** @type {import("php-parser").Class[]} */ (body.filter((node) => node.kind == "class"))
1911
+ const errorNodes = classNodes.filter((node) => node.extends?.kind == "name" &&
1912
+ ["RuntimeException", "\\RuntimeException"].includes(node.extends.name))
1913
+ const referenceClassNodes = classNodes.filter((node) => !errorNodes.includes(node) &&
1914
+ node.body.some((member) => member.kind == "propertystatement"))
1915
+ const recordNodes = classNodes.filter((node) => !errorNodes.includes(node) && !referenceClassNodes.includes(node))
1916
+ const errorDeclarations = errorNodes.map((node, index) => ({
1917
+ id: `error:${index}`,
1918
+ kind: /** @type {const} */ ("ErrorDeclaration"),
1919
+ location: nodeLocation(node, filename, source),
1920
+ name: typeof node.name == "string" ? node.name : node.name.name
1921
+ }))
1922
+ const recordDeclarations = recordNodes.map((node, index) => {
1923
+ const id = `record:${index}`
1924
+ const typeParameters = classDocumentedTypeParameters(node, id, filename, source)
1925
+
1926
+ return {
1927
+ fields: [],
1928
+ id,
1929
+ kind: /** @type {const} */ ("RecordDeclaration"),
1930
+ location: nodeLocation(node, filename, source),
1931
+ name: typeof node.name == "string" ? node.name : node.name.name,
1932
+ ...(typeParameters ? {typeParameters} : {})
1933
+ }
1934
+ })
1935
+ const classDeclarations = referenceClassNodes.map((node, index) => ({
1936
+ constructor: /** @type {import("../semantic/types.js").ConstructorDeclaration} */ ({}),
1937
+ fields: [], id: `class:${index}`, kind: /** @type {const} */ ("ClassDeclaration"),
1938
+ location: nodeLocation(node, filename, source), methods: [],
1939
+ name: typeof node.name == "string" ? node.name : node.name.name
1940
+ }))
1941
+ const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} */ (
1942
+ new Map(programContext?.records ?? []))
1943
+ for (const resource of capabilities.flatMap(({resources}) => resources)) recordNames.set(resource.name, resource)
1944
+ const errorNames = new Map(programContext?.errors ?? [])
1945
+ for (const failure of capabilities.flatMap(({failures}) => failures)) {
1946
+ errorNames.set(failure.name, /** @type {import("../semantic/types.js").ErrorDeclaration} */ (/** @type {unknown} */ (failure)))
1947
+ }
1948
+ for (const declaration of errorDeclarations) errorNames.set(declaration.name, declaration)
1949
+ const errorsById = new Map([...errorNames.values()].map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
1950
+ const errors = errorNodes.map((node, index) => convertPhpError(node, errorDeclarations[index], filename, source))
1951
+ for (const declaration of recordDeclarations) recordNames.set(declaration.name, declaration)
1952
+ for (const declaration of classDeclarations) recordNames.set(declaration.name, declaration)
1953
+ const recordsById = new Map([...recordNames.values()]
1954
+ .filter((declaration) => declaration.kind == "RecordDeclaration")
1955
+ .map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
1956
+ const classesById = new Map(classDeclarations.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
1957
+ const records = recordNodes.map((node, index) => convertPhpRecord(node, recordDeclarations[index],
1958
+ /** @type {Map<string, import("../semantic/types.js").RecordDeclaration>} */ (/** @type {unknown} */ (recordNames)), filename, source))
1959
+ const functionNodes = /** @type {import("php-parser").Function[]} */ (body.filter((node) => node.kind == "function"))
1960
+ const signatures = functionNodes.map((node, index) => convertFunctionSignature(node, filename, source, recordNames, `function:${index}`))
1961
+ const functionSignatures = new Map([...capabilityFunctionSignatures(capabilities, moduleLocation(filename, source)),
1962
+ ...(programContext?.functions ?? [])].map(([localName, declaration]) => [localName, {
1963
+ location: declaration.location,
1964
+ name: localName,
1965
+ nameLocation: declaration.location,
1966
+ parameters: declaration.parameters,
1967
+ returnType: declaration.returnType,
1968
+ ...(declaration.typeParameters ? {typeParameters: declaration.typeParameters} : {})
1969
+ }]))
1970
+ for (const signature of signatures) functionSignatures.set(signature.name, signature)
1971
+ const classes = referenceClassNodes.map((node, index) => convertPhpReferenceClass(node, classDeclarations[index],
1972
+ recordNames, recordsById, classesById, functionSignatures, errorNames, errorsById, filename, source))
1973
+ const functions = functionNodes.map((node, index) =>
1974
+ convertFunction(node, signatures[index], functionSignatures, recordNames, recordsById, classesById, errorNames, errorsById,
1975
+ filename, source))
1976
+ const executableNodes = body.filter((node) => !["class", "function", "declare", "noop"].includes(node.kind))
524
1977
  const location = moduleLocation(filename, source)
525
1978
 
526
- if (functions.length == 0) return unsupportedSyntax("php", "module without a function", location)
1979
+ if (programContext && !programContext.isEntry && executableNodes.length > 0) {
1980
+ return unsupportedSyntax("php", "top-level side effect outside the selected entry module", nodeLocation(executableNodes[0], filename, source))
1981
+ }
527
1982
  const first = executableNodes[0]
528
1983
  const last = executableNodes.at(-1) ?? first
529
1984
  const entryLocation = first
530
1985
  ? locationFromOffsets(filename, source, first.loc?.start.offset ?? 0, last?.loc?.end.offset ?? source.length)
531
1986
  : location
532
- const entryVisible = new Set()
1987
+ const entryContext = {
1988
+ bindings: new Map(),
1989
+ classes: classesById,
1990
+ errorNames,
1991
+ errors: errorsById,
1992
+ functions: functionSignatures,
1993
+ orderedMapDeclarations: orderedMapDeclarationsForPhp(executableNodes),
1994
+ recordNames,
1995
+ records: recordsById,
1996
+ returnType: undefined
1997
+ }
533
1998
  const entryBlock = {
534
1999
  kind: /** @type {const} */ ("Block"),
535
2000
  location: entryLocation,
536
- statements: executableNodes.map((node) => convertStatement(node, entryVisible, filename, source))
2001
+ statements: executableNodes.map((node) => convertStatement(node, filename, source, entryContext))
537
2002
  }
538
2003
 
2004
+ if (!programContext && functions.length == 0 && capabilities.length == 0 && !blockReferencesProtectedEntry(entryBlock)) {
2005
+ return unsupportedSyntax("php", "module without a function", location)
2006
+ }
539
2007
  return {
540
2008
  entryPoint: {
541
2009
  body: entryBlock,
@@ -544,10 +2012,156 @@ export function parsePhp({filename, source}) {
544
2012
  },
545
2013
  functions,
546
2014
  kind: "Module",
547
- location
2015
+ location,
2016
+ ...(errors.length > 0 ? {errors} : {}),
2017
+ ...(classes.length > 0 ? {classes} : {}),
2018
+ ...(records.length > 0 ? {records} : {})
548
2019
  }
549
2020
  }
550
2021
 
2022
+ /**
2023
+ * Removes exact protected PHP probe support before portable conversion.
2024
+ * @param {import("php-parser").Statement[]} body - Parsed top-level statements.
2025
+ * @param {readonly import("../semantic/types.js").EffectCapabilityDeclaration[]} capabilities - Authorized declarations.
2026
+ * @param {string} filename - Source filename.
2027
+ * @param {string} source - Complete source.
2028
+ * @returns {import("php-parser").Statement[]} Portable statements.
2029
+ */
2030
+ function task034PhpBody(body, capabilities, filename, source) {
2031
+ if (!isTask034ResourceProbe(capabilities)) return body
2032
+ const start = body.findIndex((node) => node.leadingComments?.some(({value}) => value == "/* semantifold-task034-support */"))
2033
+
2034
+ if (start < 0) return body
2035
+ if (!hasExactEffectSupport(source, "php")) {
2036
+ return unsupportedSyntax("php", "malformed protected Task 034 support", nodeLocation(body[start], filename, source))
2037
+ }
2038
+ const expected = [
2039
+ ["class", "ProbeOperationFailure"], ["class", "ProbeAcquireFailure"], ["class", "ProbeReadFailure"],
2040
+ ["class", "ProbeCloseFailure"], ["class", "ProbeResourceClosed"], ["class", "ProbeResource"],
2041
+ ["expressionstatement", undefined], ["function", "probeEffect"], ["function", "probeAcquire"],
2042
+ ["function", "probeRead"], ["function", "probeClose"], ["function", "probeTrace"]
2043
+ ]
2044
+ const matches = expected.every(([kind, name], index) => {
2045
+ const node = body[start + index]
2046
+ const rawName = node ? Reflect.get(node, "name") : undefined
2047
+ const actualName = typeof rawName == "string" ? rawName : rawName && typeof rawName == "object" ? Reflect.get(rawName, "name") : undefined
2048
+ return node?.kind == kind && actualName == name
2049
+ })
2050
+ const next = body[start + expected.length]
2051
+
2052
+ if (!matches || next && !next.leadingComments?.some(({value}) => value == "/* semantifold-task034-support-end */")) {
2053
+ return unsupportedSyntax("php", "malformed protected Task 034 support", nodeLocation(body[start], filename, source))
2054
+ }
2055
+ return [...body.slice(0, start), ...body.slice(start + expected.length)]
2056
+ }
2057
+
2058
+ /**
2059
+ * Reads the canonical parser-owned PHP namespace, use bindings, and literal require_once edges.
2060
+ * @param {object} input - Parser input.
2061
+ * @param {string} input.filename - Source filename.
2062
+ * @param {string} input.source - Source text.
2063
+ * @returns {{bindings: {importedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, nativeName: string, symbolKind: import("../semantic/types.js").SemanticDeclarationKind, typeOnly: false}[], imports: {importedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, namespace: true, pathLocation: import("../semantic/types.js").SourceLocation, specifier: string, typeOnly: false}[], exports: {exportedName: string, localName: string, location: import("../semantic/types.js").SourceLocation, typeOnly: false}[], nativeName: string}} Parser-owned PHP module header.
2064
+ */
2065
+ export function inspectPhpModule({filename, source}) {
2066
+ const program = parsePhpProgram(filename, source)
2067
+
2068
+ validateDeclareNodes(program.children, filename, source)
2069
+ const namespaces = program.children.filter((node) => node.kind == "namespace")
2070
+ const invalidOuter = program.children.find((node) => !["declare", "namespace", "noop"].includes(node.kind))
2071
+ const namespace = /** @type {import("php-parser").Namespace | undefined} */ (namespaces[0])
2072
+
2073
+ if (!namespace || namespaces.length != 1 || namespace.withBrackets || typeof namespace.name != "string" || invalidOuter) {
2074
+ return unsupportedSyntax("php", "one canonical unbracketed namespace",
2075
+ nodeLocation(invalidOuter ?? namespaces[1] ?? namespace ?? program, filename, source))
2076
+ }
2077
+ const bindings = []
2078
+
2079
+ for (const node of namespace.children.filter((child) => child.kind == "usegroup")) {
2080
+ const group = /** @type {import("php-parser").UseGroup} */ (node)
2081
+
2082
+ if (group.name !== null || ![null, "function"].includes(group.type) || group.items.length == 0) {
2083
+ return unsupportedSyntax("php", "grouped, constant, or empty use declaration", nodeLocation(group, filename, source))
2084
+ }
2085
+ for (const item of group.items) {
2086
+ if (item.type !== null || typeof item.name != "string" || !item.name.includes("\\")) {
2087
+ return unsupportedSyntax("php", "grouped or unqualified use item", nodeLocation(item, filename, source))
2088
+ }
2089
+ const importedName = item.name.split("\\").at(-1)
2090
+ const alias = item.alias
2091
+
2092
+ if (!importedName || alias && alias.kind != "identifier") {
2093
+ return unsupportedSyntax("php", "invalid use alias", nodeLocation(item, filename, source))
2094
+ }
2095
+ bindings.push({
2096
+ importedName,
2097
+ localName: alias ? alias.name : importedName,
2098
+ location: nodeLocation(item, filename, source),
2099
+ nativeName: item.name,
2100
+ symbolKind: /** @type {import("../semantic/types.js").SemanticDeclarationKind} */ (group.type == "function" ? "function" : "record"),
2101
+ typeOnly: /** @type {const} */ (false)
2102
+ })
2103
+ }
2104
+ }
2105
+ const imports = namespace.children.filter(isPhpIncludeStatement).map((node) => {
2106
+ if (!isPhpRequireOnce(node)) {
2107
+ return unsupportedSyntax("php", "include/require outside literal require_once __DIR__ profile", nodeLocation(node, filename, source))
2108
+ }
2109
+ const statement = /** @type {import("php-parser").ExpressionStatement} */ (node)
2110
+ const include = /** @type {import("php-parser").Include} */ (statement.expression)
2111
+ const target = /** @type {import("php-parser").Bin} */ (include.target)
2112
+ const path = /** @type {import("php-parser").String} */ (target.right)
2113
+
2114
+ if (!path.value.startsWith("/") || path.value.includes("\0")) {
2115
+ return unsupportedSyntax("php", "unsafe require_once path", nodeLocation(path, filename, source))
2116
+ }
2117
+
2118
+ return {
2119
+ importedName: "*",
2120
+ localName: "",
2121
+ location: nodeLocation(node, filename, source),
2122
+ namespace: /** @type {const} */ (true),
2123
+ pathLocation: nodeLocation(path, filename, source),
2124
+ specifier: `.${path.value}`,
2125
+ typeOnly: /** @type {const} */ (false)
2126
+ }
2127
+ })
2128
+ const exports = namespace.children.flatMap((node) => {
2129
+ if (node.kind != "class" && node.kind != "function") return []
2130
+ const declaration = /** @type {import("php-parser").Class | import("php-parser").Function} */ (node)
2131
+ const name = typeof declaration.name == "string" ? declaration.name : declaration.name.name
2132
+
2133
+ return [{exportedName: name, localName: name, location: nodeLocation(node, filename, source), typeOnly: /** @type {const} */ (false)}]
2134
+ })
2135
+
2136
+ return {bindings, exports, imports, nativeName: namespace.name}
2137
+ }
2138
+
2139
+ /**
2140
+ * Reports whether a PHP statement is any include/require expression.
2141
+ * @param {import("php-parser").Node} node - Candidate statement.
2142
+ * @returns {boolean} Whether the node is an include expression statement.
2143
+ */
2144
+ function isPhpIncludeStatement(node) {
2145
+ return node.kind == "expressionstatement" &&
2146
+ /** @type {import("php-parser").ExpressionStatement} */ (node).expression.kind == "include"
2147
+ }
2148
+
2149
+ /**
2150
+ * Reports whether a PHP statement is the canonical literal project load edge.
2151
+ * @param {import("php-parser").Node} node - Candidate statement.
2152
+ * @returns {boolean} Whether the node is a supported require-once statement.
2153
+ */
2154
+ function isPhpRequireOnce(node) {
2155
+ if (!isPhpIncludeStatement(node)) return false
2156
+ const statement = /** @type {import("php-parser").ExpressionStatement} */ (node)
2157
+ const include = /** @type {import("php-parser").Include} */ (statement.expression)
2158
+ const target = include.target.kind == "bin" ? /** @type {import("php-parser").Bin} */ (include.target) : undefined
2159
+ const left = target?.left.kind == "magic" ? /** @type {import("php-parser").Magic} */ (target.left) : undefined
2160
+
2161
+ return include.once && include.require && target?.type == "." &&
2162
+ left?.value == "__DIR__" && target.right.kind == "string"
2163
+ }
2164
+
551
2165
  /**
552
2166
  * Accepts only an optional single `declare(strict_types=1)` directive.
553
2167
  * @param {import("php-parser").Node[]} nodes - Top-level PHP nodes.
@@ -588,6 +2202,38 @@ function parsePhpProgram(filename, source) {
588
2202
  try {
589
2203
  return parser.parseCode(source, filename)
590
2204
  } catch (error) {
591
- return parseFailure("php", error)
2205
+ return parseFailure("php", error, phpParserFailureLocation(error, filename, source))
2206
+ }
2207
+ }
2208
+
2209
+ /**
2210
+ * Normalizes php-parser's one-based line and zero-based column when present.
2211
+ * A module range remains the deterministic fallback for opaque parser errors.
2212
+ * @param {unknown} error - Native parser error.
2213
+ * @param {string} filename - Requested source filename.
2214
+ * @param {string} source - Complete source.
2215
+ * @returns {import("../semantic/types.js").SourceLocation} Failure range.
2216
+ */
2217
+ function phpParserFailureLocation(error, filename, source) {
2218
+ if (!error || typeof error != "object") return moduleLocation(filename, source)
2219
+ const line = Reflect.get(error, "lineNumber")
2220
+ const column = Reflect.get(error, "columnNumber")
2221
+
2222
+ if (typeof line != "number" || !Number.isSafeInteger(line) || line < 1 ||
2223
+ typeof column != "number" || !Number.isSafeInteger(column) || column < 0) {
2224
+ return moduleLocation(filename, source)
592
2225
  }
2226
+ let lineStart = 0
2227
+
2228
+ for (let current = 1; current < line; current++) {
2229
+ const newline = source.indexOf("\n", lineStart)
2230
+
2231
+ if (newline < 0) return moduleLocation(filename, source)
2232
+ lineStart = newline + 1
2233
+ }
2234
+ const lineEnd = source.indexOf("\n", lineStart)
2235
+ const boundedEnd = lineEnd < 0 ? source.length : lineEnd
2236
+ const offset = Math.min(lineStart + column, boundedEnd)
2237
+
2238
+ return locationFromOffsets(filename, source, offset, Math.min(offset + 1, source.length))
593
2239
  }