• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / py-slang / 29064527519

10 Jul 2026 02:23AM UTC coverage: 77.591% (-0.02%) from 77.614%
29064527519

Pull #255

github

web-flow
Merge 19e528ff4 into ebd09e224
Pull Request #255: Testing `sourceacademy-sicp` and py-slang Jest test suite

2702 of 3773 branches covered (71.61%)

Branch coverage included in aggregate %.

79 of 89 new or added lines in 1 file covered. (88.76%)

10 existing lines in 3 files now uncovered.

6259 of 7776 relevant lines covered (80.49%)

12294.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.47
/src/resolver/resolver.ts
1
import { ExprNS, StmtNS } from "../ast-types";
18✔
2
import { Group } from "../stdlib/utils";
3
import { Token, TokenType } from "../tokenizer/tokenizer";
18✔
4
import { FeatureValidator } from "../validator/types";
5
import { ResolverErrors } from "./errors";
18✔
6
type Expr = ExprNS.Expr;
7
type Stmt = StmtNS.Stmt;
8

9
import levenshtein from "fast-levenshtein";
18✔
10
// const levenshtein = require('fast-levenshtein');
11

12
export type FunctionEnvironments = Map<
13
  StmtNS.FileInput | StmtNS.FunctionDef | ExprNS.Lambda | ExprNS.MultiLambda,
14
  Environment
15
>;
16

17
const RedefineableTokenSentinel = new Token(TokenType.AT, "", 0, 0, 0);
18✔
18

19
export class Environment {
18✔
20
  source: string;
21
  // The parent of this environment
22
  enclosing: Environment | null;
23
  names: Map<string, Token>;
24
  // Function names in the environment.
25
  functions: Set<string>;
26
  // Names that are from import bindings, like 'y' in `from x import y`.
27
  // This only set at the top level environment. Child environments do not
28
  // copy this field.
29
  moduleBindings: Set<string>;
30
  definedNames: Set<string>;
31
  // Names bound as parameters of the function/lambda this scope belongs to (empty for the
32
  // module scope). Distinct from `names`, which also gains entries for names assigned within
33
  // the scope's body — this set lets a chapter's no-reassignment validator tell "declared as a
34
  // parameter" apart from "declared by a body statement" so it can flag a parameter reassignment.
35
  parameters: Set<string>;
36
  constructor(
37
    source: string,
38
    enclosing: Environment | null,
39
    names: Map<string, Token>,
40
    parameters: Set<string> = new Set(),
11,650✔
41
  ) {
42
    this.source = source;
13,221✔
43
    this.enclosing = enclosing;
13,221✔
44
    this.names = names;
13,221✔
45
    this.functions = new Set();
13,221✔
46
    this.moduleBindings = new Set();
13,221✔
47
    this.definedNames = new Set();
13,221✔
48
    this.parameters = parameters;
13,221✔
49
  }
50

51
  /*
52
   * Does a full lookup up the environment chain for a name.
53
   * Returns the distance of the name from the current environment.
54
   * If name isn't found, return -1.
55
   * */
56
  lookupName(identifier: Token): number {
57
    const name = identifier.lexeme;
6,108✔
58
    let distance = 0;
6,108✔
59
    // eslint-disable-next-line @typescript-eslint/no-this-alias
60
    let curr: Environment | null = this;
6,108✔
61
    while (curr !== null) {
6,108✔
62
      if (curr.names.has(name)) {
8,884✔
63
        break;
6,074✔
64
      }
65
      distance += 1;
2,810✔
66
      curr = curr.enclosing;
2,810✔
67
    }
68
    return curr === null ? -1 : distance;
6,108✔
69
  }
70

71
  /**
72
   * Looks up the name in the environment chain.
73
   * Returns the Environment where the name is found, or null if not found.
74
   */
75
  lookupNameEnv(identifier: Token): Environment | null {
76
    if (this.names.has(identifier.lexeme)) {
1,504✔
77
      return this;
984✔
78
    }
79
    for (let curr = this.enclosing; curr !== null; curr = curr.enclosing) {
520✔
80
      if (curr.names.has(identifier.lexeme)) {
802✔
81
        return curr;
520✔
82
      }
83
    }
84
    return null;
×
85
  }
86

87
  /* Looks up the name but only for the current environment. */
88
  lookupNameCurrentEnv(identifier: Token): Token | undefined {
89
    return this.names.get(identifier.lexeme);
×
90
  }
91
  lookupNameCurrentEnvWithError(identifier: Token) {
92
    if (this.lookupName(identifier) < 0) {
4,893✔
93
      throw new ResolverErrors.NameNotFoundError(
34✔
94
        identifier.line,
95
        identifier.col,
96
        this.source,
97
        identifier.indexInSource,
98
        identifier.indexInSource + identifier.lexeme.length,
99
        this.suggestName(identifier),
100
      );
101
    }
102
  }
103
  lookupNameParentEnvWithError(identifier: Token) {
104
    const name = identifier.lexeme;
×
105
    const parent = this.enclosing;
×
106

107
    if (parent === null || !parent.names.has(name)) {
×
108
      throw new ResolverErrors.NameNotFoundError(
×
109
        identifier.line,
110
        identifier.col,
111
        this.source,
112
        identifier.indexInSource,
113
        identifier.indexInSource + name.length,
114
        this.suggestName(identifier),
115
      );
116
    }
117
  }
118
  declareName(identifier: Token) {
119
    this.names.set(identifier.lexeme, identifier);
875✔
120
    this.definedNames.add(identifier.lexeme);
875✔
121
  }
122
  // Same as declareName but allowed to re-declare later.
123
  declarePlaceholderName(identifier: Token) {
124
    const lookup = this.lookupNameCurrentEnv(identifier);
×
125
    if (lookup !== undefined) {
×
126
      throw new ResolverErrors.NameReassignmentError(
×
127
        identifier.line,
128
        identifier.col,
129
        this.source,
130
        identifier.indexInSource,
131
        identifier.indexInSource + identifier.lexeme.length,
132
        lookup,
133
      );
134
    }
135
    this.names.set(identifier.lexeme, RedefineableTokenSentinel);
×
136
  }
137
  suggestNameCurrentEnv(identifier: Token): string | null {
138
    const name = identifier.lexeme;
×
139
    let minDistance = Infinity;
×
140
    let minName = null;
×
141
    for (const declName of this.names.keys()) {
×
142
      const dist = levenshtein.get(name, declName);
×
143
      if (dist < minDistance) {
×
144
        minDistance = dist;
×
145
        minName = declName;
×
146
      }
147
    }
148
    return minName;
×
149
  }
150
  /*
151
   * Finds name closest to name in all environments up to builtin environment.
152
   * Calculated using min levenshtein distance.
153
   * */
154
  suggestName(identifier: Token): string | null {
155
    const name = identifier.lexeme;
38✔
156
    let minDistance = Infinity;
38✔
157
    let minName = null;
38✔
158
    // eslint-disable-next-line @typescript-eslint/no-this-alias
159
    let curr: Environment | null = this;
38✔
160
    while (curr !== null) {
38✔
161
      for (const declName of curr.names.keys()) {
97✔
162
        const dist = levenshtein.get(name, declName);
3,063✔
163
        if (dist < minDistance) {
3,063✔
164
          minDistance = dist;
103✔
165
          minName = declName;
103✔
166
        }
167
      }
168
      curr = curr.enclosing;
97✔
169
    }
170
    if (minDistance >= 4) {
38✔
171
      // This is pretty far, so just return null
172
      return null;
9✔
173
    }
174
    return minName;
29✔
175
  }
176
}
177
export class Resolver implements StmtNS.Visitor<void>, ExprNS.Visitor<void> {
18✔
178
  source: string;
179
  ast: Stmt;
180
  environment: Environment | null;
181
  functionScope: Environment | null;
182
  errors: Error[];
183
  functionEnvironments: FunctionEnvironments;
184
  private validators: FeatureValidator[];
185
  // Names declared `global` in the current function body (reset on function entry/exit).
186
  private globalNamesInCurrentFunction: Set<string> = new Set();
5,825✔
187
  // Names declared `nonlocal` in the current function body (reset on function entry/exit).
188
  private nonlocalNamesInCurrentFunction: Set<string> = new Set();
5,825✔
189
  // Stack of enclosing FunctionDef nodes (innermost last), used to resolve `nonlocal`
190
  // against a whole-function-body scan rather than the incremental (textual-order)
191
  // environment, since a binding construct may appear anywhere in the enclosing
192
  // function — including nested in `if`/`while`/`for`, and even after the nested
193
  // `def` that declares it `nonlocal` (matches CPython's whole-function static scoping).
194
  private functionDefStack: StmtNS.FunctionDef[] = [];
5,825✔
195
  // Top-level module statements, set once by visitFileInputStmt. Used as the outermost
196
  // level of the whole-scope binding scan (see nameHasStaticBinding) — a module-level name
197
  // can legitimately be bound anywhere in the module body, not just textually before a
198
  // nested function that reads it.
199
  private moduleStatements: StmtNS.Stmt[] = [];
5,825✔
200

201
  constructor(
202
    source: string,
203
    ast: Stmt,
204
    validators: FeatureValidator[] = [],
×
205
    groups: Group[] = [],
×
206
    preludeNames: string[] = [],
264✔
207
  ) {
208
    this.source = source;
5,825✔
209
    this.ast = ast;
5,825✔
210
    this.source = source;
5,825✔
211
    this.ast = ast;
5,825✔
212
    this.validators = validators;
5,825✔
213
    this.errors = [];
5,825✔
214
    this.functionEnvironments = new Map();
5,825✔
215
    // The global environment
216
    this.environment = new Environment(
5,825✔
217
      source,
218
      null,
219
      new Map([
220
        ["range", new Token(TokenType.NAME, "range", 0, 0, 0)],
221
        ["__program__", new Token(TokenType.NAME, "__program__", 0, 0, 0)],
222
        ...groups.flatMap(group =>
223
          Array.from(group.builtins.entries()).map(
5,767✔
224
            ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const,
139,947✔
225
          ),
226
        ),
227
        ...preludeNames.map(name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const),
21,103✔
228
      ]),
229
    );
230
    this.functionScope = null;
5,825✔
231
  }
232

233
  resolveEnvironments(program: StmtNS.FileInput): FunctionEnvironments {
234
    this.resolve(program);
173✔
235
    return this.functionEnvironments;
173✔
236
  }
237

238
  private runValidators(node: StmtNS.Stmt | ExprNS.Expr): void {
239
    try {
43,571✔
240
      for (const v of this.validators) v.validate(node, this.environment ?? undefined);
259,895!
241
    } catch (e) {
242
      if (e instanceof Error) {
72✔
243
        this.errors.push(e);
72✔
244
        return;
72✔
245
      }
246
      throw e;
×
247
    }
248
  }
249

250
  resolve(stmt: Stmt[] | Stmt | Expr[] | Expr | null): Error[] {
251
    if (stmt === null) {
40,927✔
252
      return this.errors;
39✔
253
    }
254
    if (stmt instanceof Array) {
40,888✔
255
      for (const st of stmt) {
9,385✔
256
        if (st instanceof StmtNS.FunctionDef) {
12,068✔
257
          if (
370✔
258
            !this.globalNamesInCurrentFunction.has(st.name.lexeme) &&
740✔
259
            !this.nonlocalNamesInCurrentFunction.has(st.name.lexeme)
260
          ) {
261
            this.environment?.declareName(st.name);
370✔
262
          }
263
        }
264
        if (st instanceof StmtNS.Assign && st.target instanceof ExprNS.Variable) {
12,068✔
265
          if (
482✔
266
            !this.globalNamesInCurrentFunction.has(st.target.name.lexeme) &&
942✔
267
            !this.nonlocalNamesInCurrentFunction.has(st.target.name.lexeme)
268
          ) {
269
            this.environment?.declareName(st.target.name);
428✔
270
          }
271
        }
272
      }
273
      for (const st of stmt) {
9,385✔
274
        this.runValidators(st);
12,068✔
275
        st.accept(this);
12,068✔
276
      }
277
    } else {
278
      this.runValidators(stmt);
31,503✔
279
      stmt.accept(this);
31,503✔
280
    }
281
    return this.errors;
40,888✔
282
  }
283

284
  varDeclNames(names: Map<string, Token>): Token[] | null {
285
    const res = Array.from(names.values()).filter(
×
286
      name =>
287
        // Filter out functions and module bindings.
288
        // Those will be handled separately, so they don't
289
        // need to be hoisted.
290
        !this.environment?.functions.has(name.lexeme) &&
×
291
        !this.environment?.moduleBindings.has(name.lexeme),
292
    );
293
    return res.length === 0 ? null : res;
×
294
  }
295

296
  functionVarConstraint(identifier: Token): void {
297
    if (this.functionScope == null) {
488✔
298
      return;
380✔
299
    }
300
    let curr = this.environment;
108✔
301
    while (curr !== this.functionScope) {
108✔
302
      if (curr !== null && curr.names.has(identifier.lexeme)) {
×
303
        const token = curr.names.get(identifier.lexeme);
×
304
        if (token === undefined) {
×
305
          this.errors.push(new Error("placeholder error"));
×
306
          return;
×
307
        }
308

309
        this.errors.push(
×
310
          new ResolverErrors.NameReassignmentError(
311
            identifier.line,
312
            identifier.col,
313
            this.source,
314
            identifier.indexInSource,
315
            identifier.indexInSource + identifier.lexeme.length,
316
            token,
317
          ),
318
        );
319
        return;
×
320
      }
321
      curr = curr?.enclosing ?? null;
×
322
    }
323
  }
324

325
  //// STATEMENTS
326
  visitFileInputStmt(stmt: StmtNS.FileInput): void {
327
    // Create a new environment.
328
    const oldEnv = this.environment;
5,825✔
329
    this.environment = new Environment(this.source, this.environment, new Map());
5,825✔
330
    this.functionEnvironments.set(stmt, this.environment);
5,825✔
331
    // #181 also applies at module level: e.g. `i = 3` followed by `global i` is a
332
    // SyntaxError in real Python, even though `global` is otherwise a no-op there.
333
    this.checkDeclarationOrder(stmt.statements, true);
5,825✔
334
    this.moduleStatements = stmt.statements;
5,825✔
335
    this.resolve(stmt.statements);
5,825✔
336
    // Grab identifiers from that new environment. That are NOT functions.
337
    // stmt.varDecls = this.varDeclNames(this.environment.names)
338
    this.environment = oldEnv;
5,825✔
339
  }
340

341
  visitFunctionDefStmt(stmt: StmtNS.FunctionDef) {
342
    this.environment?.functions.add(stmt.name.lexeme);
370✔
343

344
    // Create a new environment.
345
    const oldEnv = this.environment;
370✔
346
    // Assign the parameters to the new environment.
347
    const newEnv = new Map(stmt.parameters.map(param => [param.lexeme, param]));
370✔
348
    this.environment = new Environment(
370✔
349
      this.source,
350
      this.environment,
351
      newEnv,
352
      new Set(newEnv.keys()),
353
    );
354
    this.functionEnvironments.set(stmt, this.environment);
370✔
355
    this.functionScope = this.environment;
370✔
356

357
    const oldGlobalNames = this.globalNamesInCurrentFunction;
370✔
358
    const oldNonlocalNames = this.nonlocalNamesInCurrentFunction;
370✔
359
    this.globalNamesInCurrentFunction = this.scanGlobalDeclarations(stmt.body);
370✔
360
    this.nonlocalNamesInCurrentFunction = this.scanNonlocalDeclarations(stmt.body);
370✔
361

362
    // Run scope conflict checks before resolving the body.
363
    this.checkFunctionScopeConflicts(stmt);
370✔
364

365
    // Declare global names in the outermost (module-level) environment so that
366
    // variable lookups within this function can find them via the chain walk.
367
    if (this.globalNamesInCurrentFunction.size > 0) {
370✔
368
      let globalEnv: Environment | null = this.environment;
30✔
369
      while (globalEnv?.enclosing !== null) {
30✔
370
        globalEnv = globalEnv?.enclosing ?? null;
67!
371
      }
372
      if (globalEnv) {
30✔
373
        for (const name of this.globalNamesInCurrentFunction) {
30✔
374
          if (!globalEnv.names.has(name)) {
30✔
375
            // Use a sentinel token so the resolver accepts references to this name.
376
            globalEnv.names.set(name, new Token(TokenType.NAME, name, 0, 0, 0));
30✔
377
          }
378
        }
379
      }
380
    }
381

382
    this.functionDefStack.push(stmt);
370✔
383
    this.resolve(stmt.body);
370✔
384
    this.functionDefStack.pop();
370✔
385
    // Restore old environment
386
    this.globalNamesInCurrentFunction = oldGlobalNames;
370✔
387
    this.nonlocalNamesInCurrentFunction = oldNonlocalNames;
370✔
388
    this.functionScope = null;
370✔
389
    this.environment = oldEnv;
370✔
390
  }
391

392
  visitAnnAssignStmt(stmt: StmtNS.AnnAssign): void {
393
    this.resolve(stmt.ann);
6✔
394
    this.resolve(stmt.value);
6✔
395
    this.functionVarConstraint(stmt.target.name);
6✔
396
  }
397

398
  visitAssignStmt(stmt: StmtNS.Assign): void {
399
    const target = stmt.target;
490✔
400
    if (target instanceof ExprNS.Subscript) {
490✔
401
      this.resolve(target); // dispatches to visitSubscriptExpr
8✔
402
      this.resolve(stmt.value);
8✔
403
      return;
8✔
404
    }
405
    this.resolve(stmt.value);
482✔
406
    this.functionVarConstraint(target.name);
482✔
407
  }
408

409
  visitAssertStmt(stmt: StmtNS.Assert): void {
410
    this.resolve(stmt.value);
×
411
  }
412
  visitForStmt(stmt: StmtNS.For): void {
413
    if (
76✔
414
      !this.globalNamesInCurrentFunction.has(stmt.target.lexeme) &&
152✔
415
      !this.nonlocalNamesInCurrentFunction.has(stmt.target.lexeme)
416
    ) {
417
      this.environment?.declareName(stmt.target);
76✔
418
    }
419
    this.resolve(stmt.iter);
76✔
420
    this.resolve(stmt.body);
76✔
421
  }
422

423
  visitIfStmt(stmt: StmtNS.If): void {
424
    this.resolve(stmt.condition);
144✔
425
    this.resolve(stmt.body);
144✔
426
    this.resolve(stmt.elseBlock);
144✔
427
  }
428
  visitGlobalStmt(stmt: StmtNS.Global): void {
429
    // Function-level `global x` is handled entirely in visitFunctionDefStmt (scanning +
430
    // declaring in the outermost env). At module level `global x` is semantically a no-op
431
    // (the name is already module-scope) — but it still must make `x` a recognized name for
432
    // the *resolver*, exactly like a real assignment would, even though no value is bound
433
    // yet. Real Python defers to a runtime NameError if `x` is never actually assigned
434
    // before use; without this, py-slang would wrongly reject `global x` immediately.
435
    const env = this.environment;
37✔
436
    const isModuleLevel =
437
      env !== null && env.enclosing !== null && env.enclosing.enclosing === null;
37✔
438
    if (isModuleLevel && !env.names.has(stmt.name.lexeme)) {
37✔
439
      env.names.set(stmt.name.lexeme, new Token(TokenType.NAME, stmt.name.lexeme, 0, 0, 0));
3✔
440
    }
441
  }
442

443
  // Recursively collects names declared with `global` anywhere in the function body,
444
  // without descending into nested function/lambda definitions.
445
  private scanGlobalDeclarations(stmts: StmtNS.Stmt[]): Set<string> {
446
    const globals = new Set<string>();
433✔
447
    const scan = (stmts: StmtNS.Stmt[]) => {
433✔
448
      for (const stmt of stmts) {
635✔
449
        if (stmt instanceof StmtNS.Global) {
1,071✔
450
          globals.add(stmt.name.lexeme);
31✔
451
        } else if (stmt instanceof StmtNS.If) {
1,040✔
452
          scan(stmt.body);
97✔
453
          if (Array.isArray(stmt.elseBlock)) {
97✔
454
            scan(stmt.elseBlock);
83✔
455
          } else if (stmt.elseBlock) {
14!
456
            scan([stmt.elseBlock]);
×
457
          }
458
        } else if (stmt instanceof StmtNS.While) {
943!
UNCOV
459
          scan(stmt.body);
×
460
        } else if (stmt instanceof StmtNS.For) {
943✔
461
          scan(stmt.body);
22✔
462
        }
463
        // Do not recurse into FunctionDef or Lambda bodies.
464
      }
465
    };
466
    scan(stmts);
433✔
467
    return globals;
433✔
468
  }
469

470
  // Recursively collects names declared with `nonlocal` anywhere in the function body,
471
  // without descending into nested function/lambda definitions.
472
  private scanNonlocalDeclarations(stmts: StmtNS.Stmt[]): Set<string> {
473
    const nonlocals = new Set<string>();
432✔
474
    const scan = (stmts: StmtNS.Stmt[]) => {
432✔
475
      for (const stmt of stmts) {
634✔
476
        if (stmt instanceof StmtNS.NonLocal) {
1,067✔
477
          nonlocals.add(stmt.name.lexeme);
53✔
478
        } else if (stmt instanceof StmtNS.If) {
1,014✔
479
          scan(stmt.body);
97✔
480
          if (Array.isArray(stmt.elseBlock)) {
97✔
481
            scan(stmt.elseBlock);
83✔
482
          } else if (stmt.elseBlock) {
14!
483
            scan([stmt.elseBlock]);
×
484
          }
485
        } else if (stmt instanceof StmtNS.While) {
917!
UNCOV
486
          scan(stmt.body);
×
487
        } else if (stmt instanceof StmtNS.For) {
917✔
488
          scan(stmt.body);
22✔
489
        }
490
        // Do not recurse into FunctionDef or Lambda bodies.
491
      }
492
    };
493
    scan(stmts);
432✔
494
    return nonlocals;
432✔
495
  }
496

497
  // Checks scope conflicts within a FunctionDef for issues #178, #179, #180, #181.
498
  private checkFunctionScopeConflicts(stmt: StmtNS.FunctionDef): void {
499
    const globalTokens = this.scanScopeDeclarationTokens(stmt.body, "Global");
370✔
500
    const nonlocalTokens = this.scanScopeDeclarationTokens(stmt.body, "NonLocal");
370✔
501
    const paramNames = new Set(stmt.parameters.map(p => p.lexeme));
370✔
502

503
    // #178: name is both global and nonlocal in the same function
504
    for (const [name, token] of nonlocalTokens) {
370✔
505
      if (globalTokens.has(name)) {
47✔
506
        this.errors.push(
4✔
507
          new ResolverErrors.ScopeConflictError(
508
            token.line,
509
            token.col,
510
            this.source,
511
            token.indexInSource,
512
            token.indexInSource + name.length,
513
            `name '${name}' is nonlocal and global`,
514
          ),
515
        );
516
      }
517
    }
518

519
    // #179: parameter and nonlocal conflict
520
    for (const [name, token] of nonlocalTokens) {
370✔
521
      if (paramNames.has(name)) {
47✔
522
        this.errors.push(
4✔
523
          new ResolverErrors.ScopeConflictError(
524
            token.line,
525
            token.col,
526
            this.source,
527
            token.indexInSource,
528
            token.indexInSource + name.length,
529
            `name '${name}' is parameter and nonlocal`,
530
          ),
531
        );
532
      }
533
    }
534

535
    // #180: parameter and global conflict
536
    for (const [name, token] of globalTokens) {
370✔
537
      if (paramNames.has(name)) {
30✔
538
        this.errors.push(
4✔
539
          new ResolverErrors.ScopeConflictError(
540
            token.line,
541
            token.col,
542
            this.source,
543
            token.indexInSource,
544
            token.indexInSource + name.length,
545
            `name '${name}' is parameter and global`,
546
          ),
547
        );
548
      }
549
    }
550

551
    // #181: textual order — name used/assigned before its global/nonlocal declaration
552
    this.checkDeclarationOrder(stmt.body);
370✔
553
  }
554

555
  // Returns a map from name to its declaration token for all `global` or `nonlocal`
556
  // statements in the given body (without descending into nested functions).
557
  private scanScopeDeclarationTokens(
558
    stmts: StmtNS.Stmt[],
559
    kind: "Global" | "NonLocal",
560
  ): Map<string, Token> {
561
    const result = new Map<string, Token>();
740✔
562
    const scan = (stmts: StmtNS.Stmt[]) => {
740✔
563
      for (const stmt of stmts) {
1,116✔
564
        if (kind === "Global" && stmt instanceof StmtNS.Global) {
1,712✔
565
          result.set(stmt.name.lexeme, stmt.name);
30✔
566
        } else if (kind === "NonLocal" && stmt instanceof StmtNS.NonLocal) {
1,682✔
567
          result.set(stmt.name.lexeme, stmt.name);
47✔
568
        } else if (stmt instanceof StmtNS.If) {
1,635✔
569
          scan(stmt.body);
188✔
570
          if (Array.isArray(stmt.elseBlock)) {
188✔
571
            scan(stmt.elseBlock);
166✔
572
          } else if (stmt.elseBlock) {
22!
573
            scan([stmt.elseBlock]);
×
574
          }
575
        } else if (stmt instanceof StmtNS.While) {
1,447!
UNCOV
576
          scan(stmt.body);
×
577
        } else if (stmt instanceof StmtNS.For) {
1,447✔
578
          scan(stmt.body);
22✔
579
        }
580
        // Do not recurse into FunctionDef or Lambda bodies.
581
      }
582
    };
583
    scan(stmts);
740✔
584
    return result;
740✔
585
  }
586

587
  // Checks that no name is used or assigned before its `global`/`nonlocal` declaration
588
  // in the function body (#181). Traverses in textual order.
589
  // `isModuleLevel` additionally rejects bare `nonlocal` declarations (nonlocal is never
590
  // valid at module scope, matching CPython's `SyntaxError: nonlocal declaration not
591
  // allowed at module level` — but only once declaration-order has been ruled out, since
592
  // CPython itself prioritises the order error when both apply).
593
  private checkDeclarationOrder(body: StmtNS.Stmt[], isModuleLevel: boolean = false): void {
370✔
594
    const seen = new Map<string, { kind: "used" | "assigned"; token: Token }>();
6,195✔
595

596
    const recordUse = (token: Token) => {
6,195✔
597
      if (!seen.has(token.lexeme)) {
3,327✔
598
        seen.set(token.lexeme, { kind: "used", token });
2,147✔
599
      }
600
    };
601
    const recordAssign = (token: Token) => {
6,195✔
602
      if (!seen.has(token.lexeme)) {
928✔
603
        seen.set(token.lexeme, { kind: "assigned", token });
855✔
604
      }
605
    };
606

607
    const collectExpr = (expr: ExprNS.Expr | null | undefined) => {
6,195✔
608
      if (!expr) return;
28,772!
609
      if (expr instanceof ExprNS.Variable) {
28,772✔
610
        recordUse(expr.name);
3,327✔
611
      } else if (expr instanceof ExprNS.Binary || expr instanceof ExprNS.Compare) {
25,445✔
612
        collectExpr(expr.left);
5,811✔
613
        collectExpr(expr.right);
5,811✔
614
      } else if (expr instanceof ExprNS.Unary) {
19,634✔
615
        collectExpr(expr.right);
128✔
616
      } else if (expr instanceof ExprNS.BoolOp) {
19,506✔
617
        collectExpr(expr.left);
521✔
618
        collectExpr(expr.right);
521✔
619
      } else if (expr instanceof ExprNS.Call) {
18,985✔
620
        collectExpr(expr.callee);
2,074✔
621
        expr.args.forEach(collectExpr);
2,074✔
622
      } else if (expr instanceof ExprNS.Grouping) {
16,911✔
623
        collectExpr(expr.expression);
2,238✔
624
      } else if (expr instanceof ExprNS.Ternary) {
14,673✔
625
        collectExpr(expr.predicate);
58✔
626
        collectExpr(expr.consequent);
58✔
627
        collectExpr(expr.alternative);
58✔
628
      } else if (expr instanceof ExprNS.List) {
14,615✔
629
        expr.elements.forEach(collectExpr);
704✔
630
      } else if (expr instanceof ExprNS.Subscript) {
13,911✔
631
        collectExpr(expr.value);
21✔
632
        collectExpr(expr.index);
21✔
633
      } else if (expr instanceof ExprNS.Starred) {
13,890✔
634
        collectExpr(expr.value);
28✔
635
      }
636
      // Literal, BigIntLiteral, None, Complex, Lambda: no outer-scope variable references
637
    };
638

639
    const scanBody = (stmts: StmtNS.Stmt[]) => {
6,195✔
640
      for (const stmt of stmts) {
6,541✔
641
        if (stmt instanceof StmtNS.Global || stmt instanceof StmtNS.NonLocal) {
7,514✔
642
          const name = stmt.name.lexeme;
86✔
643
          const prior = seen.get(name);
86✔
644
          if (prior) {
86✔
645
            const declKind = stmt instanceof StmtNS.Global ? "global" : "nonlocal";
15✔
646
            const msg =
647
              prior.kind === "assigned"
15✔
648
                ? `name '${name}' is assigned to before ${declKind} declaration`
649
                : `name '${name}' is used prior to ${declKind} declaration`;
650
            this.errors.push(
15✔
651
              new ResolverErrors.ScopeConflictError(
652
                stmt.name.line,
653
                stmt.name.col,
654
                this.source,
655
                stmt.name.indexInSource,
656
                stmt.name.indexInSource + name.length,
657
                msg,
658
              ),
659
            );
660
          } else if (isModuleLevel && stmt instanceof StmtNS.NonLocal) {
71✔
661
            this.errors.push(
1✔
662
              new ResolverErrors.ScopeConflictError(
663
                stmt.name.line,
664
                stmt.name.col,
665
                this.source,
666
                stmt.name.indexInSource,
667
                stmt.name.indexInSource + name.length,
668
                "nonlocal declaration not allowed at module level",
669
              ),
670
            );
671
          }
672
        } else if (stmt instanceof StmtNS.FunctionDef) {
7,428✔
673
          // Record the function name as an assignment; don't recurse into its body.
674
          recordAssign(stmt.name);
370✔
675
        } else if (stmt instanceof StmtNS.Assign) {
7,058✔
676
          collectExpr(stmt.value);
490✔
677
          if (stmt.target instanceof ExprNS.Variable) {
490✔
678
            recordAssign(stmt.target.name);
482✔
679
          } else {
680
            collectExpr(stmt.target);
8✔
681
          }
682
        } else if (stmt instanceof StmtNS.SimpleExpr) {
6,568✔
683
          collectExpr(stmt.expression);
5,920✔
684
        } else if (stmt instanceof StmtNS.Return) {
648✔
685
          if (stmt.value) collectExpr(stmt.value);
311✔
686
        } else if (stmt instanceof StmtNS.If) {
337✔
687
          collectExpr(stmt.condition);
144✔
688
          scanBody(stmt.body);
144✔
689
          if (Array.isArray(stmt.elseBlock)) scanBody(stmt.elseBlock);
144✔
690
          else if (stmt.elseBlock) scanBody([stmt.elseBlock]);
39!
691
        } else if (stmt instanceof StmtNS.While) {
193✔
692
          collectExpr(stmt.condition);
21✔
693
          scanBody(stmt.body);
21✔
694
        } else if (stmt instanceof StmtNS.For) {
172✔
695
          collectExpr(stmt.iter);
76✔
696
          recordAssign(stmt.target);
76✔
697
          scanBody(stmt.body);
76✔
698
        } else if (stmt instanceof StmtNS.Assert) {
96!
699
          collectExpr(stmt.value);
×
700
        }
701
        // Pass, Break, Continue: no variable references
702
      }
703
    };
704

705
    scanBody(body);
6,195✔
706
  }
707

708
  // Recursively checks whether `name` has a binding construct (assignment, `def`, or
709
  // `for` target) anywhere in the given statement list, without descending into nested
710
  // function/lambda bodies (they introduce their own scope). Mirrors scanGlobalDeclarations
711
  // / scanNonlocalDeclarations, but collects "is this name bound here at all" instead.
712
  private hasBindingConstruct(stmts: StmtNS.Stmt[], name: string): boolean {
713
    for (const stmt of stmts) {
93✔
714
      if (
134✔
715
        stmt instanceof StmtNS.Assign &&
218✔
716
        stmt.target instanceof ExprNS.Variable &&
717
        stmt.target.name.lexeme === name
718
      ) {
719
        return true;
37✔
720
      } else if (stmt instanceof StmtNS.FunctionDef && stmt.name.lexeme === name) {
97!
721
        return true;
×
722
      } else if (stmt instanceof StmtNS.For) {
97✔
723
        if (stmt.target.lexeme === name || this.hasBindingConstruct(stmt.body, name)) {
11!
724
          return true;
11✔
725
        }
726
      } else if (stmt instanceof StmtNS.If) {
86✔
727
        if (this.hasBindingConstruct(stmt.body, name)) return true;
7✔
728
        if (Array.isArray(stmt.elseBlock)) {
3✔
729
          if (this.hasBindingConstruct(stmt.elseBlock, name)) return true;
1!
730
        } else if (stmt.elseBlock && this.hasBindingConstruct([stmt.elseBlock], name)) {
2!
731
          return true;
×
732
        }
733
      } else if (stmt instanceof StmtNS.While) {
79!
734
        if (this.hasBindingConstruct(stmt.body, name)) return true;
×
735
      }
736
      // Do not recurse into nested FunctionDef or Lambda bodies.
737
    }
738
    return false;
41✔
739
  }
740

741
  // Checks whether `name` has a binding construct (parameter, assignment, def, or `for`
742
  // target) in the enclosing function at functionDefStack[startIndex], or in any function
743
  // further out, stopping at (but not entering) module scope. Shared by visitNonLocalStmt
744
  // (which starts one level out, since nonlocal never refers to the current function) and
745
  // nameHasStaticBinding (which starts at the current function, inclusive).
746
  private hasEnclosingFunctionBinding(name: string, startIndex: number): boolean {
747
    for (let i = startIndex; i >= 0; i--) {
83✔
748
      const fn = this.functionDefStack[i];
66✔
749
      if (fn.parameters.some(p => p.lexeme === name)) {
66✔
750
        return true;
3✔
751
      }
752
      const fnGlobals = this.scanGlobalDeclarations(fn.body);
63✔
753
      if (fnGlobals.has(name)) {
63✔
754
        // `x` is explicitly global in this function — it is not a local binding here,
755
        // and CPython does not skip past it to look further out.
756
        return false;
1✔
757
      }
758
      const fnNonlocals = this.scanNonlocalDeclarations(fn.body);
62✔
759
      if (fnNonlocals.has(name)) {
62✔
760
        // `x` is itself nonlocal in this function — defer to whatever that resolves to
761
        // further out (already separately validated for `fn`).
762
        continue;
6✔
763
      }
764
      if (this.hasBindingConstruct(fn.body, name)) {
56✔
765
        return true;
47✔
766
      }
767
      // Not mentioned in this function at all — keep searching further out.
768
    }
769
    return false;
32✔
770
  }
771

772
  // Whole-scope-chain check for whether `name` can resolve at all: the current function
773
  // (if any), each enclosing function, and finally module scope — each scanned as a whole
774
  // body rather than relying on the incremental (textual-order) environment. This mirrors
775
  // CPython's static LEGB classification: a binding construct may appear anywhere in its
776
  // owning scope's body, not just textually before the point of reference (e.g. nested in
777
  // `if`/`while`/`for`, or after a nested `def` that reads/writes it).
778
  private nameHasStaticBinding(name: string): boolean {
779
    if (this.hasEnclosingFunctionBinding(name, this.functionDefStack.length - 1)) {
34✔
780
      return true;
5✔
781
    }
782
    return this.hasBindingConstruct(this.moduleStatements, name);
29✔
783
  }
784

785
  visitNonLocalStmt(stmt: StmtNS.NonLocal): void {
786
    const name = stmt.name.lexeme;
49✔
787
    // Search enclosing FUNCTION scopes (never the module scope), innermost first,
788
    // skipping the current function itself (nonlocal can never bind to it).
789
    const found = this.hasEnclosingFunctionBinding(name, this.functionDefStack.length - 2);
49✔
790
    if (!found) {
49✔
791
      this.errors.push(
4✔
792
        new ResolverErrors.NameNotFoundError(
793
          stmt.name.line,
794
          stmt.name.col,
795
          this.source,
796
          stmt.name.indexInSource,
797
          stmt.name.indexInSource + stmt.name.lexeme.length,
798
          this.environment?.suggestName(stmt.name) ?? null,
4!
799
        ),
800
      );
801
    }
802
  }
803

804
  visitReturnStmt(stmt: StmtNS.Return): void {
805
    if (stmt.value !== null) {
311✔
806
      this.resolve(stmt.value);
309✔
807
    }
808
  }
809

810
  visitWhileStmt(stmt: StmtNS.While): void {
811
    this.resolve(stmt.condition);
21✔
812
    this.resolve(stmt.body);
21✔
813
  }
814
  visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): void {
815
    this.resolve(stmt.expression);
5,920✔
816
  }
817

818
  visitFromImportStmt(stmt: StmtNS.FromImport): void {
819
    for (const entry of stmt.names) {
1✔
820
      const binding = entry.alias ?? entry.name;
1✔
821
      this.environment?.declareName(binding);
1✔
822
      this.environment?.moduleBindings.add(binding.lexeme);
1✔
823
    }
824
  }
825

826
  visitContinueStmt(_stmt: StmtNS.Continue): void {}
827
  visitBreakStmt(_stmt: StmtNS.Break): void {}
828
  visitPassStmt(_stmt: StmtNS.Pass): void {}
829

830
  //// EXPRESSIONS
831
  visitVariableExpr(expr: ExprNS.Variable): void {
832
    try {
4,613✔
833
      this.environment?.lookupNameCurrentEnvWithError(expr.name);
4,613✔
834
    } catch (e) {
835
      if (e instanceof Error) {
34✔
836
        // The incremental (textual-order) environment didn't find it — but the name may
837
        // still have a legitimate binding construct somewhere in the current function, an
838
        // enclosing function, or the module, just not yet registered because the resolver
839
        // hasn't visited that part of the tree (e.g. it's nested in an `if`/`while`/`for`,
840
        // or in a `def` that appears later in the same body). Real Python only fails at
841
        // *runtime* (UnboundLocalError / NameError) for such forward references — it never
842
        // rejects them statically — so accept here and let the interpreter's own dynamic
843
        // checks (which already do a correct whole-function scan) catch genuine misuse.
844
        if (this.nameHasStaticBinding(expr.name.lexeme)) {
34✔
845
          return;
6✔
846
        }
847
        this.errors.push(e);
28✔
848
        return;
28✔
849
      }
850
      throw e;
×
851
    }
852
  }
853
  visitLambdaExpr(expr: ExprNS.Lambda): void {
854
    // Create a new environment.
855
    const oldEnv = this.environment;
1,201✔
856
    // Assign the parameters to the new environment.
857
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
1,201✔
858
    this.environment = new Environment(
1,201✔
859
      this.source,
860
      this.environment,
861
      newEnv,
862
      new Set(newEnv.keys()),
863
    );
864
    this.functionEnvironments.set(expr, this.environment);
1,201✔
865
    this.resolve(expr.body);
1,201✔
866
    // Restore old environment
867
    this.environment = oldEnv;
1,201✔
868
  }
869
  visitMultiLambdaExpr(expr: ExprNS.MultiLambda): void {
870
    // Create a new environment.
871
    const oldEnv = this.environment;
×
872
    // Assign the parameters to the new environment.
873
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
×
874
    this.environment = new Environment(
×
875
      this.source,
876
      this.environment,
877
      newEnv,
878
      new Set(newEnv.keys()),
879
    );
880
    this.functionEnvironments.set(expr, this.environment);
×
881
    this.resolve(expr.body);
×
882
    // Grab identifiers from that new environment.
883
    expr.varDecls = Array.from(this.environment.names.values());
×
884
    // Restore old environment
885
    this.environment = oldEnv;
×
886
  }
887
  visitUnaryExpr(expr: ExprNS.Unary): void {
888
    this.resolve(expr.right);
129✔
889
  }
890
  visitGroupingExpr(expr: ExprNS.Grouping): void {
891
    this.resolve(expr.expression);
2,238✔
892
  }
893
  visitBinaryExpr(expr: ExprNS.Binary): void {
894
    this.resolve(expr.left);
3,443✔
895
    this.resolve(expr.right);
3,443✔
896
  }
897
  visitBoolOpExpr(expr: ExprNS.BoolOp): void {
898
    this.resolve(expr.left);
521✔
899
    this.resolve(expr.right);
521✔
900
  }
901
  visitCompareExpr(expr: ExprNS.Compare): void {
902
    this.resolve(expr.left);
2,409✔
903
    this.resolve(expr.right);
2,409✔
904
  }
905

906
  visitCallExpr(expr: ExprNS.Call): void {
907
    this.resolve(expr.callee);
2,140✔
908
    this.resolve(expr.args);
2,140✔
909
  }
910
  visitStarredExpr(expr: ExprNS.Starred): void {
911
    this.resolve(expr.value);
28✔
912
  }
913
  visitTernaryExpr(expr: ExprNS.Ternary): void {
914
    this.resolve(expr.predicate);
58✔
915
    this.resolve(expr.consequent);
58✔
916
    this.resolve(expr.alternative);
58✔
917
  }
918
  visitNoneExpr(_expr: ExprNS.None): void {}
919
  visitLiteralExpr(_expr: ExprNS.Literal): void {}
920
  visitBigIntLiteralExpr(_expr: ExprNS.BigIntLiteral): void {}
921
  visitComplexExpr(_expr: ExprNS.Complex): void {}
922
  visitListExpr(expr: ExprNS.List): void {
923
    this.resolve(expr.elements);
704✔
924
  }
925
  visitSubscriptExpr(expr: ExprNS.Subscript): void {
926
    this.resolve(expr.value);
21✔
927
    this.resolve(expr.index);
21✔
928
  }
929
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc