Hoi hoi!
I'm @nyaomaru, a frontend engineer who was recently knocked off my feet by how good the zeetong (sole) from a Dutch fish shop was. ππΈ
In the previous article, I explained how DSA View View takes the TypeScript written by a user, analyzes the function or class signature, and generates a Verification form.
This time, we are entering the heart of DSA View View: the Runtime and Visualization layers.
If you call an ordinary JavaScript function, the direct result you get back is usually the final return value.
But when I am studying DSA, that is not the part I really want to see.
I want to know:
- What happened in the middle of the loop?
- When did the pointer move?
- At which exact moment did the variable change?
- How deep did the recursion go?
In other words, I want to see the journey to the answer, not only the answer itself.
JavaScript, unfortunately, does not provide an API that accepts:
"Excuse me, could you move the current line one step backward?"
So how does DSA View View rewind execution?
Here is the answer up front. πΈ
Before execution, DSA View View rewrites the code into a version that can record its intermediate state, then runs it until it completes or reaches a configured limit.
The UI then plays those recorded states forward and backward like security-camera footage.
In this article, we will look at that record-and-replay system through:
- AST transformation with Babel
- Injecting
recordStepcalls - Preserving the original meaning of the code
-
ExecutionStepand snapshot patches - Rewinding and jumping to variable changes
- Running code inside a Web Worker
- Detecting the appropriate Visualization
- Tests that protect the AST transformation
The source code is available here π
nyaomaru
/
dsa-view-view
DSA View View allows you to understand DSA to see the data flow. ππ Of course, it's free.
DSA View View
DSA View View turns TypeScript algorithm functions into step-by-step visual stories. ππ
Write code, run it with structured inputs, and see the arrays, matrices trees, lists, stacks, pointers, and return values move as the function executes.
It is built for those moments when reading the code is not enough and you want to view why the answer changes.
Why Try It?
-
π§ Step through real TypeScript
Paste or edit a function, validate it, then run the exact code in the browser. -
π§© Views that match the data
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view. -
π³ DSA-friendly inputs out of the box
TreeNode,ListNode,MinHeap,MaxHeap,PriorityQueue, nested arrays matrices, strings, numbers, and class-style inputs are supported without ceremony. -
π 39 built-in examples
Search by name, browseβ¦
All right, letβs open the Runtime and see what Mr. View has been watching. ππ
π The Big Picture: Turning Execution into an Array of Steps
The Runtime pipeline looks roughly like this.
User TypeScript
β
Prepare common DSA classes
β
Parse the code into an AST with Babel Parser
β
Visitors inject recordStep(...)
β
Transform TypeScript into JavaScript with Babel
β
Execute it with new Function inside a Web Worker
β
Generate ExecutionStep[]
β
React moves currentStep forward and backward
β
Display the Visualization that matches the trace
The most important design decision here is that the userβs code is not connected directly to a React component.
Instead, ExecutionStep[] acts as a shared recording format.
That lets arrays, pointers, recursion, trees, heaps, and other structures all live on the same timeline.
Letβs walk through how that works.
π Runtime: Planting Observation Points in the Code
This is the heart of DSA View View. π
The Runtime needs intermediate states, so it does not execute the userβs code exactly as written.
First, Babel transforms the AST and injects calls to recordStep.
Injecting recordStep into the AST
Suppose the user writes.
let left = 0;
while (left < right) {
left++;
}
DSA View View parses it into an AST and transforms it into something roughly like this.
let left = 0;
recordStep("variable-declaration", 1, "let left = 0", { left });
while (left < right) {
recordStep("loop-iteration", 3, "while (left < right)", { left, right });
left++;
recordStep("assignment", 4, "left++", { left, right });
}
The second argument to
recordStepis the original source line number, not the execution-step number.
Because line 2 is empty, the recorded lines are1,3, and4.
In the real implementation, dedicated visitors handle AST nodes such as:
FunctionDeclarationFunctionExpressionArrowFunctionExpressionClassMethodVariableDeclarationAssignmentExpressionUpdateExpression-
CallExpressionnodes that mutate arrays -
for/while/for...of/for...in ReturnStatement
A step can therefore be recorded when:
- A function is entered
- A variable is declared
- A loop completes an iteration
- A value is assigned
- An array is mutated
- A function returns
Current limitation
callback bodies inside methods such assort,reduce,map,filter, andforEachare intentionally excluded from instrumentation for now.The goal is not to trace every corner of JavaScript immediately. I am starting with patterns that are useful for DSA traces and can be instrumented without breaking the original behavior.
Injecting the Code Is Easier Than Not Breaking It
The hardest part of the AST transformation was not calling recordStep.
The hard part was making sure that,
after adding the recording logic, every original expression is still evaluated the same number of times and still returns the same value.
For example, if a return expression were evaluated once for recording and once for the actual return, its side effects would run twice.
return someFunction();
So the transformed code first stores the value in a temporary variable.
const algorithmVisualizerReturnValue = someFunction();
recordStep("return", line, description, {
"return value": algorithmVisualizerReturnValue,
"return location": returnLocation,
});
return algorithmVisualizerReturnValue;
Assignment expressions are another fun little trap.
Consider this.
const result = (arr[i++] = 7);
The transformed code must preserve all three facts:
-
i++runs exactly once - The pre-increment value of
iis used as the array index - The assignment expression itself evaluates to
7
DSA View View wraps the assignment in a synthetic arrow function, stores the result in a temporary variable, records the step, and returns the same value.
Using an arrow function also avoids accidentally stealing the surrounding this or arguments.
AST transformation cannot be "close enough".
If adding more visible steps changes the answer, the visualizer has somehow become the least trustworthy character in the story.
Not ideal.
ποΈ ExecutionStep: Making Execution Replayable
The Runtime records each observed state as an ExecutionStep.
A shortened version of the real type looks like this.
type ExecutionStep = {
stepNumber: number;
type: ExecutionStepType;
line: number;
column?: number;
description: string;
variables: Record<string, unknown>;
timestamp: number;
callStack?: string[];
scope?: string;
metadata?: {
loopIteration?: number;
conditionResult?: boolean;
functionName?: string;
heapTrace?: HeapTraceSnapshot;
};
};
The Runtime currently generates steps including:
function-callfunction-entryvariable-declarationassignmentarray-mutationloop-iterationreturn
line and description are used in the timeline.
variables provides the data needed to reconstruct the UI at that moment.
Recursive traces also use callStack, while prepared heap implementations can attach metadata.heapTrace.
Saving References Would Rewrite the Past
Suppose an array reference were stored directly inside a step.
const stack = [0, 1];
recordStep(/* save stack */);
stack.pop();
If the recorded step keeps the same reference, the earlier state will also appear as [0] after pop() runs.
That is not a historical record.
That is the past quietly editing itself.
So values passed to recordStep are deep-cloned at that moment.
The Runtime normally uses structuredClone. When it encounters values that need different handling, it falls back to a cloning implementation that supports cycles, Map, Set, Date, RegExp, and other cases.
The "Delta Snapshot" Is Really More Like a Patch
The implementation contains a name called variableDeltas, but this deserves a more precise explanation.
The current Runtime does not compare every value with the previous state and store only the keys that truly changed.
Instead:
- The first step stores the initial state, including the inputs
- Later steps deep-clone the visible variables passed to that
recordStep - When a full snapshot is needed, the patches are merged in order
- Reconstructed snapshots are cached
So, strictly speaking, these are closer to per-step patches than minimal deltas.
Step 0 patch: { left: 0, right: 5 }
Step 1 patch: { left: 1 }
Step 2 patch: { right: 4 }
Reconstructing them produces.
Step 0 snapshot: { left: 0, right: 5 }
Step 1 snapshot: { left: 1, right: 5 }
Step 2 snapshot: { left: 1, right: 4 }
The variables property of each ExecutionStep is a getter.
It starts from a previously cached snapshot and merges only the patches needed to reach the requested step.
There is one more implementation detail worth mentioning.
The Runtime sends state from the Worker to the UI with postMessage. During that structured clone, reading the getter also materializes the snapshot.
So the data does not remain a tiny delta forever and magically arrive on the main thread in that form.
The design mainly reduces eager reconstruction and cloning while the execution is being recorded. π½οΈ
Why Can the Timeline Move Backward?
DSA View View is not reversing JavaScript while it runs.
Inside the Web Worker, it first rewrites the code into an instrumented version and records the execution.
During playback, the main value that changes is currentStep.
type ExecutionState = {
currentStep: number;
totalSteps: number;
steps: ExecutionStep[];
isComplete: boolean;
returnValue?: unknown;
error?: string;
};
Moving forward displays the snapshot for currentStep + 1.
Moving backward displays the snapshot for currentStep - 1.
So rewinding means,
selecting an earlier recorded frame, not undoing the execution itself
Once recording has finished, users can navigate without rerunning their code:
- Pause
- Move one step forward or backward
- Jump to any step in the timeline
- Move to the first or final step
Users can also select a variable and jump directly to the previous step where its value changed.
That is especially useful for pointers such as i, left, and right.
No more clicking the Back button 47 times while whispering, "Where did you go wrong?" πΈ
There Is a Generator, but It Does Not Suspend the Userβs Code Line by Line
The Runtime API contains a Generator<ExecutionStep, ...>.
That might make it look as if the userβs code itself is being paused line by line with yield.
It is not.
After yielding the first function-call step, the instrumented code runs once and accumulates steps inside the execution context.
The generator then yields the already-recorded steps in order.
In the path used by the UI, the Worker consumes the generator completely and returns the finished ExecutionState.
Separating execution from playback is what makes the rewind behavior relatively simple.
βοΈ Web Worker: Do Not Take the UI Down with You
User code runs inside a dedicated Web Worker instead of the main thread.
new Worker(new URL("./execution-worker.ts", import.meta.url), {
type: "module",
name: "algorithm-execution",
});
An infinite loop or expensive computation on the main thread would freeze the entire page.
By moving execution into a Worker:
- The React UI stays on the main thread
- Starting a new run or leaving the page can terminate the Worker through an
AbortController
The Runtime also has limits:
- Execution timeout: 10 seconds
- Maximum recorded steps: 3,000
When a limit is reached, the Worker can be terminated or execution can stop early with a warning.
A Worker Is Not a Security Sandbox
A Web Worker does not have access to the page DOM, localStorage, or sessionStorage.
DSA View View also does not execute user code on a server.
However, a Web Worker is not a security boundary that makes untrusted code safe.
The user code runs through new Function in the Worker global scope.
Web APIs available inside Workers, including fetch, are not automatically disabled.
The purpose of this architecture is to:
- Isolate heavy execution from the main thread
- Terminate an execution unit on timeout or cancellation
It is an execution boundary for responsiveness and control, not a magical prison for JavaScript.
JavaScript has watched enough escape-room videos to know better.
π Visualization: Variables Alone Are Not Enough
The Runtime returns an ExecutionState and snapshots for each step.
The next problem is deciding which View should display that trace.
DSA View View does not ask AI,
"Hey, does this look like Binary Search?"
on every run.
Instead, visualization candidates are detected through logic based on:
- Variable names
- Variable types and data structures
- Whether arrays or matrices actually changed
- Pointers such as
left,right, andmid - The call stack
- Function-entry and return history
- Heap metadata
- Changes across multiple steps
For example, displaying a Sort Graph whenever a number[] exists would classify algorithms that only read an input array as sorting algorithms.
So the detection logic checks whether the numeric array actually changes between steps.
It also looks for context such as sort, swap, partition, and pivot in descriptions or the call stack.
Some representative mappings are π
| Runtime state | Displayed View |
|---|---|
| A changing numeric array | Sort Graph |
| An array used as a stack | Stack View |
Search using left / right / mid
|
Index View |
| Sliding Window pointers | Sliding Window View |
| A two-dimensional array | Matrix View |
| DP arrays or rolling state | DP View |
TreeNode |
Tree Graph |
ListNode |
List Graph |
| Recursive function entry and return | Recursion Tree |
Logic using Map
|
Map View |
Logic using both MinHeap and MaxHeap
|
Heap View |
| Word Ladder-style BFS | Word Ladder View |
| Container / rain-water / histogram pointers | Area View |
| Calculator character positions and sign stack | Expression View |
When multiple candidate Views are found, DSA View View assigns priorities and selects a primary Visualization.
For example, if heap state is available, Heap View takes priority over an ordinary array View.
Visualization Is Built from ExecutionState
Each Visualizer knows nothing about the userβs source code.
It extracts what it needs from the current ExecutionState and converts that data into View-specific state.
<BinarySearchVisualizer
data={data}
indexState={{
left,
right,
mid,
}}
/>
When currentStep changes, the same Visualizer receives a new state.
CSS transitions and Framer Motion animate the difference, making pointers and nodes appear to move.
This creates a clean separation of responsibilities:
- Runtime creates a correct trace while preserving the original behavior
- Detection decides which View best represents the trace
- Visualization focuses on how to display that state
Adding a new visualization generally follows this path:
- Detect the target execution state
- Convert it into View-specific state
- Render it with a React component
- Add it to the primary-visualization selection
- Add the smallest reproducible code sample as a test
π§ͺ Tests: Reducing the Number of "It Happened to Work" Cases
Users can write code freely, which means they can also write it in ways I never expected.
AST transformations are especially dangerous because fixing one syntax pattern can quietly break another.
So when a real issue appears, I keep the smallest version of that code as a regression test.
Examples include:
- Arguments and local variables for each recursive call
- Postfix
i++ - XOR assignment
- Nested array mutations
- Class constructors and
super() -
TreeNode/ListNodeinputs - Heap operations
- Binary Search and Sliding Window pointers
- Word Ladder BFS state
- Worker timeouts and step limits
Trusting human memory to say,
"Surely this syntax still works."
is not a particularly strong testing strategy.
Memory is surprisingly optimistic.
And it would be a little embarrassing to build a tool for avoiding confusion in DSA, only to get lost inside the toolβs own behavior.
π― Summary
DSA View View is not simply turning an array into a bar chart.
At a high level, it works like this:
- Write TypeScript in Monaco
- Analyze the source code and signature with Babel
- Inject
recordStepinto the AST - Execute the code inside a Web Worker
- Record variable snapshots and the call stack
- Replay the steps with React
- Display a View matching the data structure
The most important step was converting the userβs code into an array of ExecutionSteps.
Once execution becomes a sequence of steps, the same ExecutionState can power:
- Pause
- Forward and backward navigation
- Timeline jumps
- Navigation to the previous variable change
- Reconstruction of recursion trees
- Data-structure visualizations
The key idea is "Do not try to reverse JavaScript itself. First, transform execution into a recordable domain model".
Then,
"Runtime focuses on producing a correct trace, while Visualization focuses on presenting that trace."
Keeping Editor, Runtime, and Visualization independent from one another has been essential to making the tool extensible. Stable models such as ExecutionStep and ExecutionState connect these layers without coupling them directly.
There are still unsupported algorithms and syntax patterns, so I would love to hear from you if:
- An implementation does not run
- You want another data structure visualized
- You think a trace would be clearer in a different View
Issues and PRs are always welcome π
https://github.com/nyaomaru/dsa-view-view
Now letβs run our code and watch what it is doing inside together. ππ
And if you enjoy the project, I would be very happy if you left a GitHub β!



Top comments (2)
very cool!π±
Thx!! πΈ