semantifold 0.13.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 (59) hide show
  1. package/README.md +1 -0
  2. package/build/src/backends/effects.js +3 -1
  3. package/build/src/backends/php.d.ts.map +1 -1
  4. package/build/src/backends/php.js +56 -3
  5. package/build/src/backends/scalars.js +2 -2
  6. package/build/src/backends/shared.d.ts +2 -1
  7. package/build/src/backends/shared.d.ts.map +1 -1
  8. package/build/src/backends/shared.js +209 -7
  9. package/build/src/backends/writer.d.ts +18 -4
  10. package/build/src/backends/writer.d.ts.map +1 -1
  11. package/build/src/backends/writer.js +40 -4
  12. package/build/src/frontends/java.d.ts +1 -1
  13. package/build/src/frontends/java.d.ts.map +1 -1
  14. package/build/src/frontends/java.js +14 -11
  15. package/build/src/frontends/javascript-typescript.d.ts +1 -1
  16. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  17. package/build/src/frontends/javascript-typescript.js +17 -17
  18. package/build/src/frontends/php.d.ts +5 -1
  19. package/build/src/frontends/php.d.ts.map +1 -1
  20. package/build/src/frontends/php.js +159 -31
  21. package/build/src/frontends/ruby.d.ts +1 -1
  22. package/build/src/frontends/ruby.d.ts.map +1 -1
  23. package/build/src/frontends/ruby.js +12 -9
  24. package/build/src/frontends/types.d.ts +9 -2
  25. package/build/src/frontends/types.d.ts.map +1 -1
  26. package/build/src/frontends/types.js +26 -9
  27. package/build/src/language-registry.d.ts.map +1 -1
  28. package/build/src/language-registry.js +14 -6
  29. package/build/src/mapping.d.ts.map +1 -1
  30. package/build/src/mapping.js +4 -2
  31. package/build/src/semantic/effects.d.ts.map +1 -1
  32. package/build/src/semantic/effects.js +3 -1
  33. package/build/src/semantic/provenance.d.ts.map +1 -1
  34. package/build/src/semantic/provenance.js +37 -4
  35. package/build/src/semantic/types.d.ts +140 -9
  36. package/build/src/semantic/types.d.ts.map +1 -1
  37. package/build/src/semantic/types.js +37 -6
  38. package/build/src/semantic/validate.d.ts +31 -2
  39. package/build/src/semantic/validate.d.ts.map +1 -1
  40. package/build/src/semantic/validate.js +346 -27
  41. package/docs/adr-bounded-subtyping.md +180 -0
  42. package/docs/language-support.md +1 -1
  43. package/package.json +1 -1
  44. package/src/backends/effects.js +1 -0
  45. package/src/backends/php.js +56 -2
  46. package/src/backends/scalars.js +1 -1
  47. package/src/backends/shared.js +210 -6
  48. package/src/backends/writer.js +41 -3
  49. package/src/frontends/java.js +13 -10
  50. package/src/frontends/javascript-typescript.js +16 -16
  51. package/src/frontends/php.js +175 -31
  52. package/src/frontends/ruby.js +11 -8
  53. package/src/frontends/types.js +26 -8
  54. package/src/language-registry.js +13 -5
  55. package/src/mapping.js +3 -1
  56. package/src/semantic/effects.js +1 -0
  57. package/src/semantic/provenance.js +34 -3
  58. package/src/semantic/types.js +39 -5
  59. package/src/semantic/validate.js +377 -26
@@ -12,7 +12,7 @@ import {hasExactEffectSupport} from "../backends/effects.js"
12
12
  import {hasOnlyUnicodeScalars, scalarType} from "../semantic/scalars.js"
13
13
  import {typeContainsAnyVariable} from "../semantic/generics.js"
14
14
  import {requireSourceReturnType, requireSourceScalarType, sourceScalarType} from "./scalars.js"
