From adc69b2ffe178ba18e2ef5874e283e046fc3d245 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Sun, 22 Sep 2024 10:47:01 -0700 Subject: [PATCH] special handling for Completion/NormalCompletion in the typechecker (#613) --- src/type-logic.ts | 56 +++++++++++++++++++++++---- src/typechecker.ts | 2 +- test/typecheck.js | 94 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 11 deletions(-) diff --git a/src/type-logic.ts b/src/type-logic.ts index 15f1fecd..0e1ccc78 100644 --- a/src/type-logic.ts +++ b/src/type-logic.ts @@ -207,11 +207,15 @@ export function meet(a: Type, b: Type): Type { // union is join. meet distributes over join. return a.of.map(t => meet(t, b)).reduce(join); } - if ( - (a.kind === 'list' && b.kind === 'list') || - (a.kind === 'normal completion' && b.kind === 'normal completion') - ) { - return { kind: a.kind, of: meet(a.of, b.of) }; + if (a.kind === 'list' && b.kind === 'list') { + return { kind: 'list', of: meet(a.of, b.of) }; + } + if (a.kind === 'normal completion' && b.kind === 'normal completion') { + const inner = meet(a.of, b.of); + if (inner.kind === 'never') { + return { kind: 'never' }; + } + return { kind: 'normal completion', of: inner }; } return { kind: 'never' }; } @@ -318,7 +322,11 @@ export function serialize(type: Type): string { } } -export function typeFromExpr(expr: Expr, biblio: Biblio): Type { +export function typeFromExpr( + expr: Expr, + biblio: Biblio, + warn: (offset: number, message: string) => void, +): Type { seq: if (expr.name === 'seq') { const items = stripWhitespace(expr.items); if (items.length === 1) { @@ -356,7 +364,7 @@ export function typeFromExpr(expr: Expr, biblio: Biblio): Type { if (items[0]?.name === 'text' && ['!', '?'].includes(items[0].contents.trim())) { const remaining = stripWhitespace(items.slice(1)); if (remaining.length === 1 && ['call', 'sdo-call'].includes(remaining[0].name)) { - const callType = typeFromExpr(remaining[0], biblio); + const callType = typeFromExpr(remaining[0], biblio, warn); if (isCompletion(callType)) { const normal: Type = callType.kind === 'normal completion' @@ -384,7 +392,7 @@ export function typeFromExpr(expr: Expr, biblio: Biblio): Type { case 'list': { return { kind: 'list', - of: expr.elements.map(t => typeFromExpr(t, biblio)).reduce(join, { kind: 'never' }), + of: expr.elements.map(t => typeFromExpr(t, biblio, warn)).reduce(join, { kind: 'never' }), }; } case 'record': { @@ -398,6 +406,37 @@ export function typeFromExpr(expr: Expr, biblio: Biblio): Type { } const calleeName = callee[0].contents; + // special case: `Completion` is identity on completions + if (expr.name === 'call' && calleeName === 'Completion') { + if (expr.arguments.length === 1) { + const inner = typeFromExpr(expr.arguments[0], biblio, warn); + if (!isCompletion(inner)) { + // probably unknown, we might as well refine to "some completion" + return { + kind: 'union', + of: [ + { kind: 'normal completion', of: { kind: 'unknown' } }, + { kind: 'abrupt completion' }, + ], + }; + } + } else { + warn(expr.location.start.offset, 'expected Completion to be passed exactly one argument'); + } + } + + // special case: `NormalCompletion` wraps its input + if (expr.name === 'call' && calleeName === 'NormalCompletion') { + if (expr.arguments.length === 1) { + return { kind: 'normal completion', of: typeFromExpr(expr.arguments[0], biblio, warn) }; + } else { + warn( + expr.location.start.offset, + 'expected NormalCompletion to be passed exactly one argument', + ); + } + } + const biblioEntry = biblio.byAoid(calleeName); if (biblioEntry?.signature?.return == null) { break; @@ -433,6 +472,7 @@ export function typeFromExpr(expr: Expr, biblio: Biblio): Type { } return { kind: 'unknown' }; } + export function typeFromExprType(type: BiblioType): Type { switch (type.kind) { case 'union': { diff --git a/src/typechecker.ts b/src/typechecker.ts index c6b4ddb0..f57e3f54 100644 --- a/src/typechecker.ts +++ b/src/typechecker.ts @@ -74,7 +74,7 @@ const getExpressionVisitor = const params = signature.parameters.concat(signature.optionalParameters); for (const [arg, param] of zip(args, params, true)) { if (param.type == null) continue; - const argType = typeFromExpr(arg, spec.biblio); + const argType = typeFromExpr(arg, spec.biblio, warn); const paramType = typeFromExprType(param.type); // often we can't infer the argument precisely, so we check only that the intersection is nonempty rather than that the argument type is a subtype of the parameter type diff --git a/test/typecheck.js b/test/typecheck.js index a1be0d57..f952f5d0 100644 --- a/test/typecheck.js +++ b/test/typecheck.js @@ -1511,7 +1511,7 @@ describe('type system', () => { await assertTypeError( 'an ECMAScript language value', 'NormalCompletion(42)', - 'argument type (a normal completion) does not look plausibly assignable to parameter type (ECMAScript language value)', + 'argument type (a normal completion containing 42) does not look plausibly assignable to parameter type (ECMAScript language value)', [completionBiblio], ); @@ -1535,7 +1535,14 @@ describe('type system', () => { await assertTypeError( 'a Boolean', 'NormalCompletion(*false*)', - 'argument type (a normal completion) does not look plausibly assignable to parameter type (Boolean)', + 'argument type (a normal completion containing false) does not look plausibly assignable to parameter type (Boolean)', + [completionBiblio], + ); + + await assertTypeError( + 'a normal completion containing a Number', + 'NormalCompletion(*false*)', + 'argument type (a normal completion containing false) does not look plausibly assignable to parameter type (a normal completion containing Number)', [completionBiblio], ); @@ -1544,6 +1551,13 @@ describe('type system', () => { 'NormalCompletion(*false*)', [completionBiblio], ); + + await assertNoTypeError( + 'either a normal completion containing an ECMAScript language value or an abrupt completion', + 'NormalCompletion(*false*)', + [completionBiblio], + ); + await assertNoTypeError('a Boolean', '! Throwy()', [completionBiblio]); }); @@ -1709,3 +1723,79 @@ describe('error location', () => { ); }); }); + +describe('special cases', () => { + it('NormalCompletion takes one argument', async () => { + await assertLint( + positioned` + +

NormalCompletion ( )

+
+ + +

+ TakesCompletion ( + _x_: a normal completion or an abrupt completion + ): ~unused~ +

+
+
+ + 1. Do something with _x_. + +
+ + +

Example ()

+
+
+ + 1. Perform TakesCompletion(${M}NormalCompletion()). + +
+ `, + { + ruleId: 'typecheck', + nodeType: 'emu-alg', + message: 'expected NormalCompletion to be passed exactly one argument', + }, + ); + }); + + it('NormalCompletion takes one argument', async () => { + await assertLint( + positioned` + +

Completion ( )

+
+ + +

+ TakesCompletion ( + _x_: a normal completion or an abrupt completion + ): ~unused~ +

+
+
+ + 1. Do something with _x_. + +
+ + +

Example ()

+
+
+ + 1. Perform TakesCompletion(${M}Completion()). + +
+ `, + { + ruleId: 'typecheck', + nodeType: 'emu-alg', + message: 'expected Completion to be passed exactly one argument', + }, + ); + }); +});