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
@@ -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
  /**
@@ -571,6 +571,21 @@ const records = [
571
571
  artifactMultiplicity: "multiple",
572
572
  check: {factory: createCCheckPlan, stages: ["compile", "link"], toolchains: ["clang"]},
573
573
  defaultFilename: "program.c",
574
+ features: {
575
+ closedRecords: false,
576
+ conditionControlledLoops: false,
577
+ conditionalExpressions: false,
578
+ stringInterpolation: false,
579
+ effectfulCapabilitiesAndResources: false,
580
+ generalFunctionsAndCalls: true,
581
+ immutableCollections: false,
582
+ optionalValues: false,
583
+ orderedListIteration: false,
584
+ orderedMapIteration: false,
585
+ referenceClasses: false,
586
+ typedErrors: false,
587
+ typeParametersAndGenerics: false
588
+ },
574
589
  frontend: parseC,
575
590
  id: "c",
576
591
  mediaType: "text/x-c",
@@ -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
  }
@@ -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
  }
@@ -580,8 +580,14 @@ export function semanticEntries(module) {
580
580
  visit(node.body, `${path}/body`, location)
581
581
  } else if (node.kind == "RaiseStatement") {
582
582
  visit(node.error, `${path}/error`, location)
583
+ } else if (node.kind == "RethrowStatement") {
584
+ visit(node.bindingNode, `${path}/bindingNode`, location)
585
+ visit(node.bindingNode, `${path}/bindingNode/name`, location)
583
586
  } else if (node.kind == "TryStatement") {
584
587
  visit(node.body, `${path}/body`, location)
588
+ node.handlers.forEach((child, index) => visit(child, `${path}/handlers/${index}`, location))
589
+ if (node.finallyBody) visit(node.finallyBody, `${path}/finallyBody`, location)
590
+ } else if (node.kind == "CatchHandler") {
585
591
  visit(node.catchType, `${path}/catchType`, location)
586
592
  visit(node.catchBinding, `${path}/catchBinding`, location)
587
593
  visit(node.catchBody, `${path}/catchBody`, location)
@@ -895,19 +901,28 @@ function resolveSymbols(module, records) {
895
901
  visitType(statement.error.error, `${statementPath}/error/error`)
896
902
  reference(statement.error, errorSymbols.get(statement.error.error.declarationId), "construct", `${statementPath}/error`)
897
903
  visitExpression(statement.error.message, scope, `${statementPath}/error/message`)
904
+ } else if (statement.kind == "RethrowStatement") {
905
+ const symbol = scope.get(statement.binding)
906
+
907
+ if (symbol) reference(statement, symbol, "read", statementPath)
898
908
  } else if (statement.kind == "TryStatement") {
899
909
  visitBlock(statement.body, scope, `${statementPath}/body`)
900
- visitType(statement.catchType, `${statementPath}/catchType`)
901
- visitType(statement.catchBinding.type, `${statementPath}/catchBinding/type`)
902
- const catchScope = new Map(scope)
903
-
904
- catchScope.set(statement.catchBinding.name, declare(
905
- statement.catchBinding,
906
- statement.catchBinding.name,
907
- "catch",
908
- `${statementPath}/catchBinding`
909
- ))
910
- visitBlock(statement.catchBody, catchScope, `${statementPath}/catchBody`)
910
+ for (const [index, handler] of statement.handlers.entries()) {
911
+ const handlerPath = `${statementPath}/handlers/${index}`
912
+
913
+ visitType(handler.catchType, `${handlerPath}/catchType`)
914
+ visitType(handler.catchBinding.type, `${handlerPath}/catchBinding/type`)
915
+ const catchScope = new Map(scope)
916
+
917
+ catchScope.set(handler.catchBinding.name, declare(
918
+ handler.catchBinding,
919
+ handler.catchBinding.name,
920
+ "catch",
921
+ `${handlerPath}/catchBinding`
922
+ ))
923
+ visitBlock(handler.catchBody, catchScope, `${handlerPath}/catchBody`)
924
+ }
925
+ if (statement.finallyBody) visitBlock(statement.finallyBody, scope, `${statementPath}/finallyBody`)
911
926
  }
912
927
  }
913
928
  }
@@ -1005,7 +1020,7 @@ function resolveSymbols(module, records) {
1005
1020
 
1006
1021
  /**
1007
1022
  * Attaches one resolved symbol reference.
1008
- * @param {import("./types.js").IdentifierExpression | import("./types.js").ReceiverExpression | import("./types.js").CallExpression | import("./types.js").EffectCallExpression | import("./types.js").MethodCallExpression | import("./types.js").TypeVariableReference | import("./types.js").RecordType | import("./types.js").ReferenceType | import("./types.js").OwnedReferenceType | import("./types.js").OwnedResourceType | import("./types.js").RecordConstruction | import("./types.js").ReferenceConstruction | import("./types.js").MemberRead | import("./types.js").PrivateFieldRead | import("./types.js").PrivateFieldWriteStatement | import("./types.js").ErrorType | import("./types.js").ErrorConstruction} node - Reference node.
1023
+ * @param {import("./types.js").IdentifierExpression | import("./types.js").ReceiverExpression | import("./types.js").CallExpression | import("./types.js").EffectCallExpression | import("./types.js").MethodCallExpression | import("./types.js").TypeVariableReference | import("./types.js").RecordType | import("./types.js").ReferenceType | import("./types.js").OwnedReferenceType | import("./types.js").OwnedResourceType | import("./types.js").RecordConstruction | import("./types.js").ReferenceConstruction | import("./types.js").MemberRead | import("./types.js").PrivateFieldRead | import("./types.js").PrivateFieldWriteStatement | import("./types.js").RethrowStatement | import("./types.js").ErrorType | import("./types.js").ErrorConstruction} node - Reference node.
1009
1024
  * @param {string | undefined} symbolId - Resolved symbol.
1010
1025
  * @param {"type" | "construct" | "member" | "read" | "write" | "call"} role - Reference role.
1011
1026
  * @param {string} path - Reference occurrence path.
@@ -1229,6 +1229,15 @@
1229
1229
  * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned raise range.
1230
1230
  */
