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',
+ },
+ );
+ });
+});