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

source-academy / js-slang / 24868044425

24 Apr 2026 01:47AM UTC coverage: 78.522% (+0.1%) from 78.391%
24868044425

push

github

web-flow
Error Handling and Stringify Changes (#1893)

* Modify stringify to prioritize toReplString

* Make the extract declarations helper actually work

* Add ability to change loader for source modules

* Add a new option for controlling how Source modules are loaded

* Improve typing for CSE machine

* Add ability to check if modules are loaded with the wrong Source chapter

* Refactor errors to extend from Error class

* Refactor modules errors

* Refactor parser errors

* Refactor cse machine errors

* Mostly fix error handling in the tracer

* Tidy up generator and explainer implementations for tracer

* Remove unnecessary imports and type guards

* Adjust rttc checks to be type guards instead of returning errors

* Adjust miscellanous error changes

* Add eslint rule for useless constructor

* Fix incorrect ordering for checking exceptionerrors

* Minor changes

* Run format

* Run linting

* Override the message property, but also enable noImplicitOverride to prevent accidental overriding

* Add some documentation to some errors

* Add errors to possible imports for modules

* Update getIds helper

* Minor fix to test case

* Run format

* Allow modules to try and load the context of other modules without crashing

* Fix incorrect stdlib name

* Add some more functions to the stdlib list library

* Change to use GeneralRuntimeError for list

* Revert "Change to use GeneralRuntimeError for list"

This reverts commit 642bd99e6.

* Update src/errors/errors.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Add ability to change manifest and docs importers

* Change how external builtins are defined

* Fix typings and list lib overloads

* Miscellanous changes

* Add the Source equality function to stdlib/misc

* Merge from main branch for tracer

* Add handling for the new importers

* Change errors and made redex a local variable

* Improve tracer typing

* Relocate... (continued)

3125 of 4193 branches covered (74.53%)

Branch coverage included in aggregate %.

899 of 1089 new or added lines in 96 files covered. (82.55%)

21 existing lines in 12 files now uncovered.

7031 of 8741 relevant lines covered (80.44%)

185057.72 hits per line

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

85.81
/src/stepper/nodes/Expression/BlockExpression.ts
1
import type { BlockStatement, Comment, SourceLocation } from 'estree';
2
import { type StepperExpression, type StepperPattern, undefinedNode } from '..';
3
import { convert } from '../../generator';
4
import { StepperBaseNode } from '../../interface';
5
import { assignMuTerms, getFreshName } from '../../utils';
6
import type { StepperStatement } from '../Statement';
7
import type { RedexInfo } from '../..';
8
import { InternalRuntimeError } from '../../../errors/base';
9

10
// TODO: add docs, because this is a block expression, not a block statement, and this does not follow official estree spec
11
export class StepperBlockExpression extends StepperBaseNode<BlockStatement> {
12
  constructor(
13
    public readonly body: StepperStatement[],
351✔
14
    public readonly innerComments?: Comment[] | undefined,
351✔
15
    leadingComments?: Comment[] | undefined,
16
    trailingComments?: Comment[] | undefined,
17
    loc?: SourceLocation | null | undefined,
18
    range?: [number, number] | undefined,
19
  ) {
20
    super('BlockStatement', leadingComments, trailingComments, loc, range);
351✔
21
  }
22

23
  static create(node: BlockStatement) {
NEW
24
    return new StepperBlockExpression(node.body.map(node => convert(node)));
×
25
  }
26

27
  public override isContractible(redex: RedexInfo): boolean {
28
    return (
1,118✔
29
      this.body.length === 0 ||
4,246✔
30
      (this.body.length === 1 && !this.body[0].isOneStepPossible(redex)) || // { 1; } -> undefined;
31
      this.body[0].type === 'ReturnStatement'
32
    );
33
  }
34

35
  public override isOneStepPossible(redex: RedexInfo): boolean {
36
    return (
875✔
37
      this.isContractible(redex) || this.body[0].isOneStepPossible(redex) || this.body.length >= 2
1,726✔
38
    );
39
  }
40

41
  public override contract(redex: RedexInfo): StepperExpression | typeof undefinedNode {
42
    if (
50✔
43
      this.body.length === 0 ||
144✔
44
      (this.body.length === 1 && !this.body[0].isOneStepPossible(redex))
45
    ) {
46
      redex.preRedex = [this];
12✔
47
      redex.postRedex = [];
12✔
48
      return undefinedNode;
12✔
49
    }
50

51
    if (this.body[0].type === 'ReturnStatement') {
38!
52
      const returnStmt = this.body[0];
38✔
53
      returnStmt.contract(redex);
38✔
54
      return returnStmt.argument || undefinedNode;
38!
55
    }
NEW
56
    throw new InternalRuntimeError('Cannot contract ineligible BlockExpression', this);
×
57
  }
58

59
  public override oneStep(
60
    redex: RedexInfo,
61
  ): StepperBlockExpression | typeof undefinedNode | StepperExpression {
62
    if (this.isContractible(redex)) {
243✔
63
      return this.contract(redex);
50✔
64
    }
65
    // reduce the first statement
66
    if (this.body[0].isOneStepPossible(redex)) {
193✔
67
      const firstStatementOneStep = this.body[0].oneStep(redex);
151✔
68
      const afterSubstitutedScope = this.body.slice(1);
151✔
69
      if (firstStatementOneStep === undefinedNode) {
151!
70
        return new StepperBlockExpression(
×
71
          afterSubstitutedScope,
72
          this.innerComments,
73
          this.leadingComments,
74
          this.trailingComments,
75
          this.loc,
76
          this.range,
77
        );
78
      }
79
      return new StepperBlockExpression(
151✔
80
        [firstStatementOneStep as StepperStatement, ...afterSubstitutedScope],
81
        this.innerComments,
82
        this.leadingComments,
83
        this.trailingComments,
84
        this.loc,
85
        this.range,
86
      );
87
    }
88

89
    // If the first statement is constant declaration, gracefully handle it!
90
    if (this.body[0].type === 'VariableDeclaration') {
42✔
91
      const declarations = assignMuTerms(this.body[0].declarations);
9✔
92
      const afterSubstitutedScope = this.body
9✔
93
        .slice(1)
94
        .map(current =>
95
          declarations
9✔
96
            .filter(declarator => declarator.init)
9✔
97
            .reduce(
98
              (statement, declarator) =>
99
                statement.substitute(declarator.id, declarator.init!, redex) as StepperStatement,
9✔
100
              current,
101
            ),
102
        );
103
      const substitutedProgram = new StepperBlockExpression(
9✔
104
        afterSubstitutedScope,
105
        this.innerComments,
106
        this.leadingComments,
107
        this.trailingComments,
108
        this.loc,
109
        this.range,
110
      );
111
      redex.preRedex = [this.body[0]];
9✔
112
      redex.postRedex = declarations.map(x => x.id);
9✔
113
      return substitutedProgram;
9✔
114
    }
115

116
    // If the first statement is function declaration, also gracefully handle it!
117
    if (this.body[0].type === 'FunctionDeclaration') {
33✔
118
      const arrowFunction = this.body[0].getArrowFunctionExpression();
20✔
119
      const functionIdentifier = this.body[0].id;
20✔
120
      const afterSubstitutedScope = this.body
20✔
121
        .slice(1)
122
        .map(
123
          statement =>
124
            statement.substitute(functionIdentifier, arrowFunction, redex) as StepperStatement,
21✔
125
        );
126
      const substitutedProgram = new StepperBlockExpression(
20✔
127
        afterSubstitutedScope,
128
        this.innerComments,
129
        this.leadingComments,
130
        this.trailingComments,
131
        this.loc,
132
        this.range,
133
      );
134
      redex.preRedex = [this.body[0]];
20✔
135
      redex.postRedex = afterSubstitutedScope;
20✔
136
      return substitutedProgram;
20✔
137
    }
138

139
    const firstValueStatement = this.body[0];
13✔
140

141
    // After this stage, the first statement is a value statement. Now, proceed until getting the second value statement.
142

143
    // if the second statement is return statement, remove the first statement
144
    if (this.body.length >= 2 && this.body[1].type === 'ReturnStatement') {
13✔
145
      redex.preRedex = [this.body[0]];
2✔
146
      const afterSubstitutedScope = this.body.slice(1);
2✔
147
      redex.postRedex = [];
2✔
148
      return new StepperBlockExpression(
2✔
149
        afterSubstitutedScope,
150
        this.innerComments,
151
        this.leadingComments,
152
        this.trailingComments,
153
        this.loc,
154
        this.range,
155
      );
156
    }
157

158
    if (this.body.length >= 2 && this.body[1].isOneStepPossible(redex)) {
11✔
159
      const secondStatementOneStep = this.body[1].oneStep(redex);
7✔
160
      const afterSubstitutedScope = this.body.slice(2);
7✔
161
      if (secondStatementOneStep === undefinedNode) {
7✔
162
        return new StepperBlockExpression(
1✔
163
          [firstValueStatement, afterSubstitutedScope].flat(),
164
          this.innerComments,
165
          this.leadingComments,
166
          this.trailingComments,
167
          this.loc,
168
          this.range,
169
        );
170
      }
171
      return new StepperBlockExpression(
6✔
172
        [
173
          firstValueStatement,
174
          secondStatementOneStep as StepperStatement,
175
          afterSubstitutedScope,
176
        ].flat(),
177
        this.innerComments,
178
        this.leadingComments,
179
        this.trailingComments,
180
        this.loc,
181
        this.range,
182
      );
183
    }
184

185
    // If the second statement is constant declaration, gracefully handle it!
186
    if (this.body.length >= 2 && this.body[1].type === 'VariableDeclaration') {
4✔
187
      const declarations = assignMuTerms(this.body[1].declarations);
1✔
188
      const afterSubstitutedScope = this.body
1✔
189
        .slice(2)
190
        .map(current =>
191
          declarations
1✔
192
            .filter(declarator => declarator.init)
1✔
193
            .reduce(
194
              (statement, declarator) =>
195
                statement.substitute(declarator.id, declarator.init!, redex) as StepperStatement,
1✔
196
              current,
197
            ),
198
        );
199
      const substitutedProgram = new StepperBlockExpression(
1✔
200
        [firstValueStatement, afterSubstitutedScope].flat(),
201
        this.innerComments,
202
        this.leadingComments,
203
        this.trailingComments,
204
        this.loc,
205
        this.range,
206
      );
207
      redex.preRedex = [this.body[1]];
1✔
208
      redex.postRedex = declarations.map(x => x.id);
1✔
209
      return substitutedProgram;
1✔
210
    }
211

212
    // If the second statement is function declaration, also gracefully handle it!
213
    if (this.body.length >= 2 && this.body[1].type === 'FunctionDeclaration') {
3!
214
      const arrowFunction = this.body[1].getArrowFunctionExpression();
×
215
      const functionIdentifier = this.body[1].id;
×
216
      const afterSubstitutedScope = this.body
×
217
        .slice(2)
218
        .map(
219
          statement =>
NEW
220
            statement.substitute(functionIdentifier, arrowFunction, redex) as StepperStatement,
×
221
        );
222
      const substitutedProgram = new StepperBlockExpression(
×
223
        [firstValueStatement, afterSubstitutedScope].flat(),
224
        this.innerComments,
225
        this.leadingComments,
226
        this.trailingComments,
227
        this.loc,
228
        this.range,
229
      );
230
      redex.preRedex = [this.body[1]];
×
231
      redex.postRedex = afterSubstitutedScope;
×
232
      return substitutedProgram;
×
233
    }
234

235
    // After this stage, we have two value inducing statement. Remove the first one.
236
    this.body[0].contractEmpty(redex); // update the contracted statement onto redex
3✔
237
    return new StepperBlockExpression(
3✔
238
      this.body.slice(1),
239
      this.innerComments,
240
      this.leadingComments,
241
      this.trailingComments,
242
      this.loc,
243
      this.range,
244
    );
245
  }
246

247
  public override substitute(
248
    id: StepperPattern,
249
    value: StepperExpression,
250
    redex: RedexInfo,
251
  ): StepperBlockExpression {
252
    const valueFreeNames = value.freeNames();
111✔
253
    const scopeNames = this.scanAllDeclarationNames();
111✔
254
    const repeatedNames = valueFreeNames.filter(name => scopeNames.includes(name));
111✔
255
    const protectedNamesSet = new Set(this.allNames());
111✔
256
    repeatedNames.forEach(name => protectedNamesSet.delete(name));
111✔
257
    const protectedNames = Array.from(protectedNamesSet);
111✔
258
    const newNames = getFreshName(repeatedNames, protectedNames);
111✔
259

260
    const currentBlockExpression = newNames.reduce(
111✔
261
      (current: StepperBlockExpression, name: string, index: number) =>
262
        current.rename(repeatedNames[index], name),
×
263
      this,
264
    );
265

266
    if (currentBlockExpression.scanAllDeclarationNames().includes(id.name)) {
111✔
267
      return currentBlockExpression;
3✔
268
    }
269
    return new StepperBlockExpression(
108✔
270
      currentBlockExpression.body.map(
271
        statement => statement.substitute(id, value, redex) as StepperStatement,
177✔
272
      ),
273
      currentBlockExpression.innerComments,
274
      currentBlockExpression.leadingComments,
275
      currentBlockExpression.trailingComments,
276
      currentBlockExpression.loc,
277
      currentBlockExpression.range,
278
    );
279
  }
280

281
  scanAllDeclarationNames(): string[] {
282
    return this.body.flatMap(ast => {
222✔
283
      switch (ast.type) {
366✔
284
        case 'VariableDeclaration':
285
          return ast.declarations.map(ast => ast.id.name);
30✔
286
        case 'FunctionDeclaration':
287
          return [ast.id.name];
106✔
288
        default:
289
          return [];
230✔
290
      }
291
    });
292
  }
293

294
  public override freeNames(): string[] {
295
    const names = new Set(this.body.flatMap(ast => ast.freeNames()));
×
296
    this.scanAllDeclarationNames().forEach(name => names.delete(name));
×
297
    return Array.from(names);
×
298
  }
299

300
  public override allNames(): string[] {
301
    return Array.from(new Set(this.body.flatMap(ast => ast.allNames())));
183✔
302
  }
303

304
  public override rename(before: string, after: string): StepperBlockExpression {
305
    return new StepperBlockExpression(
×
306
      this.body.map(statement => statement.rename(before, after) as StepperStatement),
×
307
      this.innerComments,
308
      this.leadingComments,
309
      this.trailingComments,
310
      this.loc,
311
      this.range,
312
    );
313
  }
314
}
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