semantifold 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +2 -0
  2. package/build/src/backends/effects.js +5 -2
  3. package/build/src/backends/java.js +33 -14
  4. package/build/src/backends/javascript.js +83 -18
  5. package/build/src/backends/php.d.ts.map +1 -1
  6. package/build/src/backends/php.js +88 -16
  7. package/build/src/backends/ruby.js +37 -12
  8. package/build/src/backends/scalars.js +2 -2
  9. package/build/src/backends/shared.d.ts +2 -1
  10. package/build/src/backends/shared.d.ts.map +1 -1
  11. package/build/src/backends/shared.js +299 -42
  12. package/build/src/backends/typescript.js +83 -18
  13. package/build/src/backends/writer.d.ts +18 -4
  14. package/build/src/backends/writer.d.ts.map +1 -1
  15. package/build/src/backends/writer.js +40 -4
  16. package/build/src/frontends/java.d.ts +9 -1
  17. package/build/src/frontends/java.d.ts.map +1 -1
  18. package/build/src/frontends/java.js +90 -43
  19. package/build/src/frontends/javascript-typescript.d.ts +3 -1
  20. package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
  21. package/build/src/frontends/javascript-typescript.js +133 -52
  22. package/build/src/frontends/php.d.ts +13 -1
  23. package/build/src/frontends/php.d.ts.map +1 -1
  24. package/build/src/frontends/php.js +226 -59
  25. package/build/src/frontends/ruby.d.ts +4 -1
  26. package/build/src/frontends/ruby.d.ts.map +1 -1
  27. package/build/src/frontends/ruby.js +113 -37
  28. package/build/src/frontends/types.d.ts +9 -2
  29. package/build/src/frontends/types.d.ts.map +1 -1
  30. package/build/src/frontends/types.js +26 -9
  31. package/build/src/language-registry.d.ts.map +1 -1
  32. package/build/src/language-registry.js +14 -6
  33. package/build/src/mapping.d.ts.map +1 -1
  34. package/build/src/mapping.js +4 -2
  35. package/build/src/semantic/effects.d.ts.map +1 -1
  36. package/build/src/semantic/effects.js +20 -6
  37. package/build/src/semantic/lifetime.js +28 -4
  38. package/build/src/semantic/provenance.d.ts.map +1 -1
  39. package/build/src/semantic/provenance.js +61 -9
  40. package/build/src/semantic/types.d.ts +214 -23
  41. package/build/src/semantic/types.d.ts.map +1 -1
  42. package/build/src/semantic/types.js +58 -11
  43. package/build/src/semantic/validate.d.ts +31 -2
  44. package/build/src/semantic/validate.d.ts.map +1 -1
  45. package/build/src/semantic/validate.js +450 -60
  46. package/docs/adr/20261026-typed-error-handling-extension.md +55 -0
  47. package/docs/adr-bounded-subtyping.md +180 -0
  48. package/docs/adr-numeric-tower.md +156 -0
  49. package/docs/architecture.md +1 -1
  50. package/docs/language-support.md +8 -8
  51. package/docs/testing.md +5 -3
  52. package/package.json +1 -1
  53. package/src/backends/effects.js +3 -1
  54. package/src/backends/java.js +33 -14
  55. package/src/backends/javascript.js +83 -18
  56. package/src/backends/php.js +89 -16
  57. package/src/backends/ruby.js +36 -12
  58. package/src/backends/scalars.js +1 -1
  59. package/src/backends/shared.js +298 -47
  60. package/src/backends/typescript.js +83 -18
  61. package/src/backends/writer.js +41 -3
  62. package/src/frontends/java.js +95 -43
  63. package/src/frontends/javascript-typescript.js +145 -53
  64. package/src/frontends/php.js +243 -58
  65. package/src/frontends/ruby.js +117 -35
  66. package/src/frontends/types.js +26 -8
  67. package/src/language-registry.js +13 -5
  68. package/src/mapping.js +3 -1
  69. package/src/semantic/effects.js +18 -5
  70. package/src/semantic/lifetime.js +29 -3
  71. package/src/semantic/provenance.js +60 -14
  72. package/src/semantic/types.js +62 -10
  73. package/src/semantic/validate.js +481 -61
