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

source-academy / js-slang / 24834367427

23 Apr 2026 12:09PM UTC coverage: 78.541% (+0.2%) from 78.391%
24834367427

Pull #1893

github

web-flow
Merge ab101147d into 715603479
Pull Request #1893: Error Handling and Stringify Changes

3126 of 4197 branches covered (74.48%)

Branch coverage included in aggregate %.

801 of 975 new or added lines in 76 files covered. (82.15%)

20 existing lines in 11 files now uncovered.

7056 of 8767 relevant lines covered (80.48%)

173930.4 hits per line

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

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

10
export class StepperBlockStatement
11
  extends StepperBaseNode<BlockStatement>
12
  implements BlockStatement
13
{
14
  constructor(
15
    public readonly body: StepperStatement[],
2,204✔
16
    public readonly innerComments?: Comment[] | undefined,
2,204✔
17
    leadingComments?: Comment[] | undefined,
18
    trailingComments?: Comment[] | undefined,
19
    loc?: SourceLocation | null | undefined,
20
    range?: [number, number] | undefined,
21
  ) {
22
    super('BlockStatement', leadingComments, trailingComments, loc, range);
2,204✔
23
  }
24

25
  static create(node: BlockStatement) {
26
    return new StepperBlockStatement(
440✔
27
      node.body.map(node => convert(node)),
521✔
28
      node.innerComments,
29
      node.leadingComments,
30
      node.trailingComments,
31
      node.loc,
32
      node.range,
33
    );
34
  }
35

36
  public override isContractible(redex: RedexInfo): boolean {
37
    return (
238✔
38
      this.body.length === 0 || (this.body.length === 1 && !this.body[0].isContractible(redex))
504✔
39
    );
40
  }
41

42
  public override isOneStepPossible(): boolean {
43
    return true;
1,482✔
44
  }
45

46
  public override contract(
47
    redex: RedexInfo,
48
  ): StepperBlockStatement | StepperStatement | typeof undefinedNode {
49
    if (this.body.length === 0) {
40✔
50
      redex.preRedex = [this];
13✔
51
      redex.postRedex = [];
13✔
52
      return undefinedNode;
13✔
53
    }
54

55
    if (this.body.length === 1) {
27✔
56
      redex.preRedex = [this];
26✔
57
      redex.postRedex = [this.body[0]];
26✔
58
      return this.body[0];
26✔
59
    }
60

61
    throw new InternalRuntimeError('Cannot contract BlockStatement with body length > 1', this);
1✔
62
  }
63

64
  contractEmpty(redex: RedexInfo) {
65
    redex.preRedex = [this];
×
66
    redex.postRedex = [];
×
67
  }
68

69
  public override oneStep(
70
    redex: RedexInfo,
71
  ): StepperBlockStatement | StepperStatement | typeof undefinedNode {
72
    if (this.isContractible(redex)) {
231✔
73
      return this.contract(redex);
37✔
74
    }
75

76
    if (this.body[0].type === 'ReturnStatement') {
194✔
77
      const returnStmt = this.body[0];
12✔
78
      redex.preRedex = [this];
12✔
79
      redex.postRedex = [returnStmt];
12✔
80
      return returnStmt;
12✔
81
    }
82

83
    // reduce the first statement
84
    if (this.body[0].isOneStepPossible(redex)) {
182✔
85
      const firstStatementOneStep = this.body[0].oneStep(redex);
12✔
86
      const afterSubstitutedScope = this.body.slice(1);
12✔
87
      if (firstStatementOneStep === undefinedNode) {
12✔
88
        return new StepperBlockStatement(
7✔
89
          afterSubstitutedScope,
90
          this.innerComments,
91
          this.leadingComments,
92
          this.trailingComments,
93
          this.loc,
94
          this.range,
95
        );
96
      }
97
      return new StepperBlockStatement(
5✔
98
        [firstStatementOneStep as StepperStatement, afterSubstitutedScope].flat(),
99
        this.innerComments,
100
        this.leadingComments,
101
        this.trailingComments,
102
        this.loc,
103
        this.range,
104
      );
105
    }
106

107
    // If the first statement is constant declaration, gracefully handle it!
108
    if (this.body[0].type == 'VariableDeclaration') {
170✔
109
      const declarations = assignMuTerms(this.body[0].declarations);
3✔
110
      const afterSubstitutedScope = this.body
3✔
111
        .slice(1)
112
        .map(current =>
113
          declarations
6✔
114
            .filter(declarator => declarator.init)
6✔
115
            .reduce(
116
              (statement, declarator) =>
117
                statement.substitute(declarator.id, declarator.init!, redex) as StepperStatement,
6✔
118
              current,
119
            ),
120
        );
121
      const substitutedProgram = new StepperBlockStatement(
3✔
122
        afterSubstitutedScope,
123
        this.innerComments,
124
        this.leadingComments,
125
        this.trailingComments,
126
        this.loc,
127
        this.range,
128
      );
129
      redex.preRedex = [this.body[0]];
3✔
130
      redex.postRedex = declarations.map(x => x.id);
3✔
131
      return substitutedProgram;
3✔
132
    }
133

134
    // If the first statement is function declaration, also gracefully handle it!
135
    if (this.body[0].type === 'FunctionDeclaration') {
167!
136
      const arrowFunction = this.body[0].getArrowFunctionExpression();
×
137
      const functionIdentifier = this.body[0].id;
×
138
      const afterSubstitutedScope = this.body
×
139
        .slice(1)
140
        .map(
141
          statement =>
NEW
142
            statement.substitute(functionIdentifier, arrowFunction, redex) as StepperStatement,
×
143
        );
144
      const substitutedProgram = new StepperBlockStatement(
×
145
        afterSubstitutedScope,
146
        this.innerComments,
147
        this.leadingComments,
148
        this.trailingComments,
149
        this.loc,
150
        this.range,
151
      );
152
      redex.preRedex = [this.body[0]];
×
153
      redex.postRedex = afterSubstitutedScope;
×
154
      return substitutedProgram;
×
155
    }
156

157
    const firstValueStatement = this.body[0];
167✔
158
    // After this stage, the first statement is a value statement. Now, proceed until getting the second value statement.
159
    // if the second statement is return statement, remove the first statement
160
    if (this.body.length >= 2 && this.body[1].type == 'ReturnStatement') {
167✔
161
      redex.preRedex = [this.body[0]];
11✔
162
      const afterSubstitutedScope = this.body.slice(1);
11✔
163
      redex.postRedex = [];
11✔
164
      return new StepperBlockStatement(
11✔
165
        afterSubstitutedScope,
166
        this.innerComments,
167
        this.leadingComments,
168
        this.trailingComments,
169
        this.loc,
170
        this.range,
171
      );
172
    }
173

174
    if (this.body.length >= 2 && this.body[1].isOneStepPossible(redex)) {
156✔
175
      const secondStatementOneStep = this.body[1].oneStep(redex);
132✔
176
      const afterSubstitutedScope = this.body.slice(2);
132✔
177
      if (secondStatementOneStep === undefinedNode) {
132✔
178
        return new StepperBlockStatement(
1✔
179
          [firstValueStatement, afterSubstitutedScope].flat(),
180
          this.innerComments,
181
          this.leadingComments,
182
          this.trailingComments,
183
          this.loc,
184
          this.range,
185
        );
186
      }
187
      return new StepperBlockStatement(
131✔
188
        [
189
          firstValueStatement,
190
          secondStatementOneStep as StepperStatement,
191
          afterSubstitutedScope,
192
        ].flat(),
193
        this.innerComments,
194
        this.leadingComments,
195
        this.trailingComments,
196
        this.loc,
197
        this.range,
198
      );
199
    }
200

201
    // If the second statement is constant declaration, gracefully handle it!
202
    if (this.body.length >= 2 && this.body[1].type == 'VariableDeclaration') {
24✔
203
      const declarations = assignMuTerms(this.body[1].declarations);
5✔
204
      const afterSubstitutedScope = this.body
5✔
205
        .slice(2)
206
        .map(current =>
207
          declarations
4✔
208
            .filter(declarator => declarator.init)
4✔
209
            .reduce(
210
              (statement, declarator) =>
211
                statement.substitute(declarator.id, declarator.init!, redex) as StepperStatement,
4✔
212
              current,
213
            ),
214
        );
215
      const substitutedProgram = new StepperBlockStatement(
5✔
216
        [firstValueStatement, afterSubstitutedScope].flat(),
217
        this.innerComments,
218
        this.leadingComments,
219
        this.trailingComments,
220
        this.loc,
221
        this.range,
222
      );
223
      redex.preRedex = [this.body[1]];
5✔
224
      redex.postRedex = declarations.map(x => x.id);
5✔
225
      return substitutedProgram;
5✔
226
    }
227

228
    // If the second statement is function declaration, also gracefully handle it!
229
    if (this.body.length >= 2 && this.body[1].type == 'FunctionDeclaration') {
19!
230
      const arrowFunction = this.body[1].getArrowFunctionExpression();
×
231
      const functionIdentifier = this.body[1].id;
×
232
      const afterSubstitutedScope = this.body
×
233
        .slice(2)
234
        .map(
235
          statement =>
NEW
236
            statement.substitute(functionIdentifier, arrowFunction, redex) as StepperStatement,
×
237
        );
238
      const substitutedProgram = new StepperBlockStatement(
×
239
        [firstValueStatement, afterSubstitutedScope].flat(),
240
        this.innerComments,
241
        this.leadingComments,
242
        this.trailingComments,
243
        this.loc,
244
        this.range,
245
      );
246
      redex.preRedex = [this.body[1]];
×
247
      redex.postRedex = afterSubstitutedScope;
×
248
      return substitutedProgram;
×
249
    }
250

251
    // After this stage, we have two value inducing statement. Remove the first one.
252
    this.body[0].contractEmpty(redex); // update the contracted statement onto redex
19✔
253
    return new StepperBlockStatement(
19✔
254
      this.body.slice(1),
255
      this.innerComments,
256
      this.leadingComments,
257
      this.trailingComments,
258
      this.loc,
259
      this.range,
260
    );
261
  }
262

263
  public override substitute(
264
    id: StepperPattern,
265
    value: StepperExpression,
266
    redex: RedexInfo,
267
    upperBoundName?: string[],
268
  ): StepperBaseNode {
269
    // Alpha renaming
270
    // Check whether should be renamed
271
    // Renaming stage should not be counted as one step.
272
    const valueFreeNames = value.freeNames();
1,533✔
273
    const scopeNames = this.scanAllDeclarationNames();
1,533✔
274
    const repeatedNames = valueFreeNames.filter(name => scopeNames.includes(name));
1,533✔
275
    let protectedNamesSet = new Set([this.allNames(), upperBoundName ?? []].flat());
1,533✔
276
    repeatedNames.forEach(name => protectedNamesSet.delete(name));
1,533✔
277
    const protectedNames = Array.from(protectedNamesSet);
1,533✔
278
    const newNames = getFreshName(repeatedNames, protectedNames);
1,533✔
279

280
    const currentBlockStatement = newNames.reduce(
1,533✔
281
      (current: StepperBlockStatement, name: string, index: number) =>
282
        current.rename(repeatedNames[index], name),
×
283
      this,
284
    );
285

286
    if (currentBlockStatement.scanAllDeclarationNames().includes(id.name)) {
1,533✔
287
      // DO nothing
288
      return currentBlockStatement;
15✔
289
    }
290
    return new StepperBlockStatement(
1,518✔
291
      currentBlockStatement.body.map(
292
        statement => statement.substitute(id, value, redex) as StepperStatement,
1,852✔
293
      ),
294
      this.innerComments,
295
      this.leadingComments,
296
      this.trailingComments,
297
      this.loc,
298
      this.range,
299
    );
300
  }
301

302
  scanAllDeclarationNames(): string[] {
303
    return this.body
5,138✔
304
      .filter(ast => ast.type === 'VariableDeclaration' || ast.type === 'FunctionDeclaration')
6,052✔
305
      .flatMap(ast => {
306
        if (ast.type === 'VariableDeclaration') {
920✔
307
          return ast.declarations.map(ast => ast.id.name);
460✔
308
        } else {
309
          // Function Declaration
310
          return [ast.id.name];
460✔
311
        }
312
      });
313
  }
314

315
  public override freeNames(): string[] {
316
    const names = new Set(this.body.flatMap(ast => ast.freeNames()));
2,288✔
317
    this.scanAllDeclarationNames().forEach(name => names.delete(name));
2,072✔
318
    return Array.from(names);
2,072✔
319
  }
320

321
  public override allNames(): string[] {
322
    return Array.from(new Set(this.body.flatMap(ast => ast.allNames())));
5,594✔
323
  }
324

325
  public override rename(before: string, after: string): StepperBlockStatement {
326
    return new StepperBlockStatement(
33✔
327
      this.body.map(statement => statement.rename(before, after) as StepperStatement),
46✔
328
      this.innerComments,
329
      this.leadingComments,
330
      this.trailingComments,
331
      this.loc,
332
      this.range,
333
    );
334
  }
335
}
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