semantifold 0.12.0 → 0.13.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 (53) hide show
  1. package/README.md +1 -0
  2. package/build/src/backends/effects.js +3 -2
  3. package/build/src/backends/java.js +33 -14
  4. package/build/src/backends/javascript.js +83 -18
  5. package/build/src/backends/php.js +33 -14
  6. package/build/src/backends/ruby.js +37 -12
  7. package/build/src/backends/shared.d.ts.map +1 -1
  8. package/build/src/backends/shared.js +91 -36
  9. package/build/src/backends/typescript.js +83 -18
  10. package/build/src/frontends/java.d.ts +8 -0
  11. package/build/src/frontends/java.d.ts.map +1 -1
  12. package/build/src/frontends/java.js +77 -33
  13. package/build/src/frontends/javascript-typescript.d.ts +2 -0
  14. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  15. package/build/src/frontends/javascript-typescript.js +118 -37
  16. package/build/src/frontends/php.d.ts +8 -0
  17. package/build/src/frontends/php.d.ts.map +1 -1
  18. package/build/src/frontends/php.js +68 -29
  19. package/build/src/frontends/ruby.d.ts +3 -0
  20. package/build/src/frontends/ruby.d.ts.map +1 -1
  21. package/build/src/frontends/ruby.js +103 -30
  22. package/build/src/semantic/effects.d.ts.map +1 -1
  23. package/build/src/semantic/effects.js +18 -6
  24. package/build/src/semantic/lifetime.js +28 -4
  25. package/build/src/semantic/provenance.d.ts.map +1 -1
  26. package/build/src/semantic/provenance.js +26 -7
  27. package/build/src/semantic/types.d.ts +76 -16
  28. package/build/src/semantic/types.d.ts.map +1 -1
  29. package/build/src/semantic/types.js +23 -7
  30. package/build/src/semantic/validate.d.ts.map +1 -1
  31. package/build/src/semantic/validate.js +105 -34
  32. package/docs/adr/20261026-typed-error-handling-extension.md +55 -0
  33. package/docs/adr-numeric-tower.md +156 -0
  34. package/docs/architecture.md +1 -1
  35. package/docs/language-support.md +7 -7
  36. package/docs/testing.md +5 -3
  37. package/package.json +1 -1
  38. package/src/backends/effects.js +2 -1
  39. package/src/backends/java.js +33 -14
  40. package/src/backends/javascript.js +83 -18
  41. package/src/backends/php.js +33 -14
  42. package/src/backends/ruby.js +36 -12
  43. package/src/backends/shared.js +88 -41
  44. package/src/backends/typescript.js +83 -18
  45. package/src/frontends/java.js +82 -33
  46. package/src/frontends/javascript-typescript.js +130 -38
  47. package/src/frontends/php.js +68 -27
  48. package/src/frontends/ruby.js +107 -28
  49. package/src/semantic/effects.js +17 -5
  50. package/src/semantic/lifetime.js +29 -3
  51. package/src/semantic/provenance.js +27 -12
  52. package/src/semantic/types.js +24 -6
  53. package/src/semantic/validate.js +104 -35
@@ -6,7 +6,7 @@ import {missingType, parseFailure, SemantifoldDiagnostic, unsupportedSyntax} fro
6
6
  import {blockReferencesProtectedEntry} from "../semantic/effects.js"
7
7
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
8
8
  import {withAdaptedOperation} from "../semantic/operators.js"
9
- import {withParserRanges} from "../semantic/provenance.js"
9
+ import {setParserRanges, withParserRanges} from "../semantic/provenance.js"
10
10
  import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
11
11
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
12
12
  import {hasExactEffectSupport} from "../backends/effects.js"
@@ -16,7 +16,7 @@ import {documentedValueType, instantiatedRecordFieldType, iterationBindingType,
16
16
  /** @typedef {NonNullable<import("@babel/parser").ParseResult<import("@babel/types").File>["tokens"]>[number]} BabelToken */
17
17
  /** @typedef {{byStart: Map<number, BabelToken>, tokens: BabelToken[]}} BabelTokenIndex */
18
18
  /** @typedef {{name: string, nameLocation: import("../semantic/types.js").SourceLocation, parameters: import("../semantic/types.js").Parameter[], returnType: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: import("../semantic/types.js").TypeParameter[], location: import("../semantic/types.js").SourceLocation}} JavaScriptFunctionSignature */
19
- /** @typedef {{bindings: Map<string, import("../semantic/types.js").SemanticBindingType>, classes: Map<string, import("../semantic/types.js").ClassDeclaration>, currentClass?: import("../semantic/types.js").ClassDeclaration, errors: Map<string, import("../semantic/types.js").ErrorDeclaration>, errorNames: Map<string, import("../semantic/types.js").ErrorDeclaration>, functions: Map<string, JavaScriptFunctionSignature>, lexicalValueNames?: Set<string>, orderedMapDeclarations?: Set<object>, recordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, returnType?: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: Map<string, import("../semantic/types.js").TypeParameter>, valueRecordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration>}} JavaScriptConversionContext */
19
+ /** @typedef {{bindings: Map<string, import("../semantic/types.js").SemanticBindingType>, catchBindingNames?: Set<string>, catchBindingNodes?: Map<string, import("../semantic/types.js").CatchBinding>, classes: Map<string, import("../semantic/types.js").ClassDeclaration>, currentClass?: import("../semantic/types.js").ClassDeclaration, errors: Map<string, import("../semantic/types.js").ErrorDeclaration>, errorNames: Map<string, import("../semantic/types.js").ErrorDeclaration>, functions: Map<string, JavaScriptFunctionSignature>, lexicalValueNames?: Set<string>, orderedMapDeclarations?: Set<object>, recordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, returnType?: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: Map<string, import("../semantic/types.js").TypeParameter>, valueRecordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration>}} JavaScriptConversionContext */
20
20
 
21
21
  /** @type {WeakMap<object, BabelTokenIndex>} */
22
22
  const nodeTokens = new WeakMap()
@@ -966,12 +966,23 @@ function convertStatement(node, language, filename, source, canonicalZeroRequire
966
966
  * @param {string} filename - Source filename.
967
967
  * @param {string} source - Complete source.
968
968
  * @param {JavaScriptConversionContext} context - Typed lexical context.
969
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
969
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
970
970
  */
971
971
  function convertTypedThrow(node, language, filename, source, context) {
972
972
  const location = nodeLocation(node, filename, source)
973
973
  const argument = node.argument
974
974
 
975
+ if (argument?.type == "Identifier" && context.catchBindingNames?.has(argument.name) && context.bindings.get(argument.name)?.kind == "ErrorType") {
976
+ const bindingNode = context.catchBindingNodes?.get(argument.name)
977
+
978
+ if (!bindingNode) return unsupportedSyntax(language, "throw of a catch binding without an exact handler binding", location)
979
+ return withParserRanges({
980
+ binding: argument.name,
981
+ bindingNode,
982
+ kind: /** @type {const} */ ("RethrowStatement"),
983
+ location
984
+ }, {keyword: tokenLocation(node, "throw", node.start ?? 0, argument.start ?? node.end ?? source.length, filename, source)})
985
+ }
975
986
  if (!argument || argument.type != "NewExpression" || argument.callee.type != "Identifier" ||
976
987
  context.bindings.has(argument.callee.name) || !context.errorNames.has(argument.callee.name) || argument.arguments.length != 1 ||
977
988
  argument.arguments[0].type == "SpreadElement" || argument.arguments[0].type == "ArgumentPlaceholder" ||
@@ -1006,53 +1017,134 @@ function convertTypedThrow(node, language, filename, source, context) {
1006
1017
  function convertTypedTry(node, language, filename, source, canonicalZeroRequired, context) {
1007
1018
  const location = nodeLocation(node, filename, source)
1008
1019
 
1009
- if (node.finalizer || !node.handler || node.handler.param?.type != "Identifier" || node.handler.body.directives.length > 0) {
1010
- return unsupportedSyntax(language, "try without one explicit guarded catch", location)
1011
- }
1012
- const binding = node.handler.param
1013
- const guard = node.handler.body.body[0]
1014
-
1015
- if (!guard || guard.type != "IfStatement" || guard.alternate || guard.test.type != "UnaryExpression" ||
1016
- guard.test.operator != "!" || guard.test.argument.type != "BinaryExpression" || guard.test.argument.operator != "instanceof" ||
1017
- guard.test.argument.left.type != "Identifier" || guard.test.argument.left.name != binding.name ||
1018
- guard.test.argument.right.type != "Identifier" || guard.test.argument.right.name == binding.name ||
1019
- context.bindings.has(guard.test.argument.right.name) || !context.errorNames.has(guard.test.argument.right.name) ||
1020
- guard.consequent.type != "BlockStatement" || guard.consequent.body.length != 1 || guard.consequent.directives.length > 0 ||
1021
- guard.consequent.body[0].type != "ThrowStatement" || guard.consequent.body[0].argument?.type != "Identifier" ||
1022
- guard.consequent.body[0].argument.name != binding.name) {
1023
- return unsupportedSyntax(language, "catch without exact instanceof guard and unmatched rethrow", nodeLocation(guard ?? node.handler, filename, source))
1024
- }
1025
- const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(guard.test.argument.right.name))
1026
- const caughtTypeLocation = identifierLocation(guard.test.argument.right, filename, source)
1027
- const catchType = withParserRanges({
1028
- declarationId: /** @type {string} */ (declaration.id),
1029
- kind: /** @type {const} */ ("ErrorType")
1030
- }, {type: caughtTypeLocation})
1020
+ if (!node.handler) {
1021
+ if (!node.finalizer) return unsupportedSyntax(language, "try without an explicit guarded catch", location)
1022
+ const bodyContext = {...context, bindings: new Map(context.bindings)}
1023
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1024
+ body: convertBlock(node.block, language, filename, source, canonicalZeroRequired, bodyContext),
1025
+ handlers: [],
1026
+ kind: /** @type {const} */ ("TryStatement"),
1027
+ location
1028
+ }, {
1029
+ try: tokenLocation(node, "try", node.start ?? 0, node.block.start ?? node.end ?? source.length, filename, source)
1030
+ }))
1031
+
1032
+ semantic.finallyBody = convertBlock(node.finalizer.body, language, filename, source, canonicalZeroRequired,
1033
+ {...context, bindings: new Map(context.bindings)}, nodeLocation(node.finalizer, filename, source))
1034
+ setParserRanges(semantic, {finally: nodeLocation(node.finalizer, filename, source),
1035
+ try: tokenLocation(node, "try", node.start ?? 0, node.block.start ?? node.end ?? source.length, filename, source)})
1036
+ return semantic
1037
+ }
1038
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1039
+ const handlers = []
1040
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1031
1041
  const bodyContext = {...context, bindings: new Map(context.bindings)}
1032
- const catchContext = {...context, bindings: new Map(context.bindings)}
1033
1042
 
1034
- catchContext.bindings.set(binding.name, catchType)
1035
- const catchStatements = node.handler.body.body.slice(1)
1043
+ // JavaScript has no sibling catch clauses, so the exact native profile for ordered
1044
+ // multi-catch is a guarded dispatch chain: each unmatched guard rethrows into the next
1045
+ // clause. The chain flattens back to one ordered handler list here.
1046
+ /** @type {import("@babel/types").CatchClause | undefined} */
1047
+ let clause = node.handler
1048
+
1049
+ while (clause) {
1050
+ const binding = clause.param
1051
+
1052
+ if (binding?.type != "Identifier" || clause.body.directives.length > 0) {
1053
+ return unsupportedSyntax(language, "catch without one explicit binding", nodeLocation(clause, filename, source))
1054
+ }
1055
+ if (seenHandlerNames.has(binding.name)) {
1056
+ return unsupportedSyntax(language, `catch binding '${binding.name}' shadows an outer handler binding`,
1057
+ identifierLocation(binding, filename, source))
1058
+ }
1059
+ seenHandlerNames.add(binding.name)
1060
+
1061
+ /** @type {import("@babel/types").Statement | undefined} */
1062
+ const guard = clause.body.body[0]
1063
+
1064
+ if (!guard || guard.type != "IfStatement" || guard.test.type != "UnaryExpression" ||
1065
+ guard.test.operator != "!" || guard.test.argument.type != "BinaryExpression" || guard.test.argument.operator != "instanceof" ||
1066
+ guard.test.argument.left.type != "Identifier" || guard.test.argument.left.name != binding.name ||
1067
+ guard.test.argument.right.type != "Identifier" || guard.test.argument.right.name == binding.name ||
1068
+ context.bindings.has(guard.test.argument.right.name) || !context.errorNames.has(guard.test.argument.right.name) ||
1069
+ guard.consequent.type != "BlockStatement" || guard.consequent.body.length != 1 || guard.consequent.directives.length > 0 ||
1070
+ (guard.alternate && (guard.alternate.type != "BlockStatement" || guard.alternate.directives.length > 0))) {
1071
+ return unsupportedSyntax(language, "catch without exact instanceof guard", nodeLocation(guard ?? clause, filename, source))
1072
+ }
1073
+
1074
+ /** @type {import("@babel/types").Statement | undefined} */
1075
+ const dispatch = guard.consequent.body[0]
1076
+ /** @type {import("@babel/types").CatchClause | undefined} */
1077
+ let nextClause = undefined
1078
+ /** @type {import("@babel/types").Statement[]} */
1079
+ let handlerBodyStatements = clause.body.body.slice(1)
1080
+ let handlerBodyLocation = nodeLocation(clause.body, filename, source)
1081
+
1082
+ if (dispatch.type == "TryStatement") {
1083
+ if (dispatch.finalizer || !dispatch.handler || dispatch.block.body.length != 1 || dispatch.block.body[0].type != "ThrowStatement" ||
1084
+ dispatch.block.body[0].argument?.type != "Identifier" || dispatch.block.body[0].argument.name != binding.name ||
1085
+ !guard.alternate || clause.body.body.length != 1) {
1086
+ return unsupportedSyntax(language, "catch guard dispatch without exact rethrow of its own binding",
1087
+ nodeLocation(dispatch, filename, source))
1088
+ }
1089
+ nextClause = dispatch.handler
1090
+ handlerBodyStatements = /** @type {import("@babel/types").BlockStatement} */ (guard.alternate).body
1091
+ handlerBodyLocation = nodeLocation(guard.alternate, filename, source)
1092
+ } else if (dispatch.type != "ThrowStatement" || dispatch.argument?.type != "Identifier" || dispatch.argument.name != binding.name) {
1093
+ return unsupportedSyntax(language, "catch guard without exact unmatched rethrow", nodeLocation(dispatch, filename, source))
1094
+ } else if (guard.alternate) {
1095
+ if (clause.body.body.length != 1) {
1096
+ return unsupportedSyntax(language, "catch guard dispatch without exact rethrow of its own binding",
1097
+ nodeLocation(guard.alternate, filename, source))
1098
+ }
1099
+ handlerBodyStatements = /** @type {import("@babel/types").BlockStatement} */ (guard.alternate).body
1100
+ handlerBodyLocation = nodeLocation(guard.alternate, filename, source)
1101
+ }
1036
1102
 
1037
- return withParserRanges({
1038
- body: convertBlock(node.block, language, filename, source, canonicalZeroRequired, bodyContext),
1039
- catchBinding: withParserRanges({
1103
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(guard.test.argument.right.name))
1104
+ const caughtTypeLocation = identifierLocation(guard.test.argument.right, filename, source)
1105
+ const catchType = withParserRanges({
1106
+ declarationId: /** @type {string} */ (declaration.id),
1107
+ kind: /** @type {const} */ ("ErrorType")
1108
+ }, {type: caughtTypeLocation})
1109
+ const catchContext = {...context, bindings: new Map(context.bindings), catchBindingNames: new Set(seenHandlerNames),
1110
+ catchBindingNodes: new Map(context.catchBindingNodes)}
1111
+
1112
+ catchContext.bindings.set(binding.name, catchType)
1113
+ const catchBindingNode = withParserRanges({
1040
1114
  kind: /** @type {const} */ ("CatchBinding"),
1041
1115
  location: identifierLocation(binding, filename, source),
1042
1116
  mutable: /** @type {const} */ (false),
1043
1117
  name: binding.name,
1044
1118
  type: catchType
1045
- }, {name: identifierLocation(binding, filename, source)}),
1046
- catchBody: convertBlock(catchStatements, language, filename, source, canonicalZeroRequired, catchContext,
1047
- nodeLocation(node.handler.body, filename, source)),
1048
- catchType,
1119
+ }, {name: identifierLocation(binding, filename, source)})
1120
+
1121
+ catchContext.catchBindingNodes.set(binding.name, catchBindingNode)
1122
+ handlers.push(withParserRanges({
1123
+ catchBinding: catchBindingNode,
1124
+ catchBody: convertBlock(handlerBodyStatements, language, filename, source, canonicalZeroRequired, catchContext,
1125
+ handlerBodyLocation),
1126
+ catchType,
1127
+ kind: /** @type {const} */ ("CatchHandler"),
1128
+ location: nodeLocation(clause, filename, source)
1129
+ }, {catch: tokenLocation(clause, "catch", clause.start ?? 0, binding.start ?? clause.end ?? source.length, filename, source)}))
1130
+ clause = nextClause
1131
+ }
1132
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1133
+ body: convertBlock(node.block, language, filename, source, canonicalZeroRequired, bodyContext),
1134
+ handlers,
1049
1135
  kind: /** @type {const} */ ("TryStatement"),
1050
1136
  location
1051
1137
  }, {
1052
- catch: tokenLocation(node.handler, "catch", node.handler.start ?? 0, binding.start ?? node.handler.end ?? source.length, filename, source),
1053
- guard: nodeLocation(guard, filename, source),
1054
1138
  try: tokenLocation(node, "try", node.start ?? 0, node.block.start ?? node.end ?? source.length, filename, source)
1055
- })
1139
+ }))
1140
+
1141
+ if (node.finalizer) {
1142
+ semantic.finallyBody = convertBlock(node.finalizer.body, language, filename, source, canonicalZeroRequired,
1143
+ {...context, bindings: new Map(context.bindings)}, nodeLocation(node.finalizer, filename, source))
1144
+ setParserRanges(semantic, {finally: nodeLocation(node.finalizer, filename, source),
1145
+ try: tokenLocation(node, "try", node.start ?? 0, node.block.start ?? node.end ?? source.length, filename, source)})
1146
+ }
1147
+ return semantic
1056
1148
  }
1057
1149
 
1058
1150
  /**
@@ -6,7 +6,7 @@ import {missingType, parseFailure, unsupportedSyntax} from "../diagnostic.js"
6
6
  import {blockReferencesProtectedEntry} from "../semantic/effects.js"
7
7
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
8
8
  import {withAdaptedOperation} from "../semantic/operators.js"
9
- import {withParserRanges} from "../semantic/provenance.js"
9
+ import {setParserRanges, withParserRanges} from "../semantic/provenance.js"
10
10
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
11
11
  import {hasExactEffectSupport} from "../backends/effects.js"
12
12
  import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
@@ -39,6 +39,8 @@ const phpBinaryOperations = new Map([
39
39
  /**
40
40
  * @typedef PhpConversionContext
41
41
  * @property {Map<string, import("../semantic/types.js").SemanticBindingType>} bindings - Explicitly typed visible bindings.
42
+ * @property {Set<string>} [catchBindingNames] - Lexically visible exact catch binding names.
43
+ * @property {Map<string, import("../semantic/types.js").CatchBinding>} [catchBindingNodes] - Exact catch binding nodes by visible name.
42
44
  * @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
43
45
  * @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
44
46
  * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
@@ -963,12 +965,24 @@ function convertStatement(node, filename, source, context) {
963
965
  * @param {string} filename - Source filename.
964
966
  * @param {string} source - Complete source.
965
967
  * @param {PhpConversionContext} context - Typed lexical context.
966
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
968
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
967
969
  */
968
970
  function convertPhpRaise(node, filename, source, context) {
969
971
  const location = nodeLocation(node, filename, source)
970
972
  const construction = Reflect.get(node, "what")
971
973
 
974
+ if (construction?.kind == "variable" && typeof construction.name == "string" && !construction.curly &&
975
+ context.bindings.get(construction.name)?.kind == "ErrorType" && context.catchBindingNames?.has(construction.name)) {
976
+ const bindingNode = context.catchBindingNodes?.get(construction.name)
977
+
978
+ if (!bindingNode) return unsupportedSyntax("php", "throw of a catch binding without an exact handler binding", location)
979
+ return withParserRanges({
980
+ binding: construction.name,
981
+ bindingNode,
982
+ kind: /** @type {const} */ ("RethrowStatement"),
983
+ location
984
+ }, {keyword: tokenLocation("throw", node.loc?.start.offset ?? 0, construction.loc?.start.offset ?? source.length, filename, source)})
985
+ }
972
986
  if (!construction || construction.kind != "new" || construction.what?.kind != "name" ||
973
987
  construction.what.resolution != "uqn" || !context.errorNames.has(construction.what.name) ||
974
988
  !Array.isArray(construction.arguments) || construction.arguments.length != 1 ||
@@ -1002,42 +1016,69 @@ function convertPhpTry(node, filename, source, context) {
1002
1016
  const location = nodeLocation(node, filename, source)
1003
1017
  const catches = Reflect.get(node, "catches")
1004
1018
  const body = Reflect.get(node, "body")
1019
+ const always = Reflect.get(node, "always")
1005
1020
 
1006
- if (Reflect.get(node, "always") || !Array.isArray(catches) || catches.length != 1 || body?.kind != "block") {
1007
- return unsupportedSyntax("php", "try without one typed catch and no finally", location)
1021
+ if (!Array.isArray(catches) || (catches.length == 0 && !always) || body?.kind != "block") {
1022
+ return unsupportedSyntax("php", "try without typed catches or finally", location)
1008
1023
  }
1009
- const handler = catches[0]
1010
- const caught = handler.what?.[0]
1011
- const binding = handler.variable
1024
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1025
+ const handlers = []
1026
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1012
1027
 
1013
- if (handler.what?.length != 1 || caught?.kind != "name" || caught.resolution != "uqn" ||
1014
- !context.errorNames.has(caught.name) || binding?.kind != "variable" || typeof binding.name != "string" ||
1015
- binding.curly || handler.body?.kind != "block") {
1016
- return unsupportedSyntax("php", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1017
- }
1018
- const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(caught.name))
1019
- const caughtLocation = nodeLocation(caught, filename, source)
1020
- const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1021
- type: caughtLocation
1022
- })
1023
- const bodyContext = {...context, bindings: new Map(context.bindings)}
1024
- const catchContext = {...context, bindings: new Map(context.bindings)}
1028
+ for (const handler of catches) {
1029
+ const caught = handler.what?.[0]
1030
+ const binding = handler.variable
1025
1031
 
1026
- catchContext.bindings.set(binding.name, catchType)
1027
- return withParserRanges({
1028
- body: convertBlock(body, filename, source, bodyContext),
1029
- catchBinding: withParserRanges({
1032
+ if (handler.what?.length != 1 || caught?.kind != "name" || caught.resolution != "uqn" ||
1033
+ !context.errorNames.has(caught.name) || binding?.kind != "variable" || typeof binding.name != "string" ||
1034
+ binding.curly || handler.body?.kind != "block") {
1035
+ return unsupportedSyntax("php", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1036
+ }
1037
+ if (seenHandlerNames.has(binding.name)) {
1038
+ return unsupportedSyntax("php", `catch binding '${binding.name}' shadows an outer handler binding`, nodeLocation(binding, filename, source))
1039
+ }
1040
+ seenHandlerNames.add(binding.name)
1041
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(caught.name))
1042
+ const caughtLocation = nodeLocation(caught, filename, source)
1043
+ const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1044
+ type: caughtLocation
1045
+ })
1046
+ const catchContext = {...context, bindings: new Map(context.bindings), catchBindingNames: new Set(seenHandlerNames),
1047
+ catchBindingNodes: new Map(context.catchBindingNodes)}
1048
+
1049
+ catchContext.bindings.set(binding.name, catchType)
1050
+ const catchBindingNode = withParserRanges({
1030
1051
  kind: /** @type {const} */ ("CatchBinding"),
1031
1052
  location: nodeLocation(binding, filename, source),
1032
1053
  mutable: /** @type {const} */ (false),
1033
1054
  name: binding.name,
1034
1055
  type: catchType
1035
- }, {name: nodeLocation(binding, filename, source)}),
1036
- catchBody: convertBlock(handler.body, filename, source, catchContext),
1037
- catchType,
1056
+ }, {name: nodeLocation(binding, filename, source)})
1057
+
1058
+ catchContext.catchBindingNodes.set(binding.name, catchBindingNode)
1059
+ handlers.push(withParserRanges({
1060
+ catchBinding: catchBindingNode,
1061
+ catchBody: convertBlock(handler.body, filename, source, catchContext),
1062
+ catchType,
1063
+ kind: /** @type {const} */ ("CatchHandler"),
1064
+ location: nodeLocation(handler, filename, source)
1065
+ }, {catch: nodeLocation(handler, filename, source)}))
1066
+ }
1067
+ const bodyContext = {...context, bindings: new Map(context.bindings)}
1068
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1069
+ body: convertBlock(body, filename, source, bodyContext),
1070
+ handlers,
1038
1071
  kind: /** @type {const} */ ("TryStatement"),
1039
1072
  location
1040
- }, {catch: nodeLocation(handler, filename, source), try: tokenLocation("try", node.loc?.start.offset ?? 0, body.loc?.start.offset ?? source.length, filename, source)})
1073
+ }, {try: tokenLocation("try", node.loc?.start.offset ?? 0, body.loc?.start.offset ?? source.length, filename, source)}))
1074
+
1075
+ if (always) {
1076
+ if (always.kind != "block") return unsupportedSyntax("php", "malformed finally block", nodeLocation(always, filename, source))
1077
+ semantic.finallyBody = convertBlock(always, filename, source, bodyContext)
1078
+ setParserRanges(semantic, {finally: nodeLocation(always, filename, source),
1079
+ try: tokenLocation("try", node.loc?.start.offset ?? 0, body.loc?.start.offset ?? source.length, filename, source)})
1080
+ }
1081
+ return semantic
1041
1082
  }