@@ -12,9 +12,23 @@ const task005Languages = new Set(["php", "ruby", "javascript", "typescript", "ja
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 = {}) {
@@ -160,7 +174,7 @@ function validateBlockShape(block, detail, fail) {
160
174
  }
161
175
 
162
176
  for (const statement of block.statements) {
163
- if (!["AssignmentStatement", "BreakStatement", "ContinueStatement", "ExpressionStatement", "ForEachMapStatement", "ForEachStatement", "IfStatement", "LocalDeclaration", "PrivateFieldWriteStatement", "PrintStatement", "RaiseStatement", "ReturnStatement", "TryStatement", "WhileStatement"].includes(statement.kind)) {
177
+ if (!["AssignmentStatement", "BreakStatement", "ContinueStatement", "ExpressionStatement", "ForEachMapStatement", "ForEachStatement", "IfStatement", "LocalDeclaration", "PrivateFieldWriteStatement", "PrintStatement", "RaiseStatement", "RethrowStatement", "ReturnStatement", "TryStatement", "WhileStatement"].includes(statement.kind)) {
164
178
  fail(`${detail} statement ${statement.kind}`, statement.location)
165
179
  }
166
180
  if (statement.kind == "IfStatement") {
@@ -172,7 +186,18 @@ function validateBlockShape(block, detail, fail) {
172
186
  }
173
187
  if (statement.kind == "TryStatement") {
174
188
  validateBlockShape(statement.body, "try body", fail)
175
- validateBlockShape(statement.catchBody, "catch body", fail)
189
+ if (!Array.isArray(statement.handlers) || (statement.handlers.length == 0 && !statement.finallyBody)) {
190
+ fail(`${detail} try without an ordered handler list`, statement.location)
191
+ }
192
+
193
+ for (const handler of statement.handlers ?? []) {
194
+ if (!handler || handler.kind != "CatchHandler" ||
195
+ Object.keys(handler).filter((key) => key != "sourceProvenance").sort().join(",") != "catchBinding,catchBody,catchType,kind,location") {
196
+ fail(`${detail} malformed catch handler`, handler?.location ?? statement.location)
197
+ }
198
+ validateBlockShape(handler.catchBody, "catch body", fail)
199
+ }
200
+ if (statement.finallyBody) validateBlockShape(statement.finallyBody, "finally body", fail)
176
201
  }
177
202
  }
178
203
  }
@@ -183,12 +208,15 @@ function validateBlockShape(block, detail, fail) {
183
208
  * @param {string} language - Source or backend language identity.
184
209
  * @param {SemanticFail} fail - Diagnostic callback.
185
210
  * @param {boolean} normalizeOperations - Whether to replace transient frontend operation intent.
186
- * @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.
187
212
  * @returns {void}
188
213
  */
189
214
  function validateModuleTypes(module, language, fail, normalizeOperations, visible = {}) {
190
- 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)
191
217
  const records = validateRecordDeclarations(module.records ?? [], fail, normalizeOperations, visible.records, classes)
218
+
219
+ interfacesByRecordRegistry.set(records, interfaces)
192
220
  const errors = validateErrorDeclarations(module.errors ?? [], fail, normalizeOperations, visible.errors)
193
221
  /** @type {Map<string, import("./types.js").FunctionDeclaration>} */
194
222
  const functions = new Map(visible.functions ?? [])
@@ -226,6 +254,7 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
226
254
 
227
255
  const reservedValueNames = new Set([
228
256
  ...functions.keys(),
257
+ ...[...interfaces.values()].map((declaration) => declaration.name),
229
258
  ...[...records.values()].map((declaration) => declaration.name),
230
259
  ...[...classes.values()].map((declaration) => declaration.name),
231
260
  ...[...errors.values()].map((declaration) => declaration.name),
@@ -234,7 +263,7 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
234
263
 
235
264
  const nominalNames = new Set()
236
265
 
237
- for (const declaration of [...module.records ?? [], ...module.classes ?? [], ...module.errors ?? []]) {
266
+ for (const declaration of [...module.interfaces ?? [], ...module.records ?? [], ...module.classes ?? [], ...module.errors ?? []]) {
238
267
  if (nominalNames.has(declaration.name)) {
239
268
  fail("DUPLICATE_BINDING", `Duplicate nominal declaration '${declaration.name}'.`, roleLocation(declaration, "name"))
240
269
  }
@@ -249,6 +278,9 @@ function validateModuleTypes(module, language, fail, normalizeOperations, visibl
249
278
  const localFunctionNames = new Set(module.functions.map(({name}) => name))
250
279
  const callEffects = inferCallEffects(functions, visible.callEffects, localFunctionNames)
251
280
 
281
+ for (const declaration of module.classes ?? []) {
282
+ validateInterfaceContracts(declaration, interfaces, records, fail, normalizeOperations)
283
+ }
252
284
  for (const declaration of module.classes ?? []) {
253
285
  validateClassDeclaration(declaration, functions, records, errors, callEffects, reservedValueNames, fail,
254
286
  normalizeOperations, loopOwners)
@@ -310,7 +342,8 @@ function prepareLoopIdentities(module, normalizeOperations, fail) {
310
342
  if (statement.alternate) visit(statement.alternate, owner, active)
311
343
  } else if (statement.kind == "TryStatement") {
312
344
  visit(statement.body, owner, active)
313
- visit(statement.catchBody, owner, active)
345
+ for (const handler of statement.handlers ?? []) visit(handler.catchBody, owner, active)
346
+ if (statement.finallyBody) visit(statement.finallyBody, owner, active)
314
347
  }
315
348
  }
316
349
  }
@@ -339,9 +372,10 @@ export function moduleUncheckedErrorEffects(module, visible = {}) {
339
372
  * @param {SemanticFail} fail - Diagnostic callback.
340
373
  * @param {boolean} normalizeOperations - Whether parser-authored identities are assigned.
341
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.
342
376
  * @returns {ClassRegistry} Classes by stable identity.
343
377
  */
344
- function registerClassDeclarations(declarations, fail, normalizeOperations, visibleClasses = new Map()) {
378
+ function registerClassDeclarations(declarations, fail, normalizeOperations, visibleClasses = new Map(), interfaces = new Map()) {
345
379
  if (!Array.isArray(declarations)) {
346
380
  return fail("TYPE_MISMATCH", "Reference class declarations must be an ordered array.", /** @type {never} */ (undefined))
347
381
  }
@@ -429,11 +463,235 @@ function registerClassDeclarations(declarations, fail, normalizeOperations, visi
429
463
  }
430
464
  methodNames.add(method.name)
431
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
+ }
432
478
  }
433
479
 
434
480
  return classes
435
481
  }
436
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
+
437
695
  /**
438
696
  * Validates exact construction, private state, and method bodies for one class.
439
697
  * @param {import("./types.js").ClassDeclaration} declaration - Class declaration.
@@ -762,11 +1020,11 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
762
1020
  )
763
1021
 
764
1022
  scope.pending.delete(statement.name)
765
- if (!sameType(initializerType, declaredType)) {
1023
+ if (!assignableType(initializerType, declaredType, records)) {
766
1024
  if (initializerType.kind == "OptionalType" && declaredType.kind != "OptionalType") {
767
1025
  fail("FAILURE_ABSENCE_CONFLATION", "Optional absence cannot be coerced into a value or typed failure.", statement.initializer.location)
768
1026
  }
769
- 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)
770
1028
  }
771
1029
  declareBinding(statement.name, {
772
1030
  knownValue: knownValueForExpression(statement.initializer, scope),
@@ -792,8 +1050,8 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
792
1050
  if (!binding.mutable) {
793
1051
  fail("IMMUTABLE_ASSIGNMENT", `Cannot assign to immutable binding '${statement.target.name}'.`, statement.target.location)
794
1052
  }
795
- if (!sameType(expressionType, binding.type)) {
796
- 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)
797
1055
  }
798
1056
  binding.knownValue = knownValueForExpression(statement.expression, scope)
799
1057
  scope.presenceProofs.delete(binding)
@@ -890,8 +1148,8 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
890
1148
  returnType
891
1149
  )
892
1150
 
893
- if (!sameType(actualType, returnType)) {
894
- 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)
895
1153
  }
896
1154
  }
897
1155
  if (statement.expression) addExpressionEffects(raises, statement.expression, callEffects)
@@ -1086,44 +1344,87 @@ function validateBlock(block, scope, returnType, functions, records, errors, cal
1086
1344
  normal = false
1087
1345
  continue
1088
1346
  }
1089
- if (statement.kind == "TryStatement") {
1090
- const caughtType = validateErrorType(statement.catchType, statement.location, errors, fail)
1347
+ if (statement.kind == "RethrowStatement") {
1348
+ const binding = findBinding(statement.binding, scope)
1091
1349
 
1092
- if (!statement.catchBinding || statement.catchBinding.kind != "CatchBinding" || statement.catchBinding.mutable !== false) {
1093
- fail("INVALID_ERROR_HANDLER", "Catch requires one immutable typed binding.", statement.catchBinding?.location ?? statement.location)
1350
+ if (!binding || binding.mutable !== false || binding.type.kind != "ErrorType") {
1351
+ fail("INVALID_RETHROW", "Rethrow requires a catch binding in scope.", statement.location)
1094
1352
  }
1095
- const bindingType = validateErrorType(statement.catchBinding.type, statement.catchBinding.location, errors, fail)
1096
-
1097
- if (bindingType.declarationId != caughtType.declarationId) {
1098
- fail("INVALID_ERROR_HANDLER", "Catch binding type must exactly match its handler type.", statement.catchBinding.location)
1353
+ raises.add(/** @type {import("./types.js").ErrorType} */ (binding.type).declarationId)
1354
+ normal = false
1355
+ continue
1356
+ }
1357
+ if (statement.kind == "TryStatement") {
1358
+ if (!Array.isArray(statement.handlers) || (statement.handlers.length == 0 && !statement.finallyBody)) {
1359
+ fail("INVALID_ERROR_HANDLER", "Try requires at least one exact typed handler or a finally block.", statement.location)
1099
1360
  }
1100
1361
  const bodyAssignedOuterBindings = outerMutableBindingsAssignedBy(statement.body, scope)
1101
1362
  const assignedOuterBindings = new Set(bodyAssignedOuterBindings)
1102
1363
 
1103
- outerMutableBindingsAssignedBy(statement.catchBody, scope, assignedOuterBindings)
1364
+ for (const handler of statement.handlers) outerMutableBindingsAssignedBy(handler.catchBody, scope, assignedOuterBindings)
1365
+ if (statement.finallyBody) outerMutableBindingsAssignedBy(statement.finallyBody, scope, assignedOuterBindings)
1104
1366
  const bodyScope = createScope(scope, statement.body.statements)
1105
1367
  const bodyFlow = validateBlock(statement.body, bodyScope, returnType, functions, records, errors, callEffects, fail, normalizeOperations, loopContext)
1106
- const catchScope = createScope(scope, statement.catchBody.statements)
1107
1368
 
1108
- for (const binding of bodyAssignedOuterBindings) {
1109
- binding.knownValue = undefined
1110
- catchScope.presenceProofs.delete(binding)
1111
- }
1369
+ let remainingRaises = new Set(bodyFlow.raises)
1370
+ let normalCompletion = bodyFlow.normal
1371
+ const handlerRaises = new Set()
1372
+ const seenHandlerNames = new Set()
1112
1373
 
1113
- declareBinding(statement.catchBinding.name, {
1114
- knownValue: undefined,
1115
- mutable: false,
1116
- type: bindingType
1117
- }, roleLocation(statement.catchBinding, "name"), catchScope, fail)
1118
- const catchFlow = validateBlock(statement.catchBody, catchScope, returnType, functions, records, errors, callEffects, fail, normalizeOperations, loopContext)
1374
+ for (const handler of statement.handlers) {
1375
+ const caughtType = validateErrorType(handler.catchType, handler.location, errors, fail)
1119
1376
 
1120
- if (!bodyFlow.raises.has(caughtType.declarationId)) {
1121
- fail("UNREACHABLE_HANDLER", `Handler for '${errors.get(caughtType.declarationId)?.name}' cannot be reached.`, typeLocation(statement.catchType, statement.location))
1377
+ if (!handler.catchBinding || handler.catchBinding.kind != "CatchBinding" || handler.catchBinding.mutable !== false) {
1378
+ fail("INVALID_ERROR_HANDLER", "Catch requires one immutable typed binding.", handler.catchBinding?.location ?? handler.location)
1379
+ }
1380
+ const bindingType = validateErrorType(handler.catchBinding.type, handler.catchBinding.location, errors, fail)
1381
+
1382
+ if (bindingType.declarationId != caughtType.declarationId) {
1383
+ fail("INVALID_ERROR_HANDLER", "Catch binding type must exactly match its handler type.", handler.catchBinding.location)
1384
+ }
1385
+ if (!remainingRaises.has(caughtType.declarationId)) {
1386
+ fail("UNREACHABLE_HANDLER", `Handler for '${errors.get(caughtType.declarationId)?.name}' cannot be reached.`,
1387
+ typeLocation(handler.catchType, handler.location))
1388
+ }
1389
+ if (seenHandlerNames.has(handler.catchBinding.name)) {
1390
+ fail("DUPLICATE_BINDING", `Duplicate or shadowed binding '${handler.catchBinding.name}'.`, handler.catchBinding.location)
1391
+ }
1392
+ seenHandlerNames.add(handler.catchBinding.name)
1393
+ remainingRaises.delete(caughtType.declarationId)
1394
+ const catchScope = createScope(scope, handler.catchBody.statements)
1395
+
1396
+ for (const binding of bodyAssignedOuterBindings) {
1397
+ binding.knownValue = undefined
1398
+ catchScope.presenceProofs.delete(binding)
1399
+ }
1400
+ declareBinding(handler.catchBinding.name, {
1401
+ knownValue: undefined,
1402
+ mutable: false,
1403
+ type: bindingType
1404
+ }, roleLocation(handler.catchBinding, "name"), catchScope, fail)
1405
+ const catchFlow = validateBlock(handler.catchBody, catchScope, returnType, functions, records, errors, callEffects, fail,
1406
+ normalizeOperations, loopContext)
1407
+
1408
+ normalCompletion = normalCompletion || catchFlow.normal
1409
+ addAll(handlerRaises, catchFlow.raises)
1410
+ }
1411
+ const pendingRaises = new Set(remainingRaises)
1412
+
1413
+ addAll(pendingRaises, handlerRaises)
1414
+ if (statement.finallyBody) {
1415
+ const finallyScope = createScope(scope, statement.finallyBody.statements)
1416
+ const finallyFlow = validateBlock(statement.finallyBody, finallyScope, returnType, functions, records, errors, callEffects,
1417
+ fail, normalizeOperations, loopContext)
1418
+ // `raises` is the enclosing block's accumulator. A completion block replaces only this try's
1419
+ // pending completion; escapes contributed by earlier statements of the enclosing block survive
1420
+ // whichever way the completion block completes.
1421
+ if (finallyFlow.normal) addAll(raises, pendingRaises)
1422
+ addAll(raises, finallyFlow.raises)
1423
+ normal = finallyFlow.normal ? normalCompletion : false
1424
+ } else {
1425
+ addAll(raises, pendingRaises)
1426
+ normal = normalCompletion
1122
1427
  }
1123
- bodyFlow.raises.delete(caughtType.declarationId)
1124
- addAll(raises, bodyFlow.raises)
1125
- addAll(raises, catchFlow.raises)
1126
- normal = bodyFlow.normal || catchFlow.normal
1127
1428
  for (const binding of assignedOuterBindings) {
1128
1429
  binding.knownValue = undefined
1129
1430
  scope.presenceProofs.delete(binding)
@@ -1271,14 +1572,27 @@ function syntacticBlockEffects(block, callEffects, methodEffects = new Map()) {
1271
1572
  }
1272
1573
  continue
1273
1574
  }
1575
+ if (kind == "RethrowStatement") {
1576
+ const bindingNode = Reflect.get(statement, "bindingNode")
1577
+ const bindingType = bindingNode && typeof bindingNode == "object" ? Reflect.get(bindingNode, "type") : undefined
1578
+ const declarationId = bindingType && typeof bindingType == "object" ? Reflect.get(bindingType, "declarationId") : undefined
1579
+
1580
+ if (typeof declarationId == "string") result.add(declarationId)
1581
+ continue
1582
+ }
1274
1583
  if (kind == "TryStatement") {
1275
- const bodyEffects = syntacticBlockEffects(Reflect.get(statement, "body"), callEffects, methodEffects)
1276
- const catchType = Reflect.get(statement, "catchType")
1277
- const caughtId = catchType && typeof catchType == "object" ? Reflect.get(catchType, "declarationId") : undefined
1584
+ let bodyEffects = syntacticBlockEffects(Reflect.get(statement, "body"), callEffects, methodEffects)
1585
+
1586
+ for (const handler of Reflect.get(statement, "handlers") ?? []) {
1587
+ if (!handler || typeof handler != "object") continue
1588
+ const catchType = Reflect.get(handler, "catchType")
1589
+ const caughtId = catchType && typeof catchType == "object" ? Reflect.get(catchType, "declarationId") : undefined
1278
1590
 
1279
- if (typeof caughtId == "string" && bodyEffects.delete(caughtId)) {
1280
- addAll(bodyEffects, syntacticBlockEffects(Reflect.get(statement, "catchBody"), callEffects, methodEffects))
1591
+ if (typeof caughtId == "string" && bodyEffects.delete(caughtId)) {
1592
+ addAll(bodyEffects, syntacticBlockEffects(Reflect.get(handler, "catchBody"), callEffects, methodEffects))
1593
+ }
1281
1594
  }
1595
+ addAll(bodyEffects, syntacticBlockEffects(Reflect.get(statement, "finallyBody"), callEffects, methodEffects))
1282
1596
  addAll(result, bodyEffects)
1283
1597
  continue
1284
1598
  }
@@ -1555,6 +1869,14 @@ function validateTypeReference(type, location, fail, seen = new Set(), records =
1555
1869
 
1556
1870
  return candidate
1557
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
+ }
1558
1880
  if (candidate.kind == "OwnedResourceType") {
1559
1881
  if (Object.keys(candidate).filter((key) => key != "sourceProvenance").sort().join(",") != "kind,resourceId" ||
1560
1882
  typeof candidate.resourceId != "string" || !resourceIdsByRecordRegistry.get(records)?.has(candidate.resourceId)) {
@@ -1787,8 +2109,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1787
2109
  expression.value, scope, functions, records, fail, normalizeOperations, "an optional present value", expectedType.valueType
1788
2110
  )
1789
2111
 
1790
- if (!sameType(valueType, expectedType.valueType)) {
1791
- 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)
1792
2114
  }
1793
2115
 
1794
2116
  return expectedType
@@ -1843,8 +2165,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1843
2165
  inferredElementType ??= actual
1844
2166
  const requiredElementType = expectedType?.kind == "ListType" ? expectedType.elementType : inferredElementType
1845
2167
 
1846
- if (!sameType(actual, requiredElementType)) {
1847
- 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)
1848
2170
  }
1849
2171
  }
1850
2172
 
@@ -1879,8 +2201,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1879
2201
  inferredValueType ??= actual
1880
2202
  const requiredValueType = expectedType?.kind == "MapType" ? expectedType.valueType : inferredValueType
1881
2203
 
1882
- if (!sameType(actual, requiredValueType)) {
1883
- 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)
1884
2206
  }
1885
2207
  }
1886
2208
 
@@ -1917,8 +2239,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
1917
2239
  inferredValueType ??= actual
1918
2240
  const requiredValueType = expectedType?.kind == "OrderedMapType" ? expectedType.valueType : inferredValueType
1919
2241
 
1920
- if (!sameType(actual, requiredValueType)) {
1921
- 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)}.`,
1922
2244
  entry.value.location)
1923
2245
  }
1924
2246
  }
@@ -2071,6 +2393,49 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2071
2393
  const receiverType = inferValueExpressionType(expression.receiver, scope, functions, records, fail,
2072
2394
  normalizeOperations, "a method receiver")
2073
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
+ }
2074
2439
  if (receiverType.kind != "ReferenceType" && receiverType.kind != "OwnedReferenceType") {
2075
2440
  return fail("INVALID_METHOD_RECEIVER", "Method call receiver must have a reference class type.", expression.receiver.location)
2076
2441
  }
@@ -2096,8 +2461,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2096
2461
  const actual = inferValueExpressionType(expression.arguments[index], scope, functions, records, fail,
2097
2462
  normalizeOperations, "a method argument", parameterTypes[index])
2098
2463
 
2099
- if (!sameType(actual, parameterTypes[index])) {
2100
- 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])}.`,
2101
2466
  expression.arguments[index].location)
2102
2467
  }
2103
2468
  }
@@ -2227,8 +2592,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2227
2592
 
2228
2593
  if ((functionDeclaration.typeParameters?.length ?? 0) > 0) {
2229
2594
  inferTypeArguments(expectedType, actualType, substitutions, argument.location, fail)
2230
- } else if (!sameType(actualType, expectedType)) {
2231
- 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)
2232
2597
  }
2233
2598
  }
2234
2599
  for (const parameter of functionDeclaration.typeParameters ?? []) {
@@ -2251,8 +2616,8 @@ function inferExpressionType(expression, scope, functions, records, fail, normal
2251
2616
  const actual = inferValueExpressionType(expression.arguments[index], scope, functions, records, fail,
2252
2617
  normalizeOperations, "a call argument", expected)
2253
2618
 
2254
- if (!sameType(actual, expected)) {
2255
- 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)
2256
2621
  }
2257
2622
  }
2258
2623
  const resolution = signatureFor(functionDeclaration, records, fail, substitutions)
@@ -2767,6 +3132,7 @@ function typeIdentity(type) {
2767
3132
  /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(argument)))} : {})
2768
3133
  }
2769
3134
  if (type.kind == "ReferenceType") return {declarationId: type.declarationId, kind: "ReferenceType"}
3135
+ if (type.kind == "InterfaceType") return {declarationId: type.declarationId, kind: "InterfaceType"}
2770
3136
  if (type.kind == "OwnedResourceType") return {kind: "OwnedResourceType", resourceId: type.resourceId}
2771
3137
  if (type.kind == "OwnedReferenceType") return {declarationId: type.declarationId, kind: "OwnedReferenceType"}
2772
3138
  if (type.kind == "ListType") return {elementType: /** @type {import("./types.js").SemanticTypeIdentity} */ (typeIdentity(type.elementType)), kind: "ListType"}