1231
1231
 
1232
+ /**
1233
+ * @typedef RethrowStatement
1234
+ * @property {"RethrowStatement"} kind - Re-propagation of the exact caught binding in scope.
1235
+ * @property {string} binding - Name of the lexically visible immutable catch binding being re-raised.
1236
+ * @property {CatchBinding} bindingNode - Exact catch binding node being re-raised.
1237
+ * @property {SourceLocation} location - Complete rethrow source location.
1238
+ * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned rethrow range.
1239
+ */
1240
+
1232
1241
  /**
1233
1242
  * @typedef CatchBinding
1234
1243
  * @property {"CatchBinding"} kind - Exact-handler binding discriminator.
@@ -1240,18 +1249,27 @@
1240
1249
  */
1241
1250
 
1242
1251
  /**
1243
- * @typedef TryStatement
1244
- * @property {"TryStatement"} kind - One body and one exact typed handler.
1245
- * @property {Block} body - Protected body.
1252
+ * @typedef CatchHandler
1253
+ * @property {"CatchHandler"} kind - One exact nominal handler within an ordered handler list.
1246
1254
  * @property {ErrorType} catchType - Exact caught nominal error type.
1247
1255
  * @property {CatchBinding} catchBinding - Immutable handler-local binding.
1248
1256
  * @property {Block} catchBody - Handler body.
1257
+ * @property {SourceLocation} location - Complete handler source location.
1258
+ * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned catch ranges.
1259
+ */
1260
+
1261
+ /**
1262
+ * @typedef TryStatement
1263
+ * @property {"TryStatement"} kind - One body, an ordered list of exact typed handlers, and an optional completion block.
1264
+ * @property {Block} body - Protected body.
1265
+ * @property {CatchHandler[]} handlers - Ordered exact handlers, evaluated top to bottom with exact nominal matching.
1266
+ * @property {Block} [finallyBody] - Completion block executed on every normal and abrupt completion of the try.
1249
1267
  * @property {SourceLocation} location - Complete try/handler source location.
1250
- * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned try/catch ranges.
1268
+ * @property {SemanticNodeSourceProvenance} [sourceProvenance] - Parser-owned try/catch/finally ranges.
1251
1269
  */
