semantifold 0.11.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 (71) hide show
  1. package/README.md +2 -1
  2. package/build/src/backends/c.d.ts +2 -1
  3. package/build/src/backends/c.d.ts.map +1 -1
  4. package/build/src/backends/c.js +26 -5
  5. package/build/src/backends/cpp.js +3 -1
  6. package/build/src/backends/effects.js +3 -2
  7. package/build/src/backends/java.js +33 -14
  8. package/build/src/backends/javascript.js +83 -18
  9. package/build/src/backends/ordered-expressions.d.ts +19 -7
  10. package/build/src/backends/ordered-expressions.d.ts.map +1 -1
  11. package/build/src/backends/ordered-expressions.js +46 -11
  12. package/build/src/backends/php.js +33 -14
  13. package/build/src/backends/ruby.js +37 -12
  14. package/build/src/backends/shared.d.ts.map +1 -1
  15. package/build/src/backends/shared.js +92 -37
  16. package/build/src/backends/typescript.js +83 -18
  17. package/build/src/frontends/c.d.ts +34 -1
  18. package/build/src/frontends/c.d.ts.map +1 -1
  19. package/build/src/frontends/c.js +128 -21
  20. package/build/src/frontends/java.d.ts +8 -0
  21. package/build/src/frontends/java.d.ts.map +1 -1
  22. package/build/src/frontends/java.js +77 -33
  23. package/build/src/frontends/javascript-typescript.d.ts +2 -0
  24. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  25. package/build/src/frontends/javascript-typescript.js +118 -37
  26. package/build/src/frontends/php.d.ts +8 -0
  27. package/build/src/frontends/php.d.ts.map +1 -1
  28. package/build/src/frontends/php.js +68 -29
  29. package/build/src/frontends/ruby.d.ts +3 -0
  30. package/build/src/frontends/ruby.d.ts.map +1 -1
  31. package/build/src/frontends/ruby.js +103 -30
  32. package/build/src/language-registry.d.ts.map +1 -1
  33. package/build/src/language-registry.js +16 -1
  34. package/build/src/semantic/effects.d.ts.map +1 -1
  35. package/build/src/semantic/effects.js +18 -6
  36. package/build/src/semantic/lifetime.js +28 -4
  37. package/build/src/semantic/provenance.d.ts.map +1 -1
  38. package/build/src/semantic/provenance.js +26 -7
  39. package/build/src/semantic/types.d.ts +76 -16
  40. package/build/src/semantic/types.d.ts.map +1 -1
  41. package/build/src/semantic/types.js +23 -7
  42. package/build/src/semantic/validate.d.ts.map +1 -1
  43. package/build/src/semantic/validate.js +106 -35
  44. package/docs/adr/20261026-typed-error-handling-extension.md +55 -0
  45. package/docs/adr-numeric-tower.md +156 -0
  46. package/docs/architecture.md +1 -1
  47. package/docs/c.md +1 -1
  48. package/docs/language-support.md +10 -9
  49. package/docs/testing.md +8 -5
  50. package/package.json +1 -1
  51. package/src/backends/c.js +23 -4
  52. package/src/backends/cpp.js +1 -0
  53. package/src/backends/effects.js +2 -1
  54. package/src/backends/java.js +33 -14
  55. package/src/backends/javascript.js +83 -18
  56. package/src/backends/ordered-expressions.js +46 -10
  57. package/src/backends/php.js +33 -14
  58. package/src/backends/ruby.js +36 -12
  59. package/src/backends/shared.js +89 -42
  60. package/src/backends/typescript.js +83 -18
  61. package/src/frontends/c.js +135 -19
  62. package/src/frontends/java.js +82 -33
  63. package/src/frontends/javascript-typescript.js +130 -38
  64. package/src/frontends/php.js +68 -27
  65. package/src/frontends/ruby.js +107 -28
  66. package/src/language-registry.js +15 -0
  67. package/src/semantic/effects.js +17 -5
  68. package/src/semantic/lifetime.js +29 -3
  69. package/src/semantic/provenance.js +27 -12
  70. package/src/semantic/types.js +24 -6
  71. package/src/semantic/validate.js +105 -36