@@ -2823,6 +3189,9 @@ function validTypeIdentity(type, allowVoid, seen = new Set()) {
2823
3189
  } else if (candidate.kind == "ReferenceType" && Object.keys(candidate).sort().join(",") == "declarationId,kind") {
2824
3190
  valid = typeof candidate.declarationId == "string" &&
2825
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)
2826
3195
  } else if (candidate.kind == "OwnedResourceType" && Object.keys(candidate).sort().join(",") == "kind,resourceId") {
2827
3196
  valid = typeof candidate.resourceId == "string" && /^(?:module:[0-9]+\/)?capability:[0-9]+\/resource:[0-9]+$/u
2828
3197
  .test(candidate.resourceId)
@@ -2873,6 +3242,7 @@ function sameTypeIdentity(left, right) {
2873
3242
  leftArguments.every((argument, index) => sameTypeIdentity(argument, rightArguments[index]))
2874
3243
  }
2875
3244
  if (leftType.kind == "ReferenceType") return leftType.declarationId == rightType.declarationId
3245
+ if (leftType.kind == "InterfaceType") return leftType.declarationId == rightType.declarationId
2876
3246
  if (leftType.kind == "OwnedResourceType") return leftType.resourceId == rightType.resourceId
2877
3247
  if (leftType.kind == "OwnedReferenceType") return leftType.declarationId == rightType.declarationId
2878
3248
  if (leftType.kind == "MapType" || leftType.kind == "OrderedMapType") {
@@ -2892,6 +3262,54 @@ function sameType(left, right) {
2892
3262
  return sameTypeIdentity(typeIdentity(left), typeIdentity(right))
2893
3263
  }
2894
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
+
2895
3313
  /**
2896
3314
  * Formats one validated semantic type for diagnostics.
2897
3315
  * @param {import("./types.js").SemanticFunctionReturnType | import("./types.js").ErrorType} type - Semantic type.
@@ -2903,6 +3321,7 @@ function typeDescription(type) {
2903
3321
  if (type.kind == "RecordType") return `record<${type.declarationId}${type.arguments?.length
2904
3322
  ? `, ${type.arguments.map(typeDescription).join(", ")}` : ""}>`
2905
3323
  if (type.kind == "ReferenceType") return `reference<${type.declarationId}>`
3324
+ if (type.kind == "InterfaceType") return `interface<${type.declarationId}>`
2906
3325
  if (type.kind == "OwnedResourceType") return `owned-resource<${type.resourceId}>`
2907
3326
  if (type.kind == "OwnedReferenceType") return `owned-reference<${type.declarationId}>`
2908
3327
  if (type.kind == "ErrorType") return `error<${type.declarationId}>`
@@ -3046,7 +3465,8 @@ function outerMutableBindingsAssignedBy(block, outerScope, assigned = new Set())
3046
3465
  outerMutableBindingsAssignedBy(statement.body, outerScope, assigned)
3047
3466
  } else if (statement.kind == "TryStatement") {
3048
3467
  outerMutableBindingsAssignedBy(statement.body, outerScope, assigned)
3049
- outerMutableBindingsAssignedBy(statement.catchBody, outerScope, assigned)
3468
+ for (const handler of statement.handlers ?? []) outerMutableBindingsAssignedBy(handler.catchBody, outerScope, assigned)
3469
+ if (statement.finallyBody) outerMutableBindingsAssignedBy(statement.finallyBody, outerScope, assigned)
3050
3470
  }
3051
3471
  }
3052
3472