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

source-academy / py-slang / 23790160202

31 Mar 2026 09:24AM UTC coverage: 62.878% (+3.5%) from 59.421%
23790160202

Pull #129

github

web-flow
Merge b68958bdb into 0e5a36381
Pull Request #129: Add new standard library functions

778 of 1450 branches covered (53.66%)

Branch coverage included in aggregate %.

64 of 107 new or added lines in 8 files covered. (59.81%)

129 existing lines in 14 files now uncovered.

2547 of 3838 relevant lines covered (66.36%)

3347.98 hits per line

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

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

11
import levenshtein from "fast-levenshtein";
4✔
12
// const levenshtein = require('fast-levenshtein');
13

14
const RedefineableTokenSentinel = new Token(TokenType.AT, "", 0, 0, 0);
4✔
15

16
export class Environment {
4✔
17
  source: string;
18
  // The parent of this environment
19
  enclosing: Environment | null;
20
  names: Map<string, Token>;
21
  // Function names in the environment.
22
  functions: Set<string>;
23
  // Names that are from import bindings, like 'y' in `from x import y`.
24
  // This only set at the top level environment. Child environments do not
25
  // copy this field.
26
  moduleBindings: Set<string>;
27
  definedNames: Set<string>;
28
  constructor(source: string, enclosing: Environment | null, names: Map<string, Token>) {
29
    this.source = source;
2,331✔
30
    this.enclosing = enclosing;
2,331✔
31
    this.names = names;
2,331✔
32
    this.functions = new Set();
2,331✔
33
    this.moduleBindings = new Set();
2,331✔
34
    this.definedNames = new Set();
2,331✔
35
  }
36

37
  /*
38
   * Does a full lookup up the environment chain for a name.
39
   * Returns the distance of the name from the current environment.
40
   * If name isn't found, return -1.
41
   * */
42
  lookupName(identifier: Token): number {
43
    const name = identifier.lexeme;
869✔
44
    let distance = 0;
869✔
45
    // eslint-disable-next-line @typescript-eslint/no-this-alias
46
    let curr: Environment | null = this;
869✔
47
    while (curr !== null) {
869✔
48
      if (curr.names.has(name)) {
1,560✔
49
        break;
863✔
50
      }
51
      distance += 1;
697✔
52
      curr = curr.enclosing;
697✔
53
    }
54
    return curr === null ? -1 : distance;
869✔
55
  }
56

57
  /* Looks up the name but only for the current environment. */
58
  lookupNameCurrentEnv(identifier: Token): Token | undefined {
59
    return this.names.get(identifier.lexeme);
53✔
60
  }
61
  lookupNameCurrentEnvWithError(identifier: Token) {
62
    if (this.lookupName(identifier) < 0) {
869✔
63
      throw new ResolverErrors.NameNotFoundError(
6✔
64
        identifier.line,
65
        identifier.col,
66
        this.source,
67
        identifier.indexInSource,
68
        identifier.indexInSource + identifier.lexeme.length,
69
        this.suggestName(identifier),
70
      );
71
    }
72
  }
73
  lookupNameParentEnvWithError(identifier: Token) {
74
    const name = identifier.lexeme;
1✔
75
    const parent = this.enclosing;
1✔
76

77
    if (parent === null || !parent.names.has(name)) {
1!
78
      throw new ResolverErrors.NameNotFoundError(
×
79
        identifier.line,
80
        identifier.col,
81
        this.source,
82
        identifier.indexInSource,
83
        identifier.indexInSource + name.length,
84
        this.suggestName(identifier),
85
      );
86
    }
87
  }
88
  declareName(identifier: Token) {
89
    this.names.set(identifier.lexeme, identifier);
116✔
90
    this.definedNames.add(identifier.lexeme);
116✔
91
  }
92
  // Same as declareName but allowed to re-declare later.
93
  declarePlaceholderName(identifier: Token) {
94
    const lookup = this.lookupNameCurrentEnv(identifier);
53✔
95
    if (lookup !== undefined) {
53!
96
      throw new ResolverErrors.NameReassignmentError(
×
97
        identifier.line,
98
        identifier.col,
99
        this.source,
100
        identifier.indexInSource,
101
        identifier.indexInSource + identifier.lexeme.length,
102
        lookup,
103
      );
104
    }
105
    this.names.set(identifier.lexeme, RedefineableTokenSentinel);
53✔
106
  }
107
  suggestNameCurrentEnv(identifier: Token): string | null {
108
    const name = identifier.lexeme;
×
109
    let minDistance = Infinity;
×
110
    let minName = null;
×
111
    for (const declName of this.names.keys()) {
×
112
      const dist = levenshtein.get(name, declName);
×
113
      if (dist < minDistance) {
×
114
        minDistance = dist;
×
115
        minName = declName;
×
116
      }
117
    }
118
    return minName;
×
119
  }
120
  /*
121
   * Finds name closest to name in all environments up to builtin environment.
122
   * Calculated using min levenshtein distance.
123
   * */
124
  suggestName(identifier: Token): string | null {
125
    const name = identifier.lexeme;
6✔
126
    let minDistance = Infinity;
6✔
127
    let minName = null;
6✔
128
    // eslint-disable-next-line @typescript-eslint/no-this-alias
129
    let curr: Environment | null = this;
6✔
130
    while (curr !== null) {
6✔
131
      for (const declName of curr.names.keys()) {
14✔
132
        const dist = levenshtein.get(name, declName);
484✔
133
        if (dist < minDistance) {
484✔
134
          minDistance = dist;
10✔
135
          minName = declName;
10✔
136
        }
137
      }
138
      curr = curr.enclosing;
14✔
139
    }
140
    if (minDistance >= 4) {
6✔
141
      // This is pretty far, so just return null
142
      return null;
1✔
143
    }
144
    return minName;
5✔
145
  }
146
}
147
export class Resolver implements StmtNS.Visitor<void>, ExprNS.Visitor<void> {
4✔
148
  source: string;
149
  ast: Stmt;
150
  environment: Environment | null;
151
  functionScope: Environment | null;
152
  private validators: FeatureValidator[];
153

154
  constructor(
155
    source: string,
156
    ast: Stmt,
157
    validators: FeatureValidator[] = [],
×
158
    groups: Group[] = [],
28✔
159
    preludeNames: string[] = [],
28✔
160
  ) {
161
    this.source = source;
1,093✔
162
    this.ast = ast;
1,093✔
163
    this.source = source;
1,093✔
164
    this.ast = ast;
1,093✔
165
    this.validators = validators;
1,093✔
166
    // The global environment
167
    this.environment = new Environment(
1,093✔
168
      source,
169
      null,
170
      new Map([
171
        // misc library
172
        ...constants.builtInFuncs.map(
173
          (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
80,882✔
174
        ),
175
        ["range", new Token(TokenType.NAME, "range", 0, 0, 0)],
176
        ...constants.constants.map(
177
          (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
5,465✔
178
        ),
179
        ...groups.flatMap(group =>
180
          Array.from(group.builtins.entries()).map(
325✔
181
            ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
1,235✔
182
          ),
183
        ),
184
        ...preludeNames.map(
185
          name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
5,365✔
186
        ),
187
      ]),
188
    );
189
    this.functionScope = null;
1,093✔
190
  }
191

192
  private runValidators(node: StmtNS.Stmt | ExprNS.Expr): void {
193
    for (const v of this.validators) v.validate(node, this.environment ?? undefined);
58,796!
194
  }
195

196
  resolve(stmt: Stmt[] | Stmt | Expr[] | Expr | null) {
197
    if (stmt === null) {
6,667!
198
      return;
×
199
    }
200
    if (stmt instanceof Array) {
6,667✔
201
      // Resolve all top-level functions first. Python allows functions declared after
202
      // another function to be used in that function.
203
      for (const st of stmt) {
1,766✔
204
        if (st instanceof StmtNS.FunctionDef) {
2,169✔
205
          this.environment?.declarePlaceholderName(st.name);
53✔
206
        }
207
      }
208
      for (const st of stmt) {
1,766✔
209
        this.runValidators(st);
2,163✔
210
        st.accept(this);
2,131✔
211
      }
212
    } else {
213
      this.runValidators(stmt);
4,901✔
214
      stmt.accept(this);
4,888✔
215
    }
216
  }
217

218
  varDeclNames(names: Map<string, Token>): Token[] | null {
219
    const res = Array.from(names.values()).filter(
×
220
      name =>
221
        // Filter out functions and module bindings.
222
        // Those will be handled separately, so they don't
223
        // need to be hoisted.
224
        !this.environment?.functions.has(name.lexeme) &&
×
225
        !this.environment?.moduleBindings.has(name.lexeme),
226
    );
227
    return res.length === 0 ? null : res;
×
228
  }
229

230
  functionVarConstraint(identifier: Token): void {
231
    if (this.functionScope == null) {
60✔
232
      return;
54✔
233
    }
234
    let curr = this.environment;
6✔
235
    while (curr !== this.functionScope) {
6✔
236
      if (curr !== null && curr.names.has(identifier.lexeme)) {
×
237
        const token = curr.names.get(identifier.lexeme);
×
238
        if (token === undefined) {
×
239
          throw new Error("placeholder error");
×
240
        }
241
        throw new ResolverErrors.NameReassignmentError(
×
242
          identifier.line,
243
          identifier.col,
244
          this.source,
245
          identifier.indexInSource,
246
          identifier.indexInSource + identifier.lexeme.length,
247
          token,
248
        );
249
      }
250
      curr = curr?.enclosing ?? null;
×
251
    }
252
  }
253

254
  //// STATEMENTS
255
  visitFileInputStmt(stmt: StmtNS.FileInput): void {
256
    // Create a new environment.
257
    const oldEnv = this.environment;
1,093✔
258
    this.environment = new Environment(this.source, this.environment, new Map());
1,093✔
259
    this.resolve(stmt.statements);
1,093✔
260
    // Grab identifiers from that new environment. That are NOT functions.
261
    // stmt.varDecls = this.varDeclNames(this.environment.names)
262
    this.environment = oldEnv;
1,042✔
263
  }
264

265
  visitFunctionDefStmt(stmt: StmtNS.FunctionDef) {
266
    this.environment?.declareName(stmt.name);
51✔
267
    this.environment?.functions.add(stmt.name.lexeme);
51✔
268

269
    // Create a new environment.
270
    const oldEnv = this.environment;
51✔
271
    // Assign the parameters to the new environment.
272
    const newEnv = new Map(stmt.parameters.map(param => [param.lexeme, param]));
51✔
273
    this.environment = new Environment(this.source, this.environment, newEnv);
51✔
274
    // const params = new Map(
275
    //     stmt.parameters.map(param => [param.lexeme, param])
276
    // );
277
    // if (this.environment !== null) {
278
    //     this.environment.names = params;
279
    // }
280
    this.functionScope = this.environment;
51✔
281
    this.resolve(stmt.body);
51✔
282
    // Grab identifiers from that new environment. That are NOT functions.
283
    // stmt.varDecls = this.varDeclNames(this.environment.names)
284
    // Restore old environment
285
    this.functionScope = null;
43✔
286
    this.environment = oldEnv;
43✔
287
  }
288

289
  visitAnnAssignStmt(stmt: StmtNS.AnnAssign): void {
290
    this.resolve(stmt.ann);
×
291
    this.resolve(stmt.value);
×
292
    this.functionVarConstraint(stmt.target.name);
×
293
    this.environment?.declareName(stmt.target.name);
×
294
  }
295

296
  visitAssignStmt(stmt: StmtNS.Assign): void {
297
    const target = stmt.target;
73✔
298
    if (target instanceof ExprNS.Subscript) {
73✔
299
      this.resolve(target); // dispatches to visitSubscriptExpr
3✔
300
      this.resolve(stmt.value);
2✔
301
      return;
2✔
302
    }
303
    this.resolve(stmt.value);
70✔
304
    this.functionVarConstraint(target.name);
60✔
305
    this.environment?.declareName(target.name);
60✔
306
  }
307

308
  visitAssertStmt(stmt: StmtNS.Assert): void {
309
    this.resolve(stmt.value);
×
310
  }
311
  visitForStmt(stmt: StmtNS.For): void {
312
    this.environment?.declareName(stmt.target);
4✔
313
    this.resolve(stmt.iter);
4✔
314
    this.resolve(stmt.body);
4✔
315
  }
316

317
  visitIfStmt(stmt: StmtNS.If): void {
318
    this.resolve(stmt.condition);
×
319
    this.resolve(stmt.body);
×
320
    this.resolve(stmt.elseBlock);
×
321
  }
322
  // @TODO we need to treat all global statements as variable declarations in the global
323
  // scope.
324
  visitGlobalStmt(_stmt: StmtNS.Global): void {
325
    // Do nothing because global can also be declared in our
326
    // own scope.
327
  }
328
  // @TODO nonlocals mean that any variable following that name in the current env
329
  // should not create a variable declaration, but instead point to an outer variable.
330
  visitNonLocalStmt(stmt: StmtNS.NonLocal): void {
331
    this.environment?.lookupNameParentEnvWithError(stmt.name);
1✔
332
  }
333

334
  visitReturnStmt(stmt: StmtNS.Return): void {
335
    if (stmt.value !== null) {
26✔
336
      this.resolve(stmt.value);
26✔
337
    }
338
  }
339

340
  visitWhileStmt(stmt: StmtNS.While): void {
341
    this.resolve(stmt.condition);
3✔
342
    this.resolve(stmt.body);
3✔
343
  }
344
  visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): void {
345
    this.resolve(stmt.expression);
1,036✔
346
  }
347

348
  visitFromImportStmt(stmt: StmtNS.FromImport): void {
349
    for (const entry of stmt.names) {
1✔
350
      const binding = entry.alias ?? entry.name;
1✔
351
      this.environment?.declareName(binding);
1✔
352
      this.environment?.moduleBindings.add(binding.lexeme);
1✔
353
    }
354
  }
355

356
  visitContinueStmt(_stmt: StmtNS.Continue): void {}
357
  visitBreakStmt(_stmt: StmtNS.Break): void {}
358
  visitPassStmt(_stmt: StmtNS.Pass): void {}
359

360
  //// EXPRESSIONS
361
  visitVariableExpr(expr: ExprNS.Variable): void {
362
    this.environment?.lookupNameCurrentEnvWithError(expr.name);
869✔
363
  }
364
  visitLambdaExpr(expr: ExprNS.Lambda): void {
365
    // Create a new environment.
366
    const oldEnv = this.environment;
94✔
367
    // Assign the parameters to the new environment.
368
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
97✔
369
    this.environment = new Environment(this.source, this.environment, newEnv);
94✔
370
    this.resolve(expr.body);
94✔
371
    // Restore old environment
372
    this.environment = oldEnv;
94✔
373
  }
374
  visitMultiLambdaExpr(expr: ExprNS.MultiLambda): void {
375
    // Create a new environment.
376
    const oldEnv = this.environment;
×
377
    // Assign the parameters to the new environment.
378
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
×
379
    this.environment = new Environment(this.source, this.environment, newEnv);
×
380
    this.resolve(expr.body);
×
381
    // Grab identifiers from that new environment.
382
    expr.varDecls = Array.from(this.environment.names.values());
×
383
    // Restore old environment
384
    this.environment = oldEnv;
×
385
  }
386
  visitUnaryExpr(expr: ExprNS.Unary): void {
387
    this.resolve(expr.right);
43✔
388
  }
389
  visitGroupingExpr(expr: ExprNS.Grouping): void {
390
    this.resolve(expr.expression);
195✔
391
  }
392
  visitBinaryExpr(expr: ExprNS.Binary): void {
393
    this.resolve(expr.left);
529✔
394
    this.resolve(expr.right);
529✔
395
  }
396
  visitBoolOpExpr(expr: ExprNS.BoolOp): void {
397
    this.resolve(expr.left);
22✔
398
    this.resolve(expr.right);
22✔
399
  }
400
  visitCompareExpr(expr: ExprNS.Compare): void {
401
    this.resolve(expr.left);
308✔
402
    this.resolve(expr.right);
307✔
403
  }
404

405
  visitCallExpr(expr: ExprNS.Call): void {
406
    this.resolve(expr.callee);
596✔
407
    this.resolve(expr.args);
596✔
408
  }
409
  visitStarredExpr(expr: ExprNS.Starred): void {
410
    this.resolve(expr.value);
13✔
411
  }
412
  visitTernaryExpr(expr: ExprNS.Ternary): void {
UNCOV
413
    this.resolve(expr.predicate);
×
UNCOV
414
    this.resolve(expr.consequent);
×
UNCOV
415
    this.resolve(expr.alternative);
×
416
  }
417
  visitNoneExpr(_expr: ExprNS.None): void {}
418
  visitLiteralExpr(_expr: ExprNS.Literal): void {}
419
  visitBigIntLiteralExpr(_expr: ExprNS.BigIntLiteral): void {}
420
  visitComplexExpr(_expr: ExprNS.Complex): void {}
421
  visitListExpr(expr: ExprNS.List): void {
422
    this.resolve(expr.elements);
19✔
423
  }
424
  visitSubscriptExpr(expr: ExprNS.Subscript): void {
425
    this.resolve(expr.value);
3✔
426
    this.resolve(expr.index);
3✔
427
  }
428
}
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