semantifold 0.12.0 → 0.14.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 (73) hide show
  1. package/README.md +2 -0
  2. package/build/src/backends/effects.js +5 -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.d.ts.map +1 -1
  6. package/build/src/backends/php.js +88 -16
  7. package/build/src/backends/ruby.js +37 -12
  8. package/build/src/backends/scalars.js +2 -2
  9. package/build/src/backends/shared.d.ts +2 -1
  10. package/build/src/backends/shared.d.ts.map +1 -1
  11. package/build/src/backends/shared.js +299 -42
  12. package/build/src/backends/typescript.js +83 -18
  13. package/build/src/backends/writer.d.ts +18 -4
  14. package/build/src/backends/writer.d.ts.map +1 -1
  15. package/build/src/backends/writer.js +40 -4
  16. package/build/src/frontends/java.d.ts +9 -1
  17. package/build/src/frontends/java.d.ts.map +1 -1
  18. package/build/src/frontends/java.js +90 -43
  19. package/build/src/frontends/javascript-typescript.d.ts +3 -1
  20. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  21. package/build/src/frontends/javascript-typescript.js +133 -52
  22. package/build/src/frontends/php.d.ts +13 -1
  23. package/build/src/frontends/php.d.ts.map +1 -1
  24. package/build/src/frontends/php.js +226 -59
  25. package/build/src/frontends/ruby.d.ts +4 -1
  26. package/build/src/frontends/ruby.d.ts.map +1 -1
  27. package/build/src/frontends/ruby.js +113 -37
  28. package/build/src/frontends/types.d.ts +9 -2
  29. package/build/src/frontends/types.d.ts.map +1 -1
  30. package/build/src/frontends/types.js +26 -9
  31. package/build/src/language-registry.d.ts.map +1 -1
  32. package/build/src/language-registry.js +14 -6
  33. package/build/src/mapping.d.ts.map +1 -1
  34. package/build/src/mapping.js +4 -2
  35. package/build/src/semantic/effects.d.ts.map +1 -1
  36. package/build/src/semantic/effects.js +20 -6
  37. package/build/src/semantic/lifetime.js +28 -4
  38. package/build/src/semantic/provenance.d.ts.map +1 -1
  39. package/build/src/semantic/provenance.js +61 -9
  40. package/build/src/semantic/types.d.ts +214 -23
  41. package/build/src/semantic/types.d.ts.map +1 -1
  42. package/build/src/semantic/types.js +58 -11
  43. package/build/src/semantic/validate.d.ts +31 -2
  44. package/build/src/semantic/validate.d.ts.map +1 -1
  45. package/build/src/semantic/validate.js +450 -60
  46. package/docs/adr/20261026-typed-error-handling-extension.md +55 -0
  47. package/docs/adr-bounded-subtyping.md +180 -0
  48. package/docs/adr-numeric-tower.md +156 -0
  49. package/docs/architecture.md +1 -1
  50. package/docs/language-support.md +8 -8
  51. package/docs/testing.md +5 -3
  52. package/package.json +1 -1
  53. package/src/backends/effects.js +3 -1
  54. package/src/backends/java.js +33 -14
  55. package/src/backends/javascript.js +83 -18
  56. package/src/backends/php.js +89 -16
  57. package/src/backends/ruby.js +36 -12
  58. package/src/backends/scalars.js +1 -1
  59. package/src/backends/shared.js +298 -47
  60. package/src/backends/typescript.js +83 -18
  61. package/src/backends/writer.js +41 -3
  62. package/src/frontends/java.js +95 -43
  63. package/src/frontends/javascript-typescript.js +145 -53
  64. package/src/frontends/php.js +243 -58
  65. package/src/frontends/ruby.js +117 -35
  66. package/src/frontends/types.js +26 -8
  67. package/src/language-registry.js +13 -5
  68. package/src/mapping.js +3 -1
  69. package/src/semantic/effects.js +18 -5
  70. package/src/semantic/lifetime.js +29 -3
  71. package/src/semantic/provenance.js +60 -14
  72. package/src/semantic/types.js +62 -10
  73. package/src/semantic/validate.js +481 -61
@@ -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 | import("../semantic/types.js").InterfaceDeclaration>, 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"],
@@ -357,6 +357,9 @@ function convertExpression(node, filename, source, context, expectedType, preser
357
357
  reference: referenceType(/** @type {string} */ (declaration.id), nodeLocation(node.receiver, filename, source))},
358
358
  {class: nodeLocation(node.receiver, filename, source)})
359
359
  }
360
+ if (declaration.kind == "InterfaceDeclaration") {
361
+ return unsupportedSyntax("ruby", "construction of an interface", nodeLocation(node.receiver, filename, source))
362
+ }
360
363
  if (declaration.kind == "EffectResourceDeclaration") {
361
364
  return unsupportedSyntax("ruby", "owned resource construction", nodeLocation(node.receiver, filename, source))
362
365
  }
