A step-debuggable interpreter for a competitive-programming subset of C++, written in TypeScript.
Fluxa-WebCP runs C++ source code in the browser or in Node.js, one AST node at a time. Every variable, every stack frame, every array cell is observable at every step — and the entire interpreter state is a plain JSON-serializable object.
It is not a C++ compiler. It is a teaching and debugging substrate for the narrow slice of C++ that competitive programmers actually write.
Online judges show you a verdict. Wandbox and Compiler Explorer show you the
final stdout. Neither lets you watch a dp array fill in row by row, or
inspect the call stack inside a recursive dfs.
Fluxa-WebCP is built around three commitments:
- Step granularity is one AST node. Not one line — one node. You can stop
in the middle of
a[i] = f(j) + g(k)and inspect both subexpressions independently. - No undefined behavior, ever. Out-of-range array access, uninitialized reads, null-pointer deref, integer division by zero — all become explicit, recoverable runtime errors with a stack trace. The interpreter never throws.
- State is data, not a process.
InterpreterStateis a serializable object. You can JSON-stringify it, diff two snapshots, persist a session, or send it across a worker boundary.
These properties make the same engine usable as a CLI runner, a step debugger, a teaching tool, and a backend for time-travel debugging UIs.
npm install fluxa-webcp
# or
pnpm add fluxa-webcp
# or
yarn add fluxa-webcpRequires Node.js 20+. Zero runtime dependencies. Ships ESM and CJS bundles
plus .d.ts types.
import { Compiler } from "fluxa-webcp";
const source = `
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
cout << n * n << "\\n";
return 0;
}
`;
const result = new Compiler().compile(source);
if (result.kind === "error") {
for (const d of result.diagnostics) console.error(d.formatted);
process.exit(1);
}
const session = result.session;
session.provideInput("7\n");
session.run();
console.log(session.state.output); // => "49\n"
console.log(session.state.status); // => "done"const session = result.session;
session.provideInput("5\n");
while (session.state.status !== "done" && session.state.status !== "error") {
session.stepInto();
const info = session.debugInfo();
console.log(`line ${info.currentLine}`, info.localVars.at(-1));
}Each call to stepInto() advances the interpreter by one AST node and
returns control. debugInfo() exposes the full observable state — call
stack, scoped locals, globals, all live arrays and vectors, the input
cursor, and the source range currently being evaluated.
session.setBreakpoint(12);
session.run(); // pauses at line 12
console.log(session.debugInfo().pauseReason); // => "breakpoint"
session.run(); // continuesA precise specification lives in SPECIFICATION.md.
Briefly:
- Types —
int/long long(both 64-bitBigIntinternally),double,bool,char,string, fixed-length arrays,vector<T>,map<K,V>,pair<T,U>,tuple<T...>,T*,T&. - Control flow —
if/else,for, range-basedfor(over arrays,vector,map,string),while,break,continue,return. - Functions — value, reference, and pointer parameters; recursion; global variables.
- I/O —
cin,cout,cerr,endl. Commonsync_with_stdio/tieincantations are accepted as no-ops. - Standard library —
abs,max,min,swap,sort(withgreater<int>()orgreater<>()),reverse,fill,make_pair,make_tuple,get<I>. Vector methods:push_back,pop_back,size,back,empty,clear,resize,begin/end(forsort/reverse/fill). Map:m[key](default-insert),.size(), range-for(yieldspair<K,V>). - Preprocessor —
#include <bits/stdc++.h>,#include <iostream>,#include <vector>,#include <map>, and#define.
The following are deliberately unsupported, and are surfaced as compile errors rather than ignored:
- Dynamic memory:
new,delete,malloc,free - User-defined
structorclass; class/variable templates, partial/explicit specialization, overload resolution (function templates with type inference and explicit template arguments are limited-supported — see spec) - Function pointers, namespaces (other than
using namespace std;) - C-style and
static_castcasts - Reference return values
If you need any of these, you need a real compiler — Fluxa-WebCP will tell you so explicitly.
| Resource | Default | Configurable |
|---|---|---|
| Recursion depth | 10,000 frames | no |
| Execution steps | 10,000,000 | yes (UI control) |
Both are enforced as graceful runtime errors, not crashes.
The DebugSession returned by a successful compile exposes:
| Method | Behavior |
|---|---|
stepInto() |
Advance one AST node; descend into calls. |
stepOver() |
Advance one statement; treat calls as atomic. |
stepOut() |
Run until the current frame returns. |
run() |
Run until breakpoint, completion, or error. |
pause() |
Suspend a running session. |
setBreakpoint(line) / clearBreakpoint(line) |
Manage line breakpoints. |
provideInput(s) |
Append to the stdin buffer. |
debugInfo() |
Snapshot the full observable state. |
session.state is a InterpreterState:
type InterpreterState = {
callStack: Frame[];
globalStore: GlobalStore;
output: string;
errorOutput: string;
status: "running" | "paused" | "done" | "error";
error: RuntimeError | null;
};This object is plain data. JSON.stringify(session.state) works. Storing
snapshots, diffing them, or shipping them to another process all work
without ceremony.
Compile errors follow GCC/Clang format:
main.cpp:7:14: error: 'x' was not declared in this scope
Runtime errors carry a stack trace and a structured representation:
Runtime Error: index 10 out of range for array of size 5
at dfs:23
at main:41
The interpreter never throws on user-program errors. They surface
through state.status === "error" and state.error.
state.error includes both a formatted message and structured fields
such as summary, filename, and stack.
A Next.js playground in apps/web/ wraps the interpreter
with a Monaco editor, gutter breakpoints, a live variables/call-stack
panel, and step controls.
pnpm install
pnpm --filter web dev # http://localhost:3000
pnpm --filter web build # production build.
├── src/ # fluxa-webcp — interpreter core (the published package)
│ ├── index.ts # public API
│ ├── compiler.ts # parse → validate → session
│ ├── preprocessor.ts
│ ├── parser/
│ ├── runtime/ # Value union, RuntimeError, CompileError
│ ├── interpreter/ # evaluator and execution engine
│ ├── semantic/ # type checking, template instantiation
│ ├── stdlib/ # built-in function metadata, eval/check registries
│ └── debugger/session.ts
├── apps/web/ # Next.js playground
└── tests/ # Vitest, organized by feature
Internal dependency rules:
parserandruntimehave no internal dependencies and no awareness of each other.interpreterdepends onparserandruntime.debuggerdepends oninterpreterandruntime.- Cycles are forbidden.
- Files stay under 800 lines; oversized files are split by responsibility, not by line count.
pnpm install
pnpm test # run all tests
pnpm test --watch
pnpm build # build the core package via Rollup
pnpm biome check --writeTests are organized by feature area in tests/, from 01-basics.test.ts
through 10-pointers-and-references.test.ts. New language features land
with their own test file.
@ts-ignore and biome-ignore are not used without an inline
justification comment.
MIT. See LICENSE.