1252
1270
 
1253
1271
  /** @typedef {LocalDeclaration | AssignmentStatement} LocalStatement */
1254
- /** @typedef {LocalStatement | PrivateFieldWriteStatement | ExpressionStatement | IfStatement | ForEachStatement | ForEachMapStatement | WhileStatement | BreakStatement | ContinueStatement | ReturnStatement | PrintStatement | RaiseStatement | TryStatement} Statement */
1272
+ /** @typedef {LocalStatement | PrivateFieldWriteStatement | ExpressionStatement | IfStatement | ForEachStatement | ForEachMapStatement | WhileStatement | BreakStatement | ContinueStatement | ReturnStatement | PrintStatement | RaiseStatement | RethrowStatement | TryStatement} Statement */
1255
1273
 
1256
1274
  /**
1257
1275
  * @typedef Block
@@ -1468,7 +1486,7 @@
1468
1486
  * @property {StdlibFacadeProgramDescriptor} [stdlibFacades] - Complete selected source-language facade closure.
1469
1487
  */
1470
1488
 
1471
- /** @typedef {SemanticModule | SemanticProgramModule | SemanticImport | SemanticExport | RecordDeclaration | ClassDeclaration | ErrorDeclaration | EffectCapabilityDeclaration | EffectResourceDeclaration | EffectFailureDeclaration | EffectOperationDeclaration | RecordField | PrivateField | ConstructorDeclaration | MethodDeclaration | FunctionDeclaration | TypeParameter | Parameter | ValueBinding | CatchBinding | Block | Statement | EntryPoint | Expression | ErrorConstruction | ErrorType | MapEntry | SemanticValueType | FunctionReturnTypeReference} SemanticNode */
1489
+ /** @typedef {SemanticModule | SemanticProgramModule | SemanticImport | SemanticExport | RecordDeclaration | ClassDeclaration | ErrorDeclaration | EffectCapabilityDeclaration | EffectResourceDeclaration | EffectFailureDeclaration | EffectOperationDeclaration | RecordField | PrivateField | ConstructorDeclaration | MethodDeclaration | FunctionDeclaration | TypeParameter | Parameter | ValueBinding | CatchBinding | CatchHandler | Block | Statement | EntryPoint | Expression | ErrorConstruction | ErrorType | MapEntry | SemanticValueType | FunctionReturnTypeReference} SemanticNode */
1472
1490
  /** @typedef {SemanticNode} SemanticNodeWithoutLocations */
1473
1491
 
1474
1492
  export {}
@@ -8,7 +8,7 @@ import {parserRangeFor} from "./provenance.js"
8
8
  import {validateEffectGraph} from "./effects.js"
9
9
  import {validateResourceLifetimes} from "./lifetime.js"
10
10
 
11
- const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "dart", "zig"])
11
+ const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "c", "dart", "zig"])
12
12
 
13
13
  /** @typedef {Map<string, import("./types.js").RecordDeclaration>} RecordRegistry */
14
14
  /** @typedef {Map<string, import("./types.js").ClassDeclaration>} ClassRegistry */
@@ -160,7 +160,7 @@ function validateBlockShape(block, detail, fail) {
160
160
  }
161
161
 