@@ -817,7 +820,7 @@ function associatedComments(comments, node, source) {
817
820
  * @param {LocalVariableWriteNode} node - Local write.
818
821
  * @param {string} filename - Source filename.
819
822
  * @param {string} source - Complete source.
820
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
823
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} recordNames - Nominal declarations by source name.
821
824
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
822
825
  * @returns {{immutable: boolean, type: import("../semantic/types.js").SemanticValueType} | undefined} Metadata when present.
823
826
  */
@@ -969,15 +972,38 @@ function convertStatement(node, comments, context, filename, source) {
969
972
  * @param {RubyConversionContext} context - Typed lexical context.
970
973
  * @param {string} filename - Source filename.
971
974
  * @param {string} source - Complete source.
972
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
975
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
973
976
  */
974
977
  function convertRubyRaise(node, context, filename, source) {
975
978
  const location = nodeLocation(node, filename, source)
976
979
  const arguments_ = node.arguments_?.arguments_ ?? []
980
+
981
+ if (arguments_.length == 0 && context.currentCatchBindingNode) {
982
+ return withParserRanges({
983
+ binding: context.currentCatchBindingNode.name,
984
+ bindingNode: context.currentCatchBindingNode,
985
+ kind: /** @type {const} */ ("RethrowStatement"),
986
+ location
987
+ }, {keyword: prismLocation(node.messageLoc ?? node.location, filename, source)})
988
+ }
989
+ if (arguments_.length == 1 && arguments_[0] instanceof LocalVariableReadNode &&
990
+ context.catchBindingNames?.has(arguments_[0].name) && context.bindings.get(arguments_[0].name)?.kind == "ErrorType") {
991
+ const bindingNode = context.catchBindingNodes?.get(arguments_[0].name)
992
+
993
+ if (!bindingNode) return unsupportedSyntax("ruby", "raise of a catch binding without an exact handler binding", location)
994
+ return withParserRanges({
995
+ binding: arguments_[0].name,
996
+ bindingNode,
997
+ kind: /** @type {const} */ ("RethrowStatement"),
998
+ location
999
+ }, {keyword: prismLocation(node.messageLoc ?? node.location, filename, source)})
1000
+ }
1001
+ if (node.block || node.callOperatorLoc || arguments_.length != 2) {
1002
+ return unsupportedSyntax("ruby", "raise other than exact declared error and message", location)
1003
+ }
977
1004
  const errorNode = arguments_[0]
978
1005
 
979
- if (node.block || node.callOperatorLoc || arguments_.length != 2 ||
980
- !(errorNode instanceof ConstantReadNode) && !(errorNode instanceof ConstantPathNode) ||
1006
+ if (!(errorNode instanceof ConstantReadNode) && !(errorNode instanceof ConstantPathNode) ||
981
1007
  !context.errorNames.has(constantPathName(errorNode))) {
982
1008
  return unsupportedSyntax("ruby", "raise other than exact declared error and message", location)
983
1009
  }
@@ -997,48 +1023,104 @@ function convertRubyRaise(node, context, filename, source) {
997
1023
  }
998
1024
 
999
1025
  /**
1000
- * Converts one exact typed rescue with no else/ensure.
1026
+ * Converts one exact Prism ensure clause body.
1027
+ * @param {import("@ruby/prism").EnsureNode} node - Prism ensure clause.
1028
+ * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
1029
+ * @param {RubyConversionContext} context - Typed lexical context.
1030
+ * @param {string} filename - Source filename.
1031
+ * @param {string} source - Complete source.
1032
+ * @returns {import("../semantic/types.js").Block} Semantic finally block.
1033
+ */
1034
+ function convertRubyEnsure(node, comments, context, filename, source) {
1035
+ // A completion block gets its own binding scope, like every other block in convertRubyTry: a local
1036
+ // declaration inside `ensure` must not leak back into the enclosing conversion context.
1037
+ return convertBlock(node.statements ?? null, comments, {...context, bindings: new Map(context.bindings)}, filename, source,
1038
+ nodeLocation(node, filename, source))
1039
+ }
1040
+
1041
+ /**
1042
+ * Converts one begin block with ordered exact typed rescues and an optional ensure completion block.
1001
1043
  * @param {BeginNode} node - Prism begin node.
1002
1044
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
1003
1045
  * @param {RubyConversionContext} context - Typed lexical context.
1004
1046
  * @param {string} filename - Source filename.
1005
1047
  * @param {string} source - Complete source.
1006
- * @returns {import("../semantic/types.js").TryStatement} Semantic exact handler.
1048
+ * @returns {import("../semantic/types.js").TryStatement} Semantic exact handler chain.
1007
1049
  */
1008
1050
  function convertRubyTry(node, comments, context, filename, source) {
1009
1051
  const location = nodeLocation(node, filename, source)
1010
- const handler = node.rescueClause
1011
1052
 
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)
1053
+ if (node.elseClause) return unsupportedSyntax("ruby", "begin else clause", nodeLocation(node.elseClause, filename, source))
1054
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1055
+ const handlers = []
1056
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1057
+ let handler = node.rescueClause
1058
+
1059
+ if (!(handler instanceof RescueNode)) {
1060
+ if (node.ensureClause) {
1061
+ return withParserRanges({
1062
+ body: convertBlock(node.statements, comments, context, filename, source, location),
1063
+ handlers,
1064
+ kind: /** @type {const} */ ("TryStatement"),
1065
+ location,
1066
+ finallyBody: convertRubyEnsure(node.ensureClause, comments, context, filename, source)
1067
+ }, {ensure: nodeLocation(node.ensureClause, filename, source), try: location})
1068
+ }
1069
+ return unsupportedSyntax("ruby", "begin without rescue or ensure", location)
1017
1070
  }
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
1071
  const bodyContext = {...context, bindings: new Map(context.bindings)}
1025
- const catchContext = {...context, bindings: new Map(context.bindings)}
1072
+ while (handler instanceof RescueNode) {
1073
+ if (handler.exceptions.length != 1 ||
1074
+ !(handler.exceptions[0] instanceof ConstantReadNode) && !(handler.exceptions[0] instanceof ConstantPathNode) ||
1075
+ !context.errorNames.has(constantPathName(handler.exceptions[0])) || !(handler.reference instanceof LocalVariableTargetNode)) {
1076
+ return unsupportedSyntax("ruby", "rescue without one exact typed exception and binding", nodeLocation(handler, filename, source))
1077
+ }
1078
+ if (seenHandlerNames.has(handler.reference.name)) {
1079
+ return unsupportedSyntax("ruby", `rescue binding '${handler.reference.name}' shadows an outer handler binding`,
1080
+ nodeLocation(handler.reference, filename, source))
1081
+ }
1082
+ seenHandlerNames.add(handler.reference.name)
1083
+ const errorNode = handler.exceptions[0]
1084
+ const declaration = /** @type {import("../semantic/types.js").ErrorDeclaration} */ (context.errorNames.get(constantPathName(errorNode)))
1085
+ const caughtLocation = nodeLocation(errorNode, filename, source)
1086
+ const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1087
+ type: caughtLocation
1088
+ })
1089
+ const catchContext = {...context, bindings: new Map(context.bindings), catchBindingNames: new Set(seenHandlerNames),
1090
+ catchBindingNodes: new Map(context.catchBindingNodes)}
1026
1091
 
1027
- catchContext.bindings.set(handler.reference.name, catchType)
1028
- return withParserRanges({
1029
- body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
1030
- catchBinding: withParserRanges({
1092
+ catchContext.bindings.set(handler.reference.name, catchType)
1093
+ const catchBindingNode = withParserRanges({
1031
1094
  kind: /** @type {const} */ ("CatchBinding"),
1032
1095
  location: nodeLocation(handler.reference, filename, source),
1033
1096
  mutable: /** @type {const} */ (false),
1034
1097
  name: handler.reference.name,
1035
1098
  type: catchType
1036
- }, {name: nodeLocation(handler.reference, filename, source)}),
1037
- catchBody: convertBlock(handler.statements, comments, catchContext, filename, source, nodeLocation(handler, filename, source)),
1038
- catchType,
1099
+ }, {name: nodeLocation(handler.reference, filename, source)})
1100
+
1101
+ catchContext.catchBindingNodes.set(handler.reference.name, catchBindingNode)
1102
+ catchContext.currentCatchBindingNode = catchBindingNode
1103
+ handlers.push(withParserRanges({
1104
+ catchBinding: catchBindingNode,
1105
+ catchBody: convertBlock(handler.statements, comments, catchContext, filename, source, nodeLocation(handler, filename, source)),
1106
+ catchType,
1107
+ kind: /** @type {const} */ ("CatchHandler"),
1108
+ location: nodeLocation(handler, filename, source)
1109
+ }, {catch: nodeLocation(handler, filename, source)}))
1110
+ handler = handler.subsequent
1111
+ }
1112
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1113
+ body: convertBlock(node.statements, comments, bodyContext, filename, source, location),
1114
+ handlers,
1039
1115
  kind: /** @type {const} */ ("TryStatement"),
1040
1116
  location
1041
- }, {catch: nodeLocation(handler, filename, source), try: location})
1117
+ }, {try: location}))
1118
+
1119
+ if (node.ensureClause) {
1120
+ semantic.finallyBody = convertRubyEnsure(node.ensureClause, comments, context, filename, source)
1121
+ setParserRanges(semantic, {ensure: nodeLocation(node.ensureClause, filename, source), try: location})
1122
+ }
1123
+ return semantic
1042
1124
  }