@@ -4,7 +4,7 @@ import {parser} from "@lezer/java"
4
4
  import {SemantifoldDiagnostic, missingType, unsupportedSyntax} from "../diagnostic.js"
5
5
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
6
6
  import {withAdaptedOperation} from "../semantic/operators.js"
7
- import {withParserRanges} from "../semantic/provenance.js"
7
+ import {setParserRanges, withParserRanges} from "../semantic/provenance.js"
8
8
  import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
9
9
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
10
10
  import {hasExactEffectSupport} from "../backends/effects.js"
@@ -41,6 +41,8 @@ const referenceMethodHooks = new Set(["clone", "equals", "finalize", "getClass",
41
41
  /**
42
42
  * @typedef JavaConversionContext
43
43
  * @property {Map<string, import("../semantic/types.js").SemanticBindingType>} bindings - Explicitly typed visible bindings.
44
+ * @property {Set<string>} [catchBindingNames] - Lexically visible exact catch binding names.
45
+ * @property {Map<string, import("../semantic/types.js").CatchBinding>} [catchBindingNodes] - Exact catch binding nodes by visible name.
44
46
  * @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
45
47
  * @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
46
48
  * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by source name.
@@ -970,7 +972,7 @@ function convertStatement(statement, filename, source, context) {
970
972
  * @param {string} filename - Source filename.
971
973
  * @param {string} source - Complete source.
972
974
  * @param {JavaConversionContext} context - Typed lexical context.
973
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
975
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
974
976
  */
975
977
  function convertJavaRaise(statement, filename, source, context) {
976
978
  const location = nodeLocation(statement, filename, source)
@@ -980,6 +982,23 @@ function convertJavaRaise(statement, filename, source, context) {
980
982
  const arguments_ = argumentsNode ? structuralChildren(argumentsNode) : []
981
983
  const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
982
984
 
985
+ if (!construction && arguments_.length == 0) {
986
+ const throwNode = requiredChild(statement, "throw", filename, source)
987
+ const rethrown = structuralChildren(statement).find((candidate) => candidate.name == "Identifier")
988
+ const binding = rethrown ? context.bindings.get(nodeText(rethrown, source)) : undefined
989
+
990
+ if (rethrown && binding?.kind == "ErrorType" && context.catchBindingNames?.has(nodeText(rethrown, source))) {
991
+ const bindingNode = context.catchBindingNodes?.get(nodeText(rethrown, source))
992
+
993
+ if (!bindingNode) return unsupportedSyntax("java", "throw of a catch binding without an exact handler binding", location)
994
+ return withParserRanges({
995
+ binding: nodeText(rethrown, source),
996
+ bindingNode,
997
+ kind: /** @type {const} */ ("RethrowStatement"),
998
+ location
999
+ }, {keyword: nodeLocation(throwNode, filename, source)})
1000
+ }
1001
+ }
983
1002
  if (!construction || !typeNode || !argumentsNode || !declaration || arguments_.length != 1 || construction.getChild("ClassBody")) {
984
1003
  return unsupportedSyntax("java", "throw other than exact declared error construction", location)
985
1004
  }
@@ -1009,46 +1028,76 @@ function convertJavaTry(statement, filename, source, context) {
1009
1028
  const location = nodeLocation(statement, filename, source)
1010
1029
  const body = statement.getChild("Block")
1011
1030
  const catches = statement.getChildren("CatchClause")
1031
+ const finalizer = statement.getChild("FinallyClause")
1032
+
1033
+ if (!body || (catches.length == 0 && !finalizer) || statement.getChild("ResourceSpecification")) {
1034
+ return unsupportedSyntax("java", "try without catches or finally and without resources", location)
1035
+ }
1036
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1037
+ const handlers = []
1038
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1039
+
1040
+ for (const handler of catches) {
1041
+ const parameter = handler.getChild("CatchFormalParameter")
1042
+ const caught = parameter?.getChild("CatchType")
1043
+ const typeNodes = caught ? structuralChildren(caught) : []
1044
+ const definition = parameter?.getChild("Definition")
1045
+ const catchBody = handler.getChild("Block")
1046
+ const typeNode = typeNodes[0]
1047
+ const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
1048
+
1049
+ if (!parameter || !caught || typeNodes.length != 1 || typeNode?.name != "TypeName" || !definition || !catchBody || !declaration ||
1050
+ parameter.getChild("Modifiers")) {
1051
+ return unsupportedSyntax("java", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1052
+ }
1053
+ const name = nodeText(definition, source)
1012
1054
 
1013
- if (!body || catches.length != 1 || statement.getChild("FinallyClause") || statement.getChild("ResourceSpecification")) {
1014
- return unsupportedSyntax("java", "try without one catch and no resources or finally", location)
1015
- }
1016
- const handler = catches[0]
1017
- const parameter = handler.getChild("CatchFormalParameter")
1018
- const caught = parameter?.getChild("CatchType")
1019
- const typeNodes = caught ? structuralChildren(caught) : []
1020
- const definition = parameter?.getChild("Definition")
1021
- const catchBody = handler.getChild("Block")
1022
- const typeNode = typeNodes[0]
1023
- const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
1024
-
1025
- if (!parameter || !caught || typeNodes.length != 1 || typeNode?.name != "TypeName" || !definition || !catchBody || !declaration ||
1026
- parameter.getChild("Modifiers")) {
1027
- return unsupportedSyntax("java", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1028
- }
1029
- const caughtLocation = nodeLocation(typeNode, filename, source)
1030
- const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1031
- type: caughtLocation
1032
- })
1033
- const bodyContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1034
- const catchContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1035
- const name = nodeText(definition, source)
1055
+ if (seenHandlerNames.has(name)) {
1056
+ return unsupportedSyntax("java", `catch binding '${name}' shadows an outer handler binding`, nodeLocation(definition, filename, source))
1057
+ }
1058
+ seenHandlerNames.add(name)
1059
+ const caughtLocation = nodeLocation(typeNode, filename, source)
1060
+ const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1061
+ type: caughtLocation
1062
+ })
1063
+ const catchContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames),
1064
+ catchBindingNames: new Set(seenHandlerNames), catchBindingNodes: new Map(context.catchBindingNodes)}
1036
1065
 
1037
- catchContext.bindings.set(name, catchType)
1038
- return withParserRanges({
1039
- body: convertBlock(body, filename, source, bodyContext),
1040
- catchBinding: withParserRanges({
1066
+ catchContext.bindings.set(name, catchType)
1067
+ const catchBindingNode = withParserRanges({
1041
1068
  kind: /** @type {const} */ ("CatchBinding"),
1042
1069
  location: nodeLocation(definition, filename, source),
1043
1070
  mutable: /** @type {const} */ (false),
1044
1071
  name,
1045
1072
  type: catchType
1046
- }, {name: nodeLocation(definition, filename, source)}),
1047
- catchBody: convertBlock(catchBody, filename, source, catchContext),
1048
- catchType,
1073
+ }, {name: nodeLocation(definition, filename, source)})
1074
+
1075
+ catchContext.catchBindingNodes.set(name, catchBindingNode)
1076
+ handlers.push(withParserRanges({
1077
+ catchBinding: catchBindingNode,
1078
+ catchBody: convertBlock(catchBody, filename, source, catchContext),
1079
+ catchType,
1080
+ kind: /** @type {const} */ ("CatchHandler"),
1081
+ location: nodeLocation(handler, filename, source)
1082
+ }, {catch: nodeLocation(handler, filename, source)}))
1083
+ }
1084
+ const bodyContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1085
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1086
+ body: convertBlock(body, filename, source, bodyContext),
1087
+ handlers,
1049
1088
  kind: /** @type {const} */ ("TryStatement"),
1050
1089
  location
1051
- }, {catch: nodeLocation(handler, filename, source), try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)})
1090
+ }, {try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)}))
1091
+
1092
+ if (finalizer) {
1093
+ const finallyBlock = finalizer.getChild("Block")
1094
+
1095
+ if (!finallyBlock) return unsupportedSyntax("java", "malformed finally block", nodeLocation(finalizer, filename, source))
1096
+ semantic.finallyBody = convertBlock(finallyBlock, filename, source, bodyContext)
1097
+ setParserRanges(semantic, {finally: nodeLocation(finalizer, filename, source),
1098
+ try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)})
1099
+ }
1100
+ return semantic
1052
1101
  }
1053
1102
 
1054
1103
  /**
@@ -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
  /**