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_ )
+
+
+ 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"
]