• 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

90.65
/src/modules/preprocessor/directedGraph.ts
1
import { InternalRuntimeError } from '../../errors/base';
2
import assert from '../../utils/assert';
3

4
/**
5
 * The result of attempting to find a topological ordering
6
 * of nodes on a DirectedGraph.
7
 */
8
export type TopologicalOrderResult =
9
  | {
10
      isValidTopologicalOrderFound: true;
11
      topologicalOrder: string[];
12
      firstCycleFound: null;
13
    }
14
  | {
15
      isValidTopologicalOrderFound: false;
16
      topologicalOrder: null;
17
      firstCycleFound: string[];
18
    };
19

20
/**
21
 * Represents a directed graph which disallows self-loops.
22
 */
23
export class DirectedGraph {
24
  private readonly adjacencyList: Map<string, Set<string>>;
25
  private readonly differentKeysError = new Error(
2,247✔
26
    'The keys of the adjacency list & the in-degree maps are not the same. This should never occur.',
27
  );
28

29
  constructor() {
30
    this.adjacencyList = new Map();
2,247✔
31
  }
32

33
  /**
34
   * Adds a directed edge to the graph from the source node to
35
   * the destination node. Self-loops are not allowed.
36
   *
37
   * @param sourceNode      The name of the source node.
38
   * @param destinationNode The name of the destination node.
39
   */
40
  public addEdge(sourceNode: string, destinationNode: string): void {
41
    if (sourceNode === destinationNode) {
138✔
42
      throw new InternalRuntimeError('Edges that connect a node to itself are not allowed.');
1✔
43
    }
44

45
    const neighbours = this.adjacencyList.get(sourceNode) ?? new Set();
137✔
46
    neighbours.add(destinationNode);
138✔
47
    this.adjacencyList.set(sourceNode, neighbours);
138✔
48

49
    // Create an entry for the destination node if it does not exist
50
    // in the adjacency list. This is so that the set of keys of the
51
    // adjacency list is the same as the set of nodes in the graph.
52
    if (!this.adjacencyList.has(destinationNode)) {
138✔
53
      this.adjacencyList.set(destinationNode, new Set());
100✔
54
    }
55
  }
56

57
  /**
58
   * Returns whether the directed edge from the source node to the
59
   * destination node exists in the graph.
60
   *
61
   * @param sourceNode      The name of the source node.
62
   * @param destinationNode The name of the destination node.
63
   */
64
  public hasEdge(sourceNode: string, destinationNode: string): boolean {
65
    if (sourceNode === destinationNode) {
3!
NEW
66
      throw new InternalRuntimeError('Edges that connect a node to itself are not allowed.');
×
67
    }
68

69
    const neighbours = this.adjacencyList.get(sourceNode) ?? new Set();
3✔
70
    return neighbours.has(destinationNode);
3✔
71
  }
72

73
  /**
74
   * Calculates the in-degree of every node in the directed graph.
75
   *
76
   * The in-degree of a node is the number of edges coming into
77
   * the node.
78
   */
79
  private calculateInDegrees(): Map<string, number> {
80
    const inDegrees = new Map();
2,080✔
81
    for (const neighbours of this.adjacencyList.values()) {
2,080✔
82
      for (const neighbour of neighbours) {
204✔
83
        const inDegree = inDegrees.get(neighbour) ?? 0;
131✔
84
        inDegrees.set(neighbour, inDegree + 1);
131✔
85
      }
86
    }
87
    // Handle nodes which have an in-degree of 0.
88
    for (const node of this.adjacencyList.keys()) {
2,080✔
89
      if (!inDegrees.has(node)) {
204✔
90
        inDegrees.set(node, 0);
77✔
91
      }
92
    }
93
    return inDegrees;
2,080✔
94
  }
95

96
  /**
97
   * Finds a cycle of nodes in the directed graph. This operates on the
98
   * invariant that any nodes left over with a non-zero in-degree after
99
   * Kahn's algorithm has been run is part of a cycle.
100
   *
101
   * @param inDegrees The number of edges coming into each node after
102
   *                  running Kahn's algorithm.
103
   */
104
  private findCycle(inDegrees: Map<string, number>): string[] {
105
    // First, we pick any arbitrary node that is part of a cycle as our
106
    // starting node.
107
    let startingNodeInCycle: string | null = null;
6✔
108
    for (const [node, inDegree] of inDegrees) {
6✔
109
      if (inDegree !== 0) {
6!
110
        startingNodeInCycle = node;
6✔
111
        break;
6✔
112
      }
113
    }
114
    // By the invariant stated above, it is impossible that the starting
115
    // node cannot be found. The lack of a starting node implies that
116
    // all nodes have an in-degree of 0 after running Kahn's algorithm.
117
    // This in turn implies that Kahn's algorithm was able to find a
118
    // valid topological ordering & that the graph contains no cycles.
119
    assert(
6✔
120
      startingNodeInCycle !== null,
121
      'There are no cycles in this graph. This should never happen.',
122
    );
123

124
    const cycle = [startingNodeInCycle];
6✔
125
    // Then, we keep picking arbitrary nodes with non-zero in-degrees until
126
    // we pick a node that has already been picked.
127
    while (true) {
6✔
128
      const currentNode = cycle[cycle.length - 1];
18✔
129

130
      const neighbours = this.adjacencyList.get(currentNode);
18✔
131
      if (neighbours === undefined) {
18!
132
        throw this.differentKeysError;
×
133
      }
134
      // By the invariant stated above, it is impossible that any node
135
      // on the cycle has an in-degree of 0 after running Kahn's algorithm.
136
      // An in-degree of 0 implies that the node is not part of a cycle,
137
      // which is a contradiction since the current node was picked because
138
      // it is part of a cycle.
139
      assert(
18✔
140
        neighbours.size > 0,
141
        `Node '${currentNode}' has no incoming edges. This should never happen.`,
142
      );
143

144
      let nextNodeInCycle: string | null = null;
18✔
145
      for (const neighbour of neighbours) {
18✔
146
        if (inDegrees.get(neighbour) !== 0) {
18!
147
          nextNodeInCycle = neighbour;
18✔
148
          break;
18✔
149
        }
150
      }
151
      // By the invariant stated above, if the current node is part of a cycle,
152
      // then one of its neighbours must also be part of the same cycle. This
153
      // is because a cycle contains at least 2 nodes.
154
      assert(
18✔
155
        nextNodeInCycle !== null,
156
        `None of the neighbours of node '${currentNode}' are part of the same cycle. This should never happen.`,
157
      );
158

159
      // If the next node we pick is already part of the cycle,
160
      // we drop all elements before the first instance of the
161
      // next node and return the cycle.
162
      const nextNodeIndex = cycle.indexOf(nextNodeInCycle);
18✔
163
      const isNodeAlreadyInCycle = nextNodeIndex !== -1;
18✔
164
      cycle.push(nextNodeInCycle);
18✔
165
      if (isNodeAlreadyInCycle) {
18✔
166
        return cycle.slice(nextNodeIndex);
6✔
167
      }
168
    }
169
  }
170

171
  /**
172
   * Returns a topological ordering of the nodes in the directed
173
   * graph if the graph is acyclic. Otherwise, returns null.
174
   *
175
   * To get the topological ordering, Kahn's algorithm is used.
176
   */
177
  public getTopologicalOrder(): TopologicalOrderResult {
178
    let numOfVisitedNodes = 0;
2,080✔
179
    const inDegrees = this.calculateInDegrees();
2,080✔
180
    const topologicalOrder: string[] = [];
2,080✔
181

182
    const queue: string[] = [];
2,080✔
183
    for (const [node, inDegree] of inDegrees) {
2,080✔
184
      if (inDegree === 0) {
204✔
185
        queue.push(node);
77✔
186
      }
187
    }
188

189
    while (true) {
2,080✔
190
      const node = queue.shift();
2,263✔
191
      // 'node' is 'undefined' when the queue is empty.
192
      if (node === undefined) {
2,263✔
193
        break;
2,080✔
194
      }
195

196
      numOfVisitedNodes++;
183✔
197
      topologicalOrder.push(node);
183✔
198

199
      const neighbours = this.adjacencyList.get(node);
183✔
200
      if (neighbours === undefined) {
183!
201
        throw this.differentKeysError;
×
202
      }
203
      for (const neighbour of neighbours) {
183✔
204
        const inDegree = inDegrees.get(neighbour);
108✔
205
        if (inDegree === undefined) {
108!
206
          throw this.differentKeysError;
×
207
        }
208
        inDegrees.set(neighbour, inDegree - 1);
108✔
209

210
        if (inDegrees.get(neighbour) === 0) {
108✔
211
          queue.push(neighbour);
106✔
212
        }
213
      }
214
    }
215

216
    // If not all nodes are visited, then at least one
217
    // cycle exists in the graph and a topological ordering
218
    // cannot be found.
219
    if (numOfVisitedNodes !== this.adjacencyList.size) {
2,080✔
220
      const firstCycleFound = this.findCycle(inDegrees);
6✔
221
      return {
6✔
222
        isValidTopologicalOrderFound: false,
223
        topologicalOrder: null,
224
        firstCycleFound,
225
      };
226
    }
227

228
    return {
2,074✔
229
      isValidTopologicalOrderFound: true,
230
      topologicalOrder,
231
      firstCycleFound: null,
232
    };
233
  }
234
}
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