Project: JSInterpreter
Version: 1.2.0
Created: 2026-05-24
Updated: 2026-06-16
Audience: Product owners, developers, testers
🌐 日本語版
JSInterpreter is a JavaScript interpreter written in JavaScript. Its primary goal is to execute modern ES6+ syntax (including async/await) and provide an expression-level step-execution API — step-in, step-over, step-out, and step-back.
Conventional debuggers target bytecode or JIT-compiled code, making it difficult to visualize the JavaScript evaluation process at the expression level. This system interprets source code as an AST and records every evaluation step as a snapshot array, enabling step execution and reverse execution (step-back) at arbitrary granularity.
User (developer / learner)
│
├─── Programmatic API (JSDebugger class)
│ ↑ External tools · test code · IDE integration
│
└─── Interactive REPL Debugger (CLI)
↑ Operated directly from the terminal
| ID | Feature | Priority |
|---|---|---|
| F-01 | Execute JavaScript source code | Required |
| F-02 | Step-in | Required |
| F-03 | Step-over | Required |
| F-04 | Step-out | Required |
| F-05 | Step-back | Required |
| F-06 | Variable inspection | Required |
| F-07 | Call-stack inspection | Required |
| F-08 | Breakpoint execution | Required |
| F-09 | Interactive REPL debugger | Required |
| F-10 | File execution mode | Required |
| F-11 | Plain REPL (non-debug) | Required |
| F-12 | Human-friendly step | Required |
| F-13 | CodeTrace (Execution Visualizer) | Required |
let,const,var— scoping rules faithfully implemented:var— function-scoped (escapes blocks), hoisted (pre-defined asundefined)let— block-scoped, TDZ (accessing before declaration throws RuntimeError), re-declaration in same scope throws RuntimeErrorconst— block-scoped, TDZ, reassignment (=,+=,++, etc.) throws RuntimeError
for (let i = 0; ...)per-iteration binding — closures correctly capture the value at each iteration- Multiple declarations (
let a = 1, b = 2) - Object destructuring (
let { x, y } = obj) - Array destructuring (
let [a, b] = arr) - Default values in destructuring (
let { x = 0 } = obj)
| Category | Content |
|---|---|
| Literals | Numbers (integer, float, hex, octal, binary, numeric separators), strings (escape sequences), booleans, null |
| Template literals | Backtick strings, ${expr} interpolation (nestable) |
| Arithmetic | + - * / % ** |
| Comparison | == != === !== < > <= >= |
| Logical | && || ! ?? (nullish coalescing) |
| Bitwise | & | ^ ~ << >> >>> |
| Assignment | = += -= *= /= %= **= &&= ||= ??= |
| Increment | ++ -- (prefix and postfix) |
| Ternary | condition ? truthy : falsy |
| Unary | typeof, void, delete |
| Binary | instanceof, in |
| Optional chaining | ?. (optional access and call) |
| Spread / Rest | ... |
| Await | await expr (inside async functions) |
- Function declarations (
function f(a, b) { ... }) - Function expressions (
const f = function() { ... }) - Arrow functions (
x => x * 2,(a, b) => a + b, block body) - Async functions (
async function f() { ... },async () => ...,async x => ...) - Rest parameters (
...args) - Default parameters (
x = 0) - Closures (captures the scope at definition time)
- Recursive calls
if/else if/elsewhileloopdo...whileloopforloop (init, test, update)for...of(iterables)for...in(enumerable properties)break/continuereturn(with or without value)throw/try/catch/finally
classdeclarations and expressionsconstructor- Method definitions (regular and async)
extends(inheritance) andsupernewexpressions- Static methods (
static) - Getters and setters (
get/set)
- Object literals (
{ key: value }) - Shorthand properties (
{ x }) - Computed property names (
{ [expr]: value }) - Spread (
{ ...obj }) - Array literals (
[1, 2, 3]) - Spread (
[...arr]) - Property access (
obj.key,obj[expr])
async/await is simulated synchronously. Code that does not involve native I/O is fully supported.
| Syntax | Behavior |
|---|---|
async function f() { ... } |
Call result returned as a JSPromise object |
async () => expr |
Same as above |
await expr |
Synchronously resolves a JSPromise and returns its value |
Promise.resolve(val) |
Returns an immediately fulfilled JSPromise |
Promise.reject(reason) |
Returns an immediately rejected JSPromise |
Promise.all/allSettled/race/any([...]) |
Synchronously resolved results |
new Promise((resolve, reject) => { ... }) |
Runs executor synchronously, returns JSPromise |
Limitation: Does not work with real async I/O such as fetch or setTimeout.
import/export(parsed only; no runtime module loading)debuggerstatement (ignored)- Line comments (
//) and block comments (/* */) - Automatic Semicolon Insertion (ASI)
Step execution operates at the expression (node) level. Each AST node produces an enter event (evaluation starts) and an exit event (evaluation completes).
| Step | Phase | Node type | Value |
|---|---|---|---|
| 0 | enter | VariableDeclaration | — |
| 1 | enter | BinaryExpression(+) | — |
| 2 | enter | Literal | — |
| 3 | exit | Literal | 1 |
| 4 | enter | BinaryExpression(*) | — |
| 5 | enter | Literal | — |
| 6 | exit | Literal | 2 |
| 7 | enter | Literal | — |
| 8 | exit | Literal | 3 |
| 9 | exit | BinaryExpression(*) | 6 |
| 10 | exit | BinaryExpression(+) | 7 |
| 11 | exit | VariableDeclaration | — |
Action: Advance one event.
Function calls: Enters the body of the callee.
Phase: Advances regardless of enter/exit.
Boundary: Stops at the last event (done = true).
Action: Skip the current node's children and land on its exit event.
- Current is
enter(N)→ jump to matchingexit(N)(children already evaluated) - Current is
exit→ advance one step (same as step-in)
Example: Stepping over enter(CallExpression) skips the function body and lands on exit(CallExpression), where the return value is available.
Action: Exit the current function call and return to the caller.
- Jump to the first
exitevent wherecallDepthdrops below the current level - If
callDepth === 0(top level), jump to the end of the program
Action: Go back one event. Always O(1).
Accuracy: Environment snapshots are deep-cloned, so mutations inside objects and arrays are accurately restored.
Boundary: No-op when cursor === 0.
Caveat: Native objects (Map, Set, Error instances, etc.) are stored by reference, so their mutation history may be inaccurate.
| Option | Description |
|---|---|
'local' |
Innermost scope only |
'all' |
Full scope chain, flattened (outer variables included) |
Returns the call stack at the current step. Each frame contains:
- Function name (
<anonymous>or<arrow>for unnamed functions) - Call site (line number, column number)
Runs until the first enter event matching the specified line (and optional column). Without breakpoints, runs to the end of the program.
Expression-level stepping (F-02 – F-05) exposes every AST node evaluation event. For a single statement like let x = 1 + 2 * 3, this generates 12 events. While this granularity is precise, it produces too much noise for a human tracing through an algorithm on paper.
F-12 provides a "meaningful change point" granularity that matches how a human would annotate code when tracing manually.
The following events are surfaced (native calls that do not increase callDepth are skipped):
| Label | Phase | Node type | Description |
|---|---|---|---|
| 宣言 (Declare) | exit | VariableDeclaration |
let/const/var declaration completed |
| 代入 (Assign) | exit | AssignmentExpression |
Assignment or compound-assignment completed |
| 更新 (Update) | exit | UpdateExpression |
i++, --j, etc. |
| return | exit | ReturnStatement |
return executed |
| throw | exit | ThrowStatement |
throw executed |
| 呼出 (Call before) | enter | CallExpression |
User-defined function call about to happen (callee highlighted) |
| 呼出 (Call enter) | enter | first statement in body | Just entered the function body (first statement highlighted) |
| 呼出 (Call after) | exit | CallExpression |
User-defined function call completed (return value confirmed) |
| 条件 (Condition) | exit | condition test expression | true/false decision point of if, while, do...while, for, ? : |
Native function calls (e.g. Math.floor(), arr.push()) do not increase callDepth and are therefore skipped.
| Parent node | Detection method |
|---|---|
IfStatement |
The exit of trace[enterIdx + 1] (the first child = test, evaluated once) |
ConditionalExpression |
Same as IfStatement |
WhileStatement |
All exits at depth + 1 inside the loop range that are not BlockStatement (captured every iteration) |
DoWhileStatement |
Same as WhileStatement |
ForStatement |
All exits at depth + 1 that are not VariableDeclaration (init) or BlockStatement (body) |
For bubbleSort([3, 1, 2]):
| Mode | Steps |
|---|---|
| Expression-level (stepIn) | ~400 |
| Statement-level | ~50 |
| Human-friendly (F-12) | ~20 |
[宣言 ] line 3 for (let i = 0; i < n - 1; i++) {
[条件 ] line 3 for (let i = 0; i < n - 1; i++) { → true
[条件 ] line 6 if (arr[j] > arr[j + 1]) { → true
[宣言 ] line 7 const tmp = arr[j];
[代入 ] line 8 arr[j] = arr[j + 1]; → 1
[代入 ] line 9 arr[j + 1] = tmp; → 3
[更新 ] line 5 for (let j = 0; j < n-1-i; j++) { → 0
Each line shows: [label] line NNN <source line padded to 45 chars> → value
The value is displayed for AssignmentExpression, UpdateExpression, CallExpression, and condition tests. VariableDeclaration, ReturnStatement, and ThrowStatement do not display a value (the source line is self-explanatory).
Launch: node src/index.js --debug <file.js>
| Command | Action |
|---|---|
n or Enter |
Step-in |
v |
Step-over |
o |
Step-out |
b |
Step-back |
h |
Human-friendly step (F-12) |
H |
Human-friendly step-back (F-12) |
p |
Print all variables |
p <name> |
Print named variable |
stack |
Print call stack |
c |
Continue to end (or next breakpoint) |
q |
Quit |
Expression-level display format (n/v/o/b):
[▶ enter] BinaryExpression line 3:5 (depth=2, callDepth=0)
[◀ exit ] BinaryExpression line 3:5 → 7 (depth=2, callDepth=0)
Human-friendly display format (h/H):
[条件 ] line 3 for (let i = 0; i < n - 1; i++) { → true
[代入 ] line 8 arr[j] = arr[j + 1]; → 1
Launch: node src/index.js <file.js>
Executes a JavaScript file without debugging. Output from console.log goes to stdout. Errors are printed to stderr without a stack trace.
Launch: node src/index.js
Interactively evaluates expressions and statements, maintaining environment state across inputs. Exit with .exit or Ctrl+D.
Build: npm run build:web
Launch: npm run dev:web → http://localhost:8000
A browser-based execution visualizer aimed at learners. The interpreter core is bundled into web/interpreter.bundle.js by esbuild; no server-side runtime is required.
Layout:
┌──────────────────────────┬────────────────────────┐
│ Source (left 55%) │ Controls (right 45%) │
│ ├────────────────────────┤
│ Edit mode: <textarea> │ Current Step │
│ Debug mode: highlighted ├────────────────────────┤
│ source lines │ Variables │
│ + expression highlight ├────────────────────────┤
│ + trace table (when ON) │ Call Stack │
│ [step N / total] ├────────────────────────┤
│ │ Console │
└──────────────────────────┴────────────────────────┘
Controls and keyboard shortcuts:
| Button | Key | Action |
|---|---|---|
| Step In | n / Enter |
stepIn() |
| Step Over | v |
stepOver() |
| Step Out | o |
stepOut() |
| Step Back | b |
stepBack() |
| Human Step | h |
humanStep() |
| Human Back | H |
humanStepBack() |
| Continue | c |
continue() |
| Reset | r |
Return to edit mode |
| 📊 Trace | t |
Toggle inline trace table |
Source panel: The active line is highlighted. When the current event is an expression node, the exact column range is further highlighted in yellow to show which sub-expression is being evaluated.
Current Step card shows: phase (enter/exit), nodeType, line:col, depth, callDepth, and evaluated value (exit events only).
Variables card: Default view shows all user-defined variables across all scopes merged (inner scope wins), excluding built-in global names (Math, console, Array, etc.). The "スコープ別" checkbox switches to a frame-by-frame view of the full scope chain (including built-in globals).
Call Stack card: Each frame shows the function name, call site, and the actual argument values at call time (e.g. fib(5), bubbleSort([3,1,2])). Values are deep-cloned at call time and are unaffected by later mutations.
Console card: Displays output from console.log() / console.warn() / console.error(). Only entries produced up to the current cursor position are shown, so stepping back also rolls back console output. Output uses a Node.js-compatible format: top-level string arguments are printed without quotes; strings nested inside arrays or objects are displayed with single quotes (e.g. console.log(['aaa']) → [ 'aaa' ]). JSPromise values are displayed as Promise { value } or Promise { <rejected> reason }.
Inline trace table: Toggled by the 📊 Trace button (or key t). When ON, the Source panel switches to a table layout in which each source line grows extra columns to its right.
- Variable columns (blue header): One column per user-defined variable, added in order of first appearance.
- Condition columns (purple header): Automatically detected test expressions from
if/while/forstatements; their boolean evaluation results are shown. - Value updates: Each row retains the last-recorded snapshot for that line; cells update live on every step.
- Flash animation: Cells whose value changed in the last step flash yellow (variables) or purple (conditions).
- Horizontal scroll: The source panel scrolls horizontally when there are many columns; the line-number column is sticky.
Example programs (dropdown): fibonacci, factorial, bubble sort, closure, class.
| Error type | Trigger | Message format |
|---|---|---|
LexError |
Lexing | [Lexer] line:col: message |
ParseError |
Parsing | [Parser] line:col: message |
RuntimeError |
Evaluation | [Runtime] line:col: message |
All errors are printed as human-readable messages without a stack trace.
| Constraint | Detail |
|---|---|
| Infinite loops | Recording phase does not terminate. Use maxSteps option (default: 100,000) to cap. |
| Step-back accuracy | Native objects (Map, Set, etc.) are stored by reference; their mutation history may be inaccurate. |
| Syntax | Status |
|---|---|
| Regular expression literals | /pattern/ causes a lex error (RegExp constructor works) |
switch statement |
Not implemented |
| Labeled statements | Not implemented |
with statement |
Not implemented (deprecated syntax) |
function* / yield |
Parsed but not executable |
| Tagged template literals | Not implemented |
for await...of |
Not implemented |
| Limitation | Detail |
|---|---|
| Native async I/O | fetch, setTimeout, etc. do not work |
| JSFunction as native callback | [1,2,3].map(x => x*2) does not work |
arguments object |
Not supported; use rest parameters (...args) |
| Module system | import/export is parse-only; no file loading |
All features are covered by unit tests. Test files are co-located with their source files. Explicit expect(result).toBe(...) assertions are used; snapshot tests are not.
Test breakdown (249 total):
| File | Count |
|---|---|
src/lexer/lexer.test.js |
45 |
src/parser/parser.test.js |
42 |
src/interpreter/interpreter.test.js |
52 |
src/interpreter/debugger.test.js |
52 |
src/interpreter/virtual-dom.test.js |
58 |