162
162
  for (const statement of block.statements) {
163
- if (!["AssignmentStatement", "BreakStatement", "ContinueStatement", "ExpressionStatement", "ForEachMapStatement", "ForEachStatement", "IfStatement", "LocalDeclaration", "PrivateFieldWriteStatement", "PrintStatement", "RaiseStatement", "ReturnStatement", "TryStatement", "WhileStatement"].includes(statement.kind)) {
163
+ if (!["AssignmentStatement", "BreakStatement", "ContinueStatement", "ExpressionStatement", "ForEachMapStatement", "ForEachStatement", "IfStatement", "LocalDeclaration", "PrivateFieldWriteStatement", "PrintStatement", "RaiseStatement", "RethrowStatement", "ReturnStatement", "TryStatement", "WhileStatement"].includes(statement.kind)) {
164
164
  fail(`${detail} statement ${statement.kind}`, statement.location)
165
165
  }
166
166
  if (statement.kind == "IfStatement") {
@@ -172,7 +172,18 @@ function validateBlockShape(block, detail, fail) {
172
172
  }
173
173
  if (statement.kind == "TryStatement") {
174
174
  validateBlockShape(statement.body, "try body", fail)
175
- validateBlockShape(statement.catchBody, "catch body", fail)
175
+ if (!Array.isArray(statement.handlers) || (statement.handlers.length == 0 && !statement.finallyBody)) {
176
+ fail(`${detail} try without an ordered handler list`, statement.location)
177
+ }
178
+
179
+ for (const handler of statement.handlers ?? []) {
180
+ if (!handler || handler.kind != "CatchHandler" ||
181
+ Object.keys(handler).filter((key) => key != "sourceProvenance").sort().join(",") != "catchBinding,catchBody,catchType,kind,location") {
182
+ fail(`${detail} malformed catch handler`, handler?.location ?? statement.location)
183
+ }
184
+ validateBlockShape(handler.catchBody, "catch body", fail)
185
+ }
186
+ if (statement.finallyBody) validateBlockShape(statement.finallyBody, "finally body", fail)
176
187
  }
177
188
  }
178
189
  }
@@ -310,7 +321,8 @@ function prepareLoopIdentities(module, normalizeOperations, fail) {
310
321
  if (statement.alternate) visit(statement.alternate, owner, active)
311
322
  } else if (statement.kind == "TryStatement") {
312
323
  visit(statement.body, owner, active)
313
- visit(statement.catchBody, owner, active)
324
+ for (const handler of statement.handlers ?? []) visit(handler.catchBody, owner, active)
325
+ if (statement.finallyBody) visit(statement.finallyBody, owner, active)
314
326
  }
315
327
  }
316
328
  }
@@ -1086,44 +1098,87 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
1086
1098
  normal = false
1087
1099
  continue
1088
1100
  }
1089
- if (statement.kind == "TryStatement") {
1090
- const caughtType = validateErrorType(statement.catchType, statement.location, errors, fail)
1101
+ if (statement.kind == "RethrowStatement") {
1102
+ const binding = findBinding(statement.binding, scope)
1091
1103
 
1092
- if (!statement.catchBinding || statement.catchBinding.kind != "CatchBinding" || statement.catchBinding.mutable !== false) {
1093
- fail("INVALID_ERROR_HANDLER", "Catch requires one immutable typed binding.", statement.catchBinding?.location ?? statement.location)
1104
+ if (!binding || binding.mutable !== false || binding.type.kind != "ErrorType") {
1105
+ fail("INVALID_RETHROW", "Rethrow requires a catch binding in scope.", statement.location)
1094
1106
  }
1095
- const bindingType = validateErrorType(statement.catchBinding.type, statement.catchBinding.location, errors, fail)
1096
-
1097
- if (bindingType.declarationId != caughtType.declarationId) {
1098
- fail("INVALID_ERROR_HANDLER", "Catch binding type must exactly match its handler type.", statement.catchBinding.location)
1107
+ raises.add(/** @type {import("./types.js").ErrorType} */ (binding.type).declarationId)
1108
+ normal = false
1109
+ continue
1110
+ }
1111
+ if (statement.kind == "TryStatement") {
1112
+ if (!Array.isArray(statement.handlers) || (statement.handlers.length == 0 && !statement.finallyBody)) {
1113
+ fail("INVALID_ERROR_HANDLER", "Try requires at least one exact typed handler or a finally block.", statement.location)
1099
1114
  }
1100
1115
  const bodyAssignedOuterBindings = outerMutableBindingsAssignedBy(statement.body, scope)
1101
1116
  const assignedOuterBindings = new Set(bodyAssignedOuterBindings)
1102
1117
 
1103
- outerMutableBindingsAssignedBy(statement.catchBody, scope, assignedOuterBindings)
1118
+ for (const handler of statement.handlers) outerMutableBindingsAssignedBy(handler.catchBody, scope, assignedOuterBindings)
1119
+ if (statement.finallyBody) outerMutableBindingsAssignedBy(statement.finallyBody, scope, assignedOuterBindings)
1104
1120
  const bodyScope = createScope(scope, statement.body.statements)
1105
1121
  const bodyFlow = validateBlock(statement.body, bodyScope, returnType, functions, records, errors, callEffects, fail, normalizeOperations, loopContext)
1106
- const catchScope = createScope(scope, statement.catchBody.statements)
1107
1122
 
1108
- for (const binding of bodyAssignedOuterBindings) {
1109
- binding.knownValue = undefined
1110
- catchScope.presenceProofs.delete(binding)
1111
- }
1123
+ let remainingRaises = new Set(bodyFlow.raises)
1124
+ let normalCompletion = bodyFlow.normal
1125
+ const handlerRaises = new Set()
1126
+ const seenHandlerNames = new Set()
1112
1127
 
1113
- declareBinding(statement.catchBinding.name, {
1114
- knownValue: undefined,
1115
- mutable: false,
1116
- type: bindingType
1117
- }, roleLocation(statement.catchBinding, "name"), catchScope, fail)
1118
- const catchFlow = validateBlock(statement.catchBody, catchScope, returnType, functions, records, errors, callEffects, fail, normalizeOperations, loopContext)
1128
+ for (const handler of statement.handlers) {
1129
+ const caughtType = validateErrorType(handler.catchType, handler.location, errors, fail)
1119
1130
 
1120
- if (!bodyFlow.raises.has(caughtType.declarationId)) {
1121
- fail("UNREACHABLE_HANDLER", `Handler for '${errors.get(caughtType.declarationId)?.name}' cannot be reached.`, typeLocation(statement.catchType, statement.location))
1131
+ if (!handler.catchBinding || handler.catchBinding.kind != "CatchBinding" || handler.catchBinding.mutable !== false) {
1132
+ fail("INVALID_ERROR_HANDLER", "Catch requires one immutable typed binding.", handler.catchBinding?.location ?? handler.location)
1133
+ }
1134
+ const bindingType = validateErrorType(handler.catchBinding.type, handler.catchBinding.location, errors, fail)
1135
+
1136
+ if (bindingType.declarationId != caughtType.declarationId) {
1137
+ fail("INVALID_ERROR_HANDLER", "Catch binding type must exactly match its handler type.", handler.catchBinding.location)
1138
+ }
1139
+ if (!remainingRaises.has(caughtType.declarationId)) {
1140
+ fail("UNREACHABLE_HANDLER", `Handler for '${errors.get(caughtType.declarationId)?.name}' cannot be reached.`,
1141
+ typeLocation(handler.catchType, handler.location))
1142
+ }
1143
+ if (seenHandlerNames.has(handler.catchBinding.name)) {
1144
+ fail("DUPLICATE_BINDING", `Duplicate or shadowed binding '${handler.catchBinding.name}'.`, handler.catchBinding.location)
1145
+ }
1146
+ seenHandlerNames.add(handler.catchBinding.name)
1147
+ remainingRaises.delete(caughtType.declarationId)
1148
+ const catchScope = createScope(scope, handler.catchBody.statements)
1149
+
1150
+ for (const binding of bodyAssignedOuterBindings) {
1151
+ binding.knownValue = undefined
1152
+ catchScope.presenceProofs.delete(binding)
1153
+ }
1154
+ declareBinding(handler.catchBinding.name, {
1155
+ knownValue: undefined,
1156
+ mutable: false,
1157
+ type: bindingType
1158
+ }, roleLocation(handler.catchBinding, "name"), catchScope, fail)
1159
+ const catchFlow = validateBlock(handler.catchBody, catchScope, returnType, functions, records, errors, callEffects, fail,
1160
+ normalizeOperations, loopContext)
1161
+
1162
+ normalCompletion = normalCompletion || catchFlow.normal
1163
+ addAll(handlerRaises, catchFlow.raises)
1164
+ }
1165
+ const pendingRaises = new Set(remainingRaises)
1166
+
1167
+ addAll(pendingRaises, handlerRaises)
1168
+ if (statement.finallyBody) {
1169
+ const finallyScope = createScope(scope, statement.finallyBody.statements)
1170
+ const finallyFlow = validateBlock(statement.finallyBody, finallyScope, returnType, functions, records, errors, callEffects,
1171
+ fail, normalizeOperations, loopContext)
1172
+ // `raises` is the enclosing block's accumulator. A completion block replaces only this try's
1173
+ // pending completion; escapes contributed by earlier statements of the enclosing block survive
1174
+ // whichever way the completion block completes.
1175
+ if (finallyFlow.normal) addAll(raises, pendingRaises)
1176
+ addAll(raises, finallyFlow.raises)
1177
+ normal = finallyFlow.normal ? normalCompletion : false
1178
+ } else {
1179
+ addAll(raises, pendingRaises)
1180
+ normal = normalCompletion
1122
1181
  }
1123
- bodyFlow.raises.delete(caughtType.declarationId)
1124
- addAll(raises, bodyFlow.raises)
1125
- addAll(raises, catchFlow.raises)
1126
- normal = bodyFlow.normal || catchFlow.normal
1127
1182
  for (const binding of assignedOuterBindings) {
1128
1183
  binding.knownValue = undefined
1129
1184
  scope.presenceProofs.delete(binding)
@@ -1271,14 +1326,27 @@ function syntacticBlockEffects(block, callEffects, methodEffects = new Map()) {
1271
1326
  }
1272
1327
  continue
1273
1328
  }
1329
+ if (kind == "RethrowStatement") {
1330
+ const bindingNode = Reflect.get(statement, "bindingNode")
1331
+ const bindingType = bindingNode && typeof bindingNode == "object" ? Reflect.get(bindingNode, "type") : undefined
1332
+ const declarationId = bindingType && typeof bindingType == "object" ? Reflect.get(bindingType, "declarationId") : undefined
1333
+
1334
+ if (typeof declarationId == "string") result.add(declarationId)
1335
+ continue
1336
+ }
1274
1337
  if (kind == "TryStatement") {
1275
- const bodyEffects = syntacticBlockEffects(Reflect.get(statement, "body"), callEffects, methodEffects)
1276
- const catchType = Reflect.get(statement, "catchType")
1277
- const caughtId = catchType && typeof catchType == "object" ? Reflect.get(catchType, "declarationId") : undefined
1338
+ let bodyEffects = syntacticBlockEffects(Reflect.get(statement, "body"), callEffects, methodEffects)
1339
+
1340
+ for (const handler of Reflect.get(statement, "handlers") ?? []) {
1341
+ if (!handler || typeof handler != "object") continue
1342
+ const catchType = Reflect.get(handler, "catchType")
1343
+ const caughtId = catchType && typeof catchType == "object" ? Reflect.get(catchType, "declarationId") : undefined
1278
1344
 
1279
- if (typeof caughtId == "string" && bodyEffects.delete(caughtId)) {
1280
- addAll(bodyEffects, syntacticBlockEffects(Reflect.get(statement, "catchBody"), callEffects, methodEffects))
1345
+ if (typeof caughtId == "string" && bodyEffects.delete(caughtId)) {
1346
+ addAll(bodyEffects, syntacticBlockEffects(Reflect.get(handler, "catchBody"), callEffects, methodEffects))
1347
+ }
1281
1348
  }
1349
+ addAll(bodyEffects, syntacticBlockEffects(Reflect.get(statement, "finallyBody"), callEffects, methodEffects))
1282
1350
  addAll(result, bodyEffects)
1283
1351
  continue
1284
1352
  }
@@ -3046,7 +3114,8 @@ function outerMutableBindingsAssignedBy(block, outerScope, assigned = new Set())
3046
3114
  outerMutableBindingsAssignedBy(statement.body, outerScope, assigned)
3047
3115
  } else if (statement.kind == "TryStatement") {
3048
3116
  outerMutableBindingsAssignedBy(statement.body, outerScope, assigned)
3049
- outerMutableBindingsAssignedBy(statement.catchBody, outerScope, assigned)
3117
+ for (const handler of statement.handlers ?? []) outerMutableBindingsAssignedBy(handler.catchBody, outerScope, assigned)
3118
+ if (statement.finallyBody) outerMutableBindingsAssignedBy(statement.finallyBody, outerScope, assigned)
3050
3119
  }
3051
3120
  }
3052
3121