15
- import {capabilityFunctionSignatures, documentedValueType, instantiatedRecordFieldType, iterationBindingType, iterationOperandType, knownCallParameterTypes, knownCallReturnType, optionalType, orderedMapTypeFromMap, ownedResourceType, preservesGenericOptionalEvidence, recordType, referenceType, sameValueType} from "./types.js"
15
+ import {capabilityFunctionSignatures, documentedValueType, instantiatedRecordFieldType, interfaceType, iterationBindingType, iterationOperandType, knownCallParameterTypes, knownCallReturnType, optionalType, orderedMapTypeFromMap, ownedResourceType, preservesGenericOptionalEvidence, recordType, referenceType, sameValueType} from "./types.js"
16
16
  const parser = new PhpParser.Engine({
17
17
  ast: {withPositions: true},
18
18
  parser: {extractDoc: true, suppressErrors: false}
@@ -43,11 +43,12 @@ const phpBinaryOperations = new Map([
43
43
  * @property {Map<string, import("../semantic/types.js").CatchBinding>} [catchBindingNodes] - Exact catch binding nodes by visible name.
44
44
  * @property {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
45
45
  * @property {import("../semantic/types.js").ClassDeclaration} [currentClass] - Enclosing reference class.
46
+ * @property {Map<string, import("../semantic/types.js").InterfaceDeclaration>} interfaces - Interface declarations by identity.
46
47
  * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
47
48
  * @property {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
48
49
  * @property {Map<string, PhpFunctionSignature>} functions - Explicit module function signatures.
49
50
  * @property {Set<object>} [orderedMapDeclarations] - Exact local declarations selected by parser-owned pair iteration.
50
- * @property {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
51
+ * @property {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.
51
52
  * @property {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
52
53
  * @property {import("../semantic/types.js").SemanticFunctionReturnType | undefined} returnType - Enclosing return type.
53
54
  * @property {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
@@ -325,6 +326,9 @@ function convertExpression(node, filename, source, context, expectedType, preser
325
326
  const declaration = context.recordNames.get(name.name)
326
327
 
327
328
  if (!declaration) return unsupportedSyntax("php", "construction of an unknown nominal class", nodeLocation(construction.what, filename, source))
329
+ if (declaration.kind == "InterfaceDeclaration") {
330
+ return unsupportedSyntax("php", "construction of an interface", nodeLocation(construction.what, filename, source))
331
+ }
328
332
  if (declaration.kind == "ClassDeclaration") {
329
333
  const unsupportedArgument = construction.arguments.find((argument) =>
330
334
  argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
@@ -382,6 +386,9 @@ function convertExpression(node, filename, source, context, expectedType, preser
382
386
  if (receiverType?.kind == "ReferenceType") {
383
387
  return unsupportedSyntax("php", "private reference-class field access", location)
384
388
  }
389
+ if (receiverType?.kind == "InterfaceType") {
390
+ return unsupportedSyntax("php", "interface receiver member access", location)
391
+ }
385
392
  const receiverRecord = receiverType?.kind == "OptionalType" ? receiverType.valueType : receiverType
386
393
  const declaration = receiverRecord?.kind == "RecordType" ? context.records.get(receiverRecord.declarationId) : undefined
387
394
 
@@ -510,8 +517,10 @@ function convertExpression(node, filename, source, context, expectedType, preser
510
517
  const member = lookup.offset.kind == "identifier" ? /** @type {import("php-parser").Identifier} */ (lookup.offset) : undefined
511
518
  const receiverType = knownExpressionType(lookup.what, context)
512
519
 
513
- if (member && typeof member.name == "string" && receiverType?.kind == "ReferenceType") {
514
- const method = context.classes.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == member.name)
520
+ if (member && typeof member.name == "string" && (receiverType?.kind == "ReferenceType" || receiverType?.kind == "InterfaceType")) {
521
+ const method = receiverType.kind == "ReferenceType"
522
+ ? context.classes.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == member.name)
523
+ : context.interfaces.get(receiverType.declarationId)?.methods.find((candidate) => candidate.name == member.name)
515
524
  const unsupportedArgument = call.arguments.find((argument) =>
516
525
  argument.kind == "namedargument" || argument.kind == "variadic" || Reflect.get(argument, "byref") === true)
517
526
 
@@ -632,6 +641,9 @@ function knownExpressionType(node, context) {
632
641
  if (receiver?.kind == "ReferenceType" && member && typeof member.name == "string") {
633
642
  return context.classes.get(receiver.declarationId)?.methods.find((method) => method.name == member.name)?.returnType
634
643
  }
644
+ if (receiver?.kind == "InterfaceType" && member && typeof member.name == "string") {
645
+ return context.interfaces.get(receiver.declarationId)?.methods.find((method) => method.name == member.name)?.returnType
646
+ }
635
647
  }
636
648
  }
637
649
  if (node.kind == "offsetlookup") {
@@ -648,7 +660,9 @@ function knownExpressionType(node, context) {
648
660
 
649
661
  if (declaration?.id) return declaration.kind == "ClassDeclaration"
650
662
  ? {declarationId: declaration.id, kind: "ReferenceType"}
651
- : {declarationId: declaration.id, kind: "RecordType"}
663
+ : declaration.kind == "InterfaceDeclaration"
664
+ ? {declarationId: declaration.id, kind: "InterfaceType"}
665
+ : {declarationId: declaration.id, kind: "RecordType"}
652
666
  }
653
667
  if (node.kind == "propertylookup") {
654
668
  const read = /** @type {import("php-parser").PropertyLookup} */ (node)
@@ -756,7 +770,7 @@ function convertReturn(node, filename, source, context) {
756
770
  * @param {string} name - Local name.
757
771
  * @param {string} filename - Source filename.
758
772
  * @param {string} source - Complete source.
759
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
773
+ * @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.
760
774
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
761
775
  * @returns {{immutable: boolean, type: import("../semantic/types.js").SemanticValueType} | undefined} Metadata when present.
762
776
  */
@@ -876,7 +890,7 @@ function convertLocalStatement(node, filename, source, context) {
876
890
  const inferredType = knownExpressionType(assignment.right, context)
877
891
 
878
892
  if (inferredType?.kind == "RecordType" || inferredType?.kind == "ReferenceType" ||
879
- inferredType?.kind == "OwnedResourceType") {
893
+ inferredType?.kind == "InterfaceType" || inferredType?.kind == "OwnedResourceType") {
880
894
  const initializer = convertExpression(assignment.right, filename, source, context, inferredType)
881
895
 
882
896
  context.bindings.set(variable.name, inferredType)
@@ -1195,7 +1209,7 @@ function convertBlock(node, filename, source, context) {
1195
1209
  * @param {import("../semantic/types.js").SourceLocation} location - Source location.
1196
1210
  * @param {string} filename - Source filename.
1197
1211
  * @param {string} source - Complete source.
1198
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
1212
+ * @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.
1199
1213
  * @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
1200
1214
  */
1201
1215
  function convertType(sourceType, subject, location, filename, source, recordNames = new Map()) {
@@ -1211,9 +1225,11 @@ function convertType(sourceType, subject, location, filename, source, recordName
1211
1225
 
1212
1226
  if (declaration?.id) return declaration.kind == "ClassDeclaration"
1213
1227
  ? referenceType(declaration.id, typeLocation)
1214
- : declaration.kind == "EffectResourceDeclaration"
1215
- ? ownedResourceType(declaration.id, typeLocation)
1216
- : recordType(declaration.id, typeLocation)
1228
+ : declaration.kind == "InterfaceDeclaration"
1229
+ ? interfaceType(declaration.id, typeLocation)
1230
+ : declaration.kind == "EffectResourceDeclaration"
1231
+ ? ownedResourceType(declaration.id, typeLocation)
1232
+ : recordType(declaration.id, typeLocation)
1217
1233
 
1218
1234
  return requireSourceScalarType("php", typeName, subject, location, typeLocation)
1219
1235
  }
@@ -1331,7 +1347,7 @@ function nullableType(valueType, sourceType, ownerStart, filename, source) {
1331
1347
  * @param {import("../semantic/types.js").SourceLocation} location - Function location.
1332
1348
  * @param {string} filename - Source filename.
1333
1349
  * @param {string} source - Complete source.
1334
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} [recordNames] - Nominal declarations by source name.
1350
+ * @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.
1335
1351
  * @returns {import("../semantic/types.js").SemanticFunctionReturnType} Semantic return type.
1336
1352
  */
1337
1353
  function convertReturnType(sourceType, subject, location, filename, source, recordNames = new Map()) {
@@ -1344,9 +1360,11 @@ function convertReturnType(sourceType, subject, location, filename, source, reco
1344
1360
 
1345
1361
  if (declaration?.id) return declaration.kind == "ClassDeclaration"
1346
1362
  ? referenceType(declaration.id, nodeLocation(sourceType, filename, source))
1347
- : declaration.kind == "EffectResourceDeclaration"
1348
- ? ownedResourceType(declaration.id, nodeLocation(sourceType, filename, source))
1349
- : recordType(declaration.id, nodeLocation(sourceType, filename, source))
1363
+ : declaration.kind == "InterfaceDeclaration"
1364
+ ? interfaceType(declaration.id, nodeLocation(sourceType, filename, source))
1365
+ : declaration.kind == "EffectResourceDeclaration"
1366
+ ? ownedResourceType(declaration.id, nodeLocation(sourceType, filename, source))
1367
+ : recordType(declaration.id, nodeLocation(sourceType, filename, source))
1350
1368
 
1351
1369
  return requireSourceReturnType("php", typeName, subject, location, nodeLocation(sourceType, filename, source))
1352
1370
  }
@@ -1356,7 +1374,7 @@ function convertReturnType(sourceType, subject, location, filename, source, reco
1356
1374
  * @param {import("php-parser").Function} node - PHP function node.
1357
1375
  * @param {string} filename - Source filename.
1358
1376
  * @param {string} source - Complete source.
1359
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1377
+ * @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.
1360
1378
  * @param {string} ownerId - Stable function identity.
1361
1379
  * @returns {PhpFunctionSignature} Typed function signature.
1362
1380
  */
@@ -1457,7 +1475,7 @@ function convertFunctionSignature(node, filename, source, recordNames, ownerId)
1457
1475
  * @param {string | undefined} nativeType - Native PHP type name.
1458
1476
  * @param {boolean} nativeNullable - Whether the native carrier has PHP's nullable marker.
1459
1477
  * @param {import("../semantic/types.js").SemanticValueType} documentedType - Exact semantic document type.
1460
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} records - Visible nominal declarations by name.
1478
+ * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration | import("../semantic/types.js").InterfaceDeclaration>} records - Visible nominal declarations by name.
1461
1479
  * @returns {boolean} Whether the native carrier agrees without changing meaning.
1462
1480
  */
1463
1481
  function phpDocumentedNativeTypeMatches(nativeType, nativeNullable, documentedType, records) {
@@ -1482,6 +1500,9 @@ function phpDocumentedNativeTypeMatches(nativeType, nativeNullable, documentedTy
1482
1500
  if (documentedType.kind == "ReferenceType") {
1483
1501
  return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
1484
1502
  }
1503
+ if (documentedType.kind == "InterfaceType") {
1504
+ return nativeType !== undefined && records.get(nativeType)?.id == documentedType.declarationId
1505
+ }
1485
1506
 
1486
1507
  return false
1487
1508
  }
@@ -1491,16 +1512,17 @@ function phpDocumentedNativeTypeMatches(nativeType, nativeNullable, documentedTy
1491
1512
  * @param {import("php-parser").Function} node - PHP function node.
1492
1513
  * @param {PhpFunctionSignature} signature - Preconverted explicit signature.
1493
1514
  * @param {Map<string, PhpFunctionSignature>} functions - Module function signatures.
1494
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} recordNames - Nominal declarations by source name.
1515
+ * @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.
1495
1516
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
1496
1517
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
1518
+ * @param {Map<string, import("../semantic/types.js").InterfaceDeclaration>} interfaces - Interface declarations by identity.
1497
1519
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
1498
1520
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Error declarations by identity.
1499
1521
  * @param {string} filename - Source filename.
1500
1522
  * @param {string} source - Complete source.
1501
1523
  * @returns {import("../semantic/types.js").FunctionDeclaration} Semantic function.
1502
1524
  */
1503
- function convertFunction(node, signature, functions, recordNames, records, classes, errorNames, errors, filename, source) {
1525
+ function convertFunction(node, signature, functions, recordNames, records, classes, interfaces, errorNames, errors, filename, source) {
1504
1526
  if (!node.body) return unsupportedSyntax("php", "function body", signature.location)
1505
1527
  const context = {
1506
1528
  bindings: new Map(signature.parameters.map((parameter) => [parameter.name, parameter.type])),
@@ -1516,7 +1538,7 @@ function convertFunction(node, signature, functions, recordNames, records, class
1516
1538
  }
1517
1539
 
1518
1540
  return withParserRanges({
1519
- body: convertBlock(node.body, filename, source, context),
1541
+ body: convertBlock(node.body, filename, source, {...context, interfaces}),
1520
1542
  kind: "FunctionDeclaration",
1521
1543
  location: signature.location,
1522
1544
  name: signature.name,
@@ -1613,7 +1635,7 @@ function convertPhpError(node, declaration, filename, source) {
1613
1635
  /**
1614
1636
  * Reads one exact public PHP constructor or instance-method signature.
1615
1637
  * @param {import("php-parser").Method} node - Parser-owned method.
1616
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
1638
+ * @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.
1617
1639
  * @param {string} ownerId - Stable callable identity.
1618
1640
  * @param {boolean} constructor - Whether this is the sole constructor.
1619
1641
  * @param {string} filename - Source filename.
@@ -1696,13 +1718,106 @@ function convertPhpClassCallableSignature(node, nominalNames, ownerId, construct
1696
1718
  return {location, name, nameLocation: nodeLocation(nameNode, filename, source), parameters, returnType}
1697
1719
  }
1698
1720
 
1721
+ /**
1722
+ * Converts the signature-only PHP interface profile.
1723
+ * @param {import("php-parser").Class} node - Parser interface.
1724
+ * @param {import("../semantic/types.js").InterfaceDeclaration} declaration - Predeclared interface identity.
1725
+ * @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.
1726
+ * @param {string} filename - Source filename.
1727
+ * @param {string} source - Complete source.
1728
+ * @returns {import("../semantic/types.js").InterfaceDeclaration} Semantic interface.
1729
+ */
1730
+ function convertPhpInterface(node, declaration, nominalNames, filename, source) {
1731
+ const location = nodeLocation(node, filename, source)
1732
+ const parents = /** @type {readonly import("php-parser").Name[]} */ (/** @type {unknown} */ (node.extends ?? []))
1733
+
1734
+ if (node.isAnonymous || node.attrGroups.length) return unsupportedSyntax("php", "malformed interface declaration", location)
1735
+ const parentReferences = parents.map((parent) => {
1736
+ if (parent.kind != "name" || parent.resolution != "uqn" || typeof parent.name != "string") {
1737
+ return unsupportedSyntax("php", "qualified or dynamic interface parent", nodeLocation(parent, filename, source))
1738
+ }
1739
+ const parentDeclaration = nominalNames.get(parent.name)
1740
+
1741
+ if (!parentDeclaration?.id || parentDeclaration.kind != "InterfaceDeclaration") {
1742
+ return unsupportedSyntax("php", `interface parent '${parent.name}' is not a declared interface`, nodeLocation(parent, filename, source))
1743
+ }
1744
+
1745
+ return interfaceType(/** @type {string} */ (parentDeclaration.id), nodeLocation(parent, filename, source))
1746
+ })
1747
+ const methodNodes = /** @type {import("php-parser").Method[]} */ (node.body.filter((member) => member.kind == "method"))
1748
+
1749
+ if (methodNodes.length != node.body.length) {
1750
+ return unsupportedSyntax("php", "interface member outside one signature-only public method",
1751
+ nodeLocation(node.body.find((member) => member.kind != "method") ?? node, filename, source))
1752
+ }
1753
+ declaration.methods = methodNodes.map((method, index) => convertPhpInterfaceMethodSignature(method, nominalNames,
1754
+ `${declaration.id}:method:${index}`, filename, source))
1755
+ const nameNode = typeof node.name == "string" ? node : node.name
1756
+
1757
+ return withParserRanges({
1758
+ ...declaration,
1759
+ ...(parentReferences.length > 0 ? {extends: parentReferences} : {})
1760
+ }, {name: nodeLocation(nameNode, filename, source)})
1761
+ }
1762
+
1763
+ /**
1764
+ * Reads one exact signature-only PHP interface method.
1765
+ * @param {import("php-parser").Method} node - Parser interface method.
1766
+ * @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.
1767
+ * @param {string} ownerId - Stable interface method identity.
1768
+ * @param {string} filename - Source filename.
1769
+ * @param {string} source - Complete source.
1770
+ * @returns {import("../semantic/types.js").InterfaceMethodDeclaration} Semantic interface method.
1771
+ */
1772
+ function convertPhpInterfaceMethodSignature(node, nominalNames, ownerId, filename, source) {
1773
+ const location = nodeLocation(node, filename, source)
1774
+ const name = typeof node.name == "string" ? node.name : node.name.name
1775
+
1776
+ if (node.visibility != "public" || node.isStatic || node.isAbstract || node.isFinal || Reflect.get(node, "isReadonly") ||
1777
+ node.byref || node.nullable || node.attrGroups.length || node.body !== null) {
1778
+ return unsupportedSyntax("php", "interface method outside one public signature-only declaration", location)
1779
+ }
1780
+ const documented = functionDocumentedTypes(/** @type {import("php-parser").Function} */ (/** @type {unknown} */ (node)),
1781
+ filename, source, ownerId)
1782
+
1783
+ if (documented.parameters.size > 0 || documented.returnType || documented.typeParameters?.length) {
1784
+ return unsupportedSyntax("php", "documented interface method type annotation", location)
1785
+ }
1786
+ if (!node.type) return missingType("php", `Interface method '${name}' return`, location)
1787
+ const parameters = node.arguments.map((parameter) => {
1788
+ const parameterLocation = nodeLocation(parameter, filename, source)
1789
+ const parameterName = typeof parameter.name == "string" ? parameter.name : parameter.name.name
1790
+
1791
+ if (parameter.byref || parameter.variadic || parameter.value || parameter.flags || parameter.readonly || parameter.nullable ||
1792
+ parameter.hooks.length || parameter.attrGroups.length) {
1793
+ return unsupportedSyntax("php", "noncanonical interface method parameter", parameterLocation)
1794
+ }
1795
+ if (!parameter.type) return missingType("php", `Parameter '${parameterName}'`, parameterLocation)
1796
+ const type = convertType(parameter.type, `Parameter '${parameterName}'`, parameterLocation, filename, source, nominalNames)
1797
+
1798
+ if (type.kind == "OptionalType") return unsupportedSyntax("php", "optional interface method parameter", parameterLocation)
1799
+ const nameNode = typeof parameter.name == "string" ? parameter : parameter.name
1800
+
1801
+ return withParserRanges({kind: /** @type {const} */ ("Parameter"), location: parameterLocation,
1802
+ name: parameterName, type}, {name: nodeLocation(nameNode, filename, source)})
1803
+ })
1804
+ const returnType = convertReturnType(node.type, `Interface method '${name}' return`, location, filename, source, nominalNames)
1805
+
1806
+ if (returnType.kind == "OptionalType") return unsupportedSyntax("php", "optional interface method return", location)
1807
+ const nameNode = typeof node.name == "string" ? node : node.name
1808
+
1809
+ return withParserRanges({kind: /** @type {const} */ ("InterfaceMethodDeclaration"), location, name, parameters, returnType},
1810
+ {name: nodeLocation(nameNode, filename, source)})
1811
+ }
1812
+
1699
1813
  /**
1700
1814
  * Converts the bounded final/private-field PHP reference-class profile.
1701
1815
  * @param {import("php-parser").Class} node - Parser class.
1702
1816
  * @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
1703
- * @param {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} nominalNames - Visible nominal declarations.
1817
+ * @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.
1704
1818
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
1705
1819
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
1820
+ * @param {Map<string, import("../semantic/types.js").InterfaceDeclaration>} interfaces - Interfaces by identity.
1706
1821
  * @param {Map<string, PhpFunctionSignature>} functions - Top-level signatures.
1707
1822
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Errors by name.
1708
1823
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errors - Errors by identity.
@@ -1710,12 +1825,28 @@ function convertPhpClassCallableSignature(node, nominalNames, ownerId, construct
1710
1825
  * @param {string} source - Complete source.
1711
1826
  * @returns {import("../semantic/types.js").ClassDeclaration} Semantic class.
1712
1827
  */
1713
- function convertPhpReferenceClass(node, declaration, nominalNames, records, classes, functions, errorNames, errors,
1828
+ function convertPhpReferenceClass(node, declaration, nominalNames, records, classes, interfaces, functions, errorNames, errors,
1714
1829
  filename, source) {
1715
1830
  const location = nodeLocation(node, filename, source)
1716
1831
 
1717
- if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.extends || node.implements?.length ||
1718
- node.attrGroups.length) return unsupportedSyntax("php", "reference class inheritance or noncanonical modifier", location)
1832
+ if (node.isAnonymous || !node.isFinal || node.isReadonly || node.isAbstract || node.extends || node.attrGroups.length) {
1833
+ return unsupportedSyntax("php", "reference class inheritance or noncanonical modifier", location)
1834
+ }
1835
+ const implementedReferences = /** @type {readonly import("php-parser").Name[]} */ (/** @type {unknown} */ (node.implements ?? []))
1836
+ .map((implemented) => {
1837
+ const implementedLocation = nodeLocation(implemented, filename, source)
1838
+
1839
+ if (implemented.kind != "name" || implemented.resolution != "uqn" || typeof implemented.name != "string") {
1840
+ return unsupportedSyntax("php", "qualified or dynamic interface implementation", implementedLocation)
1841
+ }
1842
+ const implementedDeclaration = nominalNames.get(implemented.name)
1843
+
1844
+ if (!implementedDeclaration?.id || implementedDeclaration.kind != "InterfaceDeclaration") {
1845
+ return unsupportedSyntax("php", `implemented interface '${implemented.name}' is not a declared interface`, implementedLocation)
1846
+ }
1847
+
1848
+ return interfaceType(/** @type {string} */ (implementedDeclaration.id), implementedLocation)
1849
+ })
1719
1850
  const fieldStatements = node.body.filter((member) => member.kind == "propertystatement")
1720
1851
  const methodNodes = /** @type {import("php-parser").Method[]} */ (node.body.filter((member) => member.kind == "method"))
1721
1852
  const constructors = methodNodes.filter((method) => (typeof method.name == "string" ? method.name : method.name.name) == "__construct")
@@ -1756,8 +1887,9 @@ function convertPhpReferenceClass(node, declaration, nominalNames, records, clas
1756
1887
  parameters: signature.parameters,
1757
1888
  returnType: /** @type {import("../semantic/types.js").SemanticFunctionReturnType} */ (signature.returnType)
1758
1889
  }, {name: signature.nameLocation}))
1890
+ if (implementedReferences.length > 0) declaration.implements = implementedReferences
1759
1891
  const base = {bindings: new Map(), classes, currentClass: declaration, errorNames, errors, functions,
1760
- recordNames: nominalNames, records}
1892
+ interfaces, recordNames: nominalNames, records}
1761
1893
 
1762
1894
  declaration.constructor.body = convertBlock(/** @type {import("php-parser").Block} */ (constructorNode.body), filename, source,
1763
1895
  {...base, bindings: new Map(declaration.constructor.parameters.map((parameter) => [parameter.name, parameter.type])),
@@ -2007,6 +2139,7 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2007
2139
 
2008
2140
  body = namespace.children.filter((node) => node.kind != "usegroup" && !isPhpRequireOnce(node))
2009
2141
  }
2142
+ const interfaceNodes = /** @type {import("php-parser").Class[]} */ (body.filter((node) => node.kind == "interface"))
2010
2143
  const classNodes = /** @type {import("php-parser").Class[]} */ (body.filter((node) => node.kind == "class"))
2011
2144
  const errorNodes = classNodes.filter((node) => node.extends?.kind == "name" &&
2012
2145
  ["RuntimeException", "\\RuntimeException"].includes(node.extends.name))
@@ -2038,7 +2171,12 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2038
2171
  location: nodeLocation(node, filename, source), methods: [],
2039
2172
  name: typeof node.name == "string" ? node.name : node.name.name
2040
2173
  }))
2041
- const recordNames = /** @type {Map<string, import("../semantic/types.js").RecordDeclaration | import("../semantic/types.js").ClassDeclaration | import("../semantic/types.js").EffectResourceDeclaration>} */ (
2174
+ const interfaceDeclarations = interfaceNodes.map((node, index) => ({
2175
+ id: `interface:${index}`, kind: /** @type {const} */ ("InterfaceDeclaration"),
2176
+ location: nodeLocation(node, filename, source), methods: [],
2177
+ name: typeof node.name == "string" ? node.name : node.name.name
2178
+ }))
2179
+ 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>} */ (
2042
2180
  new Map(programContext?.records ?? []))
2043
2181
  for (const resource of capabilities.flatMap(({resources}) => resources)) recordNames.set(resource.name, resource)
2044
2182
  const errorNames = new Map(programContext?.errors ?? [])
@@ -2050,10 +2188,12 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2050
2188
  const errors = errorNodes.map((node, index) => convertPhpError(node, errorDeclarations[index], filename, source))
2051
2189
  for (const declaration of recordDeclarations) recordNames.set(declaration.name, declaration)
2052
2190
  for (const declaration of classDeclarations) recordNames.set(declaration.name, declaration)
2191
+ for (const declaration of interfaceDeclarations) recordNames.set(declaration.name, declaration)
2053
2192
  const recordsById = new Map([...recordNames.values()]
2054
2193
  .filter((declaration) => declaration.kind == "RecordDeclaration")
2055
2194
  .map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
2056
2195
  const classesById = new Map(classDeclarations.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
2196
+ const interfacesById = new Map(interfaceDeclarations.map((declaration) => [/** @type {string} */ (declaration.id), declaration]))
2057
2197
  const records = recordNodes.map((node, index) => convertPhpRecord(node, recordDeclarations[index],
2058
2198
  /** @type {Map<string, import("../semantic/types.js").RecordDeclaration>} */ (/** @type {unknown} */ (recordNames)), filename, source))
2059
2199
  const functionNodes = /** @type {import("php-parser").Function[]} */ (body.filter((node) => node.kind == "function"))
@@ -2068,12 +2208,14 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2068
2208
  ...(declaration.typeParameters ? {typeParameters: declaration.typeParameters} : {})
2069
2209
  }]))
2070
2210
  for (const signature of signatures) functionSignatures.set(signature.name, signature)
2211
+ const interfaces = interfaceNodes.map((node, index) => convertPhpInterface(node, interfaceDeclarations[index],
2212
+ recordNames, filename, source))
2071
2213
  const classes = referenceClassNodes.map((node, index) => convertPhpReferenceClass(node, classDeclarations[index],
2072
- recordNames, recordsById, classesById, functionSignatures, errorNames, errorsById, filename, source))
2214
+ recordNames, recordsById, classesById, interfacesById, functionSignatures, errorNames, errorsById, filename, source))
2073
2215
  const functions = functionNodes.map((node, index) =>
2074
- convertFunction(node, signatures[index], functionSignatures, recordNames, recordsById, classesById, errorNames, errorsById,
2075
- filename, source))
2076
- const executableNodes = body.filter((node) => !["class", "function", "declare", "noop"].includes(node.kind))
2216
+ convertFunction(node, signatures[index], functionSignatures, recordNames, recordsById, classesById, interfacesById,
2217
+ errorNames, errorsById, filename, source))
2218
+ const executableNodes = body.filter((node) => !["class", "function", "interface", "declare", "noop"].includes(node.kind))
2077
2219
  const location = moduleLocation(filename, source)
2078
2220
 
2079
2221
  if (programContext && !programContext.isEntry && executableNodes.length > 0) {
@@ -2090,6 +2232,7 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2090
2232
  errorNames,
2091
2233
  errors: errorsById,
2092
2234
  functions: functionSignatures,
2235
+ interfaces: interfacesById,
2093
2236
  orderedMapDeclarations: orderedMapDeclarationsForPhp(executableNodes),
2094
2237
  recordNames,
2095
2238
  records: recordsById,
@@ -2115,6 +2258,7 @@ export function parsePhp({capabilities = [], filename, source, program: programC
2115
2258
  location,
2116
2259
  ...(errors.length > 0 ? {errors} : {}),
2117
2260
  ...(classes.length > 0 ? {classes} : {}),
2261
+ ...(interfaces.length > 0 ? {interfaces} : {}),
2118
2262
  ...(records.length > 0 ? {records} : {})
2119
2263
  }
2120
2264
  }
@@ -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>, 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 */
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
  */
@@ -1301,7 +1304,7 @@ function isImmediateCommentGap(gap) {
1301
1304
  * @param {import("../semantic/types.js").SourceLocation} location - Source location.
1302
1305
  * @param {string} source - Complete parser input.
1303
1306
  * @param {import("../semantic/types.js").SourceLocation} [typeLocation] - Exact type comment token location.
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.
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.
1305
1308
  * @param {Map<string, import("../semantic/types.js").TypeParameter>} [typeParameters] - Declaration-scoped parameters.
1306
1309
  * @returns {import("../semantic/types.js").SemanticValueType} Semantic type.
1307
1310
  */
@@ -1315,7 +1318,7 @@ function convertType(sourceType, subject, location, source, typeLocation = locat
1315
1318
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Prism comments.
1316
1319
  * @param {string} filename - Source filename.
1317
1320
  * @param {string} source - Complete source.
1318
- * @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.
1319
1322
  * @param {string} ownerId - Stable function identity.
1320
1323
  * @param {boolean} [allowBuiltinOutput] - Whether compiler authority admits the exact output facade function.
1321
1324
  * @returns {RubyFunctionSignature} Semantic signature.
@@ -1380,7 +1383,7 @@ function convertFunctionSignature(node, comments, filename, source, recordNames,
1380
1383
  * @param {DefNode} node - Prism definition.
1381
1384
  * @param {RubyFunctionSignature} signature - Converted signature.
1382
1385
  * @param {Map<string, RubyFunctionSignature>} functions - Module signatures.
1383
- * @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.
1384
1387
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Record declarations by identity.
1385
1388
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Reference classes by identity.
1386
1389
  * @param {Map<string, import("../semantic/types.js").ErrorDeclaration>} errorNames - Error declarations by source name.
@@ -1491,7 +1494,7 @@ function convertRubyError(node, declaration, filename, source) {
1491
1494
  * Converts the bounded Ruby reference-class profile.
1492
1495
  * @param {ClassNode} node - Prism class declaration.
1493
1496
  * @param {import("../semantic/types.js").ClassDeclaration} declaration - Predeclared class identity.
1494
- * @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.
1495
1498
  * @param {Map<string, import("../semantic/types.js").RecordDeclaration>} records - Records by identity.
1496
1499
  * @param {Map<string, import("../semantic/types.js").ClassDeclaration>} classes - Classes by identity.
1497
1500
  * @param {Map<string, RubyFunctionSignature>} functions - Top-level signatures.
@@ -1607,7 +1610,7 @@ function convertRubyReferenceClass(node, declaration, nominalNames, records, cla
1607
1610
  * Converts one exact typed-reader/initializer/freeze Ruby record profile.
1608
1611
  * @param {ClassNode} node - Prism class declaration.
1609
1612
  * @param {import("../semantic/types.js").RecordDeclaration} declaration - Predeclared nominal identity.
1610
- * @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.
1611
1614
  * @param {import("@ruby/prism/src/deserialize.js").Comment[]} comments - Parser-owned comments.
1612
1615
  * @param {string} filename - Source filename.
1613
1616
  * @param {string} source - Complete source.
@@ -1894,7 +1897,7 @@ export function parseRuby({capabilities = [], filename, source, program}) {
1894
1897
  classDeclarations.findIndex((candidate) => candidate.name == declaration.name) != index)
1895
1898
 
1896
1899
  if (reopened) return unsupportedSyntax("ruby", `reference class reopening '${reopened.name}'`, reopened.location)
1897
- 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>} */ (
1898
1901
  new Map(program?.records ?? []))
1899
1902
  for (const [name, declaration] of program?.classes ?? []) recordNames.set(name, declaration)
1900
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))