1042
1083
 
1043
1084
  /**
@@ -49,7 +49,7 @@ import {missingType, SemantifoldDiagnostic, unsupportedSyntax} from "../diagnost
49
49
  import {blockReferencesProtectedEntry} from "../semantic/effects.js"
50
50
  import {locationFromOffsets, moduleLocation, utf8ByteOffsetToUtf16Offset} from "../semantic/location.js"
51
51
  import {withAdaptedOperation} from "../semantic/operators.js"
52
- import {withParserRanges} from "../semantic/provenance.js"
52
+ import {setParserRanges, withParserRanges} from "../semantic/provenance.js"
53
53
  import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
54
54
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
55
55
  import {hasExactEffectSupport} from "../backends/effects.js"
@@ -61,7 +61,7 @@ const referenceMethodHooks = new Set([
61
61
  "respond_to_missing?", "send", "singleton_class", "size"
62
62
  ])
63
63
  /** @typedef {{name: string, nameLocation: import("../semantic/types.js").SourceLocation, parameters: import("../semantic/types.js").Parameter[], returnType: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: import("../semantic/types.js").TypeParameter[], location: import("../semantic/types.js").SourceLocation}} RubyFunctionSignature */
64
- /** @typedef {{bindings: Map<string, import("../semantic/types.js").SemanticBindingType>, classes: Map<string, import("../semantic/types.js").ClassDeclaration>, currentClass?: import("../semantic/types.js").ClassDeclaration, errorNames: Map<string, import("../semantic/types.js").ErrorDeclaration>, errors: Map<string, import("../semantic/types.js").ErrorDeclaration>, functions: Map<string, RubyFunctionSignature>, orderedMapDeclarations?: Set<object>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, recordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>, loopDepth?: number, returnType?: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: Map<string, import("../semantic/types.js").TypeParameter>}} RubyConversionContext */
64
+ /** @typedef {{bindings: Map<string, import("../semantic/types.js").SemanticBindingType>, catchBindingNames?: Set<string>, classes: Map<string, import("../semantic/types.js").ClassDeclaration>, catchBindingNodes?: Map<string, import("../semantic/types.js").CatchBinding>, currentCatchBindingNode?: import("../semantic/types.js").CatchBinding, currentClass?: import("../semantic/types.js").ClassDeclaration, errorNames: Map<string, import("../semantic/types.js").ErrorDeclaration>, errors: Map<string, import("../semantic/types.js").ErrorDeclaration>, functions: Map<string, RubyFunctionSignature>, orderedMapDeclarations?: Set<object>, records: Map<string, import("../semantic/types.js").RecordDeclaration>, recordNames: Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>, loopDepth?: number, returnType?: import("../semantic/types.js").SemanticFunctionReturnType, typeParameters?: Map<string, import("../semantic/types.js").TypeParameter>}} RubyConversionContext */
65
65
  const rubyBinaryOperations = new Map([
66
66
  ["+", "Add"],
67
67
  ["-", "Subtract"],
@@ -969,15 +969,38 @@ function convertStatement(node, comments, context, filename, source) {
969
969
  * @param {RubyConversionContext} context - Typed lexical context.
970
970
  * @param {string} filename - Source filename.
971
971
  * @param {string} source - Complete source.
972
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
972
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
973
973
  */
974
974
  function convertRubyRaise(node, context, filename, source) {
975
975
  const location = nodeLocation(node, filename, source)
976
976
  const arguments_ = node.arguments_?.arguments_ ?? []
977
+
978
+ if (arguments_.length == 0 && context.currentCatchBindingNode) {
979
+ return withParserRanges({
980
+ binding: context.currentCatchBindingNode.name,
981
+ bindingNode: context.currentCatchBindingNode,
982
+ kind: /** @type {const} */ ("RethrowStatement"),
983
+ location
984
+ }, {keyword: prismLocation(node.messageLoc ?? node.location, filename, source)})
985
+ }
986
+ if (arguments_.length == 1 && arguments_[0] instanceof LocalVariableReadNode &&
987
+ context.catchBindingNames?.has(arguments_[0].name) && context.bindings.get(arguments_[0].name)?.kind == "ErrorType") {
988
+ const bindingNode = context.catchBindingNodes?.get(arguments_[0].name)
989
+
990
+ if (!bindingNode) return unsupportedSyntax("ruby", "raise of a catch binding without an exact handler binding", location)
991
+ return withParserRanges({
992
+ binding: arguments_[0].name,
993
+ bindingNode,
994
+ kind: /** @type {const} */ ("RethrowStatement"),
995
+ location
996
+ }, {keyword: prismLocation(node.messageLoc ?? node.location, filename, source)})
997
+ }
998
+ if (node.block || node.callOperatorLoc || arguments_.length != 2) {
999
+ return unsupportedSyntax("ruby", "raise other than exact declared error and message", location)
1000
+ }
977
1001
  const errorNode = arguments_[0]
978
1002
 
979
- if (node.block || node.callOperatorLoc || arguments_.length != 2 ||
980
- !(errorNode instanceof ConstantReadNode) && !(errorNode instanceof ConstantPathNode) ||
1003
+ if (!(errorNode instanceof ConstantReadNode) && !(errorNode instanceof ConstantPathNode) ||
981
1004
  !context.errorNames.has(constantPathName(errorNode))) {
982
1005
  return unsupportedSyntax("ruby", "raise other than exact declared error and message", location)
983
1006
  }
@@ -997,48 +1020,104 @@ function convertRubyRaise(node, context, filename, source) {
997
1020
  }
998
1021
 
999
1022
  /**
1000
- * Converts one exact typed rescue with no else/ensure.
1023
+ * Converts one exact Prism ensure clause body.
1024
+ * @param {import("@ruby/prism").EnsureNode} node - Prism ensure clause.
1025
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
1026
+ * @param {RubyConversionContext} context - Typed lexical context.
1027
+ * @param {string} filename - Source filename.
1028
+ * @param {string} source - Complete source.
1029
+ * @returns {import("../semantic/types.js").Block} Semantic finally block.
1030
+ */
1031
+ function convertRubyEnsure(node, comments, context, filename, source) {
1032
+ // A completion block gets its own binding scope, like every other block in convertRubyTry: a local
1033
+ // declaration inside `ensure` must not leak back into the enclosing conversion context.
1034
+ return convertBlock(node.statements ?? null, comments, {...context, bindings: new Map(context.bindings)}, filename, source,
1035
+ nodeLocation(node, filename, source))
1036
+ }
1037
+
1038
+ /**
1039
+ * Converts one begin block with ordered exact typed rescues and an optional ensure completion block.
1001
1040
  * @param {BeginNode} node - Prism begin node.
1002
1041
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
1003
1042
  * @param {RubyConversionContext} context - Typed lexical context.
1004
1043
  * @param {string} filename - Source filename.
1005
1044
  * @param {string} source - Complete source.
1006
- * @returns {import("../semantic/types.js").TryStatement} Semantic exact handler.
1045
+ * @returns {import("../semantic/types.js").TryStatement} Semantic exact handler chain.
1007
1046
  */
1008
1047
  function convertRubyTry(node, comments, context, filename, source) {
1009
1048
  const location = nodeLocation(node, filename, source)
1010
- const handler = node.rescueClause
1011
1049
 
1012
- if (!(handler instanceof RescueNode) || handler.subsequent || node.elseClause || node.ensureClause ||
1013
- handler.exceptions.length != 1 ||
1014
- !(handler.exceptions[0] instanceof ConstantReadNode) && !(handler.exceptions[0] instanceof ConstantPathNode) ||
1015
- !context.errorNames.has(constantPathName(handler.exceptions[0])) || !(handler.reference instanceof LocalVariableTargetNode)) {
1016
- return unsupportedSyntax("ruby", "begin without one exact typed rescue and binding", location)
1050
+ if (node.elseClause) return unsupportedSyntax("ruby", "begin else clause", nodeLocation(node.elseClause, filename, source))
1051
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1052
+ const handlers = []
1053
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1054
+ let handler = node.rescueClause
1055
+
1056
+ if (!(handler instanceof RescueNode)) {
1057
+ if (node.ensureClause) {
1058
+ return withParserRanges({
1059
+ body: convertBlock(node.statements, comments, context, filename, source, location),
1060
+ handlers,
1061
+ kind: /** @type {const} */ ("TryStatement"),
1062
+ location,
1063
+ finallyBody: convertRubyEnsure(node.ensureClause, comments, context, filename, source)
1064
+ }, {ensure: nodeLocation(node.ensureClause, filename, source), try: location})
1065
+ }
1066
+ return unsupportedSyntax("ruby", "begin without rescue or ensure", location)
1017
1067
  }
1018
- const errorNode = handler.exceptions[0]
1019
- const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(constantPathName(errorNode)))
1020
- const caughtLocation = nodeLocation(errorNode, filename, source)
1021
- const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1022
- type: caughtLocation
1023
- })
1024
1068
  const bodyContext = {...context, bindings: new Map(context.bindings)}
1025
- const catchContext = {...context, bindings: new Map(context.bindings)}
1069
+ while (handler instanceof RescueNode) {
1070
+ if (handler.exceptions.length != 1 ||
1071
+ !(handler.exceptions[0] instanceof ConstantReadNode) && !(handler.exceptions[0] instanceof ConstantPathNode) ||
1072
+ !context.errorNames.has(constantPathName(handler.exceptions[0])) || !(handler.reference instanceof LocalVariableTargetNode)) {
1073
+ return unsupportedSyntax("ruby", "rescue without one exact typed exception and binding", nodeLocation(handler, filename, source))
1074
+ }
1075
+ if (seenHandlerNames.has(handler.reference.name)) {
1076
+ return unsupportedSyntax("ruby", `rescue binding '${handler.reference.name}' shadows an outer handler binding`,
1077
+ nodeLocation(handler.reference, filename, source))
1078
+ }
1079
+ seenHandlerNames.add(handler.reference.name)
1080
+ const errorNode = handler.exceptions[0]
1081
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(constantPathName(errorNode)))
1082
+ const caughtLocation = nodeLocation(errorNode, filename, source)
1083
+ const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1084
+ type: caughtLocation
1085
+ })
1086
+ const catchContext = {...context, bindings: new Map(context.bindings), catchBindingNames: new Set(seenHandlerNames),
1087
+ catchBindingNodes: new Map(context.catchBindingNodes)}
1026
1088
 
1027
- catchContext.bindings.set(handler.reference.name, catchType)
1028
- return withParserRanges({
1029
- body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
1030
- catchBinding: withParserRanges({
1089
+ catchContext.bindings.set(handler.reference.name, catchType)
1090
+ const catchBindingNode = withParserRanges({
1031
1091
  kind: /** @type {const} */ ("CatchBinding"),
1032
1092
  location: nodeLocation(handler.reference, filename, source),
1033
1093
  mutable: /** @type {const} */ (false),
1034
1094
  name: handler.reference.name,
1035
1095
  type: catchType
1036
- }, {name: nodeLocation(handler.reference, filename, source)}),
1037
- catchBody: convertBlock(handler.statements, comments, catchContext, filename, source, nodeLocation(handler, filename, source)),
1038
- catchType,
1096
+ }, {name: nodeLocation(handler.reference, filename, source)})
1097
+
1098
+ catchContext.catchBindingNodes.set(handler.reference.name, catchBindingNode)
1099
+ catchContext.currentCatchBindingNode = catchBindingNode
1100
+ handlers.push(withParserRanges({
1101
+ catchBinding: catchBindingNode,
1102
+ catchBody: convertBlock(handler.statements, comments, catchContext, filename, source, nodeLocation(handler, filename, source)),
1103
+ catchType,
1104
+ kind: /** @type {const} */ ("CatchHandler"),
1105
+ location: nodeLocation(handler, filename, source)
1106
+ }, {catch: nodeLocation(handler, filename, source)}))
1107
+ handler = handler.subsequent
1108
+ }
1109
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1110
+ body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
1111
+ handlers,
1039
1112
  kind: /** @type {const} */ ("TryStatement"),
1040
1113
  location
1041
- }, {catch: nodeLocation(handler, filename, source), try: location})
1114
+ }, {try: location}))
1115
+
1116
+ if (node.ensureClause) {
1117
+ semantic.finallyBody = convertRubyEnsure(node.ensureClause, comments, context, filename, source)
1118
+ setParserRanges(semantic, {ensure: nodeLocation(node.ensureClause, filename, source), try: location})
1119
+ }
1120
+ return semantic
1042
1121
  }
