semantifold 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +1 -0
  2. package/build/src/backends/effects.js +3 -2
  3. package/build/src/backends/java.js +33 -14
  4. package/build/src/backends/javascript.js +83 -18
  5. package/build/src/backends/php.js +33 -14
  6. package/build/src/backends/ruby.js +37 -12
  7. package/build/src/backends/shared.d.ts.map +1 -1
  8. package/build/src/backends/shared.js +91 -36
  9. package/build/src/backends/typescript.js +83 -18
  10. package/build/src/frontends/java.d.ts +8 -0
  11. package/build/src/frontends/java.d.ts.map +1 -1
  12. package/build/src/frontends/java.js +77 -33
  13. package/build/src/frontends/javascript-typescript.d.ts +2 -0
  14. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  15. package/build/src/frontends/javascript-typescript.js +118 -37
  16. package/build/src/frontends/php.d.ts +8 -0
  17. package/build/src/frontends/php.d.ts.map +1 -1
  18. package/build/src/frontends/php.js +68 -29
  19. package/build/src/frontends/ruby.d.ts +3 -0
  20. package/build/src/frontends/ruby.d.ts.map +1 -1
  21. package/build/src/frontends/ruby.js +103 -30
  22. package/build/src/semantic/effects.d.ts.map +1 -1
  23. package/build/src/semantic/effects.js +18 -6
  24. package/build/src/semantic/lifetime.js +28 -4
  25. package/build/src/semantic/provenance.d.ts.map +1 -1
  26. package/build/src/semantic/provenance.js +26 -7
  27. package/build/src/semantic/types.d.ts +76 -16
  28. package/build/src/semantic/types.d.ts.map +1 -1
  29. package/build/src/semantic/types.js +23 -7
  30. package/build/src/semantic/validate.d.ts.map +1 -1
  31. package/build/src/semantic/validate.js +105 -34
  32. package/docs/adr/20261026-typed-error-handling-extension.md +55 -0
  33. package/docs/adr-numeric-tower.md +156 -0
  34. package/docs/architecture.md +1 -1
  35. package/docs/language-support.md +7 -7
  36. package/docs/testing.md +5 -3
  37. package/package.json +1 -1
  38. package/src/backends/effects.js +2 -1
  39. package/src/backends/java.js +33 -14
  40. package/src/backends/javascript.js +83 -18
  41. package/src/backends/php.js +33 -14
  42. package/src/backends/ruby.js +36 -12
  43. package/src/backends/shared.js +88 -41
  44. package/src/backends/typescript.js +83 -18
  45. package/src/frontends/java.js +82 -33
  46. package/src/frontends/javascript-typescript.js +130 -38
  47. package/src/frontends/php.js +68 -27
  48. package/src/frontends/ruby.js +107 -28
  49. package/src/semantic/effects.js +17 -5
  50. package/src/semantic/lifetime.js +29 -3
  51. package/src/semantic/provenance.js +27 -12
  52. package/src/semantic/types.js +24 -6
  53. package/src/semantic/validate.js +104 -35
@@ -489,9 +489,15 @@ function emitStatement(writer, statement, indent, path) {
489
489
  writer.synthetic(");\n", "error construction close", [statement.error], [`${path}/error`])
490
490
  return
491
491
  }
492
+ if (statement.kind == "RethrowStatement") {
493
+ writer.mapped("throw", {mappingKind: "anchor", node: statement, path})
494
+ writer.synthetic(" ", "throw spacing", [statement], [path])
495
+ writer.mapped(`$${statement.binding}`, {mappingKind: "exact", node: statement.bindingNode, path: `${path}/bindingNode`, role: "name"})
496
+ writer.mapped(";", {mappingKind: "anchor", node: statement, path})
497
+ writer.synthetic("\n", "line break", [statement], [path])
498
+ return
499
+ }
492
500
  if (statement.kind == "TryStatement") {
493
- const caught = writer.errorForId(statement.catchType.declarationId)
494
-
495
501
  writer.mapped("try", {mappingKind: "anchor", node: statement, path})
496
502
  writer.synthetic(" ", "try spacing", [statement], [path])
497
503
  writer.mapped("{", {mappingKind: "anchor", node: statement.body, path: `${path}/body`})
@@ -499,18 +505,31 @@ function emitStatement(writer, statement, indent, path) {
499
505
  emitBlock(writer, statement.body, `${indent} `, `${path}/body`)
500
506
  writer.synthetic(indent, "indentation", [statement], [path])
501
507
  writer.mapped("}", {mappingKind: "anchor", node: statement.body, path: `${path}/body`})
502
- writer.synthetic(" ", "catch spacing", [statement], [path])
503
- writer.mapped("catch", {mappingKind: "anchor", node: statement, path, role: "catch"})
504
- writer.synthetic(" (", "catch binding scaffolding", [statement.catchBinding], [`${path}/catchBinding`])
505
- writer.mapped(writer.errorNameForId(statement.catchType.declarationId), {mappingKind: "exact", name: caught.name, node: statement.catchType, path: `${path}/catchType`, role: "type"})
506
- writer.synthetic(" ", "catch binding spacing", [statement.catchBinding], [`${path}/catchBinding`])
507
- writer.mapped(`$${statement.catchBinding.name}`, {mappingKind: "exact", node: statement.catchBinding, path: `${path}/catchBinding`, role: "name"})
508
- writer.synthetic(") ", "catch binding scaffolding", [statement.catchBinding], [`${path}/catchBinding`])
509
- writer.mapped("{", {mappingKind: "anchor", node: statement.catchBody, path: `${path}/catchBody`})
510
- writer.synthetic("\n", "line break", [statement], [path])
511
- emitBlock(writer, statement.catchBody, `${indent} `, `${path}/catchBody`)
512
- writer.synthetic(indent, "indentation", [statement], [path])
513
- writer.mapped("}", {mappingKind: "anchor", node: statement.catchBody, path: `${path}/catchBody`})
508
+ for (const [index, handler] of statement.handlers.entries()) {
509
+ const handlerPath = `${path}/handlers/${index}`
510
+ const caught = writer.errorForId(handler.catchType.declarationId)
511
+
512
+ writer.synthetic(" ", "catch spacing", [handler], [handlerPath])
513
+ writer.mapped("catch", {mappingKind: "anchor", node: handler, path: handlerPath, role: "catch"})
514
+ writer.synthetic(" (", "catch binding scaffolding", [handler.catchBinding], [`${handlerPath}/catchBinding`])
515
+ writer.mapped(writer.errorNameForId(handler.catchType.declarationId), {mappingKind: "exact", name: caught.name, node: handler.catchType, path: `${handlerPath}/catchType`, role: "type"})
516
+ writer.synthetic(" ", "catch binding spacing", [handler.catchBinding], [`${handlerPath}/catchBinding`])
517
+ writer.mapped(`$${handler.catchBinding.name}`, {mappingKind: "exact", node: handler.catchBinding, path: `${handlerPath}/catchBinding`, role: "name"})
518
+ writer.synthetic(") ", "catch binding scaffolding", [handler.catchBinding], [`${handlerPath}/catchBinding`])
519
+ writer.mapped("{", {mappingKind: "anchor", node: handler.catchBody, path: `${handlerPath}/catchBody`})
520
+ writer.synthetic("\n", "line break", [handler], [handlerPath])
521
+ emitBlock(writer, handler.catchBody, `${indent} `, `${handlerPath}/catchBody`)
522
+ writer.synthetic(indent, "indentation", [handler], [handlerPath])
523
+ writer.mapped("}", {mappingKind: "anchor", node: handler.catchBody, path: `${handlerPath}/catchBody`})
524
+ }
525
+ if (statement.finallyBody) {
526
+ writer.synthetic(" ", "finally spacing", [statement], [path])
527
+ writer.mapped("finally", {mappingKind: "anchor", node: statement, path, role: "finally"})
528
+ writer.synthetic(" {\n", "finally scaffolding", [statement], [path])
529
+ emitBlock(writer, statement.finallyBody, `${indent} `, `${path}/finallyBody`)
530
+ writer.synthetic(indent, "indentation", [statement], [path])
531
+ writer.mapped("}", {mappingKind: "anchor", node: statement.finallyBody, path: `${path}/finallyBody`})
532
+ }
514
533
  writer.synthetic("\n", "line break", [statement], [path])
515
534
  return
516
535
  }
@@ -364,22 +364,46 @@ function emitStatement(writer, statement, indent, path) {
364
364
  writer.synthetic("\n", "line break", [statement], [path])
365
365
  return
366
366
  }
367
+ if (statement.kind == "RethrowStatement") {
368
+ writer.mapped("raise", {mappingKind: "anchor", node: statement, path})
369
+ writer.synthetic(" ", "raise spacing", [statement], [path])
370
+ writer.mapped(statement.binding, {mappingKind: "exact", node: statement.bindingNode, path: `${path}/bindingNode`, role: "name"})
371
+ writer.synthetic("\n", "line break", [statement], [path])
372
+ return
373
+ }
367
374
  if (statement.kind == "TryStatement") {
368
- const caught = writer.errorForId(statement.catchType.declarationId)
369
-
370
375
  writer.mapped("begin", {mappingKind: "anchor", node: statement, path})
371
376
  writer.synthetic("\n", "line break", [statement], [path])
372
377
  emitBlock(writer, statement.body, `${indent} `, `${path}/body`)
373
- writer.synthetic(indent, "indentation", [statement], [path])
374
- writer.mapped("rescue", {mappingKind: "anchor", node: statement, path, role: "catch"})
375
- writer.synthetic(" ", "rescue spacing", [statement], [path])
376
- writer.mapped(writer.errorNameForId(statement.catchType.declarationId), {mappingKind: "exact", name: caught.name, node: statement.catchType, path: `${path}/catchType`, role: "type"})
377
- writer.synthetic(" => ", "catch binding separator", [statement.catchBinding], [`${path}/catchBinding`])
378
- writer.mapped(statement.catchBinding.name, {mappingKind: "exact", node: statement.catchBinding, path: `${path}/catchBinding`, role: "name"})
379
- writer.synthetic("\n", "line break", [statement], [path])
380
- emitBlock(writer, statement.catchBody, `${indent} `, `${path}/catchBody`)
381
- writer.synthetic(indent, "indentation", [statement], [path])
382
- writer.mapped("end", {mappingKind: "anchor", node: statement.catchBody, path: `${path}/catchBody`})
378
+ for (const [index, handler] of statement.handlers.entries()) {
379
+ const handlerPath = `${path}/handlers/${index}`
380
+ const caught = writer.errorForId(handler.catchType.declarationId)
381
+
382
+ writer.synthetic(indent, "indentation", [handler], [handlerPath])
383
+ writer.mapped("rescue", {mappingKind: "anchor", node: handler, path: handlerPath, role: "catch"})
384
+ writer.synthetic(" ", "rescue spacing", [handler], [handlerPath])
385
+ writer.mapped(writer.errorNameForId(handler.catchType.declarationId), {mappingKind: "exact", name: caught.name, node: handler.catchType, path: `${handlerPath}/catchType`, role: "type"})
386
+ writer.synthetic(" => ", "catch binding separator", [handler.catchBinding], [`${handlerPath}/catchBinding`])
387
+ writer.mapped(handler.catchBinding.name, {mappingKind: "exact", node: handler.catchBinding, path: `${handlerPath}/catchBinding`, role: "name"})
388
+ writer.synthetic("\n", "line break", [handler], [handlerPath])
389
+ emitBlock(writer, handler.catchBody, `${indent} `, `${handlerPath}/catchBody`)
390
+ }
391
+ if (statement.finallyBody) {
392
+ writer.synthetic(indent, "indentation", [statement], [path])
393
+ writer.mapped("ensure", {mappingKind: "anchor", node: statement, path, role: "ensure"})
394
+ writer.synthetic("\n", "line break", [statement], [path])
395
+ emitBlock(writer, statement.finallyBody, `${indent} `, `${path}/finallyBody`)
396
+ writer.synthetic(indent, "indentation", [statement], [path])
397
+ writer.mapped("end", {mappingKind: "anchor", node: statement.finallyBody, path: `${path}/finallyBody`})
398
+ } else if (statement.handlers.length > 0) {
399
+ const lastIndex = statement.handlers.length - 1
400
+
401
+ writer.synthetic(indent, "indentation", [statement], [path])
402
+ writer.mapped("end", {mappingKind: "anchor", node: statement.handlers[lastIndex].catchBody, path: `${path}/handlers/${lastIndex}/catchBody`})
403
+ } else {
404
+ writer.synthetic(indent, "indentation", [statement], [path])
405
+ writer.mapped("end", {mappingKind: "anchor", node: statement.body, path: `${path}/body`})
406
+ }
383
407
  writer.synthetic("\n", "line break", [statement], [path])
384
408
  return
385
409
  }
@@ -627,7 +627,8 @@ function rejectBlockConditionLoop(block, language, seen = new Set()) {
627
627
  rejectBlockConditionLoop(Reflect.get(statement, "body"), language, seen)
628
628
  } else if (kind == "TryStatement") {
629
629
  rejectBlockConditionLoop(Reflect.get(statement, "body"), language, seen)
630
- rejectBlockConditionLoop(Reflect.get(statement, "catchBody"), language, seen)
630
+ for (const handler of Reflect.get(statement, "handlers") ?? []) rejectBlockConditionLoop(Reflect.get(handler, "catchBody"), language, seen)
631
+ rejectBlockConditionLoop(Reflect.get(statement, "finallyBody"), language, seen)
631
632
  }
632
633
  }
633
634
  }
@@ -658,7 +659,8 @@ function rejectBlockIteration(block, language, seen = new Set()) {
658
659
  rejectBlockIteration(Reflect.get(statement, "alternate"), language, seen)
659
660
  } else if (kind == "TryStatement") {
660
661
  rejectBlockIteration(Reflect.get(statement, "body"), language, seen)
661
- rejectBlockIteration(Reflect.get(statement, "catchBody"), language, seen)
662
+ for (const handler of Reflect.get(statement, "handlers") ?? []) rejectBlockIteration(Reflect.get(handler, "catchBody"), language, seen)
663
+ rejectBlockIteration(Reflect.get(statement, "finallyBody"), language, seen)
662
664
  }
663
665
  }
664
666
  }
@@ -713,7 +715,8 @@ function rejectBlockOptionalTypes(block, language) {
713
715
  if (statement.kind == "WhileStatement") rejectBlockOptionalTypes(statement.body, language)
714
716
  if (statement.kind == "TryStatement") {
715
717
  rejectBlockOptionalTypes(statement.body, language)
716
- rejectBlockOptionalTypes(statement.catchBody, language)
718
+ for (const handler of statement.handlers ?? []) rejectBlockOptionalTypes(handler.catchBody, language)
719
+ if (statement.finallyBody) rejectBlockOptionalTypes(statement.finallyBody, language)
717
720
  }
718
721
  }
719
722
  }
@@ -788,7 +791,8 @@ function rejectBlockCollectionTypes(block, language) {
788
791
  if (statement.kind == "WhileStatement") rejectBlockCollectionTypes(statement.body, language)
789
792
  if (statement.kind == "TryStatement") {
790
793
  rejectBlockCollectionTypes(statement.body, language)
791
- rejectBlockCollectionTypes(statement.catchBody, language)
794
+ for (const handler of statement.handlers ?? []) rejectBlockCollectionTypes(handler.catchBody, language)
795
+ if (statement.finallyBody) rejectBlockCollectionTypes(statement.finallyBody, language)
792
796
  }
793
797
  }
794
798
  }
@@ -856,9 +860,13 @@ function validateScaffoldingNames(module, language) {
856
860
  }
857
861
  }
858
862
  }
859
- if (statement.kind == "TryStatement" && ownedPrintReceiverNames.has(statement.catchBinding.name)) {
860
- unsupportedCapability(language, `${owner} catch binding '${statement.catchBinding.name}' captures backend scaffolding`,
861
- statement.catchBinding.location)
863
+ if (statement.kind == "TryStatement") {
864
+ for (const handler of statement.handlers ?? []) {
865
+ if (ownedPrintReceiverNames.has(handler.catchBinding.name)) {
866
+ unsupportedCapability(language, `${owner} catch binding '${handler.catchBinding.name}' captures backend scaffolding`,
867
+ handler.catchBinding.location)
868
+ }
869
+ }
862
870
  }
863
871
  }
864
872
  }
@@ -878,9 +886,13 @@ function validateScaffoldingNames(module, language) {
878
886
  }
879
887
  }
880
888
  }
881
- if (statement.kind == "TryStatement" && ownedEntryNames.has(statement.catchBinding.name)) {
882
- unsupportedCapability(language, `entry catch binding '${statement.catchBinding.name}' captures backend scaffolding`,
883
- statement.catchBinding.location)
889
+ if (statement.kind == "TryStatement") {
890
+ for (const handler of statement.handlers ?? []) {
891
+ if (ownedEntryNames.has(handler.catchBinding.name)) {
892
+ unsupportedCapability(language, `entry catch binding '${handler.catchBinding.name}' captures backend scaffolding`,
893
+ handler.catchBinding.location)
894
+ }
895
+ }
884
896
  }
885
897
  }
886
898
  for (const declaration of module.functions) {
@@ -949,12 +961,13 @@ function validateRecordConstructorNames(module, language) {
949
961
  }
950
962
  }
951
963
  }
952
- if (statement.kind == "TryStatement" && recordNames.has(statement.catchBinding.name)) {
953
- unsupportedCapability(
954
- language,
955
- `${owner} catch binding '${statement.catchBinding.name}' captures nominal constructor`,
956
- bindingNameLocation(statement.catchBinding)
957
- )
964
+ if (statement.kind == "TryStatement") {
965
+ const captured = (statement.handlers ?? []).find((handler) => recordNames.has(handler.catchBinding.name))
966
+
967
+ if (captured) {
968
+ unsupportedCapability(language, `${owner} catch binding '${captured.catchBinding.name}' captures nominal constructor`,
969
+ bindingNameLocation(captured.catchBinding))
970
+ }
958
971
  }
959
972
  }
960
973
  }
@@ -1052,12 +1065,15 @@ function validateJavaUtilFactoryNames(module) {
1052
1065
  }
1053
1066
  if (statement.kind == "TryStatement") {
1054
1067
  validateBlockNames(statement.body, capture, owner)
1055
- const catchCapture = statement.catchBinding.name == "java" ? {
1056
- detail: `${owner} catch binding 'java' captures java.util factory syntax`,
1057
- location: statement.catchBinding.location
1058
- } : capture
1068
+ for (const handler of statement.handlers ?? []) {
1069
+ const catchCapture = handler.catchBinding.name == "java" ? {
1070
+ detail: `${owner} catch binding 'java' captures java.util factory syntax`,
1071
+ location: handler.catchBinding.location
1072
+ } : capture
1059
1073
 
1060
- validateBlockNames(statement.catchBody, catchCapture, owner)
1074
+ validateBlockNames(handler.catchBody, catchCapture, owner)
1075
+ }
1076
+ if (statement.finallyBody) validateBlockNames(statement.finallyBody, capture, owner)
1061
1077
  }
1062
1078
  }
1063
1079
  }
@@ -1220,7 +1236,11 @@ function allStatements(block) {
1220
1236
  return [statement, ...allStatements(statement.body)]
1221
1237
  }
1222
1238
  if (statement.kind == "WhileStatement") return [statement, ...allStatements(statement.body)]
1223
- if (statement.kind == "TryStatement") return [statement, ...allStatements(statement.body), ...allStatements(statement.catchBody)]
1239
+ if (statement.kind == "TryStatement") {
1240
+ return [statement, ...allStatements(statement.body),
1241
+ ...(statement.handlers ?? []).flatMap((handler) => allStatements(handler.catchBody)),
1242
+ ...(statement.finallyBody ? allStatements(statement.finallyBody) : [])]
1243
+ }
1224
1244
 
1225
1245
  return [statement]
1226
1246
  })
@@ -1274,7 +1294,8 @@ function collectLoopOwners(module, language) {
1274
1294
  visit(Reflect.get(statement, "alternate"), owner, path)
1275
1295
  } else if (kind == "TryStatement") {
1276
1296
  visit(Reflect.get(statement, "body"), owner, path)
1277
- visit(Reflect.get(statement, "catchBody"), owner, path)
1297
+ for (const handler of Reflect.get(statement, "handlers") ?? []) visit(Reflect.get(handler, "catchBody"), owner, path)
1298
+ visit(Reflect.get(statement, "finallyBody"), owner, path)
1278
1299
  }
1279
1300
  }
1280
1301
  }
@@ -1536,30 +1557,55 @@ function validateStatement(statement, language, ownerLocation, loopContext, acti
1536
1557
  validateExpression(raised.message, language, raised.location, false, new Set(), externalDeclarationIds)
1537
1558
  return
1538
1559
  }
1560
+ if (kind == "RethrowStatement") {
1561
+ const rethrow = /** @type {import("../semantic/types.js").RethrowStatement} */ (statement)
1562
+ const rethrowLocation = requireSemanticLocation(ownLocation, language, "RethrowStatement location", ownerLocation)
1563
+
1564
+ if (Object.keys(rethrow).filter((key) => key != "sourceProvenance").sort().join(",") != "binding,bindingNode,kind,location" ||
1565
+ typeof rethrow.binding != "string" || rethrow.binding.length == 0 ||
1566
+ !rethrow.bindingNode || rethrow.bindingNode.kind != "CatchBinding" || rethrow.bindingNode.name != rethrow.binding) {
1567
+ unsupportedCapability(language, "malformed RethrowStatement", rethrowLocation)
1568
+ }
1569
+ validateTargetBindingIdentifier(language, rethrow.binding, "catch binding", rethrowLocation)
1570
+ return
1571
+ }
1539
1572
  if (kind == "TryStatement") {
1540
- const handler = /** @type {import("../semantic/types.js").TryStatement} */ (statement)
1573
+ const tryStatement = /** @type {import("../semantic/types.js").TryStatement} */ (statement)
1541
1574
  const tryLocation = requireSemanticLocation(ownLocation, language, "TryStatement location", ownerLocation)
1542
- const fields = Object.keys(handler).filter((key) => key != "sourceProvenance").sort().join(",")
1575
+ const fields = Object.keys(tryStatement).filter((key) => key != "sourceProvenance").sort().join(",")
1543
1576
 
1544
- if (fields != "body,catchBinding,catchBody,catchType,kind,location") {
1577
+ if (fields != "body,handlers,kind,location" && fields != "body,finallyBody,handlers,kind,location") {
1545
1578
  unsupportedCapability(language, "malformed TryStatement", tryLocation)
1546
1579
  }
1547
- validateErrorTypeShape(handler.catchType, language, tryLocation)
1548
- const binding = handler.catchBinding
1549
- const bindingFields = binding && typeof binding == "object" && !Array.isArray(binding)
1550
- ? Object.keys(binding).filter((key) => key != "sourceProvenance").sort().join(",") : ""
1551
- const bindingLocation = requireSemanticLocation(binding?.location, language, "catch binding location", tryLocation)
1552
-
1553
- if (!binding || binding.kind != "CatchBinding" || bindingFields != "kind,location,mutable,name,type" || binding.mutable !== false) {
1554
- unsupportedCapability(language, "malformed catch binding", bindingLocation)
1580
+ if (!Array.isArray(tryStatement.handlers) || (tryStatement.handlers.length == 0 && !tryStatement.finallyBody)) {
1581
+ unsupportedCapability(language, "TryStatement without an ordered handler list", tryLocation)
1555
1582
  }
1556
- validateTargetBindingIdentifier(language, binding.name, "catch binding", bindingLocation)
1557
- validateErrorTypeShape(binding.type, language, bindingLocation)
1558
- if (binding.type.declarationId != handler.catchType.declarationId) {
1559
- unsupportedCapability(language, "catch binding type mismatch", bindingLocation)
1583
+ validateBlock(tryStatement.body, language, tryLocation, loopContext, activePath, externalDeclarationIds)
1584
+ for (const handler of tryStatement.handlers) {
1585
+ const handlerLocation = requireSemanticLocation(handler?.location, language, "catch handler location", tryLocation)
1586
+ const handlerFields = handler && typeof handler == "object" && !Array.isArray(handler)
1587
+ ? Object.keys(handler).filter((key) => key != "sourceProvenance").sort().join(",") : ""
1588
+
1589
+ if (!handler || handler.kind != "CatchHandler" || handlerFields != "catchBinding,catchBody,catchType,kind,location") {
1590
+ unsupportedCapability(language, "malformed catch handler", handlerLocation)
1591
+ }
1592
+ validateErrorTypeShape(handler.catchType, language, handlerLocation)
1593
+ const binding = handler.catchBinding
1594
+ const bindingFields = binding && typeof binding == "object" && !Array.isArray(binding)
1595
+ ? Object.keys(binding).filter((key) => key != "sourceProvenance").sort().join(",") : ""
1596
+ const bindingLocation = requireSemanticLocation(binding?.location, language, "catch binding location", handlerLocation)
1597
+
1598
+ if (!binding || binding.kind != "CatchBinding" || bindingFields != "kind,location,mutable,name,type" || binding.mutable !== false) {
1599
+ unsupportedCapability(language, "malformed catch binding", bindingLocation)
1600
+ }
1601
+ validateTargetBindingIdentifier(language, binding.name, "catch binding", bindingLocation)
1602
+ validateErrorTypeShape(binding.type, language, bindingLocation)
1603
+ if (binding.type.declarationId != handler.catchType.declarationId) {
1604
+ unsupportedCapability(language, "catch binding type mismatch", bindingLocation)
1605
+ }
1606
+ validateBlock(handler.catchBody, language, handlerLocation, loopContext, activePath, externalDeclarationIds)
1560
1607
  }
1561
- validateBlock(handler.body, language, tryLocation, loopContext, activePath, externalDeclarationIds)
1562
- validateBlock(handler.catchBody, language, tryLocation, loopContext, activePath, externalDeclarationIds)
1608
+ if (tryStatement.finallyBody) validateBlock(tryStatement.finallyBody, language, tryLocation, loopContext, activePath, externalDeclarationIds)
1563
1609
  return
1564
1610
  }
1565
1611
 
@@ -2664,7 +2710,8 @@ function blockContainsSignProducingOperation(block) {
2664
2710
  return expressionContainsSignProducingOperation(statement.condition) || blockContainsSignProducingOperation(statement.body)
2665
2711
  }
2666
2712
  if (statement.kind == "TryStatement") {
2667
- return blockContainsSignProducingOperation(statement.body) || blockContainsSignProducingOperation(statement.catchBody)
2713
+ return blockContainsSignProducingOperation(statement.body) || (statement.handlers ?? []).some((handler) => blockContainsSignProducingOperation(handler.catchBody)) ||
2714
+ (statement.finallyBody ? blockContainsSignProducingOperation(statement.finallyBody) : false)
2668
2715
  }
2669
2716
  if (statement.kind == "RaiseStatement") return expressionContainsSignProducingOperation(statement.error.message)
2670
2717
  if (statement.kind == "LocalDeclaration") return expressionContainsSignProducingOperation(statement.initializer)
@@ -376,10 +376,14 @@ function emitStatement(writer, statement, indent, path, canonicalizeZero) {
376
376
  writer.synthetic(")\n", "error construction close", [statement.error], [`${path}/error`])
377
377
  return
378
378
  }
379
+ if (statement.kind == "RethrowStatement") {
380
+ writer.mapped("throw", {mappingKind: "anchor", node: statement, path})
381
+ writer.synthetic(" ", "throw spacing", [statement], [path])
382
+ writer.mapped(statement.binding, {mappingKind: "exact", node: statement.bindingNode, path: `${path}/bindingNode`, role: "name"})
383
+ writer.synthetic("\n", "line break", [statement], [path])
384
+ return
385
+ }
379
386
  if (statement.kind == "TryStatement") {
380
- const caught = writer.errorForId(statement.catchType.declarationId)
381
- const targetName = writer.errorNameForId(statement.catchType.declarationId)
382
-
383
387
  writer.mapped("try", {mappingKind: "anchor", node: statement, path})
384
388
  writer.synthetic(" ", "try spacing", [statement], [path])
385
389
  writer.mapped("{", {mappingKind: "anchor", node: statement.body, path: `${path}/body`})
@@ -387,21 +391,15 @@ function emitStatement(writer, statement, indent, path, canonicalizeZero) {
387
391
  emitBlock(writer, statement.body, `${indent} `, `${path}/body`, canonicalizeZero)
388
392
  writer.synthetic(indent, "indentation", [statement], [path])
389
393
  writer.mapped("}", {mappingKind: "anchor", node: statement.body, path: `${path}/body`})
390
- writer.synthetic(" ", "catch spacing", [statement], [path])
391
- writer.mapped("catch", {mappingKind: "anchor", node: statement, path, role: "catch"})
392
- writer.synthetic(" (", "catch binding scaffolding", [statement.catchBinding], [`${path}/catchBinding`])
393
- writer.mapped(statement.catchBinding.name, {mappingKind: "exact", node: statement.catchBinding, path: `${path}/catchBinding`, role: "name"})
394
- writer.synthetic(") {\n", "catch binding scaffolding", [statement.catchBinding], [`${path}/catchBinding`])
395
- writer.synthetic(`${indent} if (!(`, "exact catch guard", [statement], [path])
396
- writer.mapped(statement.catchBinding.name, {mappingKind: "exact", node: statement.catchBinding, path: `${path}/catchBinding`, role: "name"})
397
- writer.synthetic(" instanceof ", "exact catch guard", [statement], [path])
398
- writer.mapped(targetName, {mappingKind: "exact", name: caught.name, node: statement.catchType, path: `${path}/catchType`, role: "type"})
399
- writer.synthetic(`)) {\n${indent} throw `, "unmatched error propagation", [statement], [path])
400
- writer.mapped(statement.catchBinding.name, {mappingKind: "exact", node: statement.catchBinding, path: `${path}/catchBinding`, role: "name"})
401
- writer.synthetic(`\n${indent} }\n`, "unmatched error propagation", [statement], [path])
402
- emitBlock(writer, statement.catchBody, `${indent} `, `${path}/catchBody`, canonicalizeZero)
403
- writer.synthetic(indent, "indentation", [statement], [path])
404
- writer.mapped("}", {mappingKind: "anchor", node: statement.catchBody, path: `${path}/catchBody`})
394
+ if (statement.handlers.length > 0) emitTypeScriptHandler(writer, statement, 0, indent, path, canonicalizeZero)
395
+ if (statement.finallyBody) {
396
+ writer.synthetic(" ", "finally spacing", [statement], [path])
397
+ writer.mapped("finally", {mappingKind: "anchor", node: statement, path, role: "finally"})
398
+ writer.synthetic(" {\n", "finally scaffolding", [statement], [path])
399
+ emitBlock(writer, statement.finallyBody, `${indent} `, `${path}/finallyBody`, canonicalizeZero)
400
+ writer.synthetic(indent, "indentation", [statement], [path])
401
+ writer.mapped("}", {mappingKind: "anchor", node: statement.finallyBody, path: `${path}/finallyBody`})
402
+ }
405
403
  writer.synthetic("\n", "line break", [statement], [path])
406
404
  return
407
405
  }
@@ -453,6 +451,73 @@ function emitStatement(writer, statement, indent, path, canonicalizeZero) {
453
451
  writer.synthetic("\n", "line break", [statement], [path])
454
452
  }
455
453
 
454
+ /**
455
+ * Emits one ordered TypeScript catch handler with its exact nominal guard.
456
+ *
457
+ * TypeScript has no sibling catch clauses, so handler k > 0 hangs off handler k - 1's
458
+ * unmatched rethrow inside a nested try. Each handler body sits outside its own guard,
459
+ * so a raise from one handler body escapes outward instead of being caught by a later
460
+ * sibling handler, exactly like native ordered multi-catch.
461
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
462
+ * @param {import("../semantic/types.js").TryStatement} statement - Semantic try statement.
463
+ * @param {number} index - Zero-based ordered handler index.
464
+ * @param {string} indent - Leading indentation for this catch clause.
465
+ * @param {string} path - Exact JSON Pointer for the owning try statement.
466
+ * @param {boolean} canonicalizeZero - Whether scalar output canonicalizes zero.
467
+ * @returns {void}
468
+ */
469
+ function emitTypeScriptHandler(writer, statement, index, indent, path, canonicalizeZero) {
470
+ const handler = statement.handlers[index]
471
+ const handlerPath = `${path}/handlers/${index}`
472
+ const bodyIndent = `${indent} `
473
+ const caught = writer.errorForId(handler.catchType.declarationId)
474
+ const targetName = writer.errorNameForId(handler.catchType.declarationId)
475
+
476
+ writer.synthetic(" ", "catch spacing", [statement], [path])
477
+ writer.mapped("catch", {mappingKind: "anchor", node: handler, path: handlerPath, role: "catch"})
478
+ writer.synthetic(" (", "catch binding scaffolding", [handler.catchBinding], [`${handlerPath}/catchBinding`])
479
+ writer.mapped(handler.catchBinding.name, {mappingKind: "exact", node: handler.catchBinding, path: `${handlerPath}/catchBinding`, role: "name"})
480
+ writer.synthetic(") {\n", "catch binding scaffolding", [handler.catchBinding], [`${handlerPath}/catchBinding`])
481
+ writer.synthetic(`${bodyIndent}if (!(`, "exact catch guard", [handler], [handlerPath])
482
+ writer.mapped(handler.catchBinding.name, {mappingKind: "exact", node: handler.catchBinding, path: `${handlerPath}/catchBinding`, role: "name"})
483
+ writer.synthetic(" instanceof ", "exact catch guard", [handler], [handlerPath])
484
+ writer.mapped(targetName, {mappingKind: "exact", name: caught.name, node: handler.catchType, path: `${handlerPath}/catchType`, role: "type"})
485
+ writer.synthetic(")) {\n", "exact catch guard", [handler], [handlerPath])
486
+ if (index + 1 < statement.handlers.length) {
487
+ emitTypeScriptDispatch(writer, statement, index, indent, path, canonicalizeZero)
488
+ writer.synthetic(`${bodyIndent}} else {\n`, "matched error handling", [handler], [handlerPath])
489
+ emitBlock(writer, handler.catchBody, `${bodyIndent} `, `${handlerPath}/catchBody`, canonicalizeZero)
490
+ writer.synthetic(`${bodyIndent}}\n`, "matched error handling close", [handler], [handlerPath])
491
+ } else {
492
+ writer.synthetic(`${bodyIndent} throw `, "unmatched error propagation", [handler], [handlerPath])
493
+ writer.mapped(handler.catchBinding.name, {mappingKind: "exact", node: handler.catchBinding, path: `${handlerPath}/catchBinding`, role: "name"})
494
+ writer.synthetic(`\n${bodyIndent}}\n`, "unmatched error propagation", [handler], [handlerPath])
495
+ emitBlock(writer, handler.catchBody, bodyIndent, `${handlerPath}/catchBody`, canonicalizeZero)
496
+ }
497
+ writer.synthetic(indent, "indentation", [handler], [handlerPath])
498
+ writer.mapped("}", {mappingKind: "anchor", node: handler.catchBody, path: `${handlerPath}/catchBody`})
499
+ }
500
+
501
+ /**
502
+ * Emits the nested re-dispatch of one unmatched caught value to the remaining ordered handlers.
503
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
504
+ * @param {import("../semantic/types.js").TryStatement} statement - Semantic try statement.
505
+ * @param {number} index - Index of the handler whose value is re-dispatched.
506
+ * @param {string} indent - Leading indentation for this dispatch try.
507
+ * @param {string} path - Exact JSON Pointer for the owning try statement.
508
+ * @param {boolean} canonicalizeZero - Whether scalar output canonicalizes zero.
509
+ * @returns {void}
510
+ */
511
+ function emitTypeScriptDispatch(writer, statement, index, indent, path, canonicalizeZero) {
512
+ writer.synthetic(`${indent} try {\n`, "ordered handler dispatch", [statement], [path])
513
+ writer.synthetic(`${indent} throw `, "ordered handler dispatch", [statement], [path])
514
+ writer.mapped(statement.handlers[index].catchBinding.name, {
515
+ mappingKind: "exact", node: statement.handlers[index].catchBinding, path: `${path}/handlers/${index}/catchBinding`, role: "name"
516
+ })
517
+ writer.synthetic(`\n${indent} }`, "ordered handler dispatch", [statement], [path])
518
+ emitTypeScriptHandler(writer, statement, index + 1, `${indent} `, path, canonicalizeZero)
519
+ }
520
+
456
521
  /**
457
522
  * Emits one TypeScript local statement.
458
523
  * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
@@ -4,7 +4,7 @@ import {parser} from "@lezer/java"
4
4
  import {SemantifoldDiagnostic, missingType, unsupportedSyntax} from "../diagnostic.js"
5
5
  import {locationFromOffsets, moduleLocation} from "../semantic/location.js"
6
6
  import {withAdaptedOperation} from "../semantic/operators.js"
7
- import {withParserRanges} from "../semantic/provenance.js"
7
+ import {setParserRanges, withParserRanges} from "../semantic/provenance.js"
8
8
  import {hasOnlyUnicodeScalars} from "../semantic/scalars.js"
9
9
  import {isTask034ResourceProbe} from "../semantic/capabilities.js"
10
10
  import {hasExactEffectSupport} from "../backends/effects.js"
@@ -41,6 +41,8 @@ const referenceMethodHooks = new Set(["clone", "equals", "finalize", "getClass",
41
41
  /**
42
42
  * @typedef JavaConversionContext
43
43
  * @property {Map<string, import("../semantic/types.js").SemanticBindingType>} bindings - Explicitly typed visible bindings.
44
+ * @property {Set<string>} [catchBindingNames] - Lexically visible exact catch binding names.
45
+ * @property {Map<string, import("../semantic/types.js").CatchBinding>} [catchBindingNodes] - Exact catch binding nodes by visible name.
44
46
  * @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
45
47
  * @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
46
48
  * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by source name.
@@ -970,7 +972,7 @@ function convertStatement(statement, filename, source, context) {
970
972
  * @param {string} filename - Source filename.
971
973
  * @param {string} source - Complete source.
972
974
  * @param {JavaConversionContext} context - Typed lexical context.
973
- * @returns {import("../semantic/types.js").RaiseStatement} Semantic raise.
975
+ * @returns {import("../semantic/types.js").RaiseStatement | import("../semantic/types.js").RethrowStatement} Semantic raise or rethrow.
974
976
  */
975
977
  function convertJavaRaise(statement, filename, source, context) {
976
978
  const location = nodeLocation(statement, filename, source)
@@ -980,6 +982,23 @@ function convertJavaRaise(statement, filename, source, context) {
980
982
  const arguments_ = argumentsNode ? structuralChildren(argumentsNode) : []
981
983
  const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
982
984
 
985
+ if (!construction && arguments_.length == 0) {
986
+ const throwNode = requiredChild(statement, "throw", filename, source)
987
+ const rethrown = structuralChildren(statement).find((candidate) => candidate.name == "Identifier")
988
+ const binding = rethrown ? context.bindings.get(nodeText(rethrown, source)) : undefined
989
+
990
+ if (rethrown && binding?.kind == "ErrorType" && context.catchBindingNames?.has(nodeText(rethrown, source))) {
991
+ const bindingNode = context.catchBindingNodes?.get(nodeText(rethrown, source))
992
+
993
+ if (!bindingNode) return unsupportedSyntax("java", "throw of a catch binding without an exact handler binding", location)
994
+ return withParserRanges({
995
+ binding: nodeText(rethrown, source),
996
+ bindingNode,
997
+ kind: /** @type {const} */ ("RethrowStatement"),
998
+ location
999
+ }, {keyword: nodeLocation(throwNode, filename, source)})
1000
+ }
1001
+ }
983
1002
  if (!construction || !typeNode || !argumentsNode || !declaration || arguments_.length != 1 || construction.getChild("ClassBody")) {
984
1003
  return unsupportedSyntax("java", "throw other than exact declared error construction", location)
985
1004
  }
@@ -1009,46 +1028,76 @@ function convertJavaTry(statement, filename, source, context) {
1009
1028
  const location = nodeLocation(statement, filename, source)
1010
1029
  const body = statement.getChild("Block")
1011
1030
  const catches = statement.getChildren("CatchClause")
1031
+ const finalizer = statement.getChild("FinallyClause")
1032
+
1033
+ if (!body || (catches.length == 0 && !finalizer) || statement.getChild("ResourceSpecification")) {
1034
+ return unsupportedSyntax("java", "try without catches or finally and without resources", location)
1035
+ }
1036
+ /** @type {import("../semantic/types.js").CatchHandler[]} */
1037
+ const handlers = []
1038
+ const seenHandlerNames = new Set(context.catchBindingNames ?? [])
1039
+
1040
+ for (const handler of catches) {
1041
+ const parameter = handler.getChild("CatchFormalParameter")
1042
+ const caught = parameter?.getChild("CatchType")
1043
+ const typeNodes = caught ? structuralChildren(caught) : []
1044
+ const definition = parameter?.getChild("Definition")
1045
+ const catchBody = handler.getChild("Block")
1046
+ const typeNode = typeNodes[0]
1047
+ const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
1048
+
1049
+ if (!parameter || !caught || typeNodes.length != 1 || typeNode?.name != "TypeName" || !definition || !catchBody || !declaration ||
1050
+ parameter.getChild("Modifiers")) {
1051
+ return unsupportedSyntax("java", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1052
+ }
1053
+ const name = nodeText(definition, source)
1012
1054
 
1013
- if (!body || catches.length != 1 || statement.getChild("FinallyClause") || statement.getChild("ResourceSpecification")) {
1014
- return unsupportedSyntax("java", "try without one catch and no resources or finally", location)
1015
- }
1016
- const handler = catches[0]
1017
- const parameter = handler.getChild("CatchFormalParameter")
1018
- const caught = parameter?.getChild("CatchType")
1019
- const typeNodes = caught ? structuralChildren(caught) : []
1020
- const definition = parameter?.getChild("Definition")
1021
- const catchBody = handler.getChild("Block")
1022
- const typeNode = typeNodes[0]
1023
- const declaration = typeNode ? context.errorNames.get(nodeText(typeNode, source)) : undefined
1024
-
1025
- if (!parameter || !caught || typeNodes.length != 1 || typeNode?.name != "TypeName" || !definition || !catchBody || !declaration ||
1026
- parameter.getChild("Modifiers")) {
1027
- return unsupportedSyntax("java", "broad, multiple, or malformed catch", nodeLocation(handler, filename, source))
1028
- }
1029
- const caughtLocation = nodeLocation(typeNode, filename, source)
1030
- const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1031
- type: caughtLocation
1032
- })
1033
- const bodyContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1034
- const catchContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1035
- const name = nodeText(definition, source)
1055
+ if (seenHandlerNames.has(name)) {
1056
+ return unsupportedSyntax("java", `catch binding '${name}' shadows an outer handler binding`, nodeLocation(definition, filename, source))
1057
+ }
1058
+ seenHandlerNames.add(name)
1059
+ const caughtLocation = nodeLocation(typeNode, filename, source)
1060
+ const catchType = withParserRanges({declarationId: /** @type {string} */ (declaration.id), kind: /** @type {const} */ ("ErrorType")}, {
1061
+ type: caughtLocation
1062
+ })
1063
+ const catchContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames),
1064
+ catchBindingNames: new Set(seenHandlerNames), catchBindingNodes: new Map(context.catchBindingNodes)}
1036
1065
 
1037
- catchContext.bindings.set(name, catchType)
1038
- return withParserRanges({
1039
- body: convertBlock(body, filename, source, bodyContext),
1040
- catchBinding: withParserRanges({
1066
+ catchContext.bindings.set(name, catchType)
1067
+ const catchBindingNode = withParserRanges({
1041
1068
  kind: /** @type {const} */ ("CatchBinding"),
1042
1069
  location: nodeLocation(definition, filename, source),
1043
1070
  mutable: /** @type {const} */ (false),
1044
1071
  name,
1045
1072
  type: catchType
1046
- }, {name: nodeLocation(definition, filename, source)}),
1047
- catchBody: convertBlock(catchBody, filename, source, catchContext),
1048
- catchType,
1073
+ }, {name: nodeLocation(definition, filename, source)})
1074
+
1075
+ catchContext.catchBindingNodes.set(name, catchBindingNode)
1076
+ handlers.push(withParserRanges({
1077
+ catchBinding: catchBindingNode,
1078
+ catchBody: convertBlock(catchBody, filename, source, catchContext),
1079
+ catchType,
1080
+ kind: /** @type {const} */ ("CatchHandler"),
1081
+ location: nodeLocation(handler, filename, source)
1082
+ }, {catch: nodeLocation(handler, filename, source)}))
1083
+ }
1084
+ const bodyContext = {...context, bindings: new Map(context.bindings), erasedBindingNames: new Set(context.erasedBindingNames)}
1085
+ const semantic = /** @type {import("../semantic/types.js").TryStatement} */ (withParserRanges({
1086
+ body: convertBlock(body, filename, source, bodyContext),
1087
+ handlers,
1049
1088
  kind: /** @type {const} */ ("TryStatement"),
1050
1089
  location
1051
- }, {catch: nodeLocation(handler, filename, source), try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)})
1090
+ }, {try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)}))
1091
+
1092
+ if (finalizer) {
1093
+ const finallyBlock = finalizer.getChild("Block")
1094
+
1095
+ if (!finallyBlock) return unsupportedSyntax("java", "malformed finally block", nodeLocation(finalizer, filename, source))
1096
+ semantic.finallyBody = convertBlock(finallyBlock, filename, source, bodyContext)
1097
+ setParserRanges(semantic, {finally: nodeLocation(finalizer, filename, source),
1098
+ try: nodeLocation(requiredChild(statement, "try", filename, source), filename, source)})
1099
+ }
1100
+ return semantic
1052
1101
  }
1053
1102
 
1054
1103
  /**