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

source-academy / py-slang / 30674556622

01 Aug 2026 12:03AM UTC coverage: 86.047% (+0.003%) from 86.044%
30674556622

Pull #377

github

web-flow
Merge 74ba4d3e8 into e629e6b8e
Pull Request #377: Fix NameReassignmentError's location to point at the original declaration

4518 of 5659 branches covered (79.84%)

Branch coverage included in aggregate %.

3 of 4 new or added lines in 1 file covered. (75.0%)

10042 of 11262 relevant lines covered (89.17%)

172406.79 hits per line

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

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

9
import levenshtein from "fast-levenshtein";
57✔
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);
57✔
18

19
export class Environment {
57✔
20
  source: string;
21
  // The parent of this environment
22
  enclosing: Environment | null;
23
  names: Map<string, Token>;
24
  /**
25
   * Where each name in `names` was *first* declared in this scope — for diagnostics only (e.g.
26
   * NameReassignmentError's "already declared here" pointer). `names` itself gets overwritten on
27
   * every hoist of a name (declareName has no "first wins" rule: `resolve(Stmt[])`'s hoisting pass
28
   * runs unconditionally over every Assign/FunctionDef in a statement list, including a list
29
   * re-hoisted more than once against the same environment, e.g. an if-arm and its else-arm, which
30
   * share their enclosing scope's environment — see visitIfStmt), which is required for forward
31
   * references to resolve but means `names.get(name)` can end up pointing at an unrelated later
32
   * occurrence, or even the very statement the reassignment error is itself about, rather than the
33
   * name's true original declaration (issue #211). Seeded from the environment's initial `names`
34
   * (so a function/lambda parameter's own token is its own "first declared" location — see the
35
   * constructor), then updated by declareName only the first time a name is set, never after.
36
   */
37
  firstDeclarations: Map<string, Token>;
38
  // Function names in the environment.
39
  functions: Set<string>;
40
  // Names that are from import bindings, like 'y' in `from x import y`.
41
  // This only set at the top level environment. Child environments do not
42
  // copy this field.
43
  moduleBindings: Set<string>;
44
  definedNames: Set<string>;
45
  // Names bound as parameters of the function/lambda this scope belongs to (empty for the
46
  // module scope). Distinct from `names`, which also gains entries for names assigned within
47
  // the scope's body — this set lets a chapter's no-reassignment validator tell "declared as a
48
  // parameter" apart from "declared by a body statement" so it can flag a parameter reassignment.
49
  parameters: Set<string>;
50
  /**
51
   * Names this function scope declared `global` (empty for the module scope
52
   * and for any scope with no `global` statement — see
53
   * visitFunctionDefStmt's `globalNamesInCurrentFunction` scan). A name in
54
   * here is deliberately absent from `names` (declareName skips it — see the
55
   * `resolve(Stmt[])` array branch), so a plain `names`-chain walk starting
56
   * *inside* this scope correctly never finds a binding here — but without
57
   * consulting this set too, that same walk would keep going outward and
58
   * could wrongly land on an *enclosing function's own same-named local*
59
   * (e.g. `def outer(): x = 1; def inner(): global x` — `inner`'s `global x`
60
   * must resolve straight to module scope, never to outer's local `x`,
61
   * exactly like real Python: a `global` declaration bypasses every
62
   * enclosing function scope, not just the declaring one). See
63
   * lookupNameEnv's use of this.
64
   */
65
  globalNames: Set<string>;
66
  constructor(
67
    source: string,
68
    enclosing: Environment | null,
69
    names: Map<string, Token>,
70
    parameters: Set<string> = new Set(),
88,828✔
71
  ) {
72
    this.source = source;
561,294✔
73
    this.enclosing = enclosing;
561,294✔
74
    this.names = names;
561,294✔
75
    this.firstDeclarations = new Map(names);
561,294✔
76
    this.functions = new Set();
561,294✔
77
    this.moduleBindings = new Set();
561,294✔
78
    this.definedNames = new Set();
561,294✔
79
    this.parameters = parameters;
561,294✔
80
    this.globalNames = new Set();
561,294✔
81
  }
82

83
  /** Walk outward to the module-level environment — one level below the
84
   * absolute-root (builtins/prelude) environment. Shared by lookupNameEnv's
85
   * `global`-declaration shortcut and visitFunctionDefStmt/visitGlobalStmt's
86
   * identical "declare this name at module scope" walk. */
87
  getModuleEnvironment(): Environment | null {
88
    // eslint-disable-next-line @typescript-eslint/no-this-alias
89
    let env: Environment | null = this;
74✔
90
    while (env !== null && env.enclosing !== null && env.enclosing.enclosing !== null) {
74✔
91
      env = env.enclosing;
87✔
92
    }
93
    return env;
74✔
94
  }
95

96
  /*
97
   * Does a full lookup up the environment chain for a name.
98
   * Returns the distance of the name from the current environment.
99
   * If name isn't found, return -1.
100
   * */
101
  lookupName(identifier: Token): number {
102
    const name = identifier.lexeme;
4,239,735✔
103
    let distance = 0;
4,239,735✔
104
    // eslint-disable-next-line @typescript-eslint/no-this-alias
105
    let curr: Environment | null = this;
4,239,735✔
106
    while (curr !== null) {
4,239,735✔
107
      if (curr.globalNames.has(name)) {
6,786,087✔
108
        // A `global` declaration anywhere between here and the module scope
109
        // redirects straight to module scope, bypassing every scope's own
110
        // `names` in between -- mirrors lookupNameEnv's own identical
111
        // redirect (see globalNames' doc comment for the full rationale).
112
        // Without this, an enclosing *function's* own local of the same
113
        // name would wrongly win by being nearer (e.g. `def outer(): x = 1;
114
        // def inner(): global x` -- `inner`'s `global x` must resolve
115
        // straight to module scope, never to outer's local `x`). Matches
116
        // getModuleEnvironment()'s own walk, just counting hops instead of
117
        // returning the Environment object.
118
        let moduleEnv = curr;
14✔
119
        while (moduleEnv.enclosing !== null && moduleEnv.enclosing.enclosing !== null) {
14✔
120
          moduleEnv = moduleEnv.enclosing;
14✔
121
          distance += 1;
14✔
122
        }
123
        return distance;
14✔
124
      }
125
      if (curr.names.has(name)) {
6,786,073✔
126
        break;
4,239,673✔
127
      }
128
      distance += 1;
2,546,400✔
129
      curr = curr.enclosing;
2,546,400✔
130
    }
131
    return curr === null ? -1 : distance;
4,239,721✔
132
  }
133

134
  /**
135
   * Looks up the name in the environment chain.
136
   * Returns the Environment where the name is found, or null if not found.
137
   */
138
  lookupNameEnv(identifier: Token): Environment | null {
139
    // A `global` declaration anywhere between here and the module scope
140
    // (inclusive) redirects straight to module scope, bypassing every
141
    // scope's own `names` — including an enclosing *function's* own local of
142
    // the same name, which would otherwise wrongly win the walk below purely
143
    // by being nearer. See `globalNames`' doc comment.
144
    // eslint-disable-next-line @typescript-eslint/no-this-alias
145
    for (let curr: Environment | null = this; curr !== null; curr = curr.enclosing) {
1,491,543✔
146
      if (curr.globalNames.has(identifier.lexeme)) {
2,586,450✔
147
        return curr.getModuleEnvironment();
21✔
148
      }
149
      if (curr.names.has(identifier.lexeme)) {
2,586,429✔
150
        return curr;
1,491,522✔
151
      }
152
    }
153
    return null;
×
154
  }
155

156
  /* Looks up the name but only for the current environment. */
157
  lookupNameCurrentEnv(identifier: Token): Token | undefined {
158
    return this.names.get(identifier.lexeme);
×
159
  }
160
  lookupNameCurrentEnvWithError(identifier: Token) {
161
    if (this.lookupName(identifier) < 0) {
3,469,588✔
162
      throw new ResolverErrors.NameNotFoundError(
48✔
163
        identifier.line,
164
        identifier.col,
165
        this.source,
166
        identifier.indexInSource,
167
        identifier.indexInSource + identifier.lexeme.length,
168
        this.suggestName(identifier),
169
      );
170
    }
171
  }
172
  lookupNameParentEnvWithError(identifier: Token) {
173
    const name = identifier.lexeme;
×
174
    const parent = this.enclosing;
×
175

176
    if (parent === null || !parent.names.has(name)) {
×
177
      throw new ResolverErrors.NameNotFoundError(
×
178
        identifier.line,
179
        identifier.col,
180
        this.source,
181
        identifier.indexInSource,
182
        identifier.indexInSource + name.length,
183
        this.suggestName(identifier),
184
      );
185
    }
186
  }
187
  declareName(identifier: Token) {
188
    this.names.set(identifier.lexeme, identifier);
358,013✔
189
    if (!this.firstDeclarations.has(identifier.lexeme)) {
358,013✔
190
      this.firstDeclarations.set(identifier.lexeme, identifier);
357,817✔
191
    }
192
    this.definedNames.add(identifier.lexeme);
358,013✔
193
  }
194
  // Same as declareName but allowed to re-declare later.
195
  declarePlaceholderName(identifier: Token) {
196
    const lookup = this.lookupNameCurrentEnv(identifier);
×
197
    if (lookup !== undefined) {
×
198
      throw new ResolverErrors.NameReassignmentError(
×
199
        identifier.line,
200
        identifier.col,
201
        this.source,
202
        identifier.indexInSource,
203
        identifier.indexInSource + identifier.lexeme.length,
204
        lookup,
205
      );
206
    }
207
    this.names.set(identifier.lexeme, RedefineableTokenSentinel);
×
208
  }
209
  suggestNameCurrentEnv(identifier: Token): string | null {
210
    const name = identifier.lexeme;
×
211
    let minDistance = Infinity;
×
212
    let minName = null;
×
213
    for (const declName of this.names.keys()) {
×
214
      const dist = levenshtein.get(name, declName);
×
215
      if (dist < minDistance) {
×
216
        minDistance = dist;
×
217
        minName = declName;
×
218
      }
219
    }
220
    return minName;
×
221
  }
222
  /*
223
   * Finds name closest to name in all environments up to builtin environment.
224
   * Calculated using min levenshtein distance.
225
   * */
226
  suggestName(identifier: Token): string | null {
227
    const name = identifier.lexeme;
55✔
228
    let minDistance = Infinity;
55✔
229
    let minName = null;
55✔
230
    // eslint-disable-next-line @typescript-eslint/no-this-alias
231
    let curr: Environment | null = this;
55✔
232
    while (curr !== null) {
55✔
233
      for (const declName of curr.names.keys()) {
141✔
234
        const dist = levenshtein.get(name, declName);
4,478✔
235
        if (dist < minDistance) {
4,478✔
236
          minDistance = dist;
140✔
237
          minName = declName;
140✔
238
        }
239
      }
240
      curr = curr.enclosing;
141✔
241
    }
242
    if (minDistance >= 4) {
55✔
243
      // This is pretty far, so just return null
244
      return null;
15✔
245
    }
246
    return minName;
40✔
247
  }
248
}
249
export class Resolver implements StmtNS.Visitor<void>, ExprNS.Visitor<void> {
57✔
250
  source: string;
251
  ast: Stmt;
252
  environment: Environment | null;
253
  functionScope: Environment | null;
254
  errors: Error[];
255
  functionEnvironments: FunctionEnvironments;
256
  /**
257
   * Every identifier referenced as a Variable expression anywhere in the
258
   * resolved AST — at any nesting depth, since visitVariableExpr fires from
259
   * inside nested function/lambda bodies too (visitFunctionDefStmt/
260
   * visitLambdaExpr both recurse via `this.resolve(body)`). py2js's index.ts
261
   * uses this to decide whether a REPL chunk must compile on the async spine
262
   * (see compiler.ts's dual-mode doc) because it calls `input()` somewhere —
263
   * reusing this pass instead of a second AST walk, since the resolver
264
   * already visits every such reference while checking declarations.
265
   */
266
  readonly referencedNames: Set<string> = new Set();
44,414✔
267
  private validators: FeatureValidator[];
268
  // Names declared `global` in the current function body (reset on function entry/exit).
269
  private globalNamesInCurrentFunction: Set<string> = new Set();
44,414✔
270
  // Names declared `nonlocal` in the current function body (reset on function entry/exit).
271
  private nonlocalNamesInCurrentFunction: Set<string> = new Set();
44,414✔
272
  // Stack of enclosing FunctionDef nodes (innermost last), used to resolve `nonlocal`
273
  // against a whole-function-body scan rather than the incremental (textual-order)
274
  // environment, since a binding construct may appear anywhere in the enclosing
275
  // function — including nested in `if`/`while`/`for`, and even after the nested
276
  // `def` that declares it `nonlocal` (matches CPython's whole-function static scoping).
277
  private functionDefStack: StmtNS.FunctionDef[] = [];
44,414✔
278
  // Top-level module statements, set once by visitFileInputStmt. Used as the outermost
279
  // level of the whole-scope binding scan (see nameHasStaticBinding) — a module-level name
280
  // can legitimately be bound anywhere in the module body, not just textually before a
281
  // nested function that reads it.
282
  private moduleStatements: StmtNS.Stmt[] = [];
44,414✔
283
  /** Names already bound at module (global) scope before this resolve() call
284
   * — e.g. a REPL's previous chunks, or a prelude compiled into the same
285
   * persistent global environment. Unlike `preludeNames` (constructor param,
286
   * seeded into the *root* builtins environment), these are seeded into the
287
   * *module*-level environment `visitFileInputStmt` creates, so they resolve
288
   * as ordinary global variables/functions, not primitives — see
289
   * PVMLCompiler's `useGlobalMap` mode, which depends on that distinction. */
290
  private readonly moduleNames: string[];
291

292
  constructor(
293
    source: string,
294
    ast: Stmt,
295
    validators: FeatureValidator[] = [],
×
296
    groups: Group[] = [],
×
297
    preludeNames: string[] = [],
250✔
298
    moduleNames: string[] = [],
18,304✔
299
  ) {
300
    this.source = source;
44,414✔
301
    this.ast = ast;
44,414✔
302
    this.source = source;
44,414✔
303
    this.ast = ast;
44,414✔
304
    this.validators = validators;
44,414✔
305
    this.errors = [];
44,414✔
306
    this.functionEnvironments = new Map();
44,414✔
307
    this.moduleNames = moduleNames;
44,414✔
308
    // The global environment
309
    this.environment = new Environment(
44,414✔
310
      source,
311
      null,
312
      new Map([
313
        ["range", new Token(TokenType.NAME, "range", 0, 0, 0)],
314
        ["__program__", new Token(TokenType.NAME, "__program__", 0, 0, 0)],
315
        ...groups.flatMap(group =>
316
          Array.from(group.builtins.entries()).map(
109,537✔
317
            ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const,
1,970,212✔
318
          ),
319
        ),
320
        ...preludeNames.map(name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const),
938,197✔
321
      ]),
322
    );
323
    this.functionScope = null;
44,414✔
324
  }
325

326
  resolveEnvironments(program: StmtNS.FileInput): FunctionEnvironments {
327
    this.resolve(program);
155✔
328
    return this.functionEnvironments;
155✔
329
  }
330

331
  private runValidators(node: StmtNS.Stmt | ExprNS.Expr): void {
332
    try {
7,386,707✔
333
      for (const v of this.validators) v.validate(node, this.environment ?? undefined);
41,070,575!
334
    } catch (e) {
335
      if (e instanceof Error) {
126✔
336
        this.errors.push(e);
126✔
337
        return;
126✔
338
      }
339
      throw e;
×
340
    }
341
  }
342

343
  resolve(stmt: Stmt[] | Stmt | Expr[] | Expr | null): Error[] {
344
    if (stmt === null) {
6,051,590✔
345
      return this.errors;
20,969✔
346
    }
347
    if (stmt instanceof Array) {
6,030,621✔
348
      for (const st of stmt) {
2,264,442✔
349
        if (st instanceof StmtNS.FunctionDef) {
3,620,528✔
350
          if (
352,887✔
351
            !this.globalNamesInCurrentFunction.has(st.name.lexeme) &&
705,774✔
352
            !this.nonlocalNamesInCurrentFunction.has(st.name.lexeme)
353
          ) {
354
            this.environment?.declareName(st.name);
352,887✔
355
          }
356
        }
357
        if (st instanceof StmtNS.Assign && st.target instanceof ExprNS.Variable) {
3,620,528✔
358
          if (
5,000✔
359
            !this.globalNamesInCurrentFunction.has(st.target.name.lexeme) &&
9,958✔
360
            !this.nonlocalNamesInCurrentFunction.has(st.target.name.lexeme)
361
          ) {
362
            this.environment?.declareName(st.target.name);
4,892✔
363
          }
364
        }
365
      }
366
      for (const st of stmt) {
2,264,442✔
367
        this.runValidators(st);
3,620,528✔
368
        st.accept(this);
3,620,528✔
369
      }
370
    } else {
371
      this.runValidators(stmt);
3,766,179✔
372
      stmt.accept(this);
3,766,179✔
373
    }
374
    return this.errors;
6,030,621✔
375
  }
376

377
  varDeclNames(names: Map<string, Token>): Token[] | null {
378
    const res = Array.from(names.values()).filter(
×
379
      name =>
380
        // Filter out functions and module bindings.
381
        // Those will be handled separately, so they don't
382
        // need to be hoisted.
383
        !this.environment?.functions.has(name.lexeme) &&
×
384
        !this.environment?.moduleBindings.has(name.lexeme),
385
    );
386
    return res.length === 0 ? null : res;
×
387
  }
388

389
  functionVarConstraint(identifier: Token): void {
390
    if (this.functionScope == null) {
5,008✔
391
      return;
927✔
392
    }
393
    let curr = this.environment;
4,081✔
394
    while (curr !== this.functionScope) {
4,081✔
395
      if (curr !== null && curr.names.has(identifier.lexeme)) {
×
396
        // firstDeclarations (not names) so the reported location is the name's actual first
397
        // declaration in curr, not whatever declareName last hoisted there — see firstDeclarations'
398
        // doc comment.
NEW
399
        const token = curr.firstDeclarations.get(identifier.lexeme);
×
400
        if (token === undefined) {
×
401
          this.errors.push(new Error("placeholder error"));
×
402
          return;
×
403
        }
404

405
        this.errors.push(
×
406
          new ResolverErrors.NameReassignmentError(
407
            identifier.line,
408
            identifier.col,
409
            this.source,
410
            identifier.indexInSource,
411
            identifier.indexInSource + identifier.lexeme.length,
412
            token,
413
          ),
414
        );
415
        return;
×
416
      }
417
      curr = curr?.enclosing ?? null;
×
418
    }
419
  }
420

421
  //// STATEMENTS
422
  visitFileInputStmt(stmt: StmtNS.FileInput): void {
423
    // Create a new environment.
424
    const oldEnv = this.environment;
44,414✔
425
    this.environment = new Environment(
44,414✔
426
      this.source,
427
      this.environment,
428
      new Map(this.moduleNames.map(name => [name, new Token(TokenType.NAME, name, 0, 0, 0)])),
255,386✔
429
    );
430
    this.functionEnvironments.set(stmt, this.environment);
44,414✔
431
    // #181 also applies at module level: e.g. `i = 3` followed by `global i` is a
432
    // SyntaxError in real Python, even though `global` is otherwise a no-op there.
433
    this.checkDeclarationOrder(stmt.statements, true);
44,414✔
434
    this.moduleStatements = stmt.statements;
44,414✔
435
    this.resolve(stmt.statements);
44,414✔
436
    // Grab identifiers from that new environment. That are NOT functions.
437
    // stmt.varDecls = this.varDeclNames(this.environment.names)
438
    this.environment = oldEnv;
44,414✔
439
  }
440

441
  visitFunctionDefStmt(stmt: StmtNS.FunctionDef) {
442
    this.environment?.functions.add(stmt.name.lexeme);
352,887✔
443

444
    // Create a new environment.
445
    const oldEnv = this.environment;
352,887✔
446
    // Assign the parameters to the new environment.
447
    const newEnv = new Map(stmt.parameters.map(param => [param.lexeme, param]));
736,992✔
448
    this.environment = new Environment(
352,887✔
449
      this.source,
450
      this.environment,
451
      newEnv,
452
      new Set(newEnv.keys()),
453
    );
454
    this.functionEnvironments.set(stmt, this.environment);
352,887✔
455
    this.functionScope = this.environment;
352,887✔
456

457
    const oldGlobalNames = this.globalNamesInCurrentFunction;
352,887✔
458
    const oldNonlocalNames = this.nonlocalNamesInCurrentFunction;
352,887✔
459
    this.globalNamesInCurrentFunction = this.scanGlobalDeclarations(stmt.body);
352,887✔
460
    this.nonlocalNamesInCurrentFunction = this.scanNonlocalDeclarations(stmt.body);
352,887✔
461
    // Stamp this function's own scope with its `global` declarations — see
462
    // `globalNames`' doc comment on Environment — so lookupNameEnv can
463
    // redirect straight to module scope for these names regardless of what
464
    // an enclosing function scope happens to also bind.
465
    this.environment.globalNames = this.globalNamesInCurrentFunction;
352,887✔
466

467
    // Run scope conflict checks before resolving the body.
468
    this.checkFunctionScopeConflicts(stmt);
352,887✔
469

470
    // Declare global names in the outermost *module-level* environment (not the
471
    // absolute-root builtins/prelude environment one level further out — see
472
    // visitGlobalStmt's isModuleLevel check for the same "one below root" test)
473
    // so that variable lookups within this function can find them via the chain
474
    // walk. This matters even for a name with no top-level assignment at all
475
    // (`def f(): global y; y = 1` with no `y` anywhere at module scope): without
476
    // this, PVMLCompiler's getTokenAnnotation would resolve `y` all the way to
477
    // the builtins environment and misinterpret it as an unimplemented
478
    // primitive function, rather than a fresh module-level variable slot.
479
    if (this.globalNamesInCurrentFunction.size > 0) {
352,887✔
480
      const globalEnv = this.environment.getModuleEnvironment();
53✔
481
      if (globalEnv) {
53✔
482
        for (const name of this.globalNamesInCurrentFunction) {
53✔
483
          if (!globalEnv.names.has(name)) {
53✔
484
            // Use a sentinel token so the resolver accepts references to this name.
485
            globalEnv.names.set(name, new Token(TokenType.NAME, name, 0, 0, 0));
31✔
486
          }
487
        }
488
      }
489
    }
490

491
    this.functionDefStack.push(stmt);
352,887✔
492
    this.resolve(stmt.body);
352,887✔
493
    this.functionDefStack.pop();
352,887✔
494
    // Restore old environment
495
    this.globalNamesInCurrentFunction = oldGlobalNames;
352,887✔
496
    this.nonlocalNamesInCurrentFunction = oldNonlocalNames;
352,887✔
497
    this.functionScope = null;
352,887✔
498
    this.environment = oldEnv;
352,887✔
499
  }
500

501
  visitAnnAssignStmt(stmt: StmtNS.AnnAssign): void {
502
    this.resolve(stmt.ann);
8✔
503
    this.resolve(stmt.value);
8✔
504
    this.functionVarConstraint(stmt.target.name);
8✔
505
  }
506

507
  visitAssignStmt(stmt: StmtNS.Assign): void {
508
    const target = stmt.target;
5,072✔
509
    if (target instanceof ExprNS.Subscript) {
5,072✔
510
      this.resolve(target); // dispatches to visitSubscriptExpr
72✔
511
      this.resolve(stmt.value);
72✔
512
      return;
72✔
513
    }
514
    this.resolve(stmt.value);
5,000✔
515
    this.functionVarConstraint(target.name);
5,000✔
516
  }
517

518
  visitAssertStmt(stmt: StmtNS.Assert): void {
519
    this.resolve(stmt.value);
×
520
  }
521
  visitForStmt(stmt: StmtNS.For): void {
522
    if (
175✔
523
      !this.globalNamesInCurrentFunction.has(stmt.target.lexeme) &&
350✔
524
      !this.nonlocalNamesInCurrentFunction.has(stmt.target.lexeme)
525
    ) {
526
      this.environment?.declareName(stmt.target);
175✔
527
    }
528
    this.resolve(stmt.iter);
175✔
529
    this.resolve(stmt.body);
175✔
530
  }
531

532
  visitIfStmt(stmt: StmtNS.If): void {
533
    this.resolve(stmt.condition);
189,336✔
534
    this.resolve(stmt.body);
189,336✔
535
    this.resolve(stmt.elseBlock);
189,336✔
536
  }
537
  visitGlobalStmt(stmt: StmtNS.Global): void {
538
    // Function-level `global x` is handled entirely in visitFunctionDefStmt (scanning +
539
    // declaring in the outermost env). At module level `global x` is semantically a no-op
540
    // (the name is already module-scope) — but it still must make `x` a recognized name for
541
    // the *resolver*, exactly like a real assignment would, even though no value is bound
542
    // yet. Real Python defers to a runtime NameError if `x` is never actually assigned
543
    // before use; without this, py-slang would wrongly reject `global x` immediately.
544
    const env = this.environment;
61✔
545
    const isModuleLevel =
546
      env !== null && env.enclosing !== null && env.enclosing.enclosing === null;
61✔
547
    if (isModuleLevel && !env.names.has(stmt.name.lexeme)) {
61✔
548
      env.names.set(stmt.name.lexeme, new Token(TokenType.NAME, stmt.name.lexeme, 0, 0, 0));
4✔
549
    }
550
  }
551

552
  // Recursively collects names declared with `global` anywhere in the function body,
553
  // without descending into nested function/lambda definitions.
554
  private scanGlobalDeclarations(stmts: StmtNS.Stmt[]): Set<string> {
555
    const globals = new Set<string>();
352,994✔
556
    const scan = (stmts: StmtNS.Stmt[]) => {
352,994✔
557
      for (const stmt of stmts) {
710,636✔
558
        if (stmt instanceof StmtNS.Global) {
987,729✔
559
          globals.add(stmt.name.lexeme);
54✔
560
        } else if (stmt instanceof StmtNS.If) {
987,675✔
561
          scan(stmt.body);
189,263✔
562
          if (Array.isArray(stmt.elseBlock)) {
189,263✔
563
            scan(stmt.elseBlock);
168,332✔
564
          } else if (stmt.elseBlock) {
20,931!
565
            scan([stmt.elseBlock]);
×
566
          }
567
        } else if (stmt instanceof StmtNS.While) {
798,412✔
568
          scan(stmt.body);
2✔
569
        } else if (stmt instanceof StmtNS.For) {
798,410✔
570
          scan(stmt.body);
45✔
571
        }
572
        // Do not recurse into FunctionDef or Lambda bodies.
573
      }
574
    };
575
    scan(stmts);
352,994✔
576
    return globals;
352,994✔
577
  }
578

579
  // Recursively collects names declared with `nonlocal` anywhere in the function body,
580
  // without descending into nested function/lambda definitions.
581
  private scanNonlocalDeclarations(stmts: StmtNS.Stmt[]): Set<string> {
582
    const nonlocals = new Set<string>();
352,993✔
583
    const scan = (stmts: StmtNS.Stmt[]) => {
352,993✔
584
      for (const stmt of stmts) {
710,635✔
585
        if (stmt instanceof StmtNS.NonLocal) {
987,725✔
586
          nonlocals.add(stmt.name.lexeme);
96✔
587
        } else if (stmt instanceof StmtNS.If) {
987,629✔
588
          scan(stmt.body);
189,263✔
589
          if (Array.isArray(stmt.elseBlock)) {
189,263✔
590
            scan(stmt.elseBlock);
168,332✔
591
          } else if (stmt.elseBlock) {
20,931!
592
            scan([stmt.elseBlock]);
×
593
          }
594
        } else if (stmt instanceof StmtNS.While) {
798,366✔
595
          scan(stmt.body);
2✔
596
        } else if (stmt instanceof StmtNS.For) {
798,364✔
597
          scan(stmt.body);
45✔
598
        }
599
        // Do not recurse into FunctionDef or Lambda bodies.
600
      }
601
    };
602
    scan(stmts);
352,993✔
603
    return nonlocals;
352,993✔
604
  }
605

606
  // Checks scope conflicts within a FunctionDef for issues #178, #179, #180, #181.
607
  private checkFunctionScopeConflicts(stmt: StmtNS.FunctionDef): void {
608
    const globalTokens = this.scanScopeDeclarationTokens(stmt.body, "Global");
352,887✔
609
    const nonlocalTokens = this.scanScopeDeclarationTokens(stmt.body, "NonLocal");
352,887✔
610
    const paramNames = new Set(stmt.parameters.map(p => p.lexeme));
736,992✔
611

612
    // #178: name is both global and nonlocal in the same function
613
    for (const [name, token] of nonlocalTokens) {
352,887✔
614
      if (globalTokens.has(name)) {
85✔
615
        this.errors.push(
4✔
616
          new ResolverErrors.ScopeConflictError(
617
            token.line,
618
            token.col,
619
            this.source,
620
            token.indexInSource,
621
            token.indexInSource + name.length,
622
            `name '${name}' is nonlocal and global`,
623
          ),
624
        );
625
      }
626
    }
627

628
    // #179: parameter and nonlocal conflict
629
    for (const [name, token] of nonlocalTokens) {
352,887✔
630
      if (paramNames.has(name)) {
85✔
631
        this.errors.push(
5✔
632
          new ResolverErrors.ScopeConflictError(
633
            token.line,
634
            token.col,
635
            this.source,
636
            token.indexInSource,
637
            token.indexInSource + name.length,
638
            `name '${name}' is parameter and nonlocal`,
639
          ),
640
        );
641
      }
642
    }
643

644
    // #180: parameter and global conflict
645
    for (const [name, token] of globalTokens) {
352,887✔
646
      if (paramNames.has(name)) {
53✔
647
        this.errors.push(
4✔
648
          new ResolverErrors.ScopeConflictError(
649
            token.line,
650
            token.col,
651
            this.source,
652
            token.indexInSource,
653
            token.indexInSource + name.length,
654
            `name '${name}' is parameter and global`,
655
          ),
656
        );
657
      }
658
    }
659

660
    // #181: textual order — name used/assigned before its global/nonlocal declaration
661
    this.checkDeclarationOrder(stmt.body);
352,887✔
662
  }
663

664
  // Returns a map from name to its declaration token for all `global` or `nonlocal`
665
  // statements in the given body (without descending into nested functions).
666
  private scanScopeDeclarationTokens(
667
    stmts: StmtNS.Stmt[],
668
    kind: "Global" | "NonLocal",
669
  ): Map<string, Token> {
670
    const result = new Map<string, Token>();
705,774✔
671
    const scan = (stmts: StmtNS.Stmt[]) => {
705,774✔
672
      for (const stmt of stmts) {
1,421,008✔
673
        if (kind === "Global" && stmt instanceof StmtNS.Global) {
1,974,690✔
674
          result.set(stmt.name.lexeme, stmt.name);
53✔
675
        } else if (kind === "NonLocal" && stmt instanceof StmtNS.NonLocal) {
1,974,637✔
676
          result.set(stmt.name.lexeme, stmt.name);
85✔
677
        } else if (stmt instanceof StmtNS.If) {
1,974,552✔
678
          scan(stmt.body);
378,518✔
679
          if (Array.isArray(stmt.elseBlock)) {
378,518✔
680
            scan(stmt.elseBlock);
336,664✔
681
          } else if (stmt.elseBlock) {
41,854!
682
            scan([stmt.elseBlock]);
×
683
          }
684
        } else if (stmt instanceof StmtNS.While) {
1,596,034✔
685
          scan(stmt.body);
2✔
686
        } else if (stmt instanceof StmtNS.For) {
1,596,032✔
687
          scan(stmt.body);
50✔
688
        }
689
        // Do not recurse into FunctionDef or Lambda bodies.
690
      }
691
    };
692
    scan(stmts);
705,774✔
693
    return result;
705,774✔
694
  }
695

696
  // Checks that no name is used or assigned before its `global`/`nonlocal` declaration
697
  // in the function body (#181). Traverses in textual order.
698
  // `isModuleLevel` additionally rejects bare `nonlocal` declarations (nonlocal is never
699
  // valid at module scope, matching CPython's `SyntaxError: nonlocal declaration not
700
  // allowed at module level` — but only once declaration-order has been ruled out, since
701
  // CPython itself prioritises the order error when both apply).
702
  private checkDeclarationOrder(body: StmtNS.Stmt[], isModuleLevel: boolean = false): void {
352,887✔
703
    const seen = new Map<string, { kind: "used" | "assigned"; token: Token }>();
397,301✔
704

705
    const recordUse = (token: Token) => {
397,301✔
706
      if (!seen.has(token.lexeme)) {
2,813,184✔
707
        seen.set(token.lexeme, { kind: "used", token });
1,827,267✔
708
      }
709
    };
710
    const recordAssign = (token: Token) => {
397,301✔
711
      if (!seen.has(token.lexeme)) {
358,062✔
712
        seen.set(token.lexeme, { kind: "assigned", token });
357,866✔
713
      }
714
    };
715

716
    const collectExpr = (expr: ExprNS.Expr | null | undefined) => {
397,301✔
717
      if (!expr) return;
5,390,730!
718
      if (expr instanceof ExprNS.Variable) {
5,390,730✔
719
        recordUse(expr.name);
2,813,184✔
720
      } else if (expr instanceof ExprNS.Binary || expr instanceof ExprNS.Compare) {
2,577,546✔
721
        collectExpr(expr.left);
207,828✔
722
        collectExpr(expr.right);
207,828✔
723
      } else if (expr instanceof ExprNS.Unary) {
2,369,718✔
724
        collectExpr(expr.right);
674✔
725
      } else if (expr instanceof ExprNS.BoolOp) {
2,369,044✔
726
        collectExpr(expr.left);
18,988✔
727
        collectExpr(expr.right);
18,988✔
728
      } else if (expr instanceof ExprNS.Call) {
2,350,056✔
729
        collectExpr(expr.callee);
1,353,686✔
730
        expr.args.forEach(collectExpr);
1,353,686✔
731
      } else if (expr instanceof ExprNS.Grouping) {
996,370✔
732
        collectExpr(expr.expression);
114,089✔
733
      } else if (expr instanceof ExprNS.Ternary) {
882,281✔
734
        collectExpr(expr.predicate);
135,630✔
735
        collectExpr(expr.consequent);
135,630✔
736
        collectExpr(expr.alternative);
135,630✔
737
      } else if (expr instanceof ExprNS.List) {
746,651✔
738
        expr.elements.forEach(collectExpr);
3,862✔
739
      } else if (expr instanceof ExprNS.Subscript) {
742,789✔
740
        collectExpr(expr.value);
202✔
741
        collectExpr(expr.index);
202✔
742
      } else if (expr instanceof ExprNS.Starred) {
742,587✔
743
        collectExpr(expr.value);
56✔
744
      }
745
      // Literal, BigIntLiteral, None, Complex, Lambda: no outer-scope variable references
746
    };
747

748
    const scanBody = (stmts: StmtNS.Stmt[]) => {
397,301✔
749
      for (const stmt of stmts) {
755,239✔
750
        if (stmt instanceof StmtNS.Global || stmt instanceof StmtNS.NonLocal) {
1,367,453✔
751
          const name = stmt.name.lexeme;
148✔
752
          const prior = seen.get(name);
148✔
753
          if (prior) {
148✔
754
            const declKind = stmt instanceof StmtNS.Global ? "global" : "nonlocal";
16✔
755
            const msg =
756
              prior.kind === "assigned"
16✔
757
                ? `name '${name}' is assigned to before ${declKind} declaration`
758
                : `name '${name}' is used prior to ${declKind} declaration`;
759
            this.errors.push(
16✔
760
              new ResolverErrors.ScopeConflictError(
761
                stmt.name.line,
762
                stmt.name.col,
763
                this.source,
764
                stmt.name.indexInSource,
765
                stmt.name.indexInSource + name.length,
766
                msg,
767
              ),
768
            );
769
          } else if (isModuleLevel && stmt instanceof StmtNS.NonLocal) {
132✔
770
            this.errors.push(
1✔
771
              new ResolverErrors.ScopeConflictError(
772
                stmt.name.line,
773
                stmt.name.col,
774
                this.source,
775
                stmt.name.indexInSource,
776
                stmt.name.indexInSource + name.length,
777
                "nonlocal declaration not allowed at module level",
778
              ),
779
            );
780
          }
781
        } else if (stmt instanceof StmtNS.FunctionDef) {
1,367,305✔
782
          // Record the function name as an assignment; don't recurse into its body.
783
          recordAssign(stmt.name);
352,887✔
784
        } else if (stmt instanceof StmtNS.Assign) {
1,014,418✔
785
          collectExpr(stmt.value);
5,072✔
786
          if (stmt.target instanceof ExprNS.Variable) {
5,072✔
787
            recordAssign(stmt.target.name);
5,000✔
788
          } else {
789
            collectExpr(stmt.target);
72✔
790
          }
791
        } else if (stmt instanceof StmtNS.SimpleExpr) {
1,009,346✔
792
          collectExpr(stmt.expression);
306,238✔
793
        } else if (stmt instanceof StmtNS.Return) {
703,108✔
794
          if (stmt.value) collectExpr(stmt.value);
513,332✔
795
        } else if (stmt instanceof StmtNS.If) {
189,776✔
796
          collectExpr(stmt.condition);
189,336✔
797
          scanBody(stmt.body);
189,336✔
798
          if (Array.isArray(stmt.elseBlock)) scanBody(stmt.elseBlock);
189,336✔
799
          else if (stmt.elseBlock) scanBody([stmt.elseBlock]);
20,969!
800
        } else if (stmt instanceof StmtNS.While) {
440✔
801
          collectExpr(stmt.condition);
60✔
802
          scanBody(stmt.body);
60✔
803
        } else if (stmt instanceof StmtNS.For) {
380✔
804
          collectExpr(stmt.iter);
175✔
805
          recordAssign(stmt.target);
175✔
806
          scanBody(stmt.body);
175✔
807
        } else if (stmt instanceof StmtNS.Assert) {
205!
808
          collectExpr(stmt.value);
×
809
        }
810
        // Pass, Break, Continue: no variable references
811
      }
812
    };
813

814
    scanBody(body);
397,301✔
815
  }
816

817
  // Recursively checks whether `name` has a binding construct (assignment, `def`, or
818
  // `for` target) anywhere in the given statement list, without descending into nested
819
  // function/lambda bodies (they introduce their own scope). Mirrors scanGlobalDeclarations
820
  // / scanNonlocalDeclarations, but collects "is this name bound here at all" instead.
821
  private hasBindingConstruct(stmts: StmtNS.Stmt[], name: string): boolean {
822
    for (const stmt of stmts) {
145✔
823
      if (
203✔
824
        stmt instanceof StmtNS.Assign &&
345✔
825
        stmt.target instanceof ExprNS.Variable &&
826
        stmt.target.name.lexeme === name
827
      ) {
828
        return true;
66✔
829
      } else if (stmt instanceof StmtNS.FunctionDef && stmt.name.lexeme === name) {
137!
830
        return true;
×
831
      } else if (stmt instanceof StmtNS.For) {
137✔
832
        if (stmt.target.lexeme === name || this.hasBindingConstruct(stmt.body, name)) {
17!
833
          return true;
17✔
834
        }
835
      } else if (stmt instanceof StmtNS.If) {
120✔
836
        if (this.hasBindingConstruct(stmt.body, name)) return true;
8✔
837
        if (Array.isArray(stmt.elseBlock)) {
3✔
838
          if (this.hasBindingConstruct(stmt.elseBlock, name)) return true;
1!
839
        } else if (stmt.elseBlock && this.hasBindingConstruct([stmt.elseBlock], name)) {
2!
840
          return true;
×
841
        }
842
      } else if (stmt instanceof StmtNS.While) {
112!
843
        if (this.hasBindingConstruct(stmt.body, name)) return true;
×
844
      }
845
      // Do not recurse into nested FunctionDef or Lambda bodies.
846
    }
847
    return false;
57✔
848
  }
849

850
  // Checks whether `name` has a binding construct (parameter, assignment, def, or `for`
851
  // target) in the enclosing function at functionDefStack[startIndex], or in any function
852
  // further out, stopping at (but not entering) module scope. Shared by visitNonLocalStmt
853
  // (which starts one level out, since nonlocal never refers to the current function) and
854
  // nameHasStaticBinding (which starts at the current function, inclusive).
855
  private hasEnclosingFunctionBinding(name: string, startIndex: number): boolean {
856
    for (let i = startIndex; i >= 0; i--) {
135✔
857
      const fn = this.functionDefStack[i];
112✔
858
      if (fn.parameters.some(p => p.lexeme === name)) {
112✔
859
        return true;
5✔
860
      }
861
      const fnGlobals = this.scanGlobalDeclarations(fn.body);
107✔
862
      if (fnGlobals.has(name)) {
107✔
863
        // `x` is explicitly global in this function — it is not a local binding here,
864
        // and CPython does not skip past it to look further out.
865
        return false;
1✔
866
      }
867
      const fnNonlocals = this.scanNonlocalDeclarations(fn.body);
106✔
868
      if (fnNonlocals.has(name)) {
106✔
869
        // `x` is itself nonlocal in this function — defer to whatever that resolves to
870
        // further out (already separately validated for `fn`).
871
        continue;
11✔
872
      }
873
      if (this.hasBindingConstruct(fn.body, name)) {
95✔
874
        return true;
82✔
875
      }
876
      // Not mentioned in this function at all — keep searching further out.
877
    }
878
    return false;
47✔
879
  }
880

881
  // Whole-scope-chain check for whether `name` can resolve at all: the current function
882
  // (if any), each enclosing function, and finally module scope — each scanned as a whole
883
  // body rather than relying on the incremental (textual-order) environment. This mirrors
884
  // CPython's static LEGB classification: a binding construct may appear anywhere in its
885
  // owning scope's body, not just textually before the point of reference (e.g. nested in
886
  // `if`/`while`/`for`, or after a nested `def` that reads/writes it).
887
  private nameHasStaticBinding(name: string): boolean {
888
    if (this.hasEnclosingFunctionBinding(name, this.functionDefStack.length - 1)) {
48✔
889
      return true;
7✔
890
    }
891
    return this.hasBindingConstruct(this.moduleStatements, name);
41✔
892
  }
893

894
  visitNonLocalStmt(stmt: StmtNS.NonLocal): void {
895
    const name = stmt.name.lexeme;
87✔
896
    // Search enclosing FUNCTION scopes (never the module scope), innermost first,
897
    // skipping the current function itself (nonlocal can never bind to it).
898
    const found = this.hasEnclosingFunctionBinding(name, this.functionDefStack.length - 2);
87✔
899
    if (!found) {
87✔
900
      this.errors.push(
7✔
901
        new ResolverErrors.NameNotFoundError(
902
          stmt.name.line,
903
          stmt.name.col,
904
          this.source,
905
          stmt.name.indexInSource,
906
          stmt.name.indexInSource + stmt.name.lexeme.length,
907
          this.environment?.suggestName(stmt.name) ?? null,
7!
908
        ),
909
      );
910
    }
911
  }
912

913
  visitReturnStmt(stmt: StmtNS.Return): void {
914
    if (stmt.value !== null) {
513,332✔
915
      this.resolve(stmt.value);
513,329✔
916
    }
917
  }
918

919
  visitWhileStmt(stmt: StmtNS.While): void {
920
    this.resolve(stmt.condition);
60✔
921
    this.resolve(stmt.body);
60✔
922
  }
923
  visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): void {
924
    this.resolve(stmt.expression);
306,238✔
925
  }
926

927
  visitFromImportStmt(stmt: StmtNS.FromImport): void {
928
    for (const entry of stmt.names) {
45✔
929
      const binding = entry.alias ?? entry.name;
59✔
930
      this.environment?.declareName(binding);
59✔
931
      this.environment?.moduleBindings.add(binding.lexeme);
59✔
932
    }
933
  }
934

935
  visitContinueStmt(_stmt: StmtNS.Continue): void {}
936
  visitBreakStmt(_stmt: StmtNS.Break): void {}
937
  visitPassStmt(_stmt: StmtNS.Pass): void {}
938

939
  //// EXPRESSIONS
940
  visitVariableExpr(expr: ExprNS.Variable): void {
941
    this.referencedNames.add(expr.name.lexeme);
3,138,801✔
942
    try {
3,138,801✔
943
      this.environment?.lookupNameCurrentEnvWithError(expr.name);
3,138,801✔
944
    } catch (e) {
945
      if (e instanceof Error) {
48✔
946
        // The incremental (textual-order) environment didn't find it — but the name may
947
        // still have a legitimate binding construct somewhere in the current function, an
948
        // enclosing function, or the module, just not yet registered because the resolver
949
        // hasn't visited that part of the tree (e.g. it's nested in an `if`/`while`/`for`,
950
        // or in a `def` that appears later in the same body). Real Python only fails at
951
        // *runtime* (UnboundLocalError / NameError) for such forward references — it never
952
        // rejects them statically — so accept here and let the interpreter's own dynamic
953
        // checks (which already do a correct whole-function scan) catch genuine misuse.
954
        if (this.nameHasStaticBinding(expr.name.lexeme)) {
48✔
955
          return;
8✔
956
        }
957
        this.errors.push(e);
40✔
958
        return;
40✔
959
      }
960
      throw e;
×
961
    }
962
  }
963
  visitLambdaExpr(expr: ExprNS.Lambda): void {
964
    // Create a new environment.
965
    const oldEnv = this.environment;
119,579✔
966
    // Assign the parameters to the new environment.
967
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
119,579✔
968
    this.environment = new Environment(
119,579✔
969
      this.source,
970
      this.environment,
971
      newEnv,
972
      new Set(newEnv.keys()),
973
    );
974
    this.functionEnvironments.set(expr, this.environment);
119,579✔
975
    this.resolve(expr.body);
119,579✔
976
    // Restore old environment
977
    this.environment = oldEnv;
119,579✔
978
  }
979
  visitMultiLambdaExpr(expr: ExprNS.MultiLambda): void {
980
    // Create a new environment.
981
    const oldEnv = this.environment;
×
982
    // Assign the parameters to the new environment.
983
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
×
984
    this.environment = new Environment(
×
985
      this.source,
986
      this.environment,
987
      newEnv,
988
      new Set(newEnv.keys()),
989
    );
990
    this.functionEnvironments.set(expr, this.environment);
×
991
    this.resolve(expr.body);
×
992
    // Grab identifiers from that new environment.
993
    expr.varDecls = Array.from(this.environment.names.values());
×
994
    // Restore old environment
995
    this.environment = oldEnv;
×
996
  }
997
  visitUnaryExpr(expr: ExprNS.Unary): void {
998
    this.resolve(expr.right);
678✔
999
  }
1000
  visitGroupingExpr(expr: ExprNS.Grouping): void {
1001
    this.resolve(expr.expression);
114,089✔
1002
  }
1003
  visitBinaryExpr(expr: ExprNS.Binary): void {
1004
    this.resolve(expr.left);
150,391✔
1005
    this.resolve(expr.right);
150,391✔
1006
  }
1007
  visitBoolOpExpr(expr: ExprNS.BoolOp): void {
1008
    this.resolve(expr.left);
18,988✔
1009
    this.resolve(expr.right);
18,988✔
1010
  }
1011
  visitCompareExpr(expr: ExprNS.Compare): void {
1012
    this.resolve(expr.left);
110,832✔
1013
    this.resolve(expr.right);
110,832✔
1014
  }
1015

1016
  visitCallExpr(expr: ExprNS.Call): void {
1017
    this.resolve(expr.callee);
1,505,341✔
1018
    this.resolve(expr.args);
1,505,341✔
1019
  }
1020
  visitStarredExpr(expr: ExprNS.Starred): void {
1021
    this.resolve(expr.value);
56✔
1022
  }
1023
  visitTernaryExpr(expr: ExprNS.Ternary): void {
1024
    this.resolve(expr.predicate);
135,630✔
1025
    this.resolve(expr.consequent);
135,630✔
1026
    this.resolve(expr.alternative);
135,630✔
1027
  }
1028
  visitNoneExpr(_expr: ExprNS.None): void {}
1029
  visitLiteralExpr(_expr: ExprNS.Literal): void {}
1030
  visitBigIntLiteralExpr(_expr: ExprNS.BigIntLiteral): void {}
1031
  visitComplexExpr(_expr: ExprNS.Complex): void {}
1032
  visitListExpr(expr: ExprNS.List): void {
1033
    this.resolve(expr.elements);
3,862✔
1034
  }
1035
  visitSubscriptExpr(expr: ExprNS.Subscript): void {
1036
    this.resolve(expr.value);
206✔
1037
    this.resolve(expr.index);
206✔
1038
  }
1039
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc