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

source-academy / py-slang / 24284794110

11 Apr 2026 02:43PM UTC coverage: 69.576% (+2.7%) from 66.866%
24284794110

push

github

web-flow
Add SVML engine (#139)

* add svml interpreter

* add sinter backend

* fix Svml sinter run

* fix formatting

* fix review comments

* modify CI to --all

* fix lint

* remove newline

* fix coveralls type declaration search

* Tighten test suite

* style: format

* add tsconfig.test.json

* add parallel build for all

* refine test suite

* fix sinter

* appendum to sinter

* style:format

* full test suite, similar to stdlib.ts

* remove dead memoization machinery from SVML interpreter

isMemoized was always false and memoCache never populated.
Removes SVMLClosure.isMemoized/memoCache, CallFrame.memoArgs/memoClosure,
and all cache check/store logic in call/return paths.

1399 of 2343 branches covered (59.71%)

Branch coverage included in aggregate %.

1278 of 1550 new or added lines in 14 files covered. (82.45%)

4 existing lines in 4 files now uncovered.

4396 of 5986 relevant lines covered (73.44%)

5263.56 hits per line

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

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

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

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

19
const RedefineableTokenSentinel = new Token(TokenType.AT, "", 0, 0, 0);
8✔
20

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

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

62
  /**
63
   * Looks up the name in the environment chain.
64
   * Returns the Environment where the name is found, or null if not found.
65
   */
66
  lookupNameEnv(identifier: Token): Environment | null {
67
    if (this.names.has(identifier.lexeme)) {
560✔
68
      return this;
473✔
69
    }
70
    for (let curr = this.enclosing; curr !== null; curr = curr.enclosing) {
87✔
71
      if (curr.names.has(identifier.lexeme)) {
89✔
72
        return curr;
87✔
73
      }
74
    }
NEW
75
    return null;
×
76
  }
77

78
  /* Looks up the name but only for the current environment. */
79
  lookupNameCurrentEnv(identifier: Token): Token | undefined {
80
    return this.names.get(identifier.lexeme);
125✔
81
  }
82
  lookupNameCurrentEnvWithError(identifier: Token) {
83
    if (this.lookupName(identifier) < 0) {
1,665✔
84
      throw new ResolverErrors.NameNotFoundError(
12✔
85
        identifier.line,
86
        identifier.col,
87
        this.source,
88
        identifier.indexInSource,
89
        identifier.indexInSource + identifier.lexeme.length,
90
        this.suggestName(identifier),
91
      );
92
    }
93
  }
94
  lookupNameParentEnvWithError(identifier: Token) {
95
    const name = identifier.lexeme;
3✔
96
    const parent = this.enclosing;
3✔
97

98
    if (parent === null || !parent.names.has(name)) {
3!
99
      throw new ResolverErrors.NameNotFoundError(
×
100
        identifier.line,
101
        identifier.col,
102
        this.source,
103
        identifier.indexInSource,
104
        identifier.indexInSource + name.length,
105
        this.suggestName(identifier),
106
      );
107
    }
108
  }
109
  declareName(identifier: Token) {
110
    this.names.set(identifier.lexeme, identifier);
376✔
111
    this.definedNames.add(identifier.lexeme);
376✔
112
  }
113
  // Same as declareName but allowed to re-declare later.
114
  declarePlaceholderName(identifier: Token) {
115
    const lookup = this.lookupNameCurrentEnv(identifier);
125✔
116
    if (lookup !== undefined) {
125!
117
      throw new ResolverErrors.NameReassignmentError(
×
118
        identifier.line,
119
        identifier.col,
120
        this.source,
121
        identifier.indexInSource,
122
        identifier.indexInSource + identifier.lexeme.length,
123
        lookup,
124
      );
125
    }
126
    this.names.set(identifier.lexeme, RedefineableTokenSentinel);
125✔
127
  }
128
  suggestNameCurrentEnv(identifier: Token): string | null {
129
    const name = identifier.lexeme;
×
130
    let minDistance = Infinity;
×
131
    let minName = null;
×
132
    for (const declName of this.names.keys()) {
×
133
      const dist = levenshtein.get(name, declName);
×
134
      if (dist < minDistance) {
×
135
        minDistance = dist;
×
136
        minName = declName;
×
137
      }
138
    }
139
    return minName;
×
140
  }
141
  /*
142
   * Finds name closest to name in all environments up to builtin environment.
143
   * Calculated using min levenshtein distance.
144
   * */
145
  suggestName(identifier: Token): string | null {
146
    const name = identifier.lexeme;
12✔
147
    let minDistance = Infinity;
12✔
148
    let minName = null;
12✔
149
    // eslint-disable-next-line @typescript-eslint/no-this-alias
150
    let curr: Environment | null = this;
12✔
151
    while (curr !== null) {
12✔
152
      for (const declName of curr.names.keys()) {
26✔
153
        const dist = levenshtein.get(name, declName);
977✔
154
        if (dist < minDistance) {
977✔
155
          minDistance = dist;
23✔
156
          minName = declName;
23✔
157
        }
158
      }
159
      curr = curr.enclosing;
26✔
160
    }
161
    if (minDistance >= 4) {
12✔
162
      // This is pretty far, so just return null
163
      return null;
3✔
164
    }
165
    return minName;
9✔
166
  }
167
}
168
export class Resolver implements StmtNS.Visitor<void>, ExprNS.Visitor<void> {
8✔
169
  source: string;
170
  ast: Stmt;
171
  environment: Environment | null;
172
  functionScope: Environment | null;
173
  errors: Error[];
174
  functionEnvironments: FunctionEnvironments;
175
  private validators: FeatureValidator[];
176

177
  constructor(
178
    source: string,
179
    ast: Stmt,
180
    validators: FeatureValidator[] = [],
165✔
181
    groups: Group[] = [],
199✔
182
    preludeNames: string[] = [],
199✔
183
  ) {
184
    this.source = source;
1,413✔
185
    this.ast = ast;
1,413✔
186
    this.source = source;
1,413✔
187
    this.ast = ast;
1,413✔
188
    this.validators = validators;
1,413✔
189
    this.errors = [];
1,413✔
190
    this.functionEnvironments = new Map();
1,413✔
191
    // The global environment
192
    this.environment = new Environment(
1,413✔
193
      source,
194
      null,
195
      new Map([
196
        // misc library
197
        ...constants.builtInFuncs.map(
198
          (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
105,975✔
199
        ),
200
        ["range", new Token(TokenType.NAME, "range", 0, 0, 0)],
201
        ...constants.constants.map(
202
          (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
7,065✔
203
        ),
204
        ...groups.flatMap(group =>
205
          Array.from(group.builtins.entries()).map(
828✔
206
            ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
2,645✔
207
          ),
208
        ),
209
        ...preludeNames.map(
210
          name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token],
10,084✔
211
        ),
212
      ]),
213
    );
214
    this.functionScope = null;
1,413✔
215
  }
216

217
  resolveEnvironments(program: StmtNS.FileInput): FunctionEnvironments {
218
    this.resolve(program);
165✔
219
    return this.functionEnvironments;
165✔
220
  }
221

222
  private runValidators(node: StmtNS.Stmt | ExprNS.Expr): void {
223
    try {
10,247✔
224
      for (const v of this.validators) v.validate(node, this.environment ?? undefined);
63,502!
225
    } catch (e) {
226
      if (e instanceof Error) {
54✔
227
        this.errors.push(e);
54✔
228
        return;
54✔
229
      }
230
      throw e;
×
231
    }
232
  }
233

234
  resolve(stmt: Stmt[] | Stmt | Expr[] | Expr | null): Error[] {
235
    if (stmt === null) {
9,559✔
236
      return this.errors;
22✔
237
    }
238
    if (stmt instanceof Array) {
9,537✔
239
      // Resolve all top-level functions first. Python allows functions declared after
240
      // another function to be used in that function.
241
      for (const st of stmt) {
2,763✔
242
        if (st instanceof StmtNS.FunctionDef) {
3,473✔
243
          try {
125✔
244
            this.environment?.declarePlaceholderName(st.name);
125✔
245
          } catch (e) {
246
            if (e instanceof Error) {
×
247
              this.errors.push(e);
×
248
              continue;
×
249
            }
250
            throw e;
×
251
          }
252
        }
253
      }
254
      for (const st of stmt) {
2,763✔
255
        this.runValidators(st);
3,473✔
256
        st.accept(this);
3,473✔
257
      }
258
    } else {
259
      this.runValidators(stmt);
6,774✔
260
      stmt.accept(this);
6,774✔
261
    }
262
    return this.errors;
9,537✔
263
  }
264

265
  varDeclNames(names: Map<string, Token>): Token[] | null {
266
    const res = Array.from(names.values()).filter(
×
267
      name =>
268
        // Filter out functions and module bindings.
269
        // Those will be handled separately, so they don't
270
        // need to be hoisted.
271
        !this.environment?.functions.has(name.lexeme) &&
×
272
        !this.environment?.moduleBindings.has(name.lexeme),
273
    );
274
    return res.length === 0 ? null : res;
×
275
  }
276

277
  functionVarConstraint(identifier: Token): void {
278
    if (this.functionScope == null) {
205✔
279
      return;
195✔
280
    }
281
    let curr = this.environment;
10✔
282
    while (curr !== this.functionScope) {
10✔
283
      if (curr !== null && curr.names.has(identifier.lexeme)) {
×
284
        const token = curr.names.get(identifier.lexeme);
×
285
        if (token === undefined) {
×
286
          this.errors.push(new Error("placeholder error"));
×
287
          return;
×
288
        }
289

290
        this.errors.push(
×
291
          new ResolverErrors.NameReassignmentError(
292
            identifier.line,
293
            identifier.col,
294
            this.source,
295
            identifier.indexInSource,
296
            identifier.indexInSource + identifier.lexeme.length,
297
            token,
298
          ),
299
        );
300
        return;
×
301
      }
302
      curr = curr?.enclosing ?? null;
×
303
    }
304
  }
305

306
  //// STATEMENTS
307
  visitFileInputStmt(stmt: StmtNS.FileInput): void {
308
    // Create a new environment.
309
    const oldEnv = this.environment;
1,413✔
310
    this.environment = new Environment(this.source, this.environment, new Map());
1,413✔
311
    this.functionEnvironments.set(stmt, this.environment);
1,413✔
312
    this.resolve(stmt.statements);
1,413✔
313
    // Grab identifiers from that new environment. That are NOT functions.
314
    // stmt.varDecls = this.varDeclNames(this.environment.names)
315
    this.environment = oldEnv;
1,413✔
316
  }
317

318
  visitFunctionDefStmt(stmt: StmtNS.FunctionDef) {
319
    this.environment?.declareName(stmt.name);
125✔
320
    this.environment?.functions.add(stmt.name.lexeme);
125✔
321

322
    // Create a new environment.
323
    const oldEnv = this.environment;
125✔
324
    // Assign the parameters to the new environment.
325
    const newEnv = new Map(stmt.parameters.map(param => [param.lexeme, param]));
125✔
326
    this.environment = new Environment(this.source, this.environment, newEnv);
125✔
327
    this.functionEnvironments.set(stmt, this.environment);
125✔
328
    this.functionScope = this.environment;
125✔
329
    this.resolve(stmt.body);
125✔
330
    // Grab identifiers from that new environment. That are NOT functions.
331
    // stmt.varDecls = this.varDeclNames(this.environment.names)
332
    // Restore old environment
333
    this.functionScope = null;
125✔
334
    this.environment = oldEnv;
125✔
335
  }
336

337
  visitAnnAssignStmt(stmt: StmtNS.AnnAssign): void {
338
    this.resolve(stmt.ann);
6✔
339
    this.resolve(stmt.value);
6✔
340
    this.functionVarConstraint(stmt.target.name);
6✔
341
    this.environment?.declareName(stmt.target.name);
6✔
342
  }
343

344
  visitAssignStmt(stmt: StmtNS.Assign): void {
345
    const target = stmt.target;
204✔
346
    if (target instanceof ExprNS.Subscript) {
204✔
347
      this.resolve(target); // dispatches to visitSubscriptExpr
5✔
348
      this.resolve(stmt.value);
5✔
349
      return;
5✔
350
    }
351
    this.resolve(stmt.value);
199✔
352
    this.functionVarConstraint(target.name);
199✔
353
    this.environment?.declareName(target.name);
199✔
354
  }
355

356
  visitAssertStmt(stmt: StmtNS.Assert): void {
357
    this.resolve(stmt.value);
×
358
  }
359
  visitForStmt(stmt: StmtNS.For): void {
360
    this.environment?.declareName(stmt.target);
45✔
361
    this.resolve(stmt.iter);
45✔
362
    this.resolve(stmt.body);
45✔
363
  }
364

365
  visitIfStmt(stmt: StmtNS.If): void {
366
    this.resolve(stmt.condition);
85✔
367
    this.resolve(stmt.body);
85✔
368
    this.resolve(stmt.elseBlock);
85✔
369
  }
370
  // @TODO we need to treat all global statements as variable declarations in the global
371
  // scope.
372
  visitGlobalStmt(_stmt: StmtNS.Global): void {
373
    // Do nothing because global can also be declared in our
374
    // own scope.
375
  }
376
  // @TODO nonlocals mean that any variable following that name in the current env
377
  // should not create a variable declaration, but instead point to an outer variable.
378
  visitNonLocalStmt(stmt: StmtNS.NonLocal): void {
379
    try {
3✔
380
      this.environment?.lookupNameParentEnvWithError(stmt.name);
3✔
381
    } catch (e) {
382
      if (e instanceof Error) {
×
383
        this.errors.push(e);
×
384
        return;
×
385
      }
386
      throw e;
×
387
    }
388
  }
389

390
  visitReturnStmt(stmt: StmtNS.Return): void {
391
    if (stmt.value !== null) {
133✔
392
      this.resolve(stmt.value);
131✔
393
    }
394
  }
395

396
  visitWhileStmt(stmt: StmtNS.While): void {
397
    this.resolve(stmt.condition);
12✔
398
    this.resolve(stmt.body);
12✔
399
  }
400
  visitSimpleExprStmt(stmt: StmtNS.SimpleExpr): void {
401
    this.resolve(stmt.expression);
1,366✔
402
  }
403

404
  visitFromImportStmt(stmt: StmtNS.FromImport): void {
405
    for (const entry of stmt.names) {
1✔
406
      const binding = entry.alias ?? entry.name;
1✔
407
      this.environment?.declareName(binding);
1✔
408
      this.environment?.moduleBindings.add(binding.lexeme);
1✔
409
    }
410
  }
411

412
  visitContinueStmt(_stmt: StmtNS.Continue): void {}
413
  visitBreakStmt(_stmt: StmtNS.Break): void {}
414
  visitPassStmt(_stmt: StmtNS.Pass): void {}
415

416
  //// EXPRESSIONS
417
  visitVariableExpr(expr: ExprNS.Variable): void {
418
    try {
1,583✔
419
      this.environment?.lookupNameCurrentEnvWithError(expr.name);
1,583✔
420
    } catch (e) {
421
      if (e instanceof Error) {
12✔
422
        this.errors.push(e);
12✔
423
        return;
12✔
424
      }
425
      throw e;
×
426
    }
427
  }
428
  visitLambdaExpr(expr: ExprNS.Lambda): void {
429
    // Create a new environment.
430
    const oldEnv = this.environment;
107✔
431
    // Assign the parameters to the new environment.
432
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
113✔
433
    this.environment = new Environment(this.source, this.environment, newEnv);
107✔
434
    this.functionEnvironments.set(expr, this.environment);
107✔
435
    this.resolve(expr.body);
107✔
436
    // Restore old environment
437
    this.environment = oldEnv;
107✔
438
  }
439
  visitMultiLambdaExpr(expr: ExprNS.MultiLambda): void {
440
    // Create a new environment.
441
    const oldEnv = this.environment;
×
442
    // Assign the parameters to the new environment.
443
    const newEnv = new Map(expr.parameters.map(param => [param.lexeme, param]));
×
444
    this.environment = new Environment(this.source, this.environment, newEnv);
×
NEW
445
    this.functionEnvironments.set(expr, this.environment);
×
UNCOV
446
    this.resolve(expr.body);
×
447
    // Grab identifiers from that new environment.
448
    expr.varDecls = Array.from(this.environment.names.values());
×
449
    // Restore old environment
450
    this.environment = oldEnv;
×
451
  }
452
  visitUnaryExpr(expr: ExprNS.Unary): void {
453
    this.resolve(expr.right);
62✔
454
  }
455
  visitGroupingExpr(expr: ExprNS.Grouping): void {
456
    this.resolve(expr.expression);
199✔
457
  }
458
  visitBinaryExpr(expr: ExprNS.Binary): void {
459
    this.resolve(expr.left);
638✔
460
    this.resolve(expr.right);
638✔
461
  }
462
  visitBoolOpExpr(expr: ExprNS.BoolOp): void {
463
    this.resolve(expr.left);
30✔
464
    this.resolve(expr.right);
30✔
465
  }
466
  visitCompareExpr(expr: ExprNS.Compare): void {
467
    this.resolve(expr.left);
376✔
468
    this.resolve(expr.right);
376✔
469
  }
470

471
  visitCallExpr(expr: ExprNS.Call): void {
472
    this.resolve(expr.callee);
974✔
473
    this.resolve(expr.args);
974✔
474
  }
475
  visitStarredExpr(expr: ExprNS.Starred): void {
476
    this.resolve(expr.value);
27✔
477
  }
478
  visitTernaryExpr(expr: ExprNS.Ternary): void {
479
    this.resolve(expr.predicate);
4✔
480
    this.resolve(expr.consequent);
4✔
481
    this.resolve(expr.alternative);
4✔
482
  }
483
  visitNoneExpr(_expr: ExprNS.None): void {}
484
  visitLiteralExpr(_expr: ExprNS.Literal): void {}
485
  visitBigIntLiteralExpr(_expr: ExprNS.BigIntLiteral): void {}
486
  visitComplexExpr(_expr: ExprNS.Complex): void {}
487
  visitListExpr(expr: ExprNS.List): void {
488
    this.resolve(expr.elements);
46✔
489
  }
490
  visitSubscriptExpr(expr: ExprNS.Subscript): void {
491
    this.resolve(expr.value);
16✔
492
    this.resolve(expr.index);
16✔
493
  }
494
}
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