semantifold 0.13.0 → 0.15.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 (66) hide show
  1. package/README.md +2 -1
  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/python.js +11 -7
  6. package/build/src/backends/scalars.js +2 -2
  7. package/build/src/backends/shared.d.ts +2 -1
  8. package/build/src/backends/shared.d.ts.map +1 -1
  9. package/build/src/backends/shared.js +210 -8
  10. package/build/src/backends/writer.d.ts +18 -4
  11. package/build/src/backends/writer.d.ts.map +1 -1
  12. package/build/src/backends/writer.js +40 -4
  13. package/build/src/frontends/java.d.ts +1 -1
  14. package/build/src/frontends/java.d.ts.map +1 -1
  15. package/build/src/frontends/java.js +14 -11
  16. package/build/src/frontends/javascript-typescript.d.ts +1 -1
  17. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  18. package/build/src/frontends/javascript-typescript.js +17 -17
  19. package/build/src/frontends/php.d.ts +5 -1
  20. package/build/src/frontends/php.d.ts.map +1 -1
  21. package/build/src/frontends/php.js +159 -31
  22. package/build/src/frontends/python.d.ts.map +1 -1
  23. package/build/src/frontends/python.js +37 -7
  24. package/build/src/frontends/ruby.d.ts +1 -1
  25. package/build/src/frontends/ruby.d.ts.map +1 -1
  26. package/build/src/frontends/ruby.js +12 -9
  27. package/build/src/frontends/types.d.ts +9 -2
  28. package/build/src/frontends/types.d.ts.map +1 -1
  29. package/build/src/frontends/types.js +26 -9
  30. package/build/src/language-registry.d.ts.map +1 -1
  31. package/build/src/language-registry.js +30 -6
  32. package/build/src/mapping.d.ts.map +1 -1
  33. package/build/src/mapping.js +4 -2
  34. package/build/src/semantic/effects.d.ts.map +1 -1
  35. package/build/src/semantic/effects.js +3 -1
  36. package/build/src/semantic/provenance.d.ts.map +1 -1
  37. package/build/src/semantic/provenance.js +37 -4
  38. package/build/src/semantic/types.d.ts +140 -9
  39. package/build/src/semantic/types.d.ts.map +1 -1
  40. package/build/src/semantic/types.js +37 -6
  41. package/build/src/semantic/validate.d.ts +31 -2
  42. package/build/src/semantic/validate.d.ts.map +1 -1
  43. package/build/src/semantic/validate.js +347 -28
  44. package/docs/adr-bounded-subtyping.md +180 -0
  45. package/docs/goals.md +1 -1
  46. package/docs/language-support.md +5 -4
  47. package/docs/testing.md +2 -2
  48. package/package.json +1 -1
  49. package/src/backends/effects.js +1 -0
  50. package/src/backends/php.js +56 -2
  51. package/src/backends/python.js +10 -4
  52. package/src/backends/scalars.js +1 -1
  53. package/src/backends/shared.js +211 -7
  54. package/src/backends/writer.js +41 -3
  55. package/src/frontends/java.js +13 -10
  56. package/src/frontends/javascript-typescript.js +16 -16
  57. package/src/frontends/php.js +175 -31
  58. package/src/frontends/python.js +39 -5
  59. package/src/frontends/ruby.js +11 -8
  60. package/src/frontends/types.js +26 -8
  61. package/src/language-registry.js +29 -5
  62. package/src/mapping.js +3 -1
  63. package/src/semantic/effects.js +1 -0
  64. package/src/semantic/provenance.js +34 -3
  65. package/src/semantic/types.js +39 -5
  66. package/src/semantic/validate.js +378 -27
@@ -8,13 +8,27 @@ import {parserRangeFor} from "./provenance.js"
8
8
  import {validateEffectGraph} from "./effects.js"
9
9
  import {validateResourceLifetimes} from "./lifetime.js"
10
10
 
11
- const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "c", "dart", "zig"])
11
+ const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "java", "c", "dart", "zig", "python"])
12
12
 
13
13
  /** @typedef {Map<string, import("./types.js").RecordDeclaration>} RecordRegistry */
14
14
  /** @typedef {Map<string, import("./types.js").ClassDeclaration>} ClassRegistry */
15
+ /** @typedef {Map<string, import("./types.js").InterfaceDeclaration>} InterfaceRegistry */
15
16
  /** @typedef {Map<string, import("./types.js").ErrorDeclaration>} ErrorRegistry */
17
+ /**
18
+ * @typedef InterfaceContractMember
19
+ * @property {string} interfaceId - Declaring interface identity.
20
+ * @property {string} methodId - Declaring interface method identity.
21
+ * @property {string} name - Contract method name.
22
+ * @property {import("./types.js").Parameter[]} parameters - Source-ordered contract parameters.
23
+ * @property {import("./types.js").SemanticFunctionReturnType} returnType - Contract return type.
24
+ * @property {import("./types.js").SourceLocation} location - Declaring method source location.
25
+ */
16
26
  /** @type {WeakMap<RecordRegistry, ClassRegistry>} */
17
27
  const referenceClassesByRecordRegistry = new WeakMap()
28
+ /** @type {WeakMap<RecordRegistry, InterfaceRegistry>} */
29
+ const interfacesByRecordRegistry = new WeakMap()
30
+ /** @type {WeakMap<import("./types.js").InterfaceDeclaration, InterfaceContractMember[]>} */
31
+ const flattenedInterfaceCache = new WeakMap()
18
32
  /** @type {WeakMap<RecordRegistry, Set<string>>} */
19
33
  const resourceIdsByRecordRegistry = new WeakMap()
20
34
 
@@ -52,7 +66,7 @@ const resourceIdsByRecordRegistry = new WeakMap()
52
66
  * Enforces the coherent release-candidate semantic subset after adaptation.
53
67
  * @param {import("./types.js").SemanticModule} module - Adapted semantic module.
54
68
  * @param {import("./types.js").SemanticLanguage} language - Source language.
55
- * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Program imports visible during validation.
69
+ * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, interfaces?: Map<string, import("./types.js").InterfaceDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Program imports visible during validation.
56
70
  * @returns {import("./types.js").SemanticModule} Validated module.
57
71
  */
58
72
  export function validateParsedModule(module, language, visible = {}) {
@@ -110,7 +124,7 @@ function normalizeParsedOwnedReferences(module, visibleClasses = new Map()) {
110
124
  * Validates scalar types and bindings for a caller-supplied semantic module before emission.
111
125
  * @param {import("./types.js").SemanticModule} module - Semantic module.
112
126
  * @param {import("./types.js").BackendLanguage} language - Backend language or binary target.
113
- * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Resolved program imports.
127
+ * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, interfaces?: Map<string, import("./types.js").InterfaceDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Resolved program imports.
114
128
  * @returns {void}
115
129
  */
116
130
  export function validateBackendTypes(module, language, visible = {}) {
@@ -194,12 +208,15 @@ function validateBlockShape(block, detail, fail) {
194
208
  * @param {string} language - Source or backend language identity.
195
209
  * @param {SemanticFail} fail - Diagnostic callback.
196
210
  * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
197
- * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Program imports visible during validation.
211
+ * @param {{classes?: Map<string, import("./types.js").ClassDeclaration>, functions?: Map<string, import("./types.js").FunctionDeclaration>, interfaces?: Map<string, import("./types.js").InterfaceDeclaration>, records?: Map<string, import("./types.js").RecordDeclaration>, errors?: Map<string, import("./types.js").ErrorDeclaration>, callEffects?: Map<string, Set<string>>}} [visible] - Program imports visible during validation.
198
212
  * @returns {void}
199
213
  */
200
214
  function validateModuleTypes(module, language, fail, normalizeOperations, visible = {}) {
201
- const classes = registerClassDeclarations(module.classes ?? [], fail, normalizeOperations, visible.classes)
215
+ const interfaces = registerInterfaceDeclarations(module.interfaces ?? [], fail, normalizeOperations, visible.interfaces)
216
+ const classes = registerClassDeclarations(module.classes ?? [], fail, normalizeOperations, visible.classes, interfaces)
202
217
  const records = validateRecordDeclarations(module.records ?? [], fail, normalizeOperations, visible.records, classes)
218
+
219
+ interfacesByRecordRegistry.set(records, interfaces)
203
220
  const errors = validateErrorDeclarations(module.errors ?? [], fail, normalizeOperations, visible.errors)
204
221
  /** @type {Map<string, import("./types.js").FunctionDeclaration>} */
205
222
  const functions = new Map(visible.functions ?? [])
@@ -237,6 +254,7 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
237
254
 
238
255
  const reservedValueNames = new Set([
239
256
  ...functions.keys(),
257
+ ...[...interfaces.values()].map((declaration) => declaration.name),
240
258
  ...[...records.values()].map((declaration) => declaration.name),
241
259
  ...[...classes.values()].map((declaration) => declaration.name),
242
260
  ...[...errors.values()].map((declaration) => declaration.name),
@@ -245,7 +263,7 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
245
263
 
246
264
  const nominalNames = new Set()
247
265
 
248
- for (const declaration of [...module.records ?? [], ...module.classes ?? [], ...module.errors ?? []]) {
266
+ for (const declaration of [...module.interfaces ?? [], ...module.records ?? [], ...module.classes ?? [], ...module.errors ?? []]) {
249
267
  if (nominalNames.has(declaration.name)) {
250
268
  fail("DUPLICATE_BINDING", `Duplicate nominal declaration '${declaration.name}'.`, roleLocation(declaration, "name"))
251
269
  }
@@ -260,6 +278,9 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
260
278
  const localFunctionNames = new Set(module.functions.map(({name}) => name))
261
279
  const callEffects = inferCallEffects(functions, visible.callEffects, localFunctionNames)
262
280
 
281
+ for (const declaration of module.classes ?? []) {
282
+ validateInterfaceContracts(declaration, interfaces, records, fail, normalizeOperations)
283
+ }
263
284
  for (const declaration of module.classes ?? []) {
264
285
  validateClassDeclaration(declaration, functions, records, errors, callEffects, reservedValueNames, fail,
265
286
  normalizeOperations, loopOwners)
@@ -351,9 +372,10 @@ export function moduleUncheckedErrorEffects(module, visible = {}) {
351
372
  * @param {SemanticFail} fail - Diagnostic callback.
352
373
  * @param {boolean} normalizeOperations - Whether parser-authored identities are assigned.
353
374
  * @param {Map<string, import("./types.js").ClassDeclaration>} [visibleClasses] - Imported classes by identity.
375
+ * @param {Map<string, import("./types.js").InterfaceDeclaration>} [interfaces] - Interface declarations by identity.
354
376
  * @returns {ClassRegistry} Classes by stable identity.
355
377
  */
356
- function registerClassDeclarations(declarations, fail, normalizeOperations, visibleClasses = new Map()) {
378
+ function registerClassDeclarations(declarations, fail, normalizeOperations, visibleClasses = new Map(), interfaces = new Map()) {
357
379
  if (!Array.isArray(declarations)) {
358
380
  return fail("TYPE_MISMATCH", "Reference class declarations must be an ordered array.", /** @type {never} */ (undefined))
359
381
  }
@@ -441,11 +463,235 @@ function registerClassDeclarations(declarations, fail, normalizeOperations, visi
441
463
  }
442
464
  methodNames.add(method.name)
443
465
  }
466
+ if (declaration.implements !== undefined) {
467
+ if (!Array.isArray(declaration.implements)) {
468
+ fail("UNKNOWN_INTERFACE", `Class '${declaration.name}' has a malformed interface implementation set.`, declaration.location)
469
+ }
470
+ for (const implemented of declaration.implements) {
471
+ if (!implemented || implemented.kind != "InterfaceType" || typeof implemented.declarationId != "string" ||
472
+ !interfaces.has(implemented.declarationId)) {
473
+ fail("UNKNOWN_INTERFACE", `Class '${declaration.name}' implements unknown interface '${String(implemented?.declarationId)}'.`,
474
+ declaration.location)
475
+ }
476
+ }
477
+ }
444
478
  }
445
479
 
446
480
  return classes
447
481
  }
448
482
 
483
+ /**
484
+ * Checks the exact total override contract for one implementing class.
485
+ * @param {import("./types.js").ClassDeclaration} declaration - Class declaration.
486
+ * @param {InterfaceRegistry} interfaces - Interfaces by identity.
487
+ * @param {RecordRegistry} records - Record declarations by identity.
488
+ * @param {SemanticFail} fail - Diagnostic callback.
489
+ * @param {boolean} normalizeOperations - Whether resolved facts are assigned.
490
+ * @returns {void}
491
+ */
492
+ function validateInterfaceContracts(declaration, interfaces, records, fail, normalizeOperations) {
493
+ const implemented = declaration.implements ?? []
494
+
495
+ if (implemented.length == 0) return
496
+ /** @type {InterfaceContractMember[]} */
497
+ const required = []
498
+
499
+ for (const reference of implemented) {
500
+ for (const member of flattenedInterfaceMembers(reference.declarationId, interfaces, fail, new Set(), declaration.location)) {
501
+ if (required.some((candidate) => candidate.name == member.name)) continue
502
+ required.push(member)
503
+ }
504
+ }
505
+ /** @type {Map<string, {parameterTypes: import("./types.js").SemanticValueType[], returnType: import("./types.js").SemanticFunctionReturnType}>} */
506
+ const contractSignatures = new Map()
507
+
508
+ for (const member of required) {
509
+ contractSignatures.set(member.name, {
510
+ parameterTypes: member.parameters.map((parameter) =>
511
+ validateValueTypeReference(parameter.type, parameter.location, fail, undefined, records)),
512
+ returnType: validateReturnTypeReference(member.returnType, member.location, fail, records)
513
+ })
514
+ }
515
+ /** @type {Map<string, InterfaceContractMember>} */
516
+ const satisfied = new Map()
517
+
518
+ for (const method of declaration.methods) {
519
+ const member = required.find((candidate) => candidate.name == method.name)
520
+
521
+ if (!member) continue
522
+ if (satisfied.has(member.name)) {
523
+ fail("INTERFACE_MEMBER_DUPLICATE", `Class '${declaration.name}' supplies method '${method.name}' more than once for interface contract membership.`,
524
+ roleLocation(method, "name"))
525
+ }
526
+ const methodParameterTypes = method.parameters.map((parameter) =>
527
+ validateValueTypeReference(parameter.type, parameter.location, fail, undefined, records))
528
+ const methodReturnType = validateReturnTypeReference(method.returnType, method.location, fail, records)
529
+
530
+ const contract = contractSignatures.get(member.name)
531
+
532
+ if (!contract || methodParameterTypes.length != contract.parameterTypes.length ||
533
+ methodParameterTypes.some((type, index) => !sameType(type, contract.parameterTypes[index])) ||
534
+ !sameType(methodReturnType, contract.returnType)) {
535
+ fail("INTERFACE_MEMBER_SIGNATURE_MISMATCH",
536
+ `Class '${declaration.name}' method '${method.name}' must exactly match interface '${interfaces.get(member.interfaceId)?.name}' signature ${interfaceSignatureDescription(member)}.`,
537
+ roleLocation(method, "name"))
538
+ }
539
+ satisfied.set(member.name, member)
540
+ if (normalizeOperations) method.implements = {interfaceId: member.interfaceId, methodId: member.methodId}
541
+ }
542
+ for (const member of required) {
543
+ if (satisfied.has(member.name)) continue
544
+ fail("INTERFACE_MEMBER_MISSING", `Class '${declaration.name}' does not implement interface '${interfaces.get(member.interfaceId)?.name}' method '${member.name}'.`,
545
+ declaration.location)
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Formats one flattened interface signature for diagnostics.
551
+ * @param {InterfaceContractMember} member - Flattened member.
552
+ * @returns {string} Stable signature spelling.
553
+ */
554
+ function interfaceSignatureDescription(member) {
555
+ return `${member.name}(${member.parameters.map((parameter) => typeDescription(parameter.type)).join(", ")}): ${typeDescription(member.returnType)}`
556
+ }
557
+
558
+ /**
559
+ * Registers interface declarations, their closed extends DAG, and their flattened signature sets.
560
+ * @param {unknown} declarations - Candidate ordered interface declarations.
561
+ * @param {SemanticFail} fail - Diagnostic callback.
562
+ * @param {boolean} normalizeOperations - Whether parser-authored identities are assigned.
563
+ * @param {InterfaceRegistry} [visibleInterfaces] - Imported interface declarations by identity.
564
+ * @returns {InterfaceRegistry} Validated interfaces by identity.
565
+ */
566
+ function registerInterfaceDeclarations(declarations, fail, normalizeOperations, visibleInterfaces = new Map()) {
567
+ if (!Array.isArray(declarations)) {
568
+ return fail("TYPE_MISMATCH", "Interface declarations must be an ordered array.", /** @type {never} */ (undefined))
569
+ }
570
+ /** @type {InterfaceRegistry} */
571
+ const interfaces = new Map(visibleInterfaces)
572
+ const names = new Set([...visibleInterfaces.values()].map(({name}) => name))
573
+
574
+ for (let interfaceIndex = 0; interfaceIndex < declarations.length; interfaceIndex += 1) {
575
+ const declaration = /** @type {import("./types.js").InterfaceDeclaration} */ (declarations[interfaceIndex])
576
+
577
+ if (!declaration || declaration.kind != "InterfaceDeclaration" || !Array.isArray(declaration.methods) ||
578
+ (declaration.extends !== undefined && !Array.isArray(declaration.extends))) {
579
+ return fail("DUPLICATE_INTERFACE", "Malformed or invalid interface declaration.", declaration?.location)
580
+ }
581
+ if (normalizeOperations) declaration.id = `interface:${interfaceIndex}`
582
+ if (typeof declaration.id != "string" || !/^(?:[a-z][a-z0-9._-]*#)?interface:[0-9]+$/u.test(declaration.id) ||
583
+ interfaces.has(declaration.id) || typeof declaration.name != "string" || declaration.name.length == 0 ||
584
+ names.has(declaration.name)) {
585
+ fail("DUPLICATE_INTERFACE", `Duplicate or invalid interface '${String(declaration.name)}'.`, roleLocation(declaration, "name"))
586
+ }
587
+ names.add(declaration.name)
588
+ interfaces.set(declaration.id, declaration)
589
+
590
+ const methodNames = new Set()
591
+
592
+ for (let methodIndex = 0; methodIndex < declaration.methods.length; methodIndex += 1) {
593
+ const method = declaration.methods[methodIndex]
594
+ const expectedId = `${declaration.id}:method:${methodIndex}`
595
+
596
+ if (!method || method.kind != "InterfaceMethodDeclaration" || !Array.isArray(method.parameters) ||
597
+ Reflect.get(method, "body") !== undefined) {
598
+ fail("INTERFACE_MEMBER_DUPLICATE", "Malformed or invalid interface method declaration.", method?.location ?? declaration.location)
599
+ }
600
+ if (normalizeOperations) method.id = expectedId
601
+ if (method.id != expectedId || typeof method.name != "string" || method.name.length == 0 || methodNames.has(method.name)) {
602
+ fail("INTERFACE_MEMBER_DUPLICATE", `Duplicate or invalid interface method '${String(method.name)}'.`, roleLocation(method, "name"))
603
+ }
604
+ methodNames.add(method.name)
605
+ }
606
+ }
607
+
608
+ for (const declaration of interfaces.values()) {
609
+ for (const parent of declaration.extends ?? []) {
610
+ if (!parent || parent.kind != "InterfaceType" || typeof parent.declarationId != "string" ||
611
+ !interfaces.has(parent.declarationId)) {
612
+ fail("UNKNOWN_INTERFACE", `Unknown interface parent '${String(parent?.declarationId)}'.`, declaration.location)
613
+ }
614
+ }
615
+ flattenedInterfaceMembers(/** @type {string} */ (declaration.id), interfaces, fail)
616
+ }
617
+
618
+ return interfaces
619
+ }
620
+
621
+ /**
622
+ * Flattens one interface's transitive signature set: parents first in declaration order, then its own methods.
623
+ * @param {string} interfaceId - Interface identity.
624
+ * @param {InterfaceRegistry} interfaces - Interfaces by identity.
625
+ * @param {SemanticFail} fail - Diagnostic callback.
626
+ * @param {Set<string>} [active] - Active extends path.
627
+ * @param {import("./types.js").SourceLocation} [fallbackLocation] - Location for a missing-parent diagnostic.
628
+ * @returns {InterfaceContractMember[]} Flattened members.
629
+ */
630
+ function flattenedInterfaceMembers(interfaceId, interfaces, fail, active = new Set(), fallbackLocation = undefined) {
631
+ const declaration = interfaces.get(interfaceId)
632
+
633
+ if (!declaration) return fail("UNKNOWN_INTERFACE", `Unknown interface declaration '${interfaceId}'.`, /** @type {import("./types.js").SourceLocation} */ (fallbackLocation))
634
+ if (active.has(interfaceId)) {
635
+ return fail("INTERFACE_CYCLE", `Interface '${declaration.name}' extends itself through a cycle.`, declaration.location)
636
+ }
637
+ const cached = flattenedInterfaceCache.get(declaration)
638
+
639
+ if (cached) return cached
640
+ active.add(interfaceId)
641
+ /** @type {InterfaceContractMember[]} */
642
+ const members = []
643
+
644
+ for (const parent of declaration.extends ?? []) {
645
+ for (const member of flattenedInterfaceMembers(parent.declarationId, interfaces, fail, active)) {
646
+ const inherited = members.find((candidate) => candidate.name == member.name)
647
+
648
+ if (inherited) {
649
+ if (!sameInterfaceSignature(inherited, member)) {
650
+ fail("INTERFACE_MEMBER_DUPLICATE", `Interface '${declaration.name}' inherits conflicting signatures for '${member.name}'.`,
651
+ declaration.location)
652
+ }
653
+ continue
654
+ }
655
+ members.push(member)
656
+ }
657
+ }
658
+ for (const method of declaration.methods) {
659
+ const inherited = members.find((candidate) => candidate.name == method.name)
660
+
661
+ if (inherited) {
662
+ if (!sameInterfaceSignature(inherited, method)) {
663
+ fail("INTERFACE_MEMBER_DUPLICATE", `Interface '${declaration.name}' redeclares inherited method '${method.name}' with a different signature.`,
664
+ roleLocation(method, "name"))
665
+ }
666
+ continue
667
+ }
668
+ members.push({
669
+ interfaceId: /** @type {string} */ (declaration.id),
670
+ location: method.location,
671
+ methodId: /** @type {string} */ (method.id),
672
+ name: method.name,
673
+ parameters: method.parameters,
674
+ returnType: method.returnType
675
+ })
676
+ }
677
+ active.delete(interfaceId)
678
+ flattenedInterfaceCache.set(declaration, members)
679
+
680
+ return members
681
+ }
682
+
683
+ /**
684
+ * Compares two flattened interface signatures exactly.
685
+ * @param {{parameters: import("./types.js").Parameter[], returnType: import("./types.js").SemanticFunctionReturnType}} left - First signature.
686
+ * @param {{parameters: import("./types.js").Parameter[], returnType: import("./types.js").SemanticFunctionReturnType}} right - Second signature.
687
+ * @returns {boolean} Whether the signatures are identical.
688
+ */
689
+ function sameInterfaceSignature(left, right) {
690
+ return left.parameters.length == right.parameters.length &&
691
+ left.parameters.every((parameter, index) => sameTypeIdentity(typeIdentity(parameter.type), typeIdentity(right.parameters[index].type))) &&
692
+ sameTypeIdentity(typeIdentity(left.returnType), typeIdentity(right.returnType))
693
+ }
694
+
449
695
  /**
450
696
  * Validates exact construction, private state, and method bodies for one class.
451
697
  * @param {import("./types.js").ClassDeclaration} declaration - Class declaration.
@@ -774,11 +1020,11 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
774
1020
  )
775
1021
 
776
1022
  scope.pending.delete(statement.name)
777
- if (!sameType(initializerType, declaredType)) {
1023
+ if (!assignableType(initializerType, declaredType, records)) {
778
1024
  if (initializerType.kind == "OptionalType" && declaredType.kind != "OptionalType") {
779
1025
  fail("FAILURE_ABSENCE_CONFLATION", "Optional absence cannot be coerced into a value or typed failure.", statement.initializer.location)
780
1026
  }
781
- fail("TYPE_MISMATCH", `Initializer type ${typeDescription(initializerType)}; expected ${typeDescription(declaredType)}.`, statement.initializer.location)
1027
+ fail(nominalMismatchCode(declaredType), `Initializer type ${typeDescription(initializerType)}; expected ${typeDescription(declaredType)}.`, statement.initializer.location)
782
1028
  }
783
1029
  declareBinding(statement.name, {
784
1030
  knownValue: knownValueForExpression(statement.initializer, scope),
@@ -804,8 +1050,8 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
804
1050
  if (!binding.mutable) {
805
1051
  fail("IMMUTABLE_ASSIGNMENT", `Cannot assign to immutable binding '${statement.target.name}'.`, statement.target.location)
806
1052
  }
807
- if (!sameType(expressionType, binding.type)) {
808
- fail("TYPE_MISMATCH", `Assignment type ${typeDescription(expressionType)}; expected ${typeDescription(binding.type)}.`, statement.expression.location)
1053
+ if (!assignableType(expressionType, binding.type, records)) {
1054
+ fail(nominalMismatchCode(binding.type), `Assignment type ${typeDescription(expressionType)}; expected ${typeDescription(binding.type)}.`, statement.expression.location)
809
1055
  }
810
1056
  binding.knownValue = knownValueForExpression(statement.expression, scope)
811
1057
  scope.presenceProofs.delete(binding)
@@ -902,8 +1148,8 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
902
1148
  returnType
903
1149
  )
904
1150
 
905
- if (!sameType(actualType, returnType)) {
906
- fail("TYPE_MISMATCH", `Return type ${typeDescription(actualType)}; expected ${typeDescription(returnType)}.`, statement.expression.location)
1151
+ if (!assignableType(actualType, returnType, records)) {
1152
+ fail(nominalMismatchCode(returnType), `Return type ${typeDescription(actualType)}; expected ${typeDescription(returnType)}.`, statement.expression.location)
907
1153
  }
908
1154
  }
909
1155
  if (statement.expression) addExpressionEffects(raises, statement.expression, callEffects)
@@ -1623,6 +1869,14 @@ function validateTypeReference(type, location, fail, seen = new Set(), records =
1623
1869
 
1624
1870
  return candidate
1625
1871
  }
1872
+ if (candidate.kind == "InterfaceType") {
1873
+ if (Object.keys(candidate).filter((key) => key != "sourceProvenance").sort().join(",") != "declarationId,kind" ||
1874
+ typeof candidate.declarationId != "string" || !interfacesByRecordRegistry.get(records)?.has(candidate.declarationId)) {
1875
+ fail("UNKNOWN_INTERFACE", `Unknown interface declaration '${String(candidate.declarationId)}'.`, typeLocation(candidate, location))
1876
+ }
1877
+
1878
+ return candidate
1879
+ }
1626
1880
  if (candidate.kind == "OwnedResourceType") {
1627
1881
  if (Object.keys(candidate).filter((key) => key != "sourceProvenance").sort().join(",") != "kind,resourceId" ||
1628
1882
  typeof candidate.resourceId != "string" || !resourceIdsByRecordRegistry.get(records)?.has(candidate.resourceId)) {
@@ -1855,8 +2109,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1855
2109
  expression.value, scope, functions, records, fail, normalizeOperations, "an optional present value", expectedType.valueType
1856
2110
  )
1857
2111
 
1858
- if (!sameType(valueType, expectedType.valueType)) {
1859
- fail("TYPE_MISMATCH", `Optional present type ${typeDescription(valueType)}; expected ${typeDescription(expectedType.valueType)}.`, expression.value.location)
2112
+ if (!assignableType(valueType, expectedType.valueType, records)) {
2113
+ fail(nominalMismatchCode(expectedType.valueType), `Optional present type ${typeDescription(valueType)}; expected ${typeDescription(expectedType.valueType)}.`, expression.value.location)
1860
2114
  }
1861
2115
 
1862
2116
  return expectedType
@@ -1911,8 +2165,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1911
2165
  inferredElementType ??= actual
1912
2166
  const requiredElementType = expectedType?.kind == "ListType" ? expectedType.elementType : inferredElementType
1913
2167
 
1914
- if (!sameType(actual, requiredElementType)) {
1915
- fail("TYPE_MISMATCH", `List element type ${typeDescription(actual)}; expected ${typeDescription(requiredElementType)}.`, element.location)
2168
+ if (!assignableType(actual, requiredElementType, records)) {
2169
+ fail(nominalMismatchCode(requiredElementType), `List element type ${typeDescription(actual)}; expected ${typeDescription(requiredElementType)}.`, element.location)
1916
2170
  }
1917
2171
  }
1918
2172
 
@@ -1947,8 +2201,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1947
2201
  inferredValueType ??= actual
1948
2202
  const requiredValueType = expectedType?.kind == "MapType" ? expectedType.valueType : inferredValueType
1949
2203
 
1950
- if (!sameType(actual, requiredValueType)) {
1951
- fail("TYPE_MISMATCH", `Map value type ${typeDescription(actual)}; expected ${typeDescription(requiredValueType)}.`, entry.value.location)
2204
+ if (!assignableType(actual, requiredValueType, records)) {
2205
+ fail(nominalMismatchCode(requiredValueType), `Map value type ${typeDescription(actual)}; expected ${typeDescription(requiredValueType)}.`, entry.value.location)
1952
2206
  }
1953
2207
  }
1954
2208
 
@@ -1985,8 +2239,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1985
2239
  inferredValueType ??= actual
1986
2240
  const requiredValueType = expectedType?.kind == "OrderedMapType" ? expectedType.valueType : inferredValueType
1987
2241
 
1988
- if (!sameType(actual, requiredValueType)) {
1989
- fail("TYPE_MISMATCH", `Ordered-map value type ${typeDescription(actual)}; expected ${typeDescription(requiredValueType)}.`,
2242
+ if (!assignableType(actual, requiredValueType, records)) {
2243
+ fail(nominalMismatchCode(requiredValueType), `Ordered-map value type ${typeDescription(actual)}; expected ${typeDescription(requiredValueType)}.`,
1990
2244
  entry.value.location)
1991
2245
  }
1992
2246
  }
@@ -2139,6 +2393,49 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2139
2393
  const receiverType = inferValueExpressionType(expression.receiver, scope, functions, records, fail,
2140
2394
  normalizeOperations, "a method receiver")
2141
2395
 
2396
+ if (receiverType.kind == "InterfaceType") {
2397
+ const interfaces = interfacesByRecordRegistry.get(records) ?? new Map()
2398
+ const receiverInterface = interfaces.get(receiverType.declarationId)
2399
+
2400
+ if (!receiverInterface) return fail("UNKNOWN_INTERFACE", `Unknown interface '${receiverType.declarationId}'.`, expression.receiver.location)
2401
+ const members = flattenedInterfaceMembers(receiverType.declarationId, interfaces, fail, new Set(), expression.receiver.location)
2402
+ const member = members.find((candidate) => candidate.methodId == expression.method) ??
2403
+ (normalizeOperations ? members.find((candidate) => candidate.name == expression.method) : undefined)
2404
+
2405
+ if (!member) return fail("UNKNOWN_METHOD", `Interface '${receiverInterface.name}' has no method '${expression.method}'.`,
2406
+ roleLocation(expression, "member"))
2407
+ if (normalizeOperations) expression.method = member.methodId
2408
+ if (!isDenseArray(expression.arguments)) return fail("TYPE_MISMATCH", "Method arguments must be a dense ordered array.", expression.location)
2409
+ if (expression.arguments.length != member.parameters.length) {
2410
+ return fail("METHOD_ARITY_MISMATCH",
2411
+ `Method '${member.name}' has ${expression.arguments.length} arguments; expected ${member.parameters.length}.`, expression.location)
2412
+ }
2413
+ const parameterTypes = member.parameters.map((parameter) =>
2414
+ validateValueTypeReference(parameter.type, parameter.location, fail, undefined, records))
2415
+
2416
+ for (let index = 0; index < expression.arguments.length; index += 1) {
2417
+ expression.arguments[index] = normalizeOwnedMove(expression.arguments[index], parameterTypes[index], normalizeOperations, fail)
2418
+ const actual = inferValueExpressionType(expression.arguments[index], scope, functions, records, fail,
2419
+ normalizeOperations, "a method argument", parameterTypes[index])
2420
+
2421
+ if (!assignableType(actual, parameterTypes[index], records)) {
2422
+ fail(nominalMismatchCode(parameterTypes[index]), `Method argument type ${typeDescription(actual)}; expected ${typeDescription(parameterTypes[index])}.`,
2423
+ expression.arguments[index].location)
2424
+ }
2425
+ }
2426
+ const returnType = validateReturnTypeReference(member.returnType, member.location, fail, records)
2427
+ const resolution = {
2428
+ declarationId: member.methodId,
2429
+ kind: /** @type {const} */ ("ResolvedMethodSignature"),
2430
+ parameterTypes: parameterTypes.map((type) => /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(type))),
2431
+ returnType: typeIdentity(returnType)
2432
+ }
2433
+
2434
+ if (normalizeOperations) expression.resolution = resolution
2435
+ else validateReferenceResolution(expression.resolution, resolution, expression.location, fail, "method")
2436
+
2437
+ return isVoidType(returnType) ? "void" : returnType
2438
+ }
2142
2439
  if (receiverType.kind != "ReferenceType" && receiverType.kind != "OwnedReferenceType") {
2143
2440
  return fail("INVALID_METHOD_RECEIVER", "Method call receiver must have a reference class type.", expression.receiver.location)
2144
2441
  }
@@ -2164,8 +2461,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2164
2461
  const actual = inferValueExpressionType(expression.arguments[index], scope, functions, records, fail,
2165
2462
  normalizeOperations, "a method argument", parameterTypes[index])
2166
2463
 
2167
- if (!sameType(actual, parameterTypes[index])) {
2168
- fail("TYPE_MISMATCH", `Method argument type ${typeDescription(actual)}; expected ${typeDescription(parameterTypes[index])}.`,
2464
+ if (!assignableType(actual, parameterTypes[index], records)) {
2465
+ fail(nominalMismatchCode(parameterTypes[index]), `Method argument type ${typeDescription(actual)}; expected ${typeDescription(parameterTypes[index])}.`,
2169
2466
  expression.arguments[index].location)
2170
2467
  }
2171
2468
  }
@@ -2295,8 +2592,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2295
2592
 
2296
2593
  if ((functionDeclaration.typeParameters?.length ?? 0) > 0) {
2297
2594
  inferTypeArguments(expectedType, actualType, substitutions, argument.location, fail)
2298
- } else if (!sameType(actualType, expectedType)) {
2299
- fail("TYPE_MISMATCH", `Call argument type ${typeDescription(actualType)}; expected ${typeDescription(expectedType)}.`, argument.location)
2595
+ } else if (!assignableType(actualType, expectedType, records)) {
2596
+ fail(nominalMismatchCode(expectedType), `Call argument type ${typeDescription(actualType)}; expected ${typeDescription(expectedType)}.`, argument.location)
2300
2597
  }
2301
2598
  }
2302
2599
  for (const parameter of functionDeclaration.typeParameters ?? []) {
@@ -2319,8 +2616,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2319
2616
  const actual = inferValueExpressionType(expression.arguments[index], scope, functions, records, fail,
2320
2617
  normalizeOperations, "a call argument", expected)
2321
2618
 
2322
- if (!sameType(actual, expected)) {
2323
- fail("TYPE_MISMATCH", `Call argument type ${typeDescription(actual)}; expected ${typeDescription(expected)}.`, expression.arguments[index].location)
2619
+ if (!assignableType(actual, expected, records)) {
2620
+ fail(nominalMismatchCode(expected), `Call argument type ${typeDescription(actual)}; expected ${typeDescription(expected)}.`, expression.arguments[index].location)
2324
2621
  }
2325
2622
  }
2326
2623
  const resolution = signatureFor(functionDeclaration, records, fail, substitutions)
@@ -2835,6 +3132,7 @@ function typeIdentity(type) {
2835
3132
  /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(argument)))} : {})
2836
3133
  }
2837
3134
  if (type.kind == "ReferenceType") return {declarationId: type.declarationId, kind: "ReferenceType"}
3135
+ if (type.kind == "InterfaceType") return {declarationId: type.declarationId, kind: "InterfaceType"}
2838
3136
  if (type.kind == "OwnedResourceType") return {kind: "OwnedResourceType", resourceId: type.resourceId}
2839
3137
  if (type.kind == "OwnedReferenceType") return {declarationId: type.declarationId, kind: "OwnedReferenceType"}
2840
3138
  if (type.kind == "ListType") return {elementType: /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(type.elementType)), kind: "ListType"}
@@ -2891,6 +3189,9 @@ function validTypeIdentity(type, allowVoid, seen = new Set()) {
2891
3189
  } else if (candidate.kind == "ReferenceType" && Object.keys(candidate).sort().join(",") == "declarationId,kind") {
2892
3190
  valid = typeof candidate.declarationId == "string" &&
2893
3191
  /^(?:[a-z][a-z0-9._-]*#)?class:[0-9]+$/u.test(candidate.declarationId)
3192
+ } else if (candidate.kind == "InterfaceType" && Object.keys(candidate).sort().join(",") == "declarationId,kind") {
3193
+ valid = typeof candidate.declarationId == "string" &&
3194
+ /^(?:[a-z][a-z0-9._-]*#)?interface:[0-9]+$/u.test(candidate.declarationId)
2894
3195
  } else if (candidate.kind == "OwnedResourceType" && Object.keys(candidate).sort().join(",") == "kind,resourceId") {
2895
3196
  valid = typeof candidate.resourceId == "string" && /^(?:module:[0-9]+\/)?capability:[0-9]+\/resource:[0-9]+$/u
2896
3197
  .test(candidate.resourceId)
@@ -2941,6 +3242,7 @@ function sameTypeIdentity(left, right) {
2941
3242
  leftArguments.every((argument, index) => sameTypeIdentity(argument, rightArguments[index]))
2942
3243
  }
2943
3244
  if (leftType.kind == "ReferenceType") return leftType.declarationId == rightType.declarationId
3245
+ if (leftType.kind == "InterfaceType") return leftType.declarationId == rightType.declarationId
2944
3246
  if (leftType.kind == "OwnedResourceType") return leftType.resourceId == rightType.resourceId
2945
3247
  if (leftType.kind == "OwnedReferenceType") return leftType.declarationId == rightType.declarationId
2946
3248
  if (leftType.kind == "MapType" || leftType.kind == "OrderedMapType") {
@@ -2960,6 +3262,54 @@ function sameType(left, right) {
2960
3262
  return sameTypeIdentity(typeIdentity(left), typeIdentity(right))
2961
3263
  }
2962
3264
 
3265
+ /**
3266
+ * Reports whether one validated type may flow to a declared position.
3267
+ * @param {import("./types.js").SemanticFunctionReturnType} actual - Inferred source type.
3268
+ * @param {import("./types.js").SemanticFunctionReturnType} expected - Declared position type.
3269
+ * @param {RecordRegistry} records - Record and reference-class registries.
3270
+ * @returns {boolean} Whether the value flow is admitted.
3271
+ */
3272
+ function assignableType(actual, expected, records) {
3273
+ if (sameType(actual, expected)) return true
3274
+ if (expected.kind != "InterfaceType") return false
3275
+ const interfaces = interfacesByRecordRegistry.get(records) ?? new Map()
3276
+
3277
+ if (actual.kind == "InterfaceType") return interfaceExtendsTransitively(actual.declarationId, expected.declarationId, interfaces)
3278
+ if (actual.kind == "ReferenceType") {
3279
+ const declaration = referenceClassesFor(records).get(actual.declarationId)
3280
+
3281
+ return Boolean(declaration?.implements?.some((implemented) =>
3282
+ implemented.declarationId == expected.declarationId ||
3283
+ interfaceExtendsTransitively(implemented.declarationId, expected.declarationId, interfaces)))
3284
+ }
3285
+
3286
+ return false
3287
+ }
3288
+
3289
+ /**
3290
+ * Reports whether one interface transitively extends another through the closed DAG.
3291
+ * @param {string} interfaceId - Descendant interface identity.
3292
+ * @param {string} ancestorId - Ancestor interface identity.
3293
+ * @param {InterfaceRegistry} interfaces - Interfaces by identity.
3294
+ * @returns {boolean} Whether the relation holds.
3295
+ */
3296
+ function interfaceExtendsTransitively(interfaceId, ancestorId, interfaces) {
3297
+ const declaration = interfaces.get(interfaceId)
3298
+
3299
+ if (!declaration) return false
3300
+ return (declaration.extends ?? []).some((parent) => parent.declarationId == ancestorId ||
3301
+ interfaceExtendsTransitively(parent.declarationId, ancestorId, interfaces))
3302
+ }
3303
+
3304
+ /**
3305
+ * Chooses the stable diagnostic code for one failed nominal value flow.
3306
+ * @param {import("./types.js").SemanticFunctionReturnType} expected - Declared position type.
3307
+ * @returns {string} Stable diagnostic code.
3308
+ */
3309
+ function nominalMismatchCode(expected) {
3310
+ return expected.kind == "InterfaceType" ? "INTERFACE_TYPE_MISMATCH" : "TYPE_MISMATCH"
3311
+ }
3312
+
2963
3313
  /**
2964
3314
  * Formats one validated semantic type for diagnostics.
2965
3315
  * @param {import("./types.js").SemanticFunctionReturnType | import("./types.js").ErrorType} type - Semantic type.
@@ -2971,6 +3321,7 @@ function typeDescription(type) {
2971
3321
  if (type.kind == "RecordType") return `record<${type.declarationId}${type.arguments?.length
2972
3322
  ? `, ${type.arguments.map(typeDescription).join(", ")}` : ""}>`
2973
3323
  if (type.kind == "ReferenceType") return `reference<${type.declarationId}>`
3324
+ if (type.kind == "InterfaceType") return `interface<${type.declarationId}>`
2974
3325
  if (type.kind == "OwnedResourceType") return `owned-resource<${type.resourceId}>`
2975
3326
  if (type.kind == "OwnedReferenceType") return `owned-reference<${type.declarationId}>`
2976
3327
  if (type.kind == "ErrorType") return `error<${type.declarationId}>`