1043
1125
 
1044
1126
  /**
@@ -1222,7 +1304,7 @@ function isImmediateCommentGap(gap) {
1222
1304
  * @param {import("../semantic/types.js").SourceLocation} location - Source location.
1223
1305
  * @param {string} source - Complete parser input.
1224
1306
  * @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type comment token location.
1225
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
1307
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} [recordNames] - Nominal declarations by source name.
1226
1308
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
1227
1309
  * @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
1228
1310
  */
@@ -1236,7 +1318,7 @@ function convertType(sourceType, subject, location, source, typeLocation = locat
1236
1318
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
1237
1319
  * @param {string} filename - Source filename.
1238
1320
  * @param {string} source - Complete source.
1239
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1321
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} recordNames - Nominal declarations by source name.
1240
1322
  * @param {string} ownerId - Stable function identity.
1241
1323
  * @param {boolean} [allowBuiltinOutput] - Whether compiler authority admits the exact output facade function.
1242
1324
  * @returns {RubyFunctionSignature} Semantic signature.
@@ -1301,7 +1383,7 @@ function convertFunctionSignature(node, comments, filename, source, recordNames,
1301
1383
  * @param {DefNode} node - Prism definition.
1302
1384
  * @param {RubyFunctionSignature} signature - Converted signature.
1303
1385
  * @param {Map<string, RubyFunctionSignature>} functions - Module signatures.
1304
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1386
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} recordNames - Nominal declarations by source name.
1305
1387
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
1306
1388
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
1307
1389
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
@@ -1412,7 +1494,7 @@ function convertRubyError(node, declaration, filename, source) {
1412
1494
  * Converts the bounded Ruby reference-class profile.
1413
1495
  * @param {ClassNode} node - Prism class declaration.
1414
1496
  * @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
1415
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
1497
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} nominalNames - Visible nominal declarations.
1416
1498
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
1417
1499
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
1418
1500
  * @param {Map<string, RubyFunctionSignature>} functions - Top-level signatures.
