diff --git a/spec/index.html b/spec/index.html
index 8c0398d7..a1b8e995 100644
--- a/spec/index.html
+++ b/spec/index.html
@@ -212,6 +212,7 @@
Structured Headers
for: The type of value to which a clause of type "concrete method" or "internal method" applies.
redefinition: If "true", the name of the operation will not automatically link (i.e., it will not automatically be given an aoid).
skip global checks: If "true", disables consistency checks for this AO which require knowing every callsite.
+ skip return checks: If "true", disables checking that the returned values from this AO correspond to its declared return type. Adding this to an AO which does not require it will produce a warning.
diff --git a/src/Algorithm.ts b/src/Algorithm.ts
index 8f9b6082..e6560d86 100644
--- a/src/Algorithm.ts
+++ b/src/Algorithm.ts
@@ -68,72 +68,6 @@ export default class Algorithm extends Builder {
namespace,
}));
spec._ntStringRefs = spec._ntStringRefs.concat(nonterminals);
-
- const returnType = clause?.signature?.return;
- let containsAnyCompletionyThings = false;
- if (returnType?.kind != null) {
- function checkForCompletionyStuff(list: emd.OrderedListNode) {
- for (const step of list.contents) {
- if (
- step.contents[0].name === 'text' &&
- /^(note|assert):/i.test(step.contents[0].contents)
- ) {
- continue;
- }
- if (
- step.contents.some(
- c => c.name === 'text' && /a new (\w+ )?Abstract Closure/i.test(c.contents),
- )
- ) {
- continue;
- }
- for (const part of step.contents) {
- if (part.name !== 'text') {
- continue;
- }
- const completionyThing = part.contents.match(
- /\b(ReturnIfAbrupt\b|(^|(?<=, ))[tT]hrow (a\b|the\b|$)|[rR]eturn (Normal|Throw|Return)?Completion\(|[rR]eturn( a| a new| the)? Completion Record\b|the result of evaluating\b)|(?<=[\s(])\?\s/,
- );
- if (completionyThing != null) {
- if (returnType?.kind === 'completion') {
- containsAnyCompletionyThings = true;
- } else {
- spec.warn({
- type: 'contents',
- ruleId: 'completiony-thing-in-non-completion-algorithm',
- message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
- node,
- nodeRelativeLine: part.location.start.line,
- nodeRelativeColumn: part.location.start.column + completionyThing.index!,
- });
- }
- }
- }
- if (step.sublist?.name === 'ol') {
- checkForCompletionyStuff(step.sublist);
- }
- }
- }
- checkForCompletionyStuff(emdTree.contents);
-
- // TODO: remove 'GeneratorYield' when the spec is more coherent (https://github.com/tc39/ecma262/pull/2429)
- // TODO: remove SDOs after doing the work necessary to coordinate the `containsAnyCompletionyThings` bit across all the piecewise components of an SDO's definition
- if (
- !['Completion', 'GeneratorYield'].includes(clause.aoid!) &&
- returnType?.kind === 'completion' &&
- !containsAnyCompletionyThings &&
- !['sdo', 'internal method', 'concrete method'].includes(clause.type!)
- ) {
- spec.warn({
- type: 'node',
- ruleId: 'completion-algorithm-lacks-completiony-thing',
- message:
- 'this algorithm is declared as returning a Completion Record, but there is no step which might plausibly return an abrupt completion',
- node,
- });
- }
- }
}
const rawHtml = emd.emit(emdTree);
diff --git a/src/Biblio.ts b/src/Biblio.ts
index 0994ba2c..a6e6c192 100644
--- a/src/Biblio.ts
+++ b/src/Biblio.ts
@@ -271,8 +271,10 @@ export default class Biblio {
delete copy.location;
// @ts-ignore
delete copy.referencingIds;
- // @ts-ignore
- delete copy._node;
+ for (const key of Object.keys(copy)) {
+ // @ts-ignore
+ if (key.startsWith('_')) delete copy[key];
+ }
return copy;
}),
};
@@ -344,6 +346,7 @@ export interface AlgorithmBiblioEntry extends BiblioEntryBase {
signature: null | Signature;
effects: string[];
skipGlobalChecks?: boolean;
+ /** @internal*/ _skipReturnChecks?: boolean;
/** @internal*/ _node?: Element;
}
@@ -398,7 +401,7 @@ export type PartialBiblioEntry = Unkey;
export type ExportedBiblio = {
location: string;
- entries: PartialBiblioEntry[];
+ entries: Unkey[];
};
function dumpEnv(env: EnvRec) {
diff --git a/src/Clause.ts b/src/Clause.ts
index 039fd594..a7794f35 100644
--- a/src/Clause.ts
+++ b/src/Clause.ts
@@ -55,6 +55,7 @@ export default class Clause extends Builder {
/** @internal */ readonly effects: string[]; // this is held by identity and mutated by Spec.ts
/** @internal */ signature: Signature | null;
/** @internal */ skipGlobalChecks: boolean;
+ /** @internal */ skipReturnChecks: boolean;
constructor(spec: Spec, node: HTMLElement, parent: Clause, number: string) {
super(spec, node);
@@ -67,6 +68,7 @@ export default class Clause extends Builder {
this.examples = [];
this.effects = [];
this.skipGlobalChecks = false;
+ this.skipReturnChecks = false;
// namespace is either the entire spec or the parent clause's namespace.
let parentNamespace = spec.namespace;
@@ -206,6 +208,7 @@ export default class Clause extends Builder {
effects,
redefinition,
skipGlobalChecks,
+ skipReturnChecks,
} = parseStructuredHeaderDl(this.spec, type, dl);
const paras = formatPreamble(
@@ -237,6 +240,7 @@ export default class Clause extends Builder {
}
this.skipGlobalChecks = skipGlobalChecks;
+ this.skipReturnChecks = skipReturnChecks;
this.effects.push(...effects);
for (const effect of effects) {
@@ -373,6 +377,7 @@ export default class Clause extends Builder {
signature,
effects: clause.effects,
_node: clause.node,
+ _skipReturnChecks: clause.skipReturnChecks,
};
if (clause.skipGlobalChecks) {
op.skipGlobalChecks = true;
diff --git a/src/expr-parser.ts b/src/expr-parser.ts
index b005e1de..1d167f52 100644
--- a/src/expr-parser.ts
+++ b/src/expr-parser.ts
@@ -7,7 +7,7 @@ const periodSpaceMatcher = /(?\.(?= ))/u;
const periodSpaceOrEOFMatcher = /(?\.(?= |$))/u;
type SimpleLocation = { start: { offset: number }; end: { offset: number } };
-type BareText = {
+export type BareText = {
name: 'text';
contents: string;
location: SimpleLocation;
diff --git a/src/header-parser.ts b/src/header-parser.ts
index 7195c4c1..66dadbfd 100644
--- a/src/header-parser.ts
+++ b/src/header-parser.ts
@@ -407,12 +407,14 @@ export function parseStructuredHeaderDl(
effects: string[];
redefinition: boolean;
skipGlobalChecks: boolean;
+ skipReturnChecks: boolean;
} {
let description = null;
let _for = null;
let redefinition: boolean | null = null;
let effects: string[] = [];
let skipGlobalChecks: boolean | null = null;
+ let skipReturnChecks: boolean | null = null;
for (let i = 0; i < dl.children.length; ++i) {
const dt = dl.children[i];
if (dt.tagName !== 'DT') {
@@ -482,6 +484,7 @@ export function parseStructuredHeaderDl(
}
break;
}
+ // TODO figure out how to de-dupe the code for boolean attributes
case 'redefinition': {
if (redefinition != null) {
spec.warn({
@@ -538,6 +541,34 @@ export function parseStructuredHeaderDl(
}
break;
}
+ case 'skip return checks': {
+ if (skipReturnChecks != null) {
+ spec.warn({
+ type: 'node',
+ ruleId: 'header-format',
+ message: `duplicate "skip return checks" attribute`,
+ node: dt,
+ });
+ }
+ const contents = (dd.textContent ?? '').trim();
+ if (contents === 'true') {
+ skipReturnChecks = true;
+ } else if (contents === 'false') {
+ skipReturnChecks = false;
+ } else {
+ spec.warn({
+ type: 'contents',
+ ruleId: 'header-format',
+ message: `unknown value for "skip return checks" attribute (expected "true" or "false", got ${JSON.stringify(
+ contents,
+ )})`,
+ node: dd,
+ nodeRelativeLine: 1,
+ nodeRelativeColumn: 1,
+ });
+ }
+ break;
+ }
case '': {
spec.warn({
type: 'node',
@@ -564,6 +595,7 @@ export function parseStructuredHeaderDl(
effects,
redefinition: redefinition ?? false,
skipGlobalChecks: skipGlobalChecks ?? false,
+ skipReturnChecks: skipReturnChecks ?? false,
};
}
diff --git a/src/type-logic.ts b/src/type-logic.ts
index 0e1ccc78..cd96d7bc 100644
--- a/src/type-logic.ts
+++ b/src/type-logic.ts
@@ -2,7 +2,7 @@ import type Biblio from './Biblio';
import type { Type as BiblioType } from './Biblio';
import type { Expr, NonSeq } from './expr-parser';
-type Type =
+export type Type =
| { kind: 'unknown' } // top
| { kind: 'never' } // bottom
| { kind: 'union'; of: NonUnion[] } // constraint: nothing in the union dominates anything else in the union
@@ -80,6 +80,7 @@ const dominateGraph: Partial> = {
bigint: ['concrete bigint'],
boolean: ['concrete boolean'],
};
+
/*
The type lattice used here is very simple (aside from explicit unions).
As such we mostly only need to define the `dominates` relationship and apply trivial rules:
@@ -140,6 +141,7 @@ export function dominates(a: Type, b: Type): boolean {
}
return false;
}
+
function addToUnion(types: NonUnion[], type: NonUnion): Type {
if (type.kind === 'normal completion') {
const existingNormalCompletionIndex = types.findIndex(t => t.kind === 'normal completion');
@@ -583,8 +585,7 @@ export function typeFromExprType(type: BiblioType): Type {
break;
}
case 'unused': {
- // this is really only a return type, but might as well handle it
- return { kind: 'enum value', value: '~unused~' };
+ return { kind: 'enum value', value: 'unused' };
}
}
return { kind: 'unknown' };
@@ -600,15 +601,24 @@ export function isCompletion(
);
}
+export function isPossiblyAbruptCompletion(
+ type: Type,
+): type is Type & { kind: 'abrupt completion' | 'union' } {
+ return (
+ type.kind === 'abrupt completion' ||
+ (type.kind === 'union' && type.of.some(isPossiblyAbruptCompletion))
+ );
+}
+
export function stripWhitespace(items: NonSeq[]) {
items = [...items];
- while (items[0]?.name === 'text' && /^\s+$/.test(items[0].contents)) {
+ while (items[0]?.name === 'text' && /^\s*$/.test(items[0].contents)) {
items.shift();
}
while (
items[items.length - 1]?.name === 'text' &&
// @ts-expect-error
- /^\s+$/.test(items[items.length - 1].contents)
+ /^\s*$/.test(items[items.length - 1].contents)
) {
items.pop();
}
diff --git a/src/typechecker.ts b/src/typechecker.ts
index f57e3f54..77975dce 100644
--- a/src/typechecker.ts
+++ b/src/typechecker.ts
@@ -1,8 +1,9 @@
import type { OrderedListNode } from 'ecmarkdown';
import type { AlgorithmElementWithTree } from './Algorithm';
+import type Biblio from './Biblio';
import type { AlgorithmBiblioEntry, Type as BiblioType } from './Biblio';
import type Spec from './Spec';
-import type { Expr, PathItem, Seq } from './expr-parser';
+import type { BareText, Expr, NonSeq, PathItem, Seq } from './expr-parser';
import { walk as walkExpr, parse as parseExpr, isProsePart } from './expr-parser';
import { offsetToLineAndColumn, zip } from './utils';
import {
@@ -12,6 +13,9 @@ import {
serialize,
dominates,
stripWhitespace,
+ type Type,
+ isCompletion,
+ isPossiblyAbruptCompletion,
} from './type-logic';
type OnlyPerformedMap = Map;
@@ -77,17 +81,36 @@ const getExpressionVisitor =
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
- const intersection = meet(argType, paramType);
-
- if (
- intersection.kind === 'never' ||
- (intersection.kind === 'list' &&
- intersection.of.kind === 'never' &&
- // if the meet is list, and we're passing a concrete list, it had better be empty
- argType.kind === 'list' &&
- argType.of.kind !== 'never')
- ) {
+ const error = getErrorForUsingTypeXAsTypeY(argType, paramType);
+ if (error != null) {
+ let hint: string;
+ switch (error) {
+ case 'number-to-real': {
+ hint =
+ '\nhint: you passed an ES language Number, but this position takes a mathematical value';
+ break;
+ }
+ case 'bigint-to-real': {
+ hint =
+ '\nhint: you passed an ES language BigInt, but this position takes a mathematical value';
+ break;
+ }
+ case 'real-to-number': {
+ hint =
+ '\nhint: you passed a mathematical value, but this position takes an ES language Number';
+ break;
+ }
+ case 'real-to-bigint': {
+ hint =
+ '\nhint: you passed a mathematical value, but this position takes an ES language BigInt';
+ break;
+ }
+ case 'other': {
+ hint = '';
+ break;
+ }
+ }
+
const argDescriptor =
argType.kind.startsWith('concrete') ||
argType.kind === 'enum value' ||
@@ -96,18 +119,6 @@ const getExpressionVisitor =
? `(${serialize(argType)})`
: `type (${serialize(argType)})`;
- let hint = '';
- if (argType.kind === 'concrete number' && dominates({ kind: 'real' }, paramType)) {
- hint =
- '\nhint: you passed an ES language Number, but this position takes a mathematical value';
- } else if (argType.kind === 'concrete real' && dominates({ kind: 'number' }, paramType)) {
- hint =
- '\nhint: you passed a mathematical value, but this position takes an ES language Number';
- } else if (argType.kind === 'concrete real' && dominates({ kind: 'bigint' }, paramType)) {
- hint =
- '\nhint: you passed a mathematical value, but this position takes an ES language BigInt';
- }
-
const items = stripWhitespace(arg.items);
warn(
items[0].location.start.offset,
@@ -179,6 +190,40 @@ const getExpressionVisitor =
}
};
+type ErrorForUsingTypeXAsTypeY =
+ | null // i.e., no error
+ | 'number-to-real'
+ | 'bigint-to-real'
+ | 'real-to-number'
+ | 'real-to-bigint'
+ | 'other';
+function getErrorForUsingTypeXAsTypeY(argType: Type, paramType: Type): ErrorForUsingTypeXAsTypeY {
+ // 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
+ const intersection = meet(argType, paramType);
+
+ if (
+ intersection.kind === 'never' ||
+ (intersection.kind === 'list' &&
+ intersection.of.kind === 'never' &&
+ // if the meet is list, and we're passing a concrete list, it had better be empty
+ argType.kind === 'list' &&
+ argType.of.kind !== 'never')
+ ) {
+ if (argType.kind === 'concrete number' && dominates({ kind: 'real' }, paramType)) {
+ return 'number-to-real';
+ } else if (argType.kind === 'concrete bigint' && dominates({ kind: 'real' }, paramType)) {
+ return 'bigint-to-real';
+ } else if (argType.kind === 'concrete real' && dominates({ kind: 'number' }, paramType)) {
+ return 'real-to-number';
+ } else if (argType.kind === 'concrete real' && dominates({ kind: 'bigint' }, paramType)) {
+ return 'real-to-bigint';
+ }
+
+ return 'other';
+ }
+ return null;
+}
+
export function typecheck(spec: Spec) {
const isUnused = (t: BiblioType) =>
t.kind === 'unused' ||
@@ -222,8 +267,28 @@ export function typecheck(spec: Spec) {
});
};
+ let containingClause: Element | null = node;
+ while (containingClause?.nodeName != null && containingClause.nodeName !== 'EMU-CLAUSE') {
+ containingClause = containingClause.parentElement;
+ }
+ const aoid = containingClause?.getAttribute('aoid');
+ const biblioEntry = aoid == null ? null : spec.biblio.byAoid(aoid);
+ const signature = biblioEntry?.signature;
+
+ // hasPossibleCompletionReturn is a three-state: null to indicate we're not looking, or a boolean
+ let hasPossibleCompletionReturn =
+ signature?.return == null ||
+ ['sdo', 'internal method', 'concrete method'].includes(
+ containingClause!.getAttribute('type')!,
+ )
+ ? null
+ : false;
+ const returnType = signature?.return == null ? null : typeFromExprType(signature.return);
+ let numberOfAbstractClosuresWeAreWithin = 0;
+ let hadReturnIssue = false;
const walkLines = (list: OrderedListNode) => {
for (const line of list.contents) {
+ let thisLineIsAbstractClosure = false;
// we already parsed in collect-algorithm-diagnostics, but that was before generating the biblio
// the biblio affects the parse of calls, so we need to re-do it
// TODO do collect-algorithm-diagnostics after generating the biblio, somehow?
@@ -240,13 +305,81 @@ export function typecheck(spec: Spec) {
});
} else {
walkExpr(getExpressionVisitor(spec, warn, onlyPerformed, alwaysAssertedToBeNormal), item);
+ if (returnType != null) {
+ let idx = item.items.length - 1;
+ let last: NonSeq | undefined;
+ while (
+ idx >= 0 &&
+ ((last = item.items[idx]).name !== 'text' ||
+ (last as BareText).contents.trim() === '')
+ ) {
+ --idx;
+ }
+ if (
+ last != null &&
+ (last as BareText).contents.endsWith('performs the following steps when called:')
+ ) {
+ thisLineIsAbstractClosure = true;
+ ++numberOfAbstractClosuresWeAreWithin;
+ } else if (numberOfAbstractClosuresWeAreWithin === 0) {
+ const returnWarn = biblioEntry?._skipReturnChecks
+ ? () => {
+ hadReturnIssue = true;
+ }
+ : warn;
+ const lineHadCompletionReturn = inspectReturns(
+ returnWarn,
+ item,
+ returnType,
+ spec.biblio,
+ );
+ if (hasPossibleCompletionReturn != null) {
+ if (lineHadCompletionReturn == null) {
+ hasPossibleCompletionReturn = null;
+ } else {
+ hasPossibleCompletionReturn ||= lineHadCompletionReturn;
+ }
+ }
+ }
+ }
}
if (line.sublist?.name === 'ol') {
walkLines(line.sublist);
}
+ if (thisLineIsAbstractClosure) {
+ --numberOfAbstractClosuresWeAreWithin;
+ }
}
};
walkLines(tree.contents);
+
+ if (
+ returnType != null &&
+ isPossiblyAbruptCompletion(returnType) &&
+ hasPossibleCompletionReturn === false
+ ) {
+ if (biblioEntry!._skipReturnChecks) {
+ hadReturnIssue = true;
+ } else {
+ spec.warn({
+ type: 'node',
+ ruleId: 'completion-algorithm-lacks-completiony-thing',
+ message:
+ 'this algorithm is declared as returning an abrupt completion, but there is no step which might plausibly return an abrupt completion',
+ node,
+ });
+ }
+ }
+
+ if (biblioEntry?._skipReturnChecks && !hadReturnIssue) {
+ spec.warn({
+ type: 'node',
+ ruleId: 'unnecessary-attribute',
+ message:
+ 'this algorithm has the "skip return check" attribute, but there is nothing which would cause an issue if it were removed',
+ node,
+ });
+ }
}
for (const [aoid, state] of onlyPerformed) {
@@ -332,6 +465,157 @@ function isConsumedAsCompletion(expr: Expr, path: PathItem[]) {
return false;
}
+// returns a boolean to indicate whether this line can return an abrupt completion, or null to indicate we should stop caring
+// also checks return types in general
+function inspectReturns(
+ warn: (offset: number, message: string) => void,
+ line: Seq,
+ returnType: Type,
+ biblio: Biblio,
+): boolean | null {
+ let hadAbrupt = false;
+ // check for `throw` etc
+ const throwRegexp =
+ /\b(ReturnIfAbrupt\b|IfAbruptCloseIterator\b|(^|(?<=, ))[tT]hrow (a\b|the\b|$)|[rR]eturn( a| a new| the)? Completion Record\b|the result of evaluating\b)|(?<=[\s(]|\b)\?\s/;
+ let thrower = line.items.find(e => e.name === 'text' && throwRegexp.test(e.contents));
+ let offsetOfThrow: number;
+ if (thrower == null) {
+ // `Throw` etc are at the top level, but the `?` macro can be nested, so we need to walk the whole line looking for it
+ walkExpr(e => {
+ if (thrower != null || e.name !== 'text') return;
+ const qm = /((?<=[\s(])|\b|^)\?(\s|$)/.exec(e.contents);
+ if (qm != null) {
+ thrower = e;
+ offsetOfThrow = qm.index;
+ }
+ }, line);
+ } else {
+ offsetOfThrow = throwRegexp.exec((thrower as BareText).contents)!.index;
+ }
+ if (thrower != null) {
+ if (isPossiblyAbruptCompletion(returnType)) {
+ hadAbrupt = true;
+ } else {
+ warn(
+ thrower.location.start.offset + offsetOfThrow!,
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
+ );
+ return null;
+ }
+ }
+
+ // check return types
+ const returnIndex = line.items.findIndex(
+ e => e.name === 'text' && /\b[Rr]eturn /.test(e.contents),
+ );
+ if (returnIndex === -1) return hadAbrupt;
+ const last = line.items[line.items.length - 1];
+ if (last.name !== 'text' || !/\.\s*$/.test(last.contents)) return hadAbrupt;
+
+ const ret = line.items[returnIndex] as BareText;
+ const afterRet = /\b[Rr]eturn\b/.exec(ret.contents)!.index + 6; /* 'return'.length */
+
+ const beforePeriod = /\.\s*$/.exec(last.contents)!.index;
+ let returnedExpr: Seq;
+
+ if (ret === last) {
+ returnedExpr = {
+ name: 'seq',
+ items: [
+ {
+ name: 'text',
+ contents: ret.contents.slice(afterRet, beforePeriod),
+ location: {
+ start: { offset: ret.location.start.offset + afterRet },
+ end: { offset: ret.location.end.offset - (ret.contents.length - beforePeriod) },
+ },
+ },
+ ],
+ };
+ } else {
+ const tweakedFirst: BareText = {
+ name: 'text',
+ contents: ret.contents.slice(afterRet),
+ location: { start: { offset: ret.location.start.offset + afterRet }, end: ret.location.end },
+ };
+
+ const tweakedLast: BareText = {
+ name: 'text',
+ contents: last.contents.slice(0, beforePeriod),
+ location: {
+ start: last.location.start,
+ end: { offset: last.location.end.offset - (last.contents.length - beforePeriod) },
+ },
+ };
+
+ returnedExpr = {
+ name: 'seq',
+ items: [tweakedFirst, ...line.items.slice(returnIndex + 1, -1), tweakedLast],
+ };
+ }
+ const typeOfReturnedExpr = typeFromExpr(returnedExpr, biblio, warn);
+ if (hadAbrupt != null && isPossiblyAbruptCompletion(typeOfReturnedExpr)) {
+ hadAbrupt = true;
+ }
+
+ let error = getErrorForUsingTypeXAsTypeY(typeOfReturnedExpr, returnType);
+
+ if (error !== null) {
+ if (isCompletion(returnType) && !isCompletion(typeOfReturnedExpr)) {
+ // special case: you can return values for normal completions without wrapping
+ error = getErrorForUsingTypeXAsTypeY(
+ { kind: 'normal completion', of: typeOfReturnedExpr },
+ returnType,
+ );
+ if (error == null) return hadAbrupt;
+ }
+
+ const returnDescriptor =
+ typeOfReturnedExpr.kind.startsWith('concrete') ||
+ typeOfReturnedExpr.kind === 'enum value' ||
+ typeOfReturnedExpr.kind === 'null' ||
+ typeOfReturnedExpr.kind === 'undefined'
+ ? `returned value (${serialize(typeOfReturnedExpr)})`
+ : `type of returned value (${serialize(typeOfReturnedExpr)})`;
+
+ let hint: string;
+ switch (error) {
+ case 'number-to-real': {
+ hint =
+ '\nhint: you returned an ES language Number, but this algorithm returns a mathematical value';
+ break;
+ }
+ case 'bigint-to-real': {
+ hint =
+ '\nhint: you returned an ES language BigInt, but this algorithm returns a mathematical value';
+ break;
+ }
+ case 'real-to-number': {
+ hint =
+ '\nhint: you returned a mathematical value, but this algorithm returns an ES language Number';
+ break;
+ }
+ case 'real-to-bigint': {
+ hint =
+ '\nhint: you returned a mathematical value, but this algorithm returns an ES language BigInt';
+ break;
+ }
+ case 'other': {
+ hint = '';
+ break;
+ }
+ }
+
+ warn(
+ returnedExpr.items[0].location.start.offset +
+ /^\s*/.exec((returnedExpr.items[0] as BareText).contents)![0].length,
+ `${returnDescriptor} does not look plausibly assignable to algorithm's return type (${serialize(returnType)})${hint}`,
+ );
+ return null;
+ }
+ return hadAbrupt;
+}
+
function parentSkippingBlankSpace(expr: Expr, path: PathItem[]): PathItem | null {
for (let pointer: Expr = expr, i = path.length - 1; i >= 0; pointer = path[i].parent, --i) {
const { parent } = path[i];
diff --git a/test/typecheck.js b/test/typecheck.js
index f952f5d0..fda5985c 100644
--- a/test/typecheck.js
+++ b/test/typecheck.js
@@ -66,14 +66,28 @@ describe('typechecking completions', () => {
- ExampleCompletionAO (): a normal completion
+ ExampleCompletionAO (): a normal completion containing a number or an abrupt completion.
+ 1. If you want to, throw a new *Error* exception.
1. Return Completion Record { [[Type]]: ~normal~, [[Value]]: 0, [[Target]]: ~empty~ }.
+
+
+
+ ExampleNonCompletionAO (
+ _x_: unknown,
+ ): a mathematical value
+
+
+
+ 1. Return 0.
+
+
`);
});
@@ -83,7 +97,7 @@ describe('typechecking completions', () => {
positioned`
- ExampleAlg (): a normal completion containing a Number
+ ExampleAlg (): a normal completion containing a mathematical value
@@ -237,7 +251,7 @@ describe('typechecking completions', () => {
positioned`
- ExampleAlg (): a Number
+ ExampleAlg (): a mathematical value
@@ -269,7 +283,7 @@ describe('typechecking completions', () => {
await assertLintFree(`
- ExampleAlg (): a Number
+ ExampleAlg (): a mathematical value
@@ -310,10 +324,10 @@ describe('typechecking completions', () => {
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
},
);
@@ -331,10 +345,10 @@ describe('typechecking completions', () => {
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
},
);
@@ -352,10 +366,10 @@ describe('typechecking completions', () => {
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
},
);
@@ -374,10 +388,10 @@ describe('typechecking completions', () => {
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
},
);
@@ -385,21 +399,43 @@ describe('typechecking completions', () => {
positioned`
- ExampleAlg (): a Number
+ ExampleAlg (): a normal completion containing a Number
+
+
+
+ 1. Let _foo_ be a thing.
+ 1. ${M}Throw _foo_.
+
+
+ `,
+ {
+ ruleId: 'typecheck',
+ nodeType: 'emu-alg',
+ message:
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
+ },
+ );
+
+ await assertLint(
+ positioned`
+
+
+ ExampleAlg (): a mathematical value
1. Let _x_ be 0.
- 1. ${M}Return Completion(_x_).
+ 1. Return ${M}Completion(_x_).
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ "type of returned value (a normal completion or an abrupt completion) does not look plausibly assignable to algorithm's return type (mathematical value)",
},
{
extraBiblios: [biblio],
@@ -421,10 +457,10 @@ describe('typechecking completions', () => {
`,
{
- ruleId: 'completiony-thing-in-non-completion-algorithm',
+ ruleId: 'typecheck',
nodeType: 'emu-alg',
message:
- 'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
+ 'this would return an abrupt completion, but the containing AO is declared not to return an abrupt completion',
},
);
});
@@ -464,6 +500,29 @@ describe('typechecking completions', () => {
1. Do something with Completion(ExampleCompletionAO()).
1. NOTE: This will not throw a *TypeError* exception.
1. Consider whether something is a return completion.
+ 1. Throw a *RangeError* exception.
+
+
+ `,
+ {
+ extraBiblios: [biblio],
+ },
+ );
+
+ await assertLintFree(
+ `
+
+
+ ExampleAlg (): a mathematical value
+
+
+
+ 1. Let _addend_ be 41.
+ 1. Let _closure_ be a new Abstract Closure with parameters (_x_) that captures _addend_ and performs the following steps when called:
+ 1. Return _x_ + ? ExampleCompletionAO(_addend_).
+ 1. Let _val_ be ! _closure_(1).
+ 1. Return _val_.
`,
@@ -494,25 +553,37 @@ describe('typechecking completions', () => {
ruleId: 'completion-algorithm-lacks-completiony-thing',
nodeType: 'emu-alg',
message:
- 'this algorithm is declared as returning a Completion Record, but there is no step which might plausibly return an abrupt completion',
+ 'this algorithm is declared as returning an abrupt completion, but there is no step which might plausibly return an abrupt completion',
},
);
});
it('negative', async () => {
- await assertLintFree(`
-
-
- ExampleAlg (): either a normal completion containing a Number or an abrupt completion
-
-
-
- 1. Let _foo_ be 0.
- 1. Return ? _foo_.
-
-
- `);
+ async function assertStepIsConsideredAbrupt(step) {
+ await assertLintFree(
+ `
+
+
+ ExampleAlg (): either a normal completion containing a mathematical value or an abrupt completion
+
+
+
+ 1. Let _foo_ be 0.
+ 1. ${step}
+
+
+ `,
+ {
+ extraBiblios: [biblio],
+ },
+ );
+ }
+
+ await assertStepIsConsideredAbrupt('Return ? _foo_.');
+ await assertStepIsConsideredAbrupt('Return ? _foo_.');
+
+ await assertStepIsConsideredAbrupt('Return ExampleNonCompletionAO(? _foo_).');
await assertLintFree(`
@@ -601,7 +672,7 @@ describe('typechecking completions', () => {
positioned`
${M}
- ExampleAlg (): a Number
+ ExampleAlg (): a mathematical value
@@ -634,7 +705,7 @@ describe('typechecking completions', () => {
await assertLintFree(`
- ExampleAlg (): a Number
+ ExampleAlg (): a mathematical value
@@ -1248,7 +1319,13 @@ describe('negation', async () => {
});
describe('type system', () => {
- async function assertTypeError(paramType, arg, message, extraBiblios = []) {
+ async function assertTypeError(
+ paramType,
+ arg,
+ messageForParam,
+ messageForReturn,
+ extraBiblios = [],
+ ) {
await assertLint(
positioned`
@@ -1266,12 +1343,51 @@ describe('type system', () => {
{
ruleId: 'typecheck',
nodeType: 'emu-alg',
- message,
+ message: messageForParam,
},
{
extraBiblios,
},
);
+
+ if (messageForReturn === null) {
+ await assertLintFree(
+ `
+
+ Example (): ${paramType}
+
+
+ 1. ${paramType.includes('abrupt completion') ? 'Throw a new *Error*.' : 'Do something.'}
+ 1. Return ${arg}.
+
+
+ `,
+ {
+ extraBiblios,
+ },
+ );
+ } else {
+ await assertLint(
+ positioned`
+
+ Example (): ${paramType}
+
+
+ 1. ${paramType.includes('abrupt completion') ? 'Throw a new *Error*.' : 'Do something.'}
+ 1. Return ${M}${arg}.
+
+
+ `,
+ {
+ ruleId: 'typecheck',
+ nodeType: 'emu-alg',
+ message: messageForReturn,
+ },
+ {
+ extraBiblios,
+ },
+ );
+ }
}
async function assertNoTypeError(paramType, arg, extraBiblios = []) {
@@ -1331,6 +1447,7 @@ describe('type system', () => {
'~sync~ or ~async~',
'~iterate-strings~',
'argument (~iterate-strings~) does not look plausibly assignable to parameter type (~sync~ or ~async~)',
+ "returned value (~iterate-strings~) does not look plausibly assignable to algorithm's return type (~sync~ or ~async~)",
);
await assertNoTypeError('~sync~ or ~async~', '~sync~');
@@ -1342,12 +1459,14 @@ describe('type system', () => {
'a Number',
'*false*',
'argument (false) does not look plausibly assignable to parameter type (Number)',
+ "returned value (false) does not look plausibly assignable to algorithm's return type (Number)",
);
await assertTypeError(
'a Boolean',
'*1*๐ฝ',
'argument (*1*๐ฝ) does not look plausibly assignable to parameter type (Boolean)',
+ "returned value (*1*๐ฝ) does not look plausibly assignable to algorithm's return type (Boolean)",
);
await assertNoTypeError('a Boolean', '*false*');
@@ -1358,21 +1477,25 @@ describe('type system', () => {
'*null*',
'*undefined*',
'argument (undefined) does not look plausibly assignable to parameter type (null)',
+ "returned value (undefined) does not look plausibly assignable to algorithm's return type (null)",
);
await assertTypeError(
'a Boolean or *null*',
'*undefined*',
'argument (undefined) does not look plausibly assignable to parameter type (Boolean or null)',
+ "returned value (undefined) does not look plausibly assignable to algorithm's return type (Boolean or null)",
);
await assertTypeError(
'*undefined*',
'*null*',
'argument (null) does not look plausibly assignable to parameter type (undefined)',
+ "returned value (null) does not look plausibly assignable to algorithm's return type (undefined)",
);
await assertTypeError(
'a Boolean or *undefined*',
'*null*',
'argument (null) does not look plausibly assignable to parameter type (Boolean or undefined)',
+ "returned value (null) does not look plausibly assignable to algorithm's return type (Boolean or undefined)",
);
await assertNoTypeError('an ECMAScript language value', '*null*');
@@ -1389,12 +1512,17 @@ describe('type system', () => {
'5',
'argument (5) does not look plausibly assignable to parameter type (BigInt)\n' +
'hint: you passed a mathematical value, but this position takes an ES language BigInt',
+ "returned value (5) does not look plausibly assignable to algorithm's return type (BigInt)\n" +
+ 'hint: you returned a mathematical value, but this algorithm returns an ES language BigInt',
);
await assertTypeError(
'an integer',
'*5*โค',
- 'argument (*5*โค) does not look plausibly assignable to parameter type (integer)',
+ 'argument (*5*โค) does not look plausibly assignable to parameter type (integer)\n' +
+ 'hint: you passed an ES language BigInt, but this position takes a mathematical value',
+ "returned value (*5*โค) does not look plausibly assignable to algorithm's return type (integer)\n" +
+ 'hint: you returned an ES language BigInt, but this algorithm returns a mathematical value',
);
await assertNoTypeError('a BigInt', '*5*โค');
@@ -1405,6 +1533,7 @@ describe('type system', () => {
'an integer',
'0.5',
'argument (0.5) does not look plausibly assignable to parameter type (integer)',
+ "returned value (0.5) does not look plausibly assignable to algorithm's return type (integer)",
);
await assertNoTypeError('an integer', '2');
@@ -1413,6 +1542,7 @@ describe('type system', () => {
'a non-negative integer',
'-1',
'argument (-1) does not look plausibly assignable to parameter type (non-negative integer)',
+ "returned value (-1) does not look plausibly assignable to algorithm's return type (non-negative integer)",
);
await assertNoTypeError('a non-negative integer', '3');
@@ -1421,30 +1551,35 @@ describe('type system', () => {
'*1*๐ฝ',
'*2*๐ฝ',
'argument (*2*๐ฝ) does not look plausibly assignable to parameter type (*1*๐ฝ)',
+ "returned value (*2*๐ฝ) does not look plausibly assignable to algorithm's return type (*1*๐ฝ)",
);
await assertTypeError(
'*+0*๐ฝ',
'*-0*๐ฝ',
'argument (*-0*๐ฝ) does not look plausibly assignable to parameter type (*+0*๐ฝ)',
+ "returned value (*-0*๐ฝ) does not look plausibly assignable to algorithm's return type (*+0*๐ฝ)",
);
await assertTypeError(
'an integral Number',
'*0.5*๐ฝ',
'argument (*0.5*๐ฝ) does not look plausibly assignable to parameter type (integral Number)',
+ "returned value (*0.5*๐ฝ) does not look plausibly assignable to algorithm's return type (integral Number)",
);
await assertTypeError(
'an integral Number',
'*NaN*',
'argument (*NaN*) does not look plausibly assignable to parameter type (integral Number)',
+ "returned value (*NaN*) does not look plausibly assignable to algorithm's return type (integral Number)",
);
await assertTypeError(
'an integral Number',
'*+∞*๐ฝ',
'argument (*+∞*๐ฝ) does not look plausibly assignable to parameter type (integral Number)',
+ "returned value (*+∞*๐ฝ) does not look plausibly assignable to algorithm's return type (integral Number)",
);
await assertNoTypeError('*2*๐ฝ', '*2*๐ฝ');
@@ -1459,6 +1594,7 @@ describe('type system', () => {
'an integral Number',
'*0.5*๐ฝ',
'argument (*0.5*๐ฝ) does not look plausibly assignable to parameter type (integral Number)',
+ "returned value (*0.5*๐ฝ) does not look plausibly assignable to algorithm's return type (integral Number)",
);
await assertTypeError(
@@ -1466,6 +1602,8 @@ describe('type system', () => {
'*5*๐ฝ',
'argument (*5*๐ฝ) does not look plausibly assignable to parameter type (mathematical value)\n' +
'hint: you passed an ES language Number, but this position takes a mathematical value',
+ "returned value (*5*๐ฝ) does not look plausibly assignable to algorithm's return type (mathematical value)\n" +
+ 'hint: you returned an ES language Number, but this algorithm returns a mathematical value',
);
await assertTypeError(
@@ -1473,6 +1611,8 @@ describe('type system', () => {
'5',
'argument (5) does not look plausibly assignable to parameter type (integral Number)\n' +
'hint: you passed a mathematical value, but this position takes an ES language Number',
+ "returned value (5) does not look plausibly assignable to algorithm's return type (integral Number)\n" +
+ 'hint: you returned a mathematical value, but this algorithm returns an ES language Number',
);
});
@@ -1481,6 +1621,7 @@ describe('type system', () => {
'a time value',
'~enum-value~',
'argument (~enum-value~) does not look plausibly assignable to parameter type (integral Number or *NaN*)',
+ "returned value (~enum-value~) does not look plausibly assignable to algorithm's return type (integral Number or *NaN*)",
);
await assertTypeError(
@@ -1488,6 +1629,8 @@ describe('type system', () => {
'5',
'argument (5) does not look plausibly assignable to parameter type (integral Number or *NaN*)\n' +
'hint: you passed a mathematical value, but this position takes an ES language Number',
+ "returned value (5) does not look plausibly assignable to algorithm's return type (integral Number or *NaN*)\n" +
+ 'hint: you returned a mathematical value, but this algorithm returns an ES language Number',
);
await assertNoTypeError('a time value', '*2*๐ฝ');
@@ -1500,18 +1643,21 @@ describe('type system', () => {
'an ECMAScript language value',
'~enum-value~',
'argument (~enum-value~) does not look plausibly assignable to parameter type (ECMAScript language value)',
+ "returned value (~enum-value~) does not look plausibly assignable to algorithm's return type (ECMAScript language value)",
);
await assertTypeError(
'an ECMAScript language value',
'42',
'argument (42) does not look plausibly assignable to parameter type (ECMAScript language value)',
+ "returned value (42) does not look plausibly assignable to algorithm's return type (ECMAScript language value)",
);
await assertTypeError(
'an ECMAScript language value',
'NormalCompletion(42)',
'argument type (a normal completion containing 42) does not look plausibly assignable to parameter type (ECMAScript language value)',
+ "type of returned value (a normal completion containing 42) does not look plausibly assignable to algorithm's return type (ECMAScript language value)",
[completionBiblio],
);
@@ -1529,6 +1675,7 @@ describe('type system', () => {
'either a normal completion containing a Boolean or an abrupt completion',
'*false*',
'argument (false) does not look plausibly assignable to parameter type (a normal completion containing Boolean or an abrupt completion)',
+ null /* it is legal to return an unwrapped value from an algorithm declared to return a normal completion */,
[completionBiblio],
);
@@ -1536,6 +1683,7 @@ describe('type system', () => {
'a Boolean',
'NormalCompletion(*false*)',
'argument type (a normal completion containing false) does not look plausibly assignable to parameter type (Boolean)',
+ "type of returned value (a normal completion containing false) does not look plausibly assignable to algorithm's return type (Boolean)",
[completionBiblio],
);
@@ -1543,6 +1691,7 @@ describe('type system', () => {
'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)',
+ "type of returned value (a normal completion containing false) does not look plausibly assignable to algorithm's return type (a normal completion containing Number)",
[completionBiblio],
);
@@ -1588,6 +1737,7 @@ describe('type system', () => {
'a String',
'ReturnsNumber()',
'argument type (Number) does not look plausibly assignable to parameter type (String)',
+ "type of returned value (Number) does not look plausibly assignable to algorithm's return type (String)",
[biblio],
);
@@ -1595,6 +1745,7 @@ describe('type system', () => {
'a String',
'! ReturnsCompletionOfNumber()',
'argument type (Number) does not look plausibly assignable to parameter type (String)',
+ "type of returned value (Number) does not look plausibly assignable to algorithm's return type (String)",
[biblio],
);
@@ -1620,6 +1771,7 @@ describe('type system', () => {
'an integer',
'ReturnsListOfNumberOrString()',
'argument type (List of Number or String) does not look plausibly assignable to parameter type (integer)',
+ "type of returned value (List of Number or String) does not look plausibly assignable to algorithm's return type (integer)",
[biblio],
);
@@ -1631,12 +1783,14 @@ describe('type system', () => {
'a String',
'ยซ ยป',
'argument type (empty List) does not look plausibly assignable to parameter type (String)',
+ "type of returned value (empty List) does not look plausibly assignable to algorithm's return type (String)",
);
await assertTypeError(
'a List of Strings',
'ยซ 0.5 ยป',
'argument type (List of 0.5) does not look plausibly assignable to parameter type (List of String)',
+ "type of returned value (List of 0.5) does not look plausibly assignable to algorithm's return type (List of String)",
);
await assertNoTypeError('a List of Strings', 'ยซ "something" ยป');
@@ -1649,21 +1803,25 @@ describe('type system', () => {
'0',
'1',
'argument (1) does not look plausibly assignable to parameter type (0)',
+ "returned value (1) does not look plausibly assignable to algorithm's return type (0)",
);
await assertTypeError(
'a positive integer',
'0',
'argument (0) does not look plausibly assignable to parameter type (positive integer)',
+ "returned value (0) does not look plausibly assignable to algorithm's return type (positive integer)",
);
await assertTypeError(
'a non-negative integer',
'-1',
'argument (-1) does not look plausibly assignable to parameter type (non-negative integer)',
+ "returned value (-1) does not look plausibly assignable to algorithm's return type (non-negative integer)",
);
await assertTypeError(
'a negative integer',
'0',
'argument (0) does not look plausibly assignable to parameter type (negative integer)',
+ "returned value (0) does not look plausibly assignable to algorithm's return type (negative integer)",
);
await assertNoTypeError('a mathematical value', '0');
@@ -1683,6 +1841,7 @@ describe('type system', () => {
'"type"',
'*"value"*',
'argument ("value") does not look plausibly assignable to parameter type ("type")',
+ 'returned value ("value") does not look plausibly assignable to algorithm\'s return type ("type")',
);
await assertNoTypeError('"a"', '*"a"*');
@@ -1724,6 +1883,70 @@ describe('error location', () => {
});
});
+describe('skip return checks', () => {
+ it('respects the "skip return checks" attribute', async () => {
+ await assertLintFree(`
+
+
+ Completion (
+ _completionRecord_: a Completion Record,
+ ): a Completion Record
+
+
+
+ 1. Assert: _completionRecord_ is a Completion Record.
+ 1. Return _completionRecord_.
+
+
+ `);
+
+ await assertLintFree(`
+
+
+ ExampleAlg (): either a normal completion containing a Number or an abrupt completion
+
+
+
+ 1. Let _foo_ be 0.
+ 1. Return _foo_.
+
+
+ `);
+ });
+
+ it('warns when the "skip return checks" attribute is unnecessary', async () => {
+ await assertLint(
+ positioned`
+
+
+ ExampleAlg (): a mathematical value
+
+
+ ${M}
+ 1. Let _foo_ be 0.
+ 1. Return _foo_.
+
+
+ `,
+ {
+ ruleId: 'unnecessary-attribute',
+ nodeType: 'emu-alg',
+ message:
+ 'this algorithm has the "skip return check" attribute, but there is nothing which would cause an issue if it were removed',
+ },
+ );
+ });
+});
+
describe('special cases', () => {
it('NormalCompletion takes one argument', async () => {
await assertLint(