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 +

    +
    +
    skip return checks
    +
    true
    +
    + + 1. Assert: _completionRecord_ is a Completion Record. + 1. Return _completionRecord_. + +
    + `); + + await assertLintFree(` + +

    + ExampleAlg (): either a normal completion containing a Number or an abrupt completion +

    +
    +
    skip return checks
    +
    true
    +
    + + 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 +

    +
    +
    skip return checks
    +
    true
    +
    + ${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(