@@ -1528,7 +1610,7 @@ function convertRubyReferenceClass(node, declaration, nominalNames, records, cla
1528
1610
  * Converts one exact typed-reader/initializer/freeze Ruby record profile.
1529
1611
  * @param {ClassNode} node - Prism class declaration.
1530
1612
  * @param {import("../semantic/types.js").RecordDeclaration} declaration - Predeclared nominal identity.
1531
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by name.
1613
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} recordNames - Nominal declarations by name.
1532
1614
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
1533
1615
  * @param {string} filename - Source filename.
1534
1616
  * @param {string} source - Complete source.
@@ -1815,7 +1897,7 @@ export function parseRuby({capabilities = [], filename, source, program}) {
1815
1897
  classDeclarations.findIndex((candidate) => candidate.name == declaration.name) != index)
1816
1898
 
1817
1899
  if (reopened) return unsupportedSyntax("ruby", `reference class reopening '${reopened.name}'`, reopened.location)
1818
- const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} */ (
1900
+ const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} */ (
1819
1901
  new Map(program?.records ?? []))
1820
1902
  for (const [name, declaration] of program?.classes ?? []) recordNames.set(name, declaration)
1821
1903
  for (const resource of capabilities.flatMap(({resources}) => resources)) recordNames.set(resource.name, resource)