1043
1122
 
1044
1123
  /**
@@ -379,10 +379,19 @@ function escapingBlockFailures(block, functionSummaries, methodSummaries, capabi
379
379
 
380
380
  for (const statement of block.statements) {
381
381
  if (statement.kind == "TryStatement") {
382
- const body = escapingBlockFailures(statement.body, functionSummaries, methodSummaries, capabilityFailureIds)
382
+ let body = escapingBlockFailures(statement.body, functionSummaries, methodSummaries, capabilityFailureIds)
383
383
 
384
- if (body.delete(statement.catchType.declarationId)) {
385
- addFailures(result, escapingBlockFailures(statement.catchBody, functionSummaries, methodSummaries, capabilityFailureIds))
384
+ for (const handler of statement.handlers ?? []) {
385
+ const caughtId = handler.catchType?.declarationId
386
+
387
+ if (typeof caughtId == "string" && body.delete(caughtId)) {
388
+ addFailures(body, escapingBlockFailures(handler.catchBody, functionSummaries, methodSummaries, capabilityFailureIds))
389
+ }
390
+ }
391
+ if (statement.finallyBody) {
392
+ // A completion block that can raise replaces the pending completion on the paths where it does,
393
+ // but pending failures still escape on the paths where it completes normally, so both are reachable.
394
+ addFailures(body, escapingBlockFailures(statement.finallyBody, functionSummaries, methodSummaries, capabilityFailureIds))
386
395
  }
387
396
  addFailures(result, body)
388
397
  continue
@@ -639,7 +648,9 @@ function forEachBlockExpression(block, visitor, postorder) {
639
648
  visit(statement.condition); forEachBlockExpression(statement.body, visitor, postorder)
640
649
  } else if (statement.kind == "RaiseStatement") visit(statement.error.message)
641
650
  else if (statement.kind == "TryStatement") {
642
- forEachBlockExpression(statement.body, visitor, postorder); forEachBlockExpression(statement.catchBody, visitor, postorder)
651
+ forEachBlockExpression(statement.body, visitor, postorder)
652
+ for (const handler of statement.handlers) forEachBlockExpression(handler.catchBody, visitor, postorder)
653
+ if (statement.finallyBody) forEachBlockExpression(statement.finallyBody, visitor, postorder)
643
654
  }
644
655
  }
645
656
 
@@ -694,7 +705,8 @@ function statementReferencesProtectedEntry(statement) {
694
705
  case "RaiseStatement":
695
706
  return expressionReferencesProtectedEntry(statement.error.message)
696
707
  case "TryStatement":
697
- return blockReferencesProtectedEntry(statement.body) || blockReferencesProtectedEntry(statement.catchBody)
708
+ return blockReferencesProtectedEntry(statement.body) || statement.handlers.some((handler) => blockReferencesProtectedEntry(handler.catchBody)) ||
709
+ (statement.finallyBody ? blockReferencesProtectedEntry(statement.finallyBody) : false)
698
710
  default:
699
711
  return false
700
712
  }