Technical documentation

Under the hood of DSA Lab.

DSA Lab's core bet is that you should never write visualization code. Paste an algorithm, add JSON input, and the tracer infers every state change — arrays, strings, maps, matrices, graphs, linked lists, trees, and recursion frames — with no custom annotations. This page documents how that works: the trace engine, the sandbox, and the canvas that turns steps into pictures.

On this page

01 · Overview

A client-side trace pipeline

Everything that matters happens in the browser. Instrumentation, execution, step capture, and rendering are all client-side; the server only handles sign-in, saved collections, and feedback. There is no backend that runs your code — the sandbox is a Web Worker, not a server.

The pipeline has three stages. First, acornparses the user's code and the instrumenter rewrites it so that every state change calls a capture hook. Second, the rewritten code runs inside a sandboxed Web Worker that executes it and emits one self-contained snapshot per meaningful change. Third, the canvas is a pure function of those snapshots: it renders whatever structures a step carries and highlights what changed since the previous step.

This page is the public, engineering-level write-up — deliberately detailed, deliberately free of secrets. Day-to-day ops (env vars, database migrations, Vercel) live in the repo's SETUP.md.

02 · Tech stack

Chosen deliberately, not by default

Each dependency earns its place — most exist to make the trace pipeline fast, safe, or self-contained.

Next.js 16 · React 19 · TypeScript

App Router server components for marketing, guide, and learn pages; the visualizer workspace is a client-side React app. Type-checked end to end.

acorn

Tiny, fast ECMAScript parser. The tracer rewrites user code by parsing it into an AST and splicing instrumentation calls into the source — no regex brace-counting.

Web Workers (Blob)

User code never runs on the main thread. Instrumented source is executed inside a Blob-constructed Worker with networking disabled — a lightweight browser sandbox.

Pyodide

Real CPython compiled to WebAssembly. Python tracing runs natively in the browser via sys.settrace — no AI translation, no server round-trip.

CodeMirror 6

The code editor with line numbers, syntax highlighting, and a decoration that highlights the active trace line as you step.

framer-motion

FLIP-animates array bars when values swap positions, so a bubble-sort exchange visibly slides two bars into each other's places.

jose + Google OAuth

Server-side PKCE OAuth against Google, minting a stateless HS256 session JWT (httpOnly cookie). No third-party auth provider.

Supabase (Postgres + Storage)

Plain Postgres for users, saved algorithms, and feedback, plus a storage bucket for feedback screenshots. Supabase Auth is not used.

NVIDIA NIM

AI provider: translates Java, C++, and TypeScript the annotation stripper can't handle into instrumented JS with //# line mapping, via meta/llama-3.1-8b-instruct on the NVIDIA NIM API (NVIDIA_API_KEY from build.nvidia.com). JS/TS/Python never touch it. Cached client-side (LRU).

Vercel

Deployment with Analytics and Speed Insights wired into the root layout.

03 · Architecture

Architecture at a glance

The full trace pipeline, from pasted code to rendered state.

  1. 01Your code + JSON input (browser, main thread)
  2. 02acorn instrumenter — rewrites the source (AST)
  3. 03Instrumented JS with __auto* hooks
  4. 04Blob Web Worker — sandboxed runtime, no network
  5. 05Step snapshots — deep-cloned, deduplicated, ≤ 3000
  6. 06normalize() — whitelist + matrix phase notes
  7. 07VisualizationCanvas — renderers + playback + op counters

Client (browser)

Instrumentation, worker execution, step normalization, rendering, playback, and code editing. No app secrets ever reach the client.

Server (Next.js routes)

Google OAuth dance, session cookie minting, saved algorithms, feedback (with image upload), and the optional Gemini translation route.

  • Offline-first by design: a trace needs no network. Only sign-in, saving, and feedback require the server — the visualizer works with zero keys configured.
  • One shared schema: JS, TS, and Python all converge on the same VisualStep type, so the canvas, playback bar, and explainer never care which language produced a step.

04 · The trace engine

How the tracer works

The tracer lives in src/lib/trace/. It has four responsibilities: rewrite code, run it safely, normalize what came back, and bridge pasted LeetCode solutions.

  1. 01User code
  2. 02acorn parse + scope-aware AST walk
  3. 03Rewritten source: __autoCapture · __autoBranch · __autoEnter · __autoReturn · __autoAccess · __autoRecord
  4. 04Sandboxed Web Worker executes and emits snapshots
  5. 05normalizeSteps — whitelist + matrix phases
  6. 06VisualStep[] — one snapshot per state change

Instrumentation — rewriting code so state changes are observable

The instrumenter parses user code with acorn (locations enabled) and walks the AST with a custom scope-aware walker. Instead of evaluating code and guessing at state, it rewrites the source so every interesting moment explicitly reports itself. Statements get a capture call appended; branch conditions are wrapped so their truthiness becomes a step; functions get enter/return hooks; and double-indexed matrix accesses become instrumented calls so the exact cell is traced.

Before → after (simplified)
// before — user's code
function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}

// after — what the tracer actually runs (conceptual)
__autoCapture(1, {}, "Start");
function binarySearch(arr, target) {
  __autoEnter(1, "binarySearch", { arr, target });
  let left = 0, right = arr.length - 1;
  while (__autoBranch(4, "for left <= right", (left <= right), { left, right, arr })) {
    const mid = Math.floor((left + right) / 2);
    if (__autoBranch(6, "if arr[mid] === target", (arr[mid] === target), { left, right, mid, arr }))
      return __autoReturn(6, { left, right, mid, arr }, "return mid", mid);
    if (__autoBranch(7, "if arr[mid] < target", (arr[mid] < target), { left, right, mid, arr }))
      left = mid + 1;
    else right = mid - 1;
    __autoCapture(9, { left, right, mid, arr }, "left = mid + 1");
  }
  return __autoReturn(10, { left, right }, "return -1", -1);
}

Six hooks cover every kind of step the UI understands:

The __auto* protocol
__autoCapture(line, values, label)      emit a step snapshot after a statement
__autoBranch(line, label, value, values)  record a condition's truthiness, count comparisons
__autoEnter(line, name, values)           push a recursive call-stack frame
__autoReturn(line, values, label, value)  record the return value, pop the frame
__autoAccess(name, obj, r, c, src, kind)  capture an exact matrix cell read / write
__autoRecord(name, obj, r, c, src)        side-effect-free cell record inside conditions

The walker also handles the language's sharp edges, all of which would corrupt a naive rewrite:

  • Bare control bodiesif (c) stmt; else gets wrapped in a block so the inserted capture doesn't detach the else.
  • Temporal dead zones — a ternary inside const x = cond ? a : b evaluates while x is still uninitialized, so names declared by the enclosing declarator are excluded from its capture.
  • var hoistingvar names declared inside a loop body are collected so loop-condition branches can read them on later iterations.
  • Overlapping rewrites— wrapper edits (matrix writes, returns) claim their source span so nested edits don't double-apply against stale coordinates.
  • Line mapping — line numbers come from the AST, and //# line: Ndirectives (emitted by AI translation) remap steps back to the user's original source.

Worker runtime — a sandboxed executor that emits steps

The rewritten code never touches the main thread. A Blob-constructed Worker (built from a template string via URL.createObjectURL) receives the instrumented source and the parsed input JSON, executes it with an async function wrapper, and posts the step list back. The worker is deliberately boring — it has no UI, no DOM, no network.

  • Sandboxing: fetch, XMLHttpRequest, WebSocket, and importScripts are all disabled, so user code cannot exfiltrate data or load anything.
  • Snapshot integrity:every step is deep-cloned at emit time via a cycle-aware clone — later mutations can never rewrite history, and cyclic structures (Floyd's linked list!) serialize as the marker [cycle].
  • Coalescing: a step identical to the previous one is dropped — most loop iterations change nothing visible, and skipping them keeps traces short.
  • Limits: a 3,000-step cap (partial steps are kept so the UI can explain the truncation) and a 5–15 s adaptive timeout on the main thread.
  • Structure detection: heuristics classify every in-scope value into array / string / hash map / matrix / graph / set / linked list / tree, and name-based routing decides stack vs queue vs visited.
  • Node registries: each ListNode / TreeNode object gets a stable id across the whole run; steps carry only ids + primitives, so even cyclic lists stay JSON-safe and node boxes never shuffle between steps.
  • Operation counters: comparisons (branch conditions containing comparison operators), swaps (exactly-two-position exchanges), and writes (changed cells, positions, or characters) accumulate across the run.

Step normalization — a whitelist, not a trust boundary

Raw worker steps are untrusted data. normalizeSteps whitelists every field a step may carry, coerces types (numbers stay numbers, garbage drops out), and then derives a human-readable phase note for matrix steps — BFS vs DFS context, cell changes, traversal movement, and the exact instrumented access. The canvas only ever sees normalized steps.

A normalized step (the shape the canvas renders)
{
  "title": "Branch",
  "note": "if arr[mid] < target → false",
  "line": 7,
  "array": [1, 3, 5, 7, 9],
  "active": [3],            // every in-bounds integer pointer lands here
  "range": [0, 4],          // left … right search band
  "pointers": { "left": 0, "right": 4, "mid": 2 },
  "ops": { "comparisons": 3, "swaps": 0, "writes": 0 }
}

Entry harness — pasted LeetCode solutions just work

Students paste solutions in platform format — class Solution { twoSum(nums, target) } or a bare function — which never references the app's input JSON. The harness detects the entry point, maps the input JSON onto its parameters (by name, else positionally), converts LeetCode-style arrays into real ListNode / TreeNode chains, invokes the entry, and returns the result. The harness is appended afterinstrumentation, so helper functions build lists and trees without polluting the trace. On the way out, ListNode / TreeNode / GraphNode results are converted back to LeetCode's array encoding so the output strip shows [1, 2, 3, 4], not nested JSON.

05 · Rendering

The visualization canvas

The canvas is a pure function of (current step, previous step). Render what the step carries, diff against the last one, animate the difference.

  1. 01VisualStep + previous step
  2. 02Structure detection: array · string · hashMap · matrix · graph · linkedList/tree · callStack
  3. 03Each structure renders in its own panel
  4. 04Diff vs previous step — changed cells highlighted with tooltips
  5. 05Operation counters: comparisons · swaps · writes
  • Structure renderers — arrays as bars, strings with sliding-window bands, hash maps, matrices with an active cell, graphs as SVG with a layered BFS layout and discovery-order badges, linked lists and trees with stable node ids, plus call-stack frames and stack/queue/visited rows. A step renders every structure it carries at once, in a grid.
  • Pointer arrows & range bands — every integer-valued in-bounds variable (i, left, right, k, slow …) becomes a labeled arrow above the array or string; left/right-style pairs draw a dashed range band between them.
  • Diff highlighting — changed array positions, string characters, matrix cells, hash-map entries, and linked-list values/links are colored against the previous step, with changed from X tooltips.
  • FLIP animation— array bars are keyed by value occurrence and animated with framer-motion's layout mode, so a swap visibly slides two bars into each other's spots instead of popping.
  • Operation counters — comparisons, swaps, and writes are shown per step and update as you play, turning a trace into a complexity lesson.
  • Explanation layer — explainStep derives a plain-English sentence for each step from the data alone (no AI), and matrix phase notes narrate BFS/DFS/recursion context.
  • Playback — play/pause, step back/forward, jump to first/last, a draggable timeline, speed control, and keyboard shortcuts (space, ←/→, Home/End), with the active line highlighted in the editor.

06 · Languages

Language support

Three languages run today. Each uses the cheapest path that produces a trustworthy trace.

JavaScript

Native. Parsed and instrumented locally with acorn; runs in the worker. No network, no AI, no translation — this is the reference path.

TypeScript

Type annotations are stripped locally (LeetCode-style signatures, interfaces, casts) so it stays on the fast path. Exotic type syntax falls back to Gemini translation, and emitted //# line: directives keep line mapping honest.

Python

Real CPython via Pyodide (WASM) with a sys.settrace harness — the same technique Python Tutor uses. Raw events are converted to VisualSteps with true line numbers; zero Gemini calls.

  1. 01Python source
  2. 02Pyodide — real CPython in WASM
  3. 03sys.settrace line tracer
  4. 04Raw events: line · call · return · print
  5. 05python-steps.ts → VisualStep[]
Java and C++ are available via the AI translation path (NVIDIA NIM → instrumented JS with //# line mapping back to the original source); Go, Rust, and friends still appear as coming-soon. Language detection is a weighted regex scorer with careful tie-breaking: ambiguous code prefers JavaScript (a wrong local parse fails fast with a clear error), except JS↔TS ties, which prefer TypeScript since the stripper is a no-op on plain JS.

07 · Accounts

Auth & persistence

Sign-in is first-party and minimal: Google OAuth straight to an httpOnly cookie, with Supabase used as plain Postgres.

  1. 01Browser → GET /api/auth/google (PKCE challenge)
  2. 02Redirect to Google consent screen → authorization code
  3. 03Callback exchanges the code for an ID token
  4. 04Find or create the user in Postgres
  5. 05Set the httpOnly session cookie
  6. 06Requests with the cookie → scoped server-side queries
  • Direct Google OAuth — the app runs the PKCE dance itself against accounts.google.com (no Supabase Auth, no hosted provider), verifies the ID-token signature server-side, finds or creates the user in the users table, and mints the session.
  • Sessions — a stateless HS256 JWT (signed with SESSION_SECRET via jose) in an httpOnly, SameSite=Lax, Secure-in-prod cookie named dsa_session, valid 30 days. Nothing in the browser can read it.
  • Data — users, saved algorithms, and feedback live in Postgres; feedback screenshots in a storage bucket. Every DB access goes through the service-role key server-side; the browser never talks to Supabase directly.
  • Admins — a users.is_admin flag, re-read from the database on every admin request. The cookie is never trusted for privileges.
  • Hardened by migration — RLS was enabled and anon/authenticated grants revoked on the users table (a Supabase default had left emails readable and is_admin writable).

08 · Trust

Security model

The product runs arbitrary user code, so the threat model is taken seriously — and mostly solved by never letting that code near anything.

Code isolation

User code runs only inside a dedicated Web Worker with network APIs and importScripts disabled. A timeout and step cap bound runaway loops.

Data trust

Steps are normalized through a strict whitelist — arbitrary code cannot inject fields into the UI. All DB access is server-side with the service-role key.

  • Secrets stay server-side — the service-role key, Google secret, and session secret are server-only env vars, never NEXT_PUBLIC_, never committed. .env.local is gitignored.
  • Python's exception — the Pyodide worker may reach only the pinned Pyodide CDN (it needs the WASM runtime); everything else is blocked.

09 · Quality

Testing strategy

The tracer is the whole product, so its behavior is pinned down by golden trace tests.

  • Golden trace tests — for every built-in template, the exact worker source runs under Node via runTraceInNode, which polyfills selfso the Blob worker code runs unchanged in a test process. Tests assert algorithm invariants: bubble sort's final array equals the sorted input, binary search lands on the target index, DFS visits every reachable node, recursion frames grow and shrink correctly.
  • Unit tests — language detection, structure diffing, pointer inference, the call tree, the entry harness, Python step building, share-link round-trips, and the learning-path state machine are all covered with vitest.
  • One commandnpm test runs the whole suite; it doubles as the regression contract for any tracer refactor.

10 · How it was developed

Development history

A short timeline of how the tracer evolved from a regex hack to the current pipeline.

  1. 01

    Original visualizer

    A JS-only prototype with a brace-counting regex instrumenter embedded inside the visualizer component. Traces were fragile — ternaries, multi-line statements, and branch decisions were silently missed.

  2. 02

    AST instrumentation + golden tests

    The tracer was extracted into a dedicated module and rewritten with acorn. A custom walker with lexical scoping replaced the regex, and golden trace tests became the behavioral contract for every change.

  3. 03

    Matrix tracing

    Double-indexed accesses (grid[r][c]) are rewritten so the exact cell, access source, and read/write kind are captured — enabling active-cell highlighting and plain-language phase narration.

  4. 04

    Resizable IDE revamp

    The workspace became a CodeMirror-based, resizable IDE with a first-run tour, zoomable panels, and student learning tools.

  5. 05

    Native Python tracing

    Pyodide + sys.settrace landed: real CPython in the browser traces Python with true line numbers and zero Gemini calls. Python is the one non-JS language with a genuinely native path.

  6. 06

    Rich structure rendering

    Linked lists and binary trees get stable node ids with active-node highlighting; a LeetCode/GFG entry harness converts pasted solutions into runnable traces and back into LeetCode-encoded answers.

  7. 07

    Recursion call tree & comparisons

    Call/Return steps feed an interactive call tree, and cumulative comparisons / swaps / writes counters turn traces into complexity lessons.

  8. 08

    Practice curriculum

    The Top Interview 150 bank, topic-grouped learning paths with checkpoint quizzes, verified expected answers with pass/fail verdicts, and a side-by-side compare mode.

  9. 09

    Direct Google OAuth

    Supabase Auth was removed entirely: server-side PKCE OAuth + an httpOnly session cookie, with the users table migrated and hardened with RLS.

  10. 010

    Sharing

    Shareable run links and a chrome-free embed mode — the distribution loop.

Worth saying plainly: the single most important decision in the project's history was replacing the regex instrumenter with acorn AST rewriting and locking it down with golden tests. Everything else — Python, matrices, linked lists, the curriculum — is built on that foundation.

11 · Run & contribute

Run & contribute

The visualizer runs without any keys. Only sign-in, saving, and feedback need the optional env vars.

Commands
npm install      # install dependencies
npm run dev      # development server → http://localhost:3000
npm run build    # production build
npm run test     # vitest unit tests (incl. golden trace tests)
npm run lint     # eslint
  • Copy .env.example to .env.local for sign-in; see SETUP.md for the full environment, Google OAuth, Supabase migration, and Vercel guide.
  • The trace engine is pure TypeScript (no DOM, no workers) — new tracer features should ship with a golden test proving the trace of a real algorithm.