@@ -55,6 +55,7 @@ export function iterationBindingType(type, location) {
55
55
  if (type.kind == "RecordType") return recordType(type.declarationId, location,
56
56
  type.arguments?.map((argument) => iterationBindingType(argument, location)))
57
57
  if (type.kind == "ReferenceType") return referenceType(type.declarationId, location)
58
+ if (type.kind == "InterfaceType") return interfaceType(type.declarationId, location)
58
59
  if (type.kind == "OwnedResourceType") return ownedResourceType(type.resourceId, location)
59
60
  if (type.kind == "OwnedReferenceType") return ownedReferenceType(type.declarationId, location)
60
61
 
@@ -166,6 +167,19 @@ export function referenceType(declarationId, location) {
166
167
  return type
167
168
  }
168
169
 
170
+ /**
171
+ * Builds one nominal interface type with a parser-owned type range.
172
+ * @param {string} declarationId - Stable interface declaration identity.
173
+ * @param {import("../semantic/types.js").SourceLocation} location - Complete type-expression range.
174
+ * @returns {import("../semantic/types.js").InterfaceType} Interface type.
175
+ */
176
+ export function interfaceType(declarationId, location) {
177
+ const type = {declarationId, kind: /** @type {const} */ ("InterfaceType")}
178
+
179
+ setParserRanges(type, {type: location})
180
+ return type
181
+ }
182
+
169
183
  /**
170
184
  * Builds one compiler-owned resource type with a parser-owned name range.
171
185
  * @param {string} resourceId - Stable capability resource identity.
@@ -392,7 +406,7 @@ function collectKnownSubstitutions(formal, actual, substitutions) {
392
406
  * @param {"javascript" | "php" | "ruby"} input.language - Comment profile.
393
407
  * @param {import("../semantic/types.js").SourceLocation} input.location - Exact type token range.
394
408
  * @param {import("../semantic/types.js").SourceLocation} input.ownerLocation - Owning declaration range.
395
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [input.records] - Available nominal declarations by source name.
409
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} [input.records] - Available nominal declarations by source name.
396
410
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} [input.typeParameters] - Declaration-scoped parameters by name.
397
411
  * @param {string} input.source - Complete parser input for exact subranges.
398
412
  * @param {string | undefined} input.sourceType - Exact comment-parser/Prism-owned type token.
@@ -451,9 +465,11 @@ export function documentedValueType({language, location, ownerLocation, records
451
465
 
452
466
  if (directRecord?.id) return directRecord.kind == "ClassDeclaration"
453
467
  ? referenceType(directRecord.id, location)
454
- : directRecord.kind == "EffectResourceDeclaration"
455
- ? ownedResourceType(directRecord.id, location)
456
- : recordType(directRecord.id, location)
468
+ : directRecord.kind == "InterfaceDeclaration"
469
+ ? interfaceType(directRecord.id, location)
470
+ : directRecord.kind == "EffectResourceDeclaration"
471
+ ? ownedResourceType(directRecord.id, location)
472
+ : recordType(directRecord.id, location)
457
473
  const directTypeParameter = typeParameters.get(unwrappedName)
458
474
 
459
475
  if (directTypeParameter?.id) return typeVariable(directTypeParameter.id, location)
@@ -484,7 +500,7 @@ class DocumentedTypeParser {
484
500
  * @param {string} text - Parser-owned type text.
485
501
  * @param {import("../semantic/types.js").SourceLocation} location - Whole token location.
486
502
  * @param {string} source - Complete source.
487
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} records - Available nominal declarations.
503
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} records - Available nominal declarations.
488
504
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} typeParameters - Declaration-scoped parameters.
489
505
  */
490
506
  constructor(language, text, location, source, records, typeParameters) {
@@ -580,9 +596,11 @@ class DocumentedTypeParser {
580
596
  if (record?.id && this.text[this.offset] != "<" && this.text[this.offset] != "[") {
581
597
  const type = record.kind == "ClassDeclaration"
582
598
  ? referenceType(record.id, this.range(start, nameEnd))
583
- : record.kind == "EffectResourceDeclaration"
584
- ? ownedResourceType(record.id, this.range(start, nameEnd))
585
- : recordType(record.id, this.range(start, nameEnd))
599
+ : record.kind == "InterfaceDeclaration"
600
+ ? interfaceType(record.id, this.range(start, nameEnd))
601
+ : record.kind == "EffectResourceDeclaration"
602
+ ? ownedResourceType(record.id, this.range(start, nameEnd))
603
+ : recordType(record.id, this.range(start, nameEnd))
586
604
 
587
605
  if (this.language == "ruby" && this.consume("?")) {
588
606
  return optionalType(type, this.range(start, this.offset), this.typeRange(type, start, nameEnd))
@@ -169,6 +169,7 @@ export function createLanguageRegistry(candidateRecords) {
169
169
  }
170
170
  if (typeof candidate.roundTrip != "boolean") invalidRegistry(`Registry record '${id}' requires a Boolean round-trip declaration.`, id)
171
171
  const featuresCandidate = candidate.features ?? {
172
+ boundedSubtyping: false,
172
173
  closedRecords: false,
173
174
  conditionControlledLoops: false,
174
175
  conditionalExpressions: false,
@@ -185,7 +186,8 @@ export function createLanguageRegistry(candidateRecords) {
185
186
  }
186
187
 
187
188
  if (!isPlainObject(featuresCandidate) ||
188
- Object.keys(featuresCandidate).sort().join(",") != "closedRecords,conditionControlledLoops,conditionalExpressions,effectfulCapabilitiesAndResources,generalFunctionsAndCalls,immutableCollections,optionalValues,orderedListIteration,orderedMapIteration,referenceClasses,stringInterpolation,typeParametersAndGenerics,typedErrors" ||
189
+ Object.keys(featuresCandidate).sort().join(",") != "boundedSubtyping,closedRecords,conditionControlledLoops,conditionalExpressions,effectfulCapabilitiesAndResources,generalFunctionsAndCalls,immutableCollections,optionalValues,orderedListIteration,orderedMapIteration,referenceClasses,stringInterpolation,typeParametersAndGenerics,typedErrors" ||
190
+ typeof featuresCandidate.boundedSubtyping != "boolean" ||
189
191
  typeof featuresCandidate.closedRecords != "boolean" ||
190
192
  typeof featuresCandidate.conditionControlledLoops != "boolean" ||
191
193
  typeof featuresCandidate.conditionalExpressions != "boolean" ||
@@ -196,12 +198,14 @@ export function createLanguageRegistry(candidateRecords) {
196
198
  typeof featuresCandidate.orderedListIteration != "boolean" ||
197
199
  typeof featuresCandidate.orderedMapIteration != "boolean" ||
198
200
  typeof featuresCandidate.referenceClasses != "boolean" ||
201
+ (featuresCandidate.boundedSubtyping && !featuresCandidate.referenceClasses) ||
199
202
  typeof featuresCandidate.stringInterpolation != "boolean" ||
200
203
  typeof featuresCandidate.typedErrors != "boolean" ||
201
204
  typeof featuresCandidate.typeParametersAndGenerics != "boolean") {
202
205
  invalidRegistry(`Registry record '${id}' has an invalid feature declaration.`, id)
203
206
  }
204
207
  const features = deepFreeze(/** @type {import("./semantic/types.js").LanguageFeatureCapabilities} */ ({
208
+ boundedSubtyping: featuresCandidate.boundedSubtyping,
205
209
  closedRecords: featuresCandidate.closedRecords,
206
210
  conditionControlledLoops: featuresCandidate.conditionControlledLoops,
207
211
  conditionalExpressions: featuresCandidate.conditionalExpressions,
@@ -582,6 +586,7 @@ const records = [
582
586
  optionalValues: false,
583
587
  orderedListIteration: false,
584
588
  orderedMapIteration: false,
589
+ boundedSubtyping: false,
585
590
  referenceClasses: false,
586
591
  typedErrors: false,
587
592
  typeParametersAndGenerics: false
@@ -621,6 +626,7 @@ const records = [
621
626
  optionalValues: false,
622
627
  orderedListIteration: false,
623
628
  orderedMapIteration: false,
629
+ boundedSubtyping: false,
624
630
  referenceClasses: false,
625
631
  typedErrors: false,
626
632
  typeParametersAndGenerics: false
@@ -646,6 +652,7 @@ const records = [
646
652
  optionalValues: false,
647
653
  orderedListIteration: false,
648
654
  orderedMapIteration: false,
655
+ boundedSubtyping: false,
649
656
  referenceClasses: false,
650
657
  typedErrors: false,
651
658
  typeParametersAndGenerics: false
@@ -661,7 +668,7 @@ const records = [
661
668
  artifactMultiplicity: "multiple",
662
669
  binaryBackend: generateBrowserWasm,
663
670
  defaultFilename: "program.wasm",
664
- features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
671
+ features: {boundedSubtyping: false, closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
665
672
  id: "wasm",
666
673
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
667
674
  mediaType: "application/wasm",
@@ -671,7 +678,7 @@ const records = [
671
678
  acceptance: {stages: ["generate", "compile", "validate", "instantiate", "execute"], toolchains: ["android"]},
672
679
  applicationBackend: generateAndroidApplication,
673
680
  artifactMultiplicity: "multiple",
674
- features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
681
+ features: {boundedSubtyping: false, closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
675
682
  id: "android",
676
683
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
677
684
  roundTrip: false
@@ -680,7 +687,7 @@ const records = [
680
687
  acceptance: {stages: ["generate"], toolchains: []},
681
688
  applicationBackend: generateIosApplication,
682
689
  artifactMultiplicity: "multiple",
683
- features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
690
+ features: {boundedSubtyping: false, closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
684
691
  id: "ios",
685
692
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
686
693
  roundTrip: false
@@ -689,7 +696,7 @@ const records = [
689
696
  acceptance: {stages: ["generate", "restore", "compile", "validate", "instantiate", "execute"], toolchains: ["flutter", "android"]},
690
697
  applicationBackend: generateFlutterApplication,
691
698
  artifactMultiplicity: "multiple",
692
- features: {closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
699
+ features: {boundedSubtyping: false, closedRecords: false, conditionControlledLoops: false, conditionalExpressions: false, stringInterpolation: false, effectfulCapabilitiesAndResources: false, generalFunctionsAndCalls: false, immutableCollections: false, optionalValues: false, orderedListIteration: false, orderedMapIteration: false, referenceClasses: false, typedErrors: false, typeParametersAndGenerics: false},
693
700
  id: "flutter",
694
701
  mapping: {binaryRanges: true, richText: true, sourceMapV3: true},
695
702
  roundTrip: false
@@ -712,6 +719,7 @@ function language(values) {
712
719
  return {
713
720
  artifactMultiplicity: "single",
714
721
  features: {
722
+ boundedSubtyping: String(values.id) == "php",
715
723
  closedRecords: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
716
724
  conditionControlledLoops: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
717
725
  conditionalExpressions: ["php", "ruby", "javascript", "typescript", "java"].includes(String(values.id)),
package/src/mapping.js CHANGED
@@ -764,7 +764,7 @@ export function validateMapping(value) {
764
764
  for (const symbol of mapping.symbols) {
765
765
  if (!symbol || typeof symbol != "object" || typeof symbol.id != "string" || symbol.id.length == 0 || symbolIds.has(symbol.id) ||
766
766
  typeof symbol.name != "string" || symbol.name.length == 0 ||
767
- !["capability", "resource", "failure", "operation", "record", "class", "error", "field", "constructor", "method", "function", "typeParameter", "parameter", "local", "iteration", "catch"].includes(symbol.kind) ||
767
+ !["capability", "resource", "failure", "operation", "record", "interface", "interfaceMethod", "class", "error", "field", "constructor", "method", "function", "typeParameter", "parameter", "local", "iteration", "catch"].includes(symbol.kind) ||
768
768
  !nodeIds.has(symbol.declarationNodeId) || !Array.isArray(symbol.references) ||
769
769
  symbol.semanticDeclarationId !== undefined && (typeof symbol.semanticDeclarationId != "string" || !(
770
770
  symbol.kind == "function" && /^(?:[a-z][a-z0-9._-]*#)?function:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
@@ -774,6 +774,8 @@ export function validateMapping(value) {
774
774
  symbol.kind == "field" && /^(?:[a-z][a-z0-9._-]*#)?(?:record|class):[0-9]+:field:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
775
775
  symbol.kind == "constructor" && /^(?:[a-z][a-z0-9._-]*#)?class:[0-9]+:constructor$/u.test(symbol.semanticDeclarationId) ||
776
776
  symbol.kind == "method" && /^(?:[a-z][a-z0-9._-]*#)?class:[0-9]+:method:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
777
+ symbol.kind == "interface" && /^(?:[a-z][a-z0-9._-]*#)?interface:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
778
+ symbol.kind == "interfaceMethod" && /^(?:[a-z][a-z0-9._-]*#)?interface:[0-9]+:method:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
777
779
  symbol.kind == "typeParameter" && /^(?:[a-z][a-z0-9._-]*#)?(?:function|record):[0-9]+:type:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
778
780
  symbol.kind == "capability" && /^(?:module:[0-9]+\/)?capability:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
779
781
  symbol.kind == "resource" && /^(?:module:[0-9]+\/)?capability:[0-9]+\/resource:[0-9]+$/u.test(symbol.semanticDeclarationId) ||
@@ -326,6 +326,7 @@ function typeIdentity(type) {
326
326
  if (type.kind == "RecordType") return {arguments: type.arguments?.map((argument) =>
327
327
  /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(argument))), declarationId: type.declarationId, kind: type.kind}
328
328
  if (type.kind == "ReferenceType") return {declarationId: type.declarationId, kind: type.kind}
329
+ if (type.kind == "InterfaceType") return {declarationId: type.declarationId, kind: type.kind}
329
330
  return {kind: type.kind, parameterId: type.parameterId}
330
331
  }
331
332
 
@@ -379,10 +380,19 @@ function escapingBlockFailures(block, functionSummaries, methodSummaries, capabi
379
380
 
380
381
  for (const statement of block.statements) {
381
382
  if (statement.kind == "TryStatement") {
382
- const body = escapingBlockFailures(statement.body, functionSummaries, methodSummaries, capabilityFailureIds)
383
+ let body = escapingBlockFailures(statement.body, functionSummaries, methodSummaries, capabilityFailureIds)
383
384
 
384
- if (body.delete(statement.catchType.declarationId)) {
385
- addFailures(result, escapingBlockFailures(statement.catchBody, functionSummaries, methodSummaries, capabilityFailureIds))
385
+ for (const handler of statement.handlers ?? []) {
386
+ const caughtId = handler.catchType?.declarationId
387
+
388
+ if (typeof caughtId == "string" && body.delete(caughtId)) {
389
+ addFailures(body, escapingBlockFailures(handler.catchBody, functionSummaries, methodSummaries, capabilityFailureIds))
390
+ }
391
+ }
392
+ if (statement.finallyBody) {
393
+ // A completion block that can raise replaces the pending completion on the paths where it does,
394
+ // but pending failures still escape on the paths where it completes normally, so both are reachable.
395
+ addFailures(body, escapingBlockFailures(statement.finallyBody, functionSummaries, methodSummaries, capabilityFailureIds))
386
396
  }
387
397
  addFailures(result, body)
388
398
  continue
@@ -639,7 +649,9 @@ function forEachBlockExpression(block, visitor, postorder) {
639
649
  visit(statement.condition); forEachBlockExpression(statement.body, visitor, postorder)
640
650
  } else if (statement.kind == "RaiseStatement") visit(statement.error.message)
641
651
  else if (statement.kind == "TryStatement") {
642
- forEachBlockExpression(statement.body, visitor, postorder); forEachBlockExpression(statement.catchBody, visitor, postorder)
652
+ forEachBlockExpression(statement.body, visitor, postorder)
653
+ for (const handler of statement.handlers) forEachBlockExpression(handler.catchBody, visitor, postorder)
654
+ if (statement.finallyBody) forEachBlockExpression(statement.finallyBody, visitor, postorder)
643
655
  }
644
656
  }
645
657
 
@@ -694,7 +706,8 @@ function statementReferencesProtectedEntry(statement) {
694
706
  case "RaiseStatement":
695
707
  return expressionReferencesProtectedEntry(statement.error.message)
696
708
  case "TryStatement":
697
- return blockReferencesProtectedEntry(statement.body) || blockReferencesProtectedEntry(statement.catchBody)
709
+ return blockReferencesProtectedEntry(statement.body) || statement.handlers.some((handler) => blockReferencesProtectedEntry(handler.catchBody)) ||
710
+ (statement.finallyBody ? blockReferencesProtectedEntry(statement.finallyBody) : false)
698
711
  default:
699
712
  return false
700
713
  }
@@ -123,6 +123,10 @@ function analyzeStatement(statement, state, fail, locals, errorEffects) {
123
123
  ? {errorId: statement.error.error.declarationId, kind: "raise", location: statement.location, state: flow.state}
124
124
  : {...flow, location: statement.location})
125
125
  }
126
+ if (statement.kind == "RethrowStatement") {
127
+ // A rethrow re-raises the caught binding's exact nominal type, so an enclosing handler must see it.
128
+ return [{errorId: statement.bindingNode?.type?.declarationId, kind: "raise", location: statement.location, state: cloneState(state)}]
129
+ }
126
130
  if (statement.kind == "IfStatement") {
127
131
  const conditions = evaluateExpression(statement.condition, cloneState(state), fail, errorEffects)
128
132
  /** @type {LifetimeExit[]} */
@@ -142,10 +146,32 @@ function analyzeStatement(statement, state, fail, locals, errorEffects) {
142
146
  if (statement.kind == "TryStatement") {
143
147
  const bodyExits = analyzeBlock(statement.body, [{kind: "normal", state: cloneState(state)}], fail, errorEffects)
144
148
  /** @type {LifetimeExit[]} */
145
- const result = bodyExits.filter((flow) => flow.kind != "raise" || flow.errorId != statement.catchType.declarationId)
149
+ let result = bodyExits
150
+
151
+ for (const handler of statement.handlers ?? []) {
152
+ const caughtId = handler.catchType?.declarationId
153
+ const caught = result.filter((flow) => flow.kind == "raise" && flow.errorId == caughtId)
154
+
155
+ if (caught.length == 0) continue
156
+ result = result.filter((flow) => flow.kind != "raise" || flow.errorId != caughtId)
157
+ for (const flow of caught) {
158
+ result.push(...analyzeBlock(handler.catchBody, [{kind: "normal", state: cloneState(flow.state)}], fail, errorEffects))
159
+ }
160
+ }
161
+ if (statement.finallyBody) {
162
+ /** @type {LifetimeExit[]} */
163
+ const afterFinally = []
164
+
165
+ for (const flow of result) {
166
+ const finallyExits = analyzeBlock(statement.finallyBody, [{kind: "normal", state: cloneState(flow.state)}], fail, errorEffects)
146
167
 
147
- for (const caught of bodyExits.filter((flow) => flow.kind == "raise" && flow.errorId == statement.catchType.declarationId)) {
148
- result.push(...analyzeBlock(statement.catchBody, [{kind: "normal", state: cloneState(caught.state)}], fail, errorEffects))
168
+ for (const finallyExit of finallyExits) {
169
+ afterFinally.push(finallyExit.kind == "normal"
170
+ ? {...flow, location: statement.location}
171
+ : {...finallyExit, location: statement.location})
172
+ }
173
+ }
174
+ result = afterFinally
149
175
  }
150
176
  return mergeEquivalentNormals(result, statement.location, fail)
151
177
  }