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

source-academy / py-slang / 23727466431

30 Mar 2026 04:06AM UTC coverage: 59.421% (+18.2%) from 41.233%
23727466431

push

github

web-flow
Python chapter 2 (+ 3?) support (#81)

* feat: introduce chapter-based CSE evaluators + improve type safety of value type + add stubs for LINKED_LIST group

* add Python Chapter 1 back to the Rollup config

* Turned resolver type-checking back on + fix build_linked_list not working

* Fix bug where groups weren't loading + Make builtins more type-safe + Added native list functions (print_linked_list, head, tail)

* my changes

* Add Python chapter 3 functions + Fix syntax of stream prelude + Add list creation instruction + Make parser variant specific + Add basic tests for standard library

* Fix rounding + add tests for linked lists

* Fixed bug regarding representation of strings (printing ["a", "b"] returned [a, b])

* Mark files as unused (for now)

* Fix test cases

* Added variant to parser in WASM compiler

* Fix linked-list prelude using the old terminology of list + Fix build_linked_list(None) returning None instead of list()

* Add ability to check output from testing framework

* Add documentation for TestCases + add the repr function

* Improve alignment with Python specs + add ability to index lists + move variant checking to the resolver + improve test names + add a lot more tests

* Add more tests for operators + restrict the not operator to only booleans + disallow booleans in the unary minus operator

* FIx print_linked_list not actually printing the linked list

* Implement while loops + unpacking with for loops + implement tuples + break and continue

* Added a public is_linked_list function, added more comprehensive test cases for is_pair and is_linked_list

* extended test cases for linked lists

* Updated to have distinction between linkedlist Value and Python List for ease of future implementation of python list

* Revert "Updated to have distinction between linkedlist Value and Python List for ease of future implementation of python list"

This reverts commit d444793b1.

* fixed and/or operator bu... (continued)

779 of 1553 branches covered (50.16%)

Branch coverage included in aggregate %.

442 of 651 new or added lines in 32 files covered. (67.9%)

17 existing lines in 4 files now uncovered.

2400 of 3797 relevant lines covered (63.21%)

3195.38 hits per line

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

73.27
/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;
1,918✔
30
    this.enclosing = enclosing;
1,918✔
31
    this.names = names;
1,918✔
32
    this.functions = new Set();
1,918✔
33
    this.moduleBindings = new Set();
1,918✔
34
    this.definedNames = new Set();
1,918✔
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;
559✔
44
    let distance = 0;
559✔
45
    // eslint-disable-next-line @typescript-eslint/no-this-alias
46
    let curr: Environment | null = this;
559✔
47
    while (curr !== null) {
559✔
48
      if (curr.names.has(name)) {
971✔
49
        break;
553✔
50
      }
51
      distance += 1;
418✔
52
      curr = curr.enclosing;
418✔
53
    }
54
    return curr === null ? -1 : distance;
559✔
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);
34✔
60
  }
61
  lookupNameCurrentEnvWithError(identifier: Token) {
62
    if (this.lookupName(identifier) < 0) {
559✔
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);
94✔
90
    this.definedNames.add(identifier.lexeme);
94✔
91
  }
92
  // Same as declareName but allowed to re-declare later.
93
  declarePlaceholderName(identifier: Token) {
94
    const lookup = this.lookupNameCurrentEnv(identifier);
34✔
95
    if (lookup !== undefined) {
34!
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);
34✔
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);
466✔
133
        if (dist < minDistance) {
466✔
134
          minDistance = dist;
11✔
135
          minName = declName;
11✔
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;
901✔
162
    this.ast = ast;
901✔
163
    this.source = source;
901✔
164
    this.ast = ast;
901✔
165
    this.validators = validators;
901✔
166
    // The global environment
167
    this.environment = new Environment(
901✔
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],
63,971✔
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],
4,505✔
178
        ),
179
        ...groups.flatMap(group =>
180
          Array.from(group.builtins.entries()).map(
313✔
181
            ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
1,202✔
182
          ),
183
        ),
184
        ...preludeNames.map(
185
          name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
5,227✔
186
        ),
187
      ]),
188
    );
189
    this.functionScope = null;
901✔
190
  }
191

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

196
  resolve(stmt: Stmt[] | Stmt | Expr[] | Expr | null) {
197
    if (stmt === null) {
5,602!
198
      return;
×
199
    }
200
    if (stmt instanceof Array) {
5,602✔
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,350✔
204
        if (st instanceof StmtNS.FunctionDef) {
1,727✔
205
          this.environment?.declarePlaceholderName(st.name);
34✔
206
        }
207
      }
208
      for (const st of stmt) {
1,350✔
209
        this.runValidators(st);
1,721✔
210
        st.accept(this);
1,691✔
211
      }
212
    } else {
213
      this.runValidators(stmt);
4,252✔
214
      stmt.accept(this);
4,241✔
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) {
57✔
232
      return;
51✔
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;
901✔
258
    this.environment = new Environment(this.source, this.environment, new Map());
901✔
259
    this.resolve(stmt.statements);
901✔
260
    // Grab identifiers from that new environment. That are NOT functions.
261
    // stmt.varDecls = this.varDeclNames(this.environment.names)
262
    this.environment = oldEnv;
854✔
263
  }
264

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

269
    // Create a new environment.
270
    const oldEnv = this.environment;
32✔
271
    // Assign the parameters to the new environment.
272
    const newEnv = new Map(stmt.parameters.map(param => [param.lexeme, param]));
32✔
273
    this.environment = new Environment(this.source, this.environment, newEnv);
32✔
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;
32✔
281
    this.resolve(stmt.body);
32✔
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;
24✔
286
    this.environment = oldEnv;
24✔
287
  }
288

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

296
  visitAssignStmt(stmt: StmtNS.Assign): void {
297
    const target = stmt.target;
68✔
298
    if (target instanceof ExprNS.Subscript) {
68✔
299
      this.resolve(target); // dispatches to visitSubscriptExpr
3✔
300
      this.resolve(stmt.value);
2✔
301
      return;
2✔
302
    }
303
    this.resolve(stmt.value);
65✔
304
    this.functionVarConstraint(target.name);
57✔
305
    this.environment?.declareName(target.name);
57✔
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) {
13✔
336
      this.resolve(stmt.value);
13✔
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);
847✔
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);
559✔
363
  }
364
  visitLambdaExpr(expr: ExprNS.Lambda): void {
365
    // Create a new environment.
366
    const oldEnv = this.environment;
84✔
367
    // Assign the parameters to the new environment.
368
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
86✔
369
    this.environment = new Environment(this.source, this.environment, newEnv);
84✔
370
    this.resolve(expr.body);
84✔
371
    // Restore old environment
372
    this.environment = oldEnv;
84✔
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);
37✔
388
  }
389
  visitGroupingExpr(expr: ExprNS.Grouping): void {
390
    this.resolve(expr.expression);
194✔
391
  }
392
  visitBinaryExpr(expr: ExprNS.Binary): void {
393
    this.resolve(expr.left);
520✔
394
    this.resolve(expr.right);
520✔
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);
394✔
407
    this.resolve(expr.args);
394✔
408
  }
409
  visitTernaryExpr(expr: ExprNS.Ternary): void {
410
    this.resolve(expr.predicate);
×
411
    this.resolve(expr.consequent);
×
412
    this.resolve(expr.alternative);
×
413
  }
414
  visitNoneExpr(_expr: ExprNS.None): void {}
415
  visitLiteralExpr(_expr: ExprNS.Literal): void {}
416
  visitBigIntLiteralExpr(_expr: ExprNS.BigIntLiteral): void {}
417
  visitComplexExpr(_expr: ExprNS.Complex): void {}
418
  visitListExpr(expr: ExprNS.List): void {
419
    this.resolve(expr.elements);
16✔
420
  }
421
  visitSubscriptExpr(expr: ExprNS.Subscript): void {
422
    this.resolve(expr.value);
3✔
423
    this.resolve(expr.index);
3✔
424
  }
425
}
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