diff --git a/spec/index.html b/spec/index.html index 7172d0ed..6ac8b613 100644 --- a/spec/index.html +++ b/spec/index.html @@ -346,6 +346,16 @@

Result

1. Return the result of evaluating this |NonTerminalProduction|. +

Declarations

+

The linter checks that each used variable in an algorithm step is declared earlier. It recognizes most common forms of declaration automatically, but in case there is a declaration which is not automatically inferred, a variable can be marked as declared for the purposes of this analysis by adding a `[declared="v"]` attribute at the start of the step. Multiple variables can be listed by seperating them with commas.

+ +

Element

+

+<emu-alg>
+  1. [declared="x"] Suppose the existence of _x_.
+</emu-alg>
+  
+

Replacement algorithms

Algorithms may be specified to replace a labeled step, in which case the algorithm will adopt the numbering of that step. For example:

diff --git a/src/lint/collect-algorithm-diagnostics.ts b/src/lint/collect-algorithm-diagnostics.ts index 3be1a055..2d22f4bb 100644 --- a/src/lint/collect-algorithm-diagnostics.ts +++ b/src/lint/collect-algorithm-diagnostics.ts @@ -11,6 +11,7 @@ import lintAlgorithmStepNumbering from './rules/algorithm-step-numbering'; import lintAlgorithmStepLabels from './rules/algorithm-step-labels'; import lintForEachElement from './rules/for-each-element'; import lintStepAttributes from './rules/step-attributes'; +import { checkVariableUsage } from './rules/variable-use-def'; const algorithmRules = [ lintAlgorithmLineStyle, @@ -80,6 +81,7 @@ export function collectAlgorithmDiagnostics( } if (tree != null) { visit(tree, observer); + checkVariableUsage(algorithm.element, tree.contents, reporter); } algorithm.tree = tree; diff --git a/src/lint/rules/step-attributes.ts b/src/lint/rules/step-attributes.ts index b6486367..6b5d8e42 100644 --- a/src/lint/rules/step-attributes.ts +++ b/src/lint/rules/step-attributes.ts @@ -3,7 +3,7 @@ import type { Reporter } from '../algorithm-error-reporter-type'; const ruleId = 'unknown-step-attribute'; -const KNOWN_ATTRIBUTES = ['id', 'fence-effects']; +const KNOWN_ATTRIBUTES = ['id', 'fence-effects', 'declared']; /* Checks for unknown attributes on steps. diff --git a/src/lint/rules/variable-use-def.ts b/src/lint/rules/variable-use-def.ts new file mode 100644 index 00000000..33dd9c70 --- /dev/null +++ b/src/lint/rules/variable-use-def.ts @@ -0,0 +1,352 @@ +import type { + FragmentNode, + OrderedListNode, + UnorderedListNode, + TextNode, + UnderscoreNode, +} from 'ecmarkdown'; +import type { Reporter } from '../algorithm-error-reporter-type'; + +type HasLocation = { location: { start: { line: number; column: number } } }; +type VarKind = + | 'parameter' + | 'variable' + | 'abstract closure parameter' + | 'abstract closure capture' + | 'loop variable' + | 'attribute declaration'; +class Scope { + declare vars: Map; + declare report: Reporter; + constructor(report: Reporter) { + this.vars = new Map(); + // TODO remove this when regex state objects become less dumb + for (const name of ['captures', 'input', 'startIndex', 'endIndex']) { + this.declare(name, null); + } + this.report = report; + } + + declared(name: string): boolean { + return this.vars.has(name); + } + + // only call this for variables previously checked to be declared + used(name: string): boolean { + return this.vars.get(name)!.used; + } + + declare(name: string, nameNode: HasLocation | null, kind: VarKind = 'variable'): void { + if (this.declared(name)) { + return; + } + this.vars.set(name, { kind, used: false, node: nameNode }); + } + + undeclare(name: string) { + this.vars.delete(name); + } + + use(use: VariableNode) { + const name = use.contents[0].contents; + if (this.declared(name)) { + this.vars.get(name)!.used = true; + } else { + this.report({ + ruleId: 'use-before-def', + message: `could not find a preceding declaration for ${JSON.stringify(name)}`, + line: use.location.start.line, + column: use.location.start.column, + }); + } + } +} + +type VariableNode = UnderscoreNode & { contents: [TextNode] }; +export function checkVariableUsage( + containingAlgorithm: Element, + steps: OrderedListNode, + report: Reporter +) { + if (containingAlgorithm.hasAttribute('replaces-step')) { + // TODO someday lint these by doing the rewrite (conceptually) + return; + } + const scope = new Scope(report); + + let parentClause = containingAlgorithm.parentElement; + while (parentClause != null) { + if (parentClause.nodeName === 'EMU-CLAUSE') { + break; + } + if (parentClause.nodeName === 'EMU-ANNEX') { + // Annex B adds algorithms in a way which makes it hard to track the original + // TODO someday lint Annex B + return; + } + parentClause = parentClause.parentElement; + } + // we assume any name introduced earlier in the clause is fair game + // this is a little permissive, but it's hard to find a precise rule, and that's better than being too restrictive + let preceding = previousOrParent(containingAlgorithm, parentClause); + while (preceding != null) { + if ( + preceding.tagName !== 'EMU-ALG' && + preceding.querySelector('emu-alg') == null && + preceding.textContent != null + ) { + // `__` is for _x__y_, which has textContent `_x__y_` + for (const name of preceding.textContent.matchAll(/(?<=\b|_)_([a-zA-Z0-9]+)_(?=\b|_)/g)) { + scope.declare(name[1], null); + } + } + preceding = previousOrParent(preceding, parentClause); + } + + walkAlgorithm(steps, scope, report); + + for (const [name, { kind, used, node }] of scope.vars) { + if (!used && node != null && kind !== 'parameter' && kind !== 'abstract closure parameter') { + // prettier-ignore + const message = `${JSON.stringify(name)} is declared here, but never referred to`; + report({ + ruleId: 'unused-declaration', + message, + line: node.location.start.line, + column: node.location.start.column, + }); + } + } +} + +function walkAlgorithm(steps: OrderedListNode | UnorderedListNode, scope: Scope, report: Reporter) { + for (const step of steps.contents) { + const parts = step.contents; + const loopVars: Set = new Set(); + const declaredThisLine: Set = new Set(); + + let firstRealIndex = 0; + let first = parts[firstRealIndex]; + while (['comment', 'tag', 'opaqueTag'].includes(first.name)) { + ++firstRealIndex; + first = parts[firstRealIndex]; + } + + let lastRealIndex = parts.length - 1; + let last = parts[lastRealIndex]; + while (['comment', 'tag', 'opaqueTag'].includes(last.name)) { + --lastRealIndex; + last = parts[lastRealIndex]; + } + + // handle [declared="foo"] attributes + const extraDeclarations = step.attrs.find(d => d.key === 'declared'); + if (extraDeclarations != null) { + for (let name of extraDeclarations.value.split(',')) { + name = name.trim(); + const line = extraDeclarations.location.start.line; + const column = + extraDeclarations.location.start.column + + extraDeclarations.key.length + + 2 + // '="' + findDeclaredAttrOffset(extraDeclarations.value, name); + if (scope.declared(name)) { + // prettier-ignore + const message = `${JSON.stringify(name)} is already declared and does not need an explict annotation`; + report({ + ruleId: 'unnecessary-declared-var', + message, + line, + column, + }); + } else { + scope.declare(name, { location: { start: { line, column } } }, 'attribute declaration'); + } + } + } + + // handle loops + if (first.name === 'text' && first.contents.startsWith('For each ')) { + let loopVar = parts[firstRealIndex + 1]; + if (loopVar?.name === 'pipe') { + loopVar = parts[firstRealIndex + 3]; // 2 is the space + } + if (isVariable(loopVar)) { + loopVars.add(loopVar); + + scope.declare(loopVar.contents[0].contents, loopVar, 'loop variable'); + } + } + + // handle abstract closures + if ( + last.name === 'text' && + / performs the following steps (atomically )?when called:$/.test(last.contents) + ) { + if ( + first.name === 'text' && + first.contents === 'Let ' && + isVariable(parts[firstRealIndex + 1]) + ) { + const closureName = parts[firstRealIndex + 1] as VariableNode; + scope.declare(closureName.contents[0].contents, closureName); + } + // everything in an AC needs to be captured explicitly + const acScope = new Scope(report); + let paramsIndex = parts.findIndex( + p => p.name === 'text' && p.contents.endsWith(' with parameters (') + ); + if (paramsIndex !== -1) { + for ( + ; + paramsIndex < parts.length && + !( + parts[paramsIndex].name === 'text' && + (parts[paramsIndex].contents as string).includes(')') + ); + ++paramsIndex + ) { + const v = parts[paramsIndex]; + if (isVariable(v)) { + acScope.declare(v.contents[0].contents, v, 'abstract closure parameter'); + } + } + } + let capturesIndex = parts.findIndex( + p => p.name === 'text' && p.contents.endsWith(' that captures ') + ); + + if (capturesIndex !== -1) { + for (; capturesIndex < parts.length; ++capturesIndex) { + const v = parts[capturesIndex]; + if (v.name === 'text' && v.contents.includes(' and performs ')) { + break; + } + if (isVariable(v)) { + const name = v.contents[0].contents; + scope.use(v); + acScope.declare(name, v, 'abstract closure capture'); + } + } + } + + // we have a lint rule elsewhere which checks there are substeps for closures, but we can't guarantee that rule hasn't tripped this run, so we still need to guard + if (step.sublist != null && step.sublist.name === 'ol') { + walkAlgorithm(step.sublist, acScope, report); + for (const [name, { node, kind, used }] of acScope.vars) { + if (kind === 'abstract closure capture' && !used) { + report({ + ruleId: 'unused-capture', + message: `closure captures ${JSON.stringify(name)}, but never uses it`, + line: node!.location.start.line, + column: node!.location.start.column, + }); + } + } + } + continue; + } + + // handle let/such that/there exists declarations + for (let i = 1; i < parts.length; ++i) { + const part = parts[i]; + if (isVariable(part) && !loopVars.has(part)) { + const varName = part.contents[0].contents; + + // check for "there exists" + const prev = parts[i - 1]; + if ( + prev.name === 'text' && + /\b(?:for any |there exists |there is |there does not exist )(\w+ )*$/.test(prev.contents) + ) { + scope.declare(varName, part); + declaredThisLine.add(part); + continue; + } + + // check for "Let _x_ be" / "_x_ and _y_ such that" + if (i < parts.length - 1) { + const next = parts[i + 1]; + const isSuchThat = next.name === 'text' && next.contents.startsWith(' such that '); + const isBe = next.name === 'text' && next.contents.startsWith(' be '); + + if (isSuchThat || isBe) { + const varsDeclaredHere = [part]; + let varIndex = i - 1; + // walk backwards collecting this comma/'and' seperated list of variables + for (; varIndex >= 1; varIndex -= 2) { + if (parts[varIndex].name !== 'text') { + break; + } + const sep = parts[varIndex].contents as string; + if (![', ', ', and ', ' and '].includes(sep)) { + break; + } + const prev = parts[varIndex - 1]; + if (!isVariable(prev)) { + break; + } + varsDeclaredHere.push(prev); + } + + const cur = parts[varIndex]; + if ( + // "of"/"in" guard is to distinguish "an integer X such that" from "an integer X in Y such that" - latter should not declare Y + (isSuchThat && cur.name === 'text' && !/(?: of | in )/.test(cur.contents)) || + (isBe && cur.name === 'text' && /\blet (?:each of )?$/i.test(cur.contents)) + ) { + for (const v of varsDeclaredHere) { + scope.declare(v.contents[0].contents, v); + declaredThisLine.add(v); + } + } + continue; + } + } + } + } + + // handle uses + for (let i = 0; i < parts.length; ++i) { + const part = parts[i]; + if (isVariable(part) && !loopVars.has(part) && !declaredThisLine.has(part)) { + scope.use(part); + } + } + + if (step.sublist != null) { + walkAlgorithm(step.sublist, scope, report); + } + + for (const decl of loopVars) { + scope.undeclare(decl.contents[0].contents); + } + } +} + +function isVariable(node: FragmentNode | null): node is VariableNode { + if (node == null) { + return false; + } + return ( + node.name === 'underscore' && node.contents.length === 1 && node.contents[0].name === 'text' + ); +} + +function previousOrParent(element: Element, stopAt: Element | null): Element | null { + if (element === stopAt) { + return null; + } + if (element.previousElementSibling != null) { + return element.previousElementSibling; + } + if (element.parentElement == null) { + return null; + } + return previousOrParent(element.parentElement, stopAt); +} + +function findDeclaredAttrOffset(attrSource: string, name: string) { + const matcher = new RegExp('\\b' + name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + '\\b'); // regexp.escape when + return attrSource.match(matcher)!.index!; +} diff --git a/test/expr-parser.js b/test/expr-parser.js index 62c68dc0..48706108 100644 --- a/test/expr-parser.js +++ b/test/expr-parser.js @@ -19,6 +19,7 @@ describe('expression parsing', () => { it('calls', async () => { await assertLintFree(` + 1. Let _foo_ be a variable. 1. Let _x_ be _foo_(). 1. Set _x_ to _foo_( ). 1. Set _x_ to _foo_.[[Bar]](). @@ -41,8 +42,9 @@ describe('expression parsing', () => { it('record-spec', async () => { await assertLintFree(` + 1. Let _entries_ be a List. 1. For each Record { [[Key]], [[Value]] } _p_ of _entries_, do - 1. Something. + 1. Use _p_. `); }); @@ -54,6 +56,7 @@ describe('expression parsing', () => { 0, 1, ». + 1. Use _x_. `); }); @@ -61,10 +64,12 @@ describe('expression parsing', () => { it('trailing comma in multi-line call', async () => { await assertLintFree(` + 1. Let _foo_ be a function. 1. Let _x_ be _foo_( 0, 1, ). + 1. Use _x_. `); }); @@ -76,6 +81,7 @@ describe('expression parsing', () => { [[X]]: 0, [[Y]]: 1, }. + 1. Use _x_. `); }); @@ -87,6 +93,7 @@ describe('expression parsing', () => { 1. Let _x_ be a new Record { [[Foo]]: 0, [[Bar]]: 1 }. 1. Let _x_ be a new Record { [[Foo]]: 0, [[Bar]]: 1, [[Baz]]: 2 }. 1. Let _x_ be a new Record { [[Foo]]: 0, [[Bar]]: 1, [[Baz]]: 2 }. + 1. Use _x_. `); }); @@ -127,7 +134,9 @@ describe('expression parsing', () => { await assertLint( positioned` + 1. Let _foo_ and _a_ be variables. 1. Let _x_ be «_foo_(_a_${M}»). + 1. Use _x_. `, { @@ -143,6 +152,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be «${M},». + 1. Use _x_. `, { @@ -170,7 +180,9 @@ describe('expression parsing', () => { await assertLint( positioned` + 1. Let _foo_ be a function. 1. Let _x_ be _foo_(${M},)». + 1. Use _x_. `, { @@ -183,7 +195,9 @@ describe('expression parsing', () => { await assertLint( positioned` + 1. Let _foo_ and _a_ be variables. 1. Let _x_ be _foo_(_a_, ${M}). + 1. Use _x_. `, { @@ -199,6 +213,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be the Record { ${M}}. + 1. Use _x_. `, { @@ -214,6 +229,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be the Record { ${M}[x]: 0 }. + 1. Use _x_. `, { @@ -229,6 +245,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be the Record { [[A]]: 0, [[${M}A]]: 0 }. + 1. Use _x_. `, { @@ -244,6 +261,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be the Record { [[A]], [[B]]${M}: 0 }. + 1. Use _x_. `, { @@ -257,6 +275,7 @@ describe('expression parsing', () => { positioned` 1. Let _x_ be the Record { [[A]]: 0, [[B]]${M} }. + 1. Use _x_. `, { diff --git a/test/lint-algorithms.js b/test/lint-algorithms.js index 60f2f8c0..79d3bdea 100644 --- a/test/lint-algorithms.js +++ b/test/lint-algorithms.js @@ -45,6 +45,7 @@ describe('linting algorithms', () => { it('repeat', async () => { await assertLint( positioned` + 1. Let _x_ be a variable. 1. Repeat, while _x_ < 10${M} 1. Foo. `, @@ -59,6 +60,7 @@ describe('linting algorithms', () => { it('inline if', async () => { await assertLint( positioned` + 1. Let _x_ be a variable. 1. If _x_, then${M} `, { @@ -72,6 +74,7 @@ describe('linting algorithms', () => { it('inline if-then', async () => { await assertLint( positioned` + 1. Let _x_ be a variable. 1. If _x_, ${M}then do something. `, { @@ -85,6 +88,7 @@ describe('linting algorithms', () => { it('multiline if', async () => { await assertLint( positioned` + 1. Let _x_ be a variable. 1. If _x_,${M} 1. Foo. `, @@ -97,6 +101,7 @@ describe('linting algorithms', () => { await assertLint( positioned` + 1. Let _x_ be a variable. 1. If _x_${M}; then 1. Foo. `, @@ -112,6 +117,7 @@ describe('linting algorithms', () => { it('else if', async () => { await assertLint( positioned` + 1. Let _x_ and _y_ be variables. 1. If _x_, foo. 1. Else${M}, if _y_, bar. `, @@ -199,6 +205,7 @@ describe('linting algorithms', () => { 1. ${M}Let _constructorText_ be the source text
constructor() {}
1. Foo. + 1. Use _constructorText_. `, { ruleId, @@ -219,6 +226,7 @@ describe('linting algorithms', () => { it('negative', async () => { await assertLintFree(` + 1. Let _x_, _y_, and _z_ be variables. 1. If foo, bar. 1. Else if foo, bar. 1. Else, bar. @@ -250,7 +258,9 @@ describe('linting algorithms', () => { 1. Substep. 1. Let _constructorText_ be the source text
constructor() {}
- 1. Set _constructor_ to _parse_(_constructorText_, _methodDefinition_). + 1. Let _parse_ be a method. + 1. Let _constructor_ be a variable. + 1. Set _constructor_ to _parse_(_constructorText_). 1. A highlighted line. 1. Amend the spec with this. 1. Remove this from the spec. @@ -413,6 +423,7 @@ describe('linting algorithms', () => { await assertLint( positioned` + 1. Let _y_ be a List. 1. For each ${M}_x_ of _y_, do foo. `, { @@ -426,6 +437,8 @@ describe('linting algorithms', () => { it('negative', async () => { await assertLintFree(` + 1. Let _y_ be a List. + 1. Let _S_ be a Set. 1. For each String _x_ of _y_, do foo. 1. For each element _x_ of _y_, do foo. 1. For each integer _x_ such that _x_ ∈ _S_, do foo. diff --git a/test/lint-spelling.js b/test/lint-spelling.js index c4ebd302..20211118 100644 --- a/test/lint-spelling.js +++ b/test/lint-spelling.js @@ -45,7 +45,10 @@ describe('spelling', () => { it('*0*', async () => { await assertLint( positioned` - 1. If _x_ is ${M}*0*𝔽, do foo. + + 1. Let _x_ be a value. + 1. If _x_ is ${M}*0*𝔽, do foo. + `, { ruleId: 'spelling', diff --git a/test/lint-variable-use-def.js b/test/lint-variable-use-def.js new file mode 100644 index 00000000..ea7029c7 --- /dev/null +++ b/test/lint-variable-use-def.js @@ -0,0 +1,403 @@ +'use strict'; + +let { + assertLint, + assertLintFree, + positioned, + lintLocationMarker: M, + getBiblio, +} = require('./utils.js'); + +describe('variables are declared and used appropriately', () => { + describe('variables must be declared', () => { + it('variables must be declared', async () => { + await assertLint( + positioned` + + 1. Let _x_ be 0. + 1. Something with _x_ and ${M}_y_. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "y"', + } + ); + }); + + it('loop variables are not visible after the loop', async () => { + await assertLint( + positioned` + + 1. For each integer _k_ less than 10, do + 1. Something with _k_. + 1. Something with ${M}_k_. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "k"', + } + ); + }); + + it('abstract closure captures must be declared', async () => { + await assertLint( + positioned` + + 1. Let _closure_ be a new Abstract Closure with no parameters that captures ${M}_undeclared_ and performs the following steps when called: + 1. Do something with _undeclared_. + 1. Return *undefined*. + 1. Return _closure_. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "undeclared"', + } + ); + }); + + it('abstract closures must capture their values', async () => { + await assertLint( + positioned` + + 1. Let _v_ be 0. + 1. Let _closure_ be a new Abstract Closure with no parameters that captures nothing and performs the following steps when called: + 1. Do something with ${M}_v_. + 1. Return *undefined*. + 1. Do something with _v_ so it does not seem unused. + 1. Return _closure_. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "v"', + } + ); + }); + + it('abstract closure captures must be used', async () => { + await assertLint( + positioned` + + 1. Let _outer_ be 0. + 1. Let _closure_ be a new Abstract Closure with no parameters that captures ${M}_outer_ and performs the following steps when called: + 1. Return *undefined*. + 1. Return _closure_. + + `, + { + ruleId: 'unused-capture', + nodeType: 'emu-alg', + message: 'closure captures "outer", but never uses it', + } + ); + }); + + it('"there exists _x_ in _y_ such that" declares _x_ but not _y_"', async () => { + await assertLint( + positioned` + + 1. If there exists an integer _x_ in ${M}_y_ such that _x_ < 10, return *true*. + 1. Return *false*. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "y"', + } + ); + }); + + it('variables in a loop header other than the loop variable must be declared', async () => { + await assertLint( + positioned` + + 1. Let _c_ be a variable. + 1. For each integer _a_ of ${M}_b_ such that _c_ relates to _a_ in some way, do + 1. Something. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "b"', + } + ); + + await assertLint( + positioned` + + 1. Let _b_ be a variable. + 1. For each integer _a_ of _b_ such that ${M}_c_ relates to _a_ in some way, do + 1. Something. + + `, + { + ruleId: 'use-before-def', + nodeType: 'emu-alg', + message: 'could not find a preceding declaration for "c"', + } + ); + }); + + it('explicit "declared" annotations should not be redundant', async () => { + await assertLint( + positioned` + + 1. Let _y_ be 0. + 1. [declared="x,${M}y"] Return _x_ + _y_. + + `, + { + ruleId: 'unnecessary-declared-var', + nodeType: 'emu-alg', + message: '"y" is already declared and does not need an explict annotation', + } + ); + }); + }); + + describe('variables must be used', () => { + it('variables must be used', async () => { + await assertLint( + positioned` + + 1. Let _x_ be 0. + 1. Let ${M}_y_ be 1. + 1. Return _x_. + + `, + { + ruleId: 'unused-declaration', + nodeType: 'emu-alg', + message: '"y" is declared here, but never referred to', + } + ); + }); + }); + + describe('valid declaration + use', () => { + it('declarations are visible', async () => { + await assertLintFree( + ` + + 1. Let _x_ be 0. + 1. Return _x_. + + ` + ); + }); + + it('declarations from a nested if/else are visible', async () => { + await assertLintFree( + ` + + 1. If condition, let _result_ be 0. + 1. Else, let _result_ be 1. + 1. Return _result_. + + ` + ); + + await assertLintFree( + ` + + 1. If condition, then + 1. Let _result_ be 0. + 1. Else, + 1. Let _result_ be 1. + 1. Return _result_. + + ` + ); + }); + + it('declarations from built-in function parameters are visible', async () => { + await assertLintFree( + ` + +

Array.prototype.unshift ( ..._items_ )

+

It performs the following steps when called:

+ + 1. Let _argCount_ be the number of elements in _items_. + 1. Do something with _argCount_. + +
+ ` + ); + }); + + it('declarations from free-form headers are visible', async () => { + await assertLintFree( + ` + +

Valid Chosen Reads

+

A candidate execution _execution_ has valid chosen reads if the following algorithm returns *true*.

+ + 1. If some condition of _execution_, return *true*. + 1. Return *false*. + +
+ ` + ); + }); + + it('declarations from free-form headers are visible even when the algorithm is nested', async () => { + await assertLintFree( + ` + +

Valid Chosen Reads

+

A candidate execution _execution_ has valid chosen reads if the following algorithm returns *true*.

+ + + + + +
+ + 1. If some condition of _execution_, return *true*. + 1. Return *false*. + + + + 1. If some condition of _execution_, return *true*. + 1. Return *false*. + +
+
+ ` + ); + }); + + it('the receiver for concrete methods is visible', async () => { + await assertLintFree( + ` + +

HasBinding ( _N_ )

+
+
for
+
a Declarative Environment Record _envRec_
+
+ + 1. If _envRec_ has a binding for the name that is the value of _N_, return *true*. + 1. Return *false*. + +
+ ` + ); + }); + + it('abstract closure parameters/captures are visible', async () => { + await assertLintFree( + ` + +

Object.fromEntries ( _obj_ )

+ + 1. Let _closure_ be a new Abstract Closure with parameters (_key_, _value_) that captures _obj_ and performs the following steps when called: + 1. Do something with _obj_, _key_, and _value_. + 1. Return *undefined*. + 1. Return _closure_. + +
+ ` + ); + }); + + it('abstract closure parameters/captures are visible', async () => { + await assertLintFree( + ` + +

Object.fromEntries ( _del__obj_ )

+ + 1. Let _closure_ be a new Abstract Closure with parameters (_key_, _value_) that captures _obj_ and performs the following steps when called: + 1. Do something with _obj_, _key_, and _value_. + 1. Return *undefined*. + 1. Return _closure_. + +
+ ` + ); + }); + + it('multiple declarations are visible', async () => { + await assertLintFree( + ` + + 1. Let _x_, _y_, and _z_ be some values. + 1. Return _x_ + _y_ + _z_. + + ` + ); + }); + + it('"such that" counts as a declaration"', async () => { + await assertLintFree( + ` + + 1. If there is an integer _x_ such that _x_ < 10, return *true*. + 1. Return *false*. + + ` + ); + + await assertLintFree( + ` + + 1. If there are integers _x_ and _y_ such that _x_ < _y_, return *true*. + 1. Return *false*. + + ` + ); + }); + + it('"there exists" counts as a declaration"', async () => { + await assertLintFree( + ` + + 1. If there exists an integer _x_ between 0 and 10, return _x_. + 1. Return -1. + + ` + ); + }); + + it('declarations from explicit attributes are visible', async () => { + await assertLintFree( + ` + +

Array.prototype.unshift ( ..._items_ )

+

It performs the following steps when called:

+ + 1. [declared="x,y"] Do something with _x_ and _y_. + +
+ ` + ); + }); + + it('loop variables are visible within the loop', async () => { + let biblio = await getBiblio(` + + CaseClause : \`a\` + + `); + + await assertLintFree( + ` + + 1. For each |CaseClause| _c_ of some list, do + 1. Something with _c_. + + `, + { extraBiblios: [biblio] } + ); + }); + }); +}); diff --git a/test/lint.js b/test/lint.js index a1e1f50b..8f900fde 100644 --- a/test/lint.js +++ b/test/lint.js @@ -482,6 +482,7 @@ describe('linting whole program', () => { positioned` 1. Let _s_ be |${M}Example|. + 1. Return _s_. `, { @@ -503,6 +504,7 @@ describe('linting whole program', () => { Statement: \`;\` 1. Let _s_ be |${M}Statements|. + 1. Return _s_. `, @@ -554,6 +556,7 @@ describe('linting whole program', () => { 1. Let _s_ be |Example1|. + 1. Return _s_.

Discuss: |Example1|.

Discuss: Example1.

diff --git a/test/typecheck.js b/test/typecheck.js index cbd3cef8..183331af 100644 --- a/test/typecheck.js +++ b/test/typecheck.js @@ -84,6 +84,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return ${M}ExampleAlg of _foo_. @@ -133,6 +134,7 @@ describe('typechecking completions', () => { 1. Let _a_ be Completion(ExampleAlg()). 1. Set _a_ to ! ExampleAlg(). 1. Return ? ExampleAlg(). + 1. Let _foo_ be 0. 1. Let _a_ be Completion(ExampleSDO of _foo_). 1. Let _a_ be Completion(ExampleSDO of _foo_ with argument 0). 1. If ? ExampleSDO of _foo_ is *true*, then @@ -230,6 +232,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return ${M}? _foo_. @@ -272,6 +275,7 @@ describe('typechecking completions', () => {
+ 1. Let _x_ be 0. 1. ${M}Return Completion(_x_). @@ -298,6 +302,8 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. + 1. Let _x_ be 0. 1. Return ? _foo_. 1. Return Completion(_x_). 1. Throw a new TypeError. @@ -322,6 +328,7 @@ describe('typechecking completions', () => {
${M} + 1. Let _foo_ be 0. 1. Return _foo_. @@ -344,6 +351,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return ? _foo_. @@ -357,6 +365,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return _foo_. @@ -375,6 +384,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return _foo_. @@ -387,6 +397,7 @@ describe('typechecking completions', () => { 1. Let _x_ be ${M}ExampleAlg(). + 1. Return _x_. `, @@ -408,6 +419,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return _foo_. @@ -481,7 +493,8 @@ describe('typechecking completions', () => {
- 1. Let _x_ be ExampleAlg(). + 1. Let _x_ be ExampleAlg(). + 1. Return _x_. `); @@ -523,6 +536,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return ? _foo_. @@ -534,7 +548,8 @@ describe('typechecking completions', () => {
- 1. Let _x_ be ! ExampleAlg(). + 1. Let _x_ be ! ExampleAlg(). + 1. Return _x_. `, @@ -556,6 +571,7 @@ describe('typechecking completions', () => {
+ 1. Let _foo_ be 0. 1. Return ? _foo_. @@ -567,7 +583,8 @@ describe('typechecking completions', () => {
- 1. Let _x_ be ? ExampleAlg(). + 1. Let _x_ be ? ExampleAlg(). + 1. Return _x_. `); @@ -710,6 +727,7 @@ describe('signature agreement', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}SDOTakesNoArgs of _foo_ with argument 1. `, @@ -726,6 +744,7 @@ describe('signature agreement', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}SDOTakesNoArgs of _foo_ with argument 0. `, @@ -742,6 +761,7 @@ describe('signature agreement', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}SDOTakesOneOrTwoArgs of _foo_ with arguments 0, 1, and 2. `, @@ -794,6 +814,7 @@ describe('signature agreement', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}SDOTakesOneArg of _foo_. `, @@ -810,6 +831,7 @@ describe('signature agreement', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}SDOTakesOneOrTwoArgs of _foo_. `, @@ -828,6 +850,7 @@ describe('signature agreement', async () => { await assertLintFree( ` + 1. Let _foo_ be 0. 1. Perform TakesNoArgs(). 1. Perform TakesOneArg(0). 1. Perform TakesOneOrTwoArgs(0). @@ -885,6 +908,7 @@ describe('invocation kind', async () => { await assertLint( positioned` + 1. Let _foo_ be 0. 1. Return ${M}AO of _foo_. `, diff --git a/tsconfig.json b/tsconfig.json index c23d5b75..e8257d62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "stripInternal": true, "importsNotUsedAsValues": "error", "outDir": "lib", - "lib": ["es5", "es2019", "dom", "dom.iterable"], + "lib": ["es2020", "dom", "dom.iterable"], "typeRoots": [ "node_modules/@types" ]