Branch: feature/java-interpreter
Tagged baseline: v1.0 (Phase 1 — 75 lessons, pattern-based validation, full cosmetics)
Author: Alberto Felix
Status: Active Development — Tasks 1–9 complete (interpreter core done, SandboxEditor wired, Milestone 1 fixtures green); Task 10a complete (sandbox mode props + close button fix); editor features complete (syntax highlighting, bracket auto-pair, auto-indent, Tab key). Direction change after Task 9 — lesson migration (Task 10) replaced by the Global Sandbox. See Current Task Queue and Direction Change: The Global Sandbox.
Java-Chan's Phase 1 validation is pattern-based. patternMatcher.js and blankValidator.js check whether student code contains the right keywords or fills in the right blanks. This works — and it ships fast and offline — but it has a ceiling.
The ceiling: a student can write System.out.println("hello") and pass Phase 3 even if the surrounding logic is completely wrong. We're checking structure, not behavior. We're validating that the student typed the right words, not that the code actually does what it's supposed to do.
The goal of this update is to build a Java subset interpreter in JavaScript — a real executor that lives entirely in the browser, has zero external dependencies, needs no server, and runs the student's code against a real output expectation. When a student's for loop produces 1 2 3 4 5 but the expected output is 0 1 2 3 4, Java-Chan knows. She can say so. She reacts accordingly.
This is not a JVM. It is not a compiler. It is a tree-walking interpreter that supports a carefully chosen subset of Java — exactly the subset that appears in the CS22301 curriculum across Units 1 through 4. That scope limitation is intentional and is a product decision, not a shortcut. JavaFX (Unit 5) and multithreading (Unit 4 later lessons) are explicitly out of scope for reasons explained below.
For contributors who haven't built one before, here's the mental model.
A tree-walking interpreter works in three stages:
Source code (string)
↓
[ LEXER ] → Token stream
↓
[ PARSER ] → Abstract Syntax Tree (AST)
↓
[ EVALUATOR] → Output / side effects
The Lexer (also called a tokenizer) reads raw characters and groups them into meaningful tokens. int x = 5 + 3; becomes [KEYWORD(int), IDENTIFIER(x), EQUALS, NUMBER(5), PLUS, NUMBER(3), SEMICOLON]. It doesn't understand what the code means — it just recognizes shapes.
The Parser reads the token stream and builds a tree that represents the code's structure. 5 + 3 becomes a BinaryExpression node with a left child NumberLiteral(5), an operator +, and a right child NumberLiteral(3). The tree captures nesting, operator precedence, and block scoping that a flat token list can't express.
The Evaluator walks the AST recursively and executes each node. It knows that a BinaryExpression with operator + means "evaluate the left side, evaluate the right side, add them." It tracks variables in an environment (a scope chain — a map from name to value). It handles control flow by deciding which branches of the tree to walk. It captures everything sent to System.out.println and assembles it into an output string that the UI can compare to expectedOutput.
This is called "tree-walking" because the evaluator literally walks the tree — visits each node, evaluates it, moves to children, returns a value up the chain.
The canonical reference for building exactly this is Robert Nystrom's Crafting Interpreters (free at craftinginterpreters.com). The "Lox" language he builds is a dynamically-typed scripting language, but the architecture is identical to what we're building here. Reading Part I (the tree-walking interpreter) is strongly recommended before touching any code in this module.
This is the most important design decision in the whole document. We do not try to interpret all of Java. We interpret the Java that appears in Java-Chan's lessons. The curriculum drives the interpreter's feature set, not the other way around.
These features are needed from day one. Every lesson in Unit 1 that has a Phase 3 coding exercise uses some combination of these.
| Feature | Example | Notes |
|---|---|---|
| Primitive types | int, double, boolean, char |
Full value semantics |
| String literals | "Hello" |
Immutable, concatenation via + |
| Variable declaration & assignment | int x = 5; |
Typed declarations, no inference |
| Arithmetic operators | +, -, *, /, % |
Integer and double |
| Comparison operators | ==, !=, <, >, <=, >= |
Returns boolean |
| Logical operators | &&, ||, ! |
Short-circuit evaluation |
| String concatenation | "Age: " + age |
Mixed-type + where one side is String |
if / else if / else |
Standard branching | Nested supported |
switch / case / break / default |
Integer and String switches | Fall-through supported |
for loop |
for (int i = 0; i < 10; i++) |
Classic three-part form |
| Enhanced for-each | for (int n : array) |
1D arrays; break/continue supported |
while loop |
while (condition) |
Standard |
do-while loop |
do { } while (condition) |
Standard |
break / continue |
Loop control | Both supported |
| 1D arrays | int[] arr = new int[5]; |
Fixed size, index access |
| Array initializer | int[] arr = {1, 2, 3}; |
1D and 2D initializer syntax |
| 2D arrays | int[][] grid = new int[3][3]; |
Row/column access |
System.out.println |
Standard output | Newline after |
System.out.print |
Standard output | No newline |
| Static method declaration | static int add(int a, int b) |
Within the main class |
| Static method calls | add(5, 3) |
Within the same class |
| Return values | return result; |
All primitive and String types |
| Type casting | (int) 3.7 |
Explicit narrowing casts |
| Ternary operator | condition ? a : b |
Right-associative; nesting supported |
Math methods |
Math.PI, Math.sqrt(), Math.abs(), Math.max(), Math.min(), Math.pow() |
Static method dispatch |
These features come after the scripting subset is stable. They are harder — they require the interpreter to model an object system.
| Feature | Notes |
|---|---|
| Class definitions with fields | Instance variables typed and declared |
| Constructors | new ClassName(args) |
| Instance method calls | obj.method() |
this keyword |
Refers to the current object |
Inheritance (extends) |
Single inheritance, no diamond problem |
super() constructor call |
Delegating to parent constructor |
| Method overriding | Dynamic dispatch — the child's version runs |
instanceof |
Type checking expression |
| Static members | static fields and methods |
final keyword |
Constants |
| Enums | Simple value enums, no methods |
These features will never be interpreted by this engine. When a lesson involves them, the old pattern-matching / fill-in-the-blank validation handles Phase 3, and Phase 4 self-challenge handles the "try it in your own IDE" piece.
| Feature | Why |
|---|---|
Multithreading (Thread, synchronized, Executor) |
Cannot be safely modeled in a single-threaded JS environment. Race conditions, deadlocks, and timing-dependent behavior have no faithful representation. |
| JavaFX / Swing / AWT | GUI rendering requires a display context. Out of scope by design — these lessons use the self-challenge model. |
File I/O (FileReader, FileWriter, Scanner with files) |
No filesystem access in browser sandbox. |
Scanner with System.in |
Interactive stdin is out of scope. Lessons that need user input will use pre-seeded input values specified in lesson JSON. |
| Serialization | No meaningful browser equivalent. |
| Networking | Out of scope. |
| Reflection | Too deep — requires runtime type metadata the interpreter doesn't track. |
| Generics (beyond syntax tolerance) | The interpreter ignores generic type parameters (<T>) — they parse but carry no semantic weight. |
Exception handling (try/catch/finally/throw) |
Phase 2 target. The interpreter will surface runtime errors as interpreter errors, not as catchable Java exceptions. |
| Anonymous classes / lambdas | Complex parse surface for low curriculum coverage. |
All interpreter code lives in a new directory: src/interpreter/. It is completely self-contained. Nothing in the existing codebase changes to add interpreter support — the integration is additive.
src/
├── interpreter/
│ ├── index.js ← public API — the only file the rest of the app touches
│ ├── Lexer.js ← tokenizer: source string → token array
│ ├── Parser.js ← token array → AST
│ ├── Evaluator.js ← AST → output + side effects
│ ├── Environment.js ← scope chain for variable lookup
│ ├── JavaObject.js ← runtime representation of a Java object (Phase 2)
│ ├── StandardLibrary.js ← Math, String methods, System.out
│ ├── InterpreterError.js ← structured error type (runtime vs compile-time)
│ └── __tests__/
│ ├── lexer.test.js
│ ├── parser.test.js
│ ├── evaluator.test.js
│ └── fixtures/ ← sample Java snippets for test cases
│
├── components/
│ └── lesson/
│ ├── SandboxEditor.jsx ← NEW: full-code editor panel for interpreter lessons
│ └── SandboxEditor.css
│
├── utils/
│ └── patternMatcher.js ← UNCHANGED — still handles MCQ and fill-in-the-blank
│
└── data/
└── lessons/ ← lesson JSON gets new optional fields (see below)
This is the only file LessonCanvas or any other component ever imports from the interpreter. Everything else is internal.
/**
* run(sourceCode, options) → ExecutionResult
*
* @param {string} sourceCode — the Java source string to execute
* @param {object} options
* @param {string[]} [options.stdin] — pre-seeded input lines for Scanner
* @param {number} [options.timeout] — max execution time in ms (default: 3000)
*
* @returns {ExecutionResult}
* {
* stdout: string, — everything printed via System.out
* stderr: string, — compiler/runtime error message if any
* success: boolean, — true if execution completed without error
* errorType: string|null, — 'LexError' | 'ParseError' | 'RuntimeError' | null
* errorLine: number|null, — line number of the error, if known
* }
*/
export function run(sourceCode, options = {}) { ... }That's the entire contract. run() in, ExecutionResult out. No state. No side effects outside the return value. Pure function.
The lexer takes a source string and returns an array of Token objects.
Token shape:
{ type: TokenType, value: any, line: number }Token types to implement (exhaustive for Phase 1 scope):
// Literals
NUMBER, STRING, BOOLEAN, NULL, CHAR,
// Identifiers & keywords
IDENTIFIER,
KEYWORD_INT, KEYWORD_DOUBLE, KEYWORD_BOOLEAN, KEYWORD_CHAR,
KEYWORD_STRING, KEYWORD_VOID, KEYWORD_RETURN,
KEYWORD_IF, KEYWORD_ELSE, KEYWORD_FOR, KEYWORD_WHILE, KEYWORD_DO,
KEYWORD_BREAK, KEYWORD_CONTINUE,
KEYWORD_NEW, KEYWORD_CLASS, KEYWORD_STATIC, KEYWORD_PUBLIC,
KEYWORD_PRIVATE, KEYWORD_FINAL, KEYWORD_THIS, KEYWORD_SUPER,
KEYWORD_EXTENDS, KEYWORD_IMPLEMENTS, KEYWORD_INTERFACE,
KEYWORD_SWITCH, KEYWORD_CASE, KEYWORD_DEFAULT,
KEYWORD_NULL, KEYWORD_TRUE, KEYWORD_FALSE,
// Operators
PLUS, MINUS, STAR, SLASH, PERCENT,
EQUALS, EQUALS_EQUALS, BANG_EQUALS, BANG,
LESS, LESS_EQUALS, GREATER, GREATER_EQUALS,
AND_AND, PIPE_PIPE,
PLUS_EQUALS, MINUS_EQUALS, STAR_EQUALS, SLASH_EQUALS,
PLUS_PLUS, MINUS_MINUS,
// Delimiters
LEFT_PAREN, RIGHT_PAREN,
LEFT_BRACE, RIGHT_BRACE,
LEFT_BRACKET, RIGHT_BRACKET,
SEMICOLON, COMMA, DOT, COLON, QUESTION,
// Meta
EOFThe lexer skips whitespace and comments (// and /* */). It tracks the current line number so every token carries a line for error reporting.
Key implementation notes:
- String literals need escape handling:
\",\\,\n,\t - Char literals use single quotes:
'A','\n' - Numbers: distinguish
intfromdoubleby the presence of.in the literal ++and--are single tokens, not twoPLUStokens — order of matching matters- Keywords are recognized by checking the identifier's value against a Set after lexing it as an IDENTIFIER
The parser takes the token array and returns an AST. The root node of a well-formed Java-Chan program is always a Program node containing a list of class declarations.
AST node types (Phase 1 scope):
Program
└── ClassDeclaration (name, members[])
└── MethodDeclaration (name, params[], returnType, body)
└── BlockStatement (statements[])
├── VariableDeclaration (type, name, initializer?)
├── AssignmentStatement (target, operator, value)
├── ExpressionStatement (expression)
├── IfStatement (condition, thenBranch, elseBranch?)
├── ForStatement (init, condition, update, body)
├── EnhancedForStatement (varType, varName, iterable, body)
├── WhileStatement (condition, body)
├── DoWhileStatement (body, condition)
├── SwitchStatement (discriminant, cases[])
├── ReturnStatement (value?)
├── BreakStatement
└── ContinueStatement
Expression nodes:
BinaryExpression (left, operator, right)
UnaryExpression (operator, operand, prefix)
LogicalExpression (left, operator, right)
TernaryExpression (condition, consequent, alternate)
AssignmentExpression (target, operator, value)
CallExpression (callee, args[])
MemberExpression (object, property)
ArrayAccessExpression (array, index)
ArrayCreationExpression (type, size)
ArrayInitializerExpression (elements[])
NewExpression (className, args[])
CastExpression (type, expression)
Identifier (name)
NumberLiteral (value)
StringLiteral (value)
BooleanLiteral (value)
CharLiteral (value)
NullLiteral
The parser uses recursive descent — one function per grammar rule, each calling downward into the rules it depends on. Operator precedence is handled by the ordering of expression parsing functions (assignment → logical-or → logical-and → equality → comparison → addition → multiplication → unary → postfix → primary), not by a precedence table. This is the standard approach and produces very readable code.
Key implementation notes:
- The parser must handle both
int x = 5;(declaration) andx = 5;(assignment) — these look identical in the first few tokens and need lookahead to distinguish - Type names (
int,String,int[]) are parsed as a distinctTypeAnnotationnode, not as expressions System.out.println(...)parses as a chain of member accesses ending in a call:MemberExpression(MemberExpression(Identifier("System"), "out"), "println")— the evaluator recognizes this pattern and routes it to the standard library- Array declarations:
int[] arr = new int[5]— the[]after the type is part of the type node, andnew int[5]is anArrayCreationExpression - Generic type parameters (
ArrayList<String>) are parsed and attached to the type node but carry no semantic weight in Phase 1 — the evaluator ignores them
The evaluator walks the AST and executes it. It maintains an Environment (scope chain) and captures output.
Environment (Environment.js):
class Environment {
constructor(parent = null) {
this.vars = new Map();
this.parent = parent; // enclosing scope
}
get(name) {
if (this.vars.has(name)) return this.vars.get(name);
if (this.parent) return this.parent.get(name);
throw new InterpreterError(`Variable '${name}' is not defined`, 'RuntimeError');
}
set(name, value) {
if (this.vars.has(name)) { this.vars.set(name, value); return; }
if (this.parent) { this.parent.set(name, value); return; }
throw new InterpreterError(`Variable '${name}' is not defined`, 'RuntimeError');
}
define(name, value) {
this.vars.set(name, value); // always defines in current scope
}
}When entering a block ({}), a new child Environment is created. When leaving, it's discarded. This gives correct block scoping without any manual cleanup.
Execution control (break/continue/return):
Java's break, continue, and return are non-local control flow — they jump out of the middle of recursive evaluation. The standard approach is to use a sentinel value (a special JS object) that propagates up the call stack until something catches it:
const BREAK_SIGNAL = { __signal: 'break' };
const CONTINUE_SIGNAL = { __signal: 'continue' };
class ReturnSignal { constructor(value) { this.value = value; } }When the evaluator encounters break, it returns BREAK_SIGNAL. The for-loop evaluator checks its result after each iteration and exits if it sees BREAK_SIGNAL. Same for continue. return works the same way — the ReturnSignal carries the return value and propagates up until the method-call evaluator catches it and unwraps .value.
Timeout protection:
Student code can contain infinite loops. The evaluator must not hang the browser tab. The approach: track an operation counter that increments on every statement execution and every loop iteration. If it exceeds a configurable threshold (default: 100,000 operations), throw a RuntimeError with message "Execution timed out — check for an infinite loop." Java-Chan reacts with her frustrated expression.
class Evaluator {
constructor(options = {}) {
this.output = [];
this.opCount = 0;
this.opLimit = options.timeout || 100_000;
}
tick() {
this.opCount++;
if (this.opCount > this.opLimit) {
throw new InterpreterError(
"Execution timed out — check for an infinite loop~ 😓",
'RuntimeError'
);
}
}
}Call this.tick() at the top of every loop iteration and every statement evaluation.
Standard Library (StandardLibrary.js):
Rather than trying to implement a real Java standard library, we implement only the methods that appear in CS22301 lessons. The standard library is a plain JS object that the evaluator consults when it sees a CallExpression on a known receiver.
⚠️ Doc outdated vs implementation (Task 6 done): The pseudocode below shows raw JS values as arguments. The realStandardLibrary.jsusesJavaValuewrappers ({ value, javaType }) throughout — all args arrive as JavaValues, all return values are JavaValues, andjavaToStringreads.javaTypeto format correctly. ThemkInt/mkDouble/etc. constructors andraw()helper are exported fromStandardLibrary.js. See the source file for the canonical implementation.
export const STD = {
'System.out.println': (args, output) => {
output.push(args.map(javaToString).join('') + '\n');
},
'System.out.print': (args, output) => {
output.push(args.map(javaToString).join(''));
},
'Math.sqrt': ([x]) => Math.sqrt(x),
'Math.abs': ([x]) => Math.abs(x),
'Math.max': ([a, b]) => Math.max(a, b),
'Math.min': ([a, b]) => Math.min(a, b),
'Math.pow': ([a, b]) => Math.pow(a, b),
'Math.floor': ([x]) => Math.floor(x),
'Math.ceil': ([x]) => Math.ceil(x),
'Math.round': ([x]) => Math.round(x),
'String.valueOf': ([x]) => javaToString(x),
// String instance methods — called as obj.method()
'.length': (str) => str.length,
'.charAt': (str, [i]) => str[i] ?? '',
'.substring': (str, [a, b]) => b !== undefined ? str.slice(a, b) : str.slice(a),
'.toUpperCase': (str) => str.toUpperCase(),
'.toLowerCase': (str) => str.toLowerCase(),
'.equals': (str, [other]) => str === other,
'.contains': (str, [sub]) => str.includes(sub),
'.trim': (str) => str.trim(),
'.indexOf': (str, [sub]) => str.indexOf(sub),
'.replace': (str, [from, to]) => str.split(from).join(to),
};
// Java's string conversion rules:
// null → "null", boolean → "true"/"false", char → single char, etc.
function javaToString(val) {
if (val === null) return 'null';
if (val === true) return 'true';
if (val === false) return 'false';
if (typeof val === 'number') {
// Java prints integers without decimal: 5 not 5.0
// But doubles with decimal: 5.0 not 5
// We track type through evaluation to handle this correctly
return String(val);
}
return String(val);
}Math.PI is a field access, not a method call — the evaluator recognizes MemberExpression(Identifier("Math"), "PI") and returns Math.PI.
This is the most careful part. The interpreter must integrate without breaking anything in Phase 1.
A lesson opts into interpreter execution by adding a field to its phase3:
"phase3": {
"executionMode": true,
"scaffoldCode": "public class Main {\n public static void main(String[] args) {\n // your code here\n }\n}",
"expectedOutput": "Hello, World!\n",
"openingDialogue": "Alright, write it yourself this time~"
}When executionMode is absent or false, Phase 3 behaves exactly as it does today — ScaffoldEditor with fill-in-the-blank inputs, validated by blankValidator.js. When it's true, Phase 3 renders SandboxEditor instead, and validation goes through src/interpreter/index.js.
No existing lessons change behavior. The executionMode field is opt-in. Every lesson without it continues to work exactly as before. The rollout is additive.
SandboxEditor replaces ScaffoldEditor for interpreter-mode lessons. It renders:
- A
<textarea>(or Monaco Editor if we add it later) pre-filled withphase3.scaffoldCode - A Run button
- An Output panel showing
stdoutfrom the interpreter - An Error panel showing
stderrif execution failed - A validation feedback strip (same CSS classes as the existing
.validation-feedbackstrip inLessonCanvas)
The Run button calls run(sourceCode) from src/interpreter/index.js, gets back an ExecutionResult, and:
- If
successis false → showstderrin the error panel, set Java-Chan's expression tooopsorfrustrateddepending on attempt count - If
successis true butstdout.trim() !== expectedOutput.trim()→ show both outputs side-by-side ("Expected: ... / Got: ..."), set expression tothinking - If
successis true and output matches →passed, play success sound, award XP, advance to Phase 4 — exactly the same path as a correct blank submission today
One new conditional inside the Phase 3 block:
{currentPhase === 3 && (
<motion.div ...>
<h2 className="phase-heading phase-heading--try">✎ Code It With Me</h2>
{phase3?.executionMode ? (
<SandboxEditor
scaffoldCode={phase3.scaffoldCode}
expectedOutput={phase3.expectedOutput}
onPass={handleSandboxPass}
/>
) : hasBlanks ? (
<ScaffoldEditor ... /> {/* existing, unchanged */}
) : (
<div className="scaffold-stub-notice">...</div> {/* existing, unchanged */}
)}
</motion.div>
)}handleSandboxPass is a new handler that mirrors handleSubmitBlanks's success path — records attempts, calculates XP, calls completeLesson, advances to Phase 4.
patternMatcher.js— still used for Phase 4 MCQblankValidator.js— still used for fill-in-the-blank Phase 3 lessonsprogressStore.js— no changes;completeLessonandrecordAttemptare reused as-isxpCalculator.js— no changes;calculateEarnedXPis reused as-islessonStore.js— no changesjavaHighlighter.js— Phase 1 and 2 code display is unaffected- All existing lesson JSON files
After Task 9, the team reviewed the lesson migration plan and made a product decision: lessons will not be migrated to executionMode. The reasons, documented for future contributors:
Why the migration plan was dropped:
Every Unit 1 lesson was designed for fill-in-the-blank. The scaffold code targets token recall — "what keyword goes here?" — not behavioral output. Forcing those lessons into executionMode would require rewriting their scaffold code entirely, which is content surgery, not engineering. It risks breaking what already works, and the pedagogical value is unclear: a student who fills in i++ correctly and a student who writes a working loop body are being tested on different skills. Conflating them serves neither.
The interpreter is also a living piece of code — it will keep growing as more Java features are added. Tying every growth step to lesson migration creates a dependency chain where the interpreter can't improve without triggering a content review cycle. That's a bad coupling.
The new direction:
The interpreter becomes a Global Sandbox — a floating scratchpad available everywhere in the app, not tied to any lesson. Students can open it at any time, write whatever Java they want (within interpreter scope), and run it. No expected output, no validation, no XP. Just a place to experiment.
This solves the original problem better than lesson migration did: students can test code while studying a lesson, without the lesson needing to change. The interpreter keeps improving — every new feature just makes the sandbox more capable. The lessons stay stable.
The sandbox is a floating panel accessible from anywhere in the app via a persistent trigger button. It reuses SandboxEditor.jsx (already built in Task 8) with the lesson-validation wiring removed.
Three reasonable approaches — pick one before starting Task 10:
Option A — Floating action button (FAB) + modal overlay
A </> or terminal icon button fixed to the bottom-right corner of the screen, outside all route components. Clicking it opens a full-screen or large modal overlay with the sandbox inside. Closing the modal returns the user exactly where they were. This is the most mobile-friendly option and the least likely to interfere with existing layouts.
┌─────────────────────────────────┐
│ App (any route) │
│ │
│ [</>] │ ← fixed FAB, always visible
└─────────────────────────────────┘
On click:
┌─────────────────────────────────┐
│ ╔═══════════════════════════╗ │
│ ║ Java Sandbox [✕] ║ │
│ ║ ┌─────────────────────┐ ║ │
│ ║ │ code editor │ ║ │
│ ║ └─────────────────────┘ ║ │
│ ║ [Run] ║ │
│ ║ ┌─────────────────────┐ ║ │
│ ║ │ output │ ║ │
│ ║ └─────────────────────┘ ║ │
│ ╚═══════════════════════════╝ │
└─────────────────────────────────┘
Option B — Slide-in drawer A panel that slides in from the right (or bottom on mobile), covering part of the screen without fully obscuring the lesson beneath. The student can still see their lesson content while the sandbox is open. Better for reference use ("let me just quickly test this").
┌──────────────────┬──────────────┐
│ Lesson content │ Java Sandbox │ ← drawer slides in from right
│ (still visible)│ code editor │
│ │ [Run] │
│ │ output │
└──────────────────┴──────────────┘
Option C — Dedicated route /sandbox
A full-page sandbox accessible via the main nav. No floating button — the student navigates to it like any other page. Simplest to implement, but breaks the "always accessible" requirement — the student has to leave their lesson to use it.
Recommendation: Option A (FAB + modal) for the initial implementation. It's the least invasive to the existing layout, works on both desktop and mobile, and can be upgraded to Option B later if students prefer the side-by-side feel. Option C can exist in parallel as a dedicated route for power users once the FAB version is stable.
The sandbox panel is a new top-level component — not part of any route:
src/
├── components/
│ └── sandbox/
│ ├── GlobalSandbox.jsx ← the modal/drawer shell + open/close state
│ ├── GlobalSandbox.css
│ └── SandboxTrigger.jsx ← the FAB button (renders the trigger only)
SandboxEditor.jsx (already in src/components/lesson/) is reused as-is for the editor + output panel. GlobalSandbox.jsx wraps it without passing expectedOutput or onPass — those props become optional in SandboxEditor, and when absent, the validation strip is hidden and "Run" just shows output.
GlobalSandbox is rendered once at the app-shell level (in App.jsx or the root layout), so it persists across route changes. Its open/close state lives in a lightweight Zustand store slice or simple React context — not in progressStore or lessonStore, since it has nothing to do with lesson progress.
- Editor content — the code the student was working on. Should survive route navigation (the store holds it). Optionally persisted to
localStorageso it survives page refresh. - Output — last run result. Cleared on editor change or explicitly by a Clear button.
- Open/closed state — in the store, so it can be toggled from multiple places if needed.
expectedOutput and onPass are both optional. When neither is provided, SandboxEditor enters sandbox mode:
// Lesson mode (full grading):
<SandboxEditor
scaffoldCode={phase3.scaffoldCode}
expectedOutput={phase3.expectedOutput}
onPass={handleSandboxPass}
/>
// Sandbox mode (free-play — no grading):
<SandboxEditor
scaffoldCode={defaultCode}
/>Sandbox mode behaviour (implemented — Task 10a done):
- Only the raw output (or error) is shown after Run. No diff panel, no validation strip, no attempt counter.
- Java-Chan is not notified. No
setExpression, nosetDialogue, noplay('error'). The student is experimenting freely — reacting to "wrong" output would be hostile to that goal. There is no wrong output in the sandbox. - The Run button is never disabled. There is no terminal "passed" state.
sandboxModeis detected asexpectedOutput === undefined && onPass === undefined— both must be absent. PassingexpectedOutput=""still activates lesson mode.
A small, tasteful FAB. Java-Chan's design language is warm and educational — the trigger shouldn't feel like a developer tool. Options:
- A terminal/code icon (
</>) with a subtle hover animation - Java-Chan's chibi face with a tiny code symbol
- A simple "Try it" pill button that fits the existing button styles
Position: fixed, bottom-right, above any existing fixed elements (e.g. if there's a bottom nav on mobile, stack above it with a bottom offset).
Not every lesson needs This section is superseded by the Global Sandbox direction. Lessons will not be migrated. The executionMode.executionMode field remains in the schema and SandboxEditor still works as a lesson component — if a future content decision changes this, the infrastructure is already there. But no lesson migration is planned.
The existing fill-in-the-blank validation (blankValidator.js, patternMatcher.js) remains the sole validation path for all current lessons.
The original four-milestone plan was written when the interpreter's goal was lesson migration. That goal has changed — see Direction Change: The Global Sandbox. The milestones below reflect the new direction: build the sandbox first, then keep expanding interpreter capability indefinitely. There is no fixed end state. The interpreter grows as long as the app does.
Lexer, Parser, Evaluator for Unit 1's Java feature set. No classes, no objects. Static methods only. System.out, arithmetic, control flow, arrays, Math, ternary, enhanced for-each, array initializers.
Done: 233 tests passing. All features in the In Scope — Unit 1 table implemented and fixture-verified.
Ship the floating sandbox panel as a globally accessible feature. This is the priority after Task 9.
Definition of done:
- A trigger button (FAB or equivalent) is visible on every screen in the app
- Clicking it opens the sandbox panel without navigating away or disrupting lesson state; the drawer has a clearly visible close button
- Students can write, edit, and run any Java code within the interpreter's current feature scope
- Output is shown inline; errors are displayed as raw interpreter output — Java-Chan does not react to sandbox runs (no expressions, no sounds, no judgement — the sandbox is a free-play space)
- Editor content survives route navigation within a session
- The sandbox panel can be opened and closed without side effects
Files: src/components/sandbox/GlobalSandbox.jsx, SandboxTrigger.jsx, CSS, app-shell wiring in App.jsx. SandboxEditor.jsx gets minor props changes to support lesson-free mode.
Implement JavaObject.js and the class/instance/inheritance machinery. This is the hardest milestone — it requires the evaluator to model an object system rather than just static execution.
Definition of done:
- A class with fields, a constructor, and instance methods can be defined and instantiated
thiscorrectly refers to the current instanceextendscreates a prototype chain;super()delegates to the parent constructor- Method overriding: if
Dog extends Animaland both definespeak(), callingspeak()on aDogrunsDog's version instanceofreturns the correct boolean- Full fixture coverage for all new node types
Sandbox impact: After this milestone, students can write and run OOP programs — classes, objects, inheritance — in the sandbox. A major capability jump.
Implement ArrayList and HashMap as built-in interpreted types, backed by JS arrays and Maps respectively. Special-cased in the evaluator — new ArrayList<Integer>() creates a JS array with a Java-ArrayList-shaped interface.
ArrayList methods: add(val), get(i), set(i, val), size(), remove(i), contains(val), isEmpty(), iterator support (for (T item : list))
HashMap methods: put(k, v), get(k), containsKey(k), keySet(), values(), size()
After the object system and collections are in place, the interpreter has covered essentially the full CS22301 curriculum scope. From that point, expansion is driven by what students actually try to run in the sandbox — not by a fixed plan.
Likely additions over time, in rough priority order:
String.format()/printf- Exception handling (
try/catch/throw) — modeling Java's exception hierarchy as interpreter-level constructs - Method overloading (multiple methods with the same name, different arity/types)
- Varargs
- Interfaces (structural —
implementsis already parsed; evaluation is the gap) - Basic generics (enough to make
ArrayList<T>feel real rather than erased) - Multi-dimensional array operations beyond 2D
- Additional
MathandStringmethods as students hit them
Each addition follows the same process: fixture first, failing test, implementation, passing test, done. The "Contributing: How to Add a Feature" section covers the mechanics.
No lesson migration is planned at any milestone. The sandbox is the delivery vehicle. Lessons stay as they are.
Errors are a teaching opportunity. Java-Chan's interpreter errors should read like Java-Chan, not like a C compiler.
Every InterpreterError has a type (LexError, ParseError, RuntimeError), a message, and optionally a line. The SandboxEditor component renders these with Java-Chan's voice:
| Actual error | Displayed message |
|---|---|
| Missing semicolon | Line 3: Looks like you forgot a semicolon! Every statement needs one~ 😅 |
| Undeclared variable | Line 7: I don't see where 'x' was declared — did you forget int x = ...? |
| Array index out of bounds | Runtime error: That index is out of range! Array has 5 elements, you tried index 7 🙈 |
| Type mismatch | Line 4: Can't assign a String where an int belongs. Types have to match! |
| Infinite loop / timeout | Execution stopped: looks like this might be an infinite loop... check your condition~ 😓 |
| Division by zero | Runtime error: Can't divide by zero! Java throws ArithmeticException here 📖 |
These messages are defined in InterpreterError.js as a mapping from error codes to templates. New contributors should add error messages here rather than scattering strings through the evaluator.
The interpreter is the highest-stakes code in this project — a bug here produces wrong output that misleads students. Every evaluator feature needs a test.
Tests live in src/interpreter/__tests__/. Use whatever test runner is already in the project (Vitest, since the project uses Vite). Tests call run(sourceString) directly and assert on stdout.
Test fixture structure:
__tests__/fixtures/
unit1/
hello_world.java
variables_basic.java
for_loop_sum.java
array_access.java
...
unit2/
inheritance_basic.java
override.java
...
Each fixture is a complete valid Java program (one that runs in a real JVM). The test asserts that our interpreter produces the same stdout as the real JVM would. This keeps us honest — if our output diverges from real Java, that's a bug, not a design decision.
Minimum coverage targets:
| Module | Minimum tests |
|---|---|
| Lexer | One test per token type; one test for each escape sequence; malformed input cases |
| Parser | One test per AST node type; operator precedence; nested blocks |
| Evaluator | All arithmetic operators; all control flow structures; method calls; array operations; standard library methods; timeout trigger; each error type |
A few specific pitfalls to avoid that are common when building interpreters in this situation:
Don't try to reuse javaHighlighter.js as a lexer. The highlighter is a regex-based colorizer, not a proper tokenizer. It doesn't track state, doesn't handle context, and doesn't produce structured tokens the parser can consume. They share some vocabulary but serve different purposes. Lexer.js is a fresh implementation.
Don't eval() the student's code. Converting Java to JavaScript and running it with eval() is not an interpreter — it's a transpiler, and a dangerous one. Students could inject JS that escapes the sandbox. The AST-walking approach is safe because you control every execution step.
Don't implement Java's type system fully. Java's type system is one of the most complex in any mainstream language (generics, wildcards, intersection types, type erasure). We need just enough type tracking to handle int vs double arithmetic and to correctly format output (Java prints 5 not 5.0 for integers). Beyond that, type annotations are decorative in our interpreter.
Don't implement exceptions as exceptions. Java's try/catch semantics are their own thing. When the interpreter hits a runtime error, it surfaces it as an InterpreterError to the UI — it doesn't try to model Java's exception hierarchy. That's Phase 2 scope anyway.
Don't implement Scanner with real stdin. Lessons that use Scanner for user input will be given a stdin array in the lesson JSON — pre-seeded input values that the interpreter delivers one line at a time when Scanner.nextLine() or Scanner.nextInt() is called. This gives deterministic, testable behavior without needing a real interactive terminal.
When you need to add a new Java feature to the interpreter:
-
Write a fixture first. Put a
.javafile in__tests__/fixtures/that uses the feature and produces known output. Confirm the output is correct by running it against a real JVM. -
Write a failing test. Call
run(fixture)and assert onstdout. It should fail because the feature isn't implemented yet. -
Add the token(s) to
Lexer.js. If the feature introduces new syntax (instanceof, a new keyword), add the token type and the lexer rule. -
Add the AST node(s) to
Parser.js. Figure out where in the recursive descent grammar the new syntax appears, add the parsing function or extend an existing one, return the new node type. -
Add the evaluation case to
Evaluator.js. Handle the new node type inevaluate(). Add it toStandardLibrary.jsif it's a standard library method. -
Run the test. It should pass. If it doesn't, the evaluator logic is wrong.
-
Update this doc. If the new feature changes the scope tables, AST node list, token set, or sandbox capability in a meaningful way, update the relevant sections. Keep the doc honest.
-
Run all existing tests. Adding a feature should never break existing tests. If it does, something in the shared evaluation path changed in a way that needs investigation.
Tasks are listed in dependency order. The same session rules apply: take the first unchecked task, finish it fully (implementation + tests + passing npm run test + lint clean), check the box, update Status at the top, commit in the same PR.
Rules for picking up a task:
- Take the first unchecked
[ ]task from the top. If you're not sure it's actually free, check this file's git history/blame first — someone may have claimed it mid-session without pushing yet. - Finish the whole task in one session. These are sized to be doable in a single sitting — implementation + tests + a passing
npm run testandnpm run lint. Don't leave a task half-done and checked off, and don't check off a task you didn't finish. - When done, check the box, add your name and the PR/commit, and update
Statusat the top of this doc if a milestone's Definition of Done is now met. - Commit the checkbox update in the same PR as the code — this file is the source of truth for "what's next," so it has to move in lockstep with the code, not after it.
If a task turns out to be bigger than one session, stop and split it into two sub-tasks in this list rather than pushing through — that keeps the "one task, one session" rule honest for whoever picks it up next.
-
Task 1 —
Lexer.js: full tokenizer. Implementtokenize()for every token type listed in §"1. Lexer": literals, keywords, operators (including++/--and compound assignment), delimiters, line tracking, whitespace/comment skipping, string/char escape handling. Fill in the real tests behindlexer.test.js'sit.todos, with fixtures under__tests__/fixtures/. Files:Lexer.js,__tests__/lexer.test.jsDone: 102 tests passing. Added fixtures:hello_world.java,variables_basic.java,for_loop_sum.java,array_access.java,static_method.java. Implementation notes:NUMBERtokens carry anisDoubleboolean for int-vs-double formatting in the Evaluator;true/false/nullare emitted asBOOLEAN/NULLliteral tokens (not keyword tokens) so the Parser never seesKEYWORD_TRUEetc. and can treat them as primary expressions directly. No pre-existing lint errors were introduced. -
Task 2 —
Parser.js: full AST for Phase 1 syntax. Implementparse()via recursive descent for every node in §"2. Parser": class/method declarations, all statement types, the full expression precedence chain, array declarations. Fill inparser.test.js. Files:Parser.js,__tests__/parser.test.js· Depends on: Task 1 Done: 46 tests passing (149 total across lexer+parser),npm run lintclean onsrc/interpreter/. Blocker found and fixed first: the Lexer's token set in this doc never includedCOLON, socase 1:couldn't lex at all — addedTokenType.COLONand the:rule toLexer.jsplus a test, ahead of Task 2 since switch/case depends on it. Node-shape decisions (documented in Parser.js's header comment):AssignmentStatement/AssignmentExpressionshare one code path —assignment()always builds anAssignmentExpression, and statement position rewraps it asAssignmentStatementwhen the whole statement is exactly that expression; declaration-vs-assignment lookahead treats anIDENTIFIERas a declaration start only if (after skipping an optional<...>generic and any[]pairs) anotherIDENTIFIERfollows; casts are only recognized for(+ a primitive-type keyword +)—(x)with an identifier is always a grouping, not a cast, to dodge the classic C-style ambiguity without a symbol table;newdispatches on[vs(to produceArrayCreationExpressionvsNewExpression(the latter parses now for Milestone 2 even though the evaluator won't support it until then). Modifiers (public/private/static/final) are accepted and onlystaticis tracked (MethodDeclaration.isStatic) — matches Phase 1 scope (static methods only). -
Task 3 —
Evaluator.js: expressions and variables. Wire up arithmetic/comparison/logical operators, variable declaration and assignment, string concatenation, type casting, andif/else if/else/switch, using the existingEnvironment.js. Start filling inevaluator.test.js. Files:Evaluator.js· Depends on: Task 2 Done (combined with Tasks 4–6 in one pass — see note below). 57 evaluator tests passing, 206 total. Key design decision: all values are wrapped in aJavaValueobject{ value, javaType }(defined and constructed inStandardLibrary.js) sojavaToStringcan distinguish5(int →"5") from5.0(double →"5.0") even after values flow through compound expressions.javaTypeis one of:'int' | 'double' | 'boolean' | 'char' | 'String' | 'null' | 'void' | 'array'. Theraw(jv)helper unwraps a JavaValue to its JS primitive for arithmetic.mkInt,mkDouble,mkBool,mkChar,mkString,mkNull,mkVoid,mkArrayconstructors are exported fromStandardLibrary.jsfor use throughout the evaluator.if/switchfall-through and all comparison/logical operators covered; short-circuit evaluation confirmed by test. -
Task 4 —
Evaluator.js: loops and control flow. Implementfor/while/do-while,break/continueviaBREAK_SIGNAL/CONTINUE_SIGNAL, and wiretick()into every loop iteration and statement so the timeout guard actually fires. Add the infinite-loop test. Files:Evaluator.js· Depends on: Task 3 Done (combined with Tasks 3, 5, 6). Sentinel approach:BREAK_SIGNALandCONTINUE_SIGNALare plain objects;ReturnSignalis a class carrying.value. EveryexecuteStatementand every loop iteration callsthis.tick().for-loop init runs in its own childEnvironmentso the loop variable is properly scoped.continuefalls through to the update expression (correct Java semantics). Timeout test passes with{ timeout: 1000 }option. -
Task 5 —
Evaluator.js: arrays and static methods. Implement 1D/2D array creation and indexed access, static method declarations/calls within a class, andreturnviaReturnSignal. Files:Evaluator.js· Depends on: Task 4 Done (combined with Tasks 3, 4, 6). 1D and 2D arrays implemented; elements are individually wrappedJavaValues with the correct default (mkInt(0)forint[], etc.). Array.lengthis handled inMemberExpressionfor bothStringandarrayjavaTypes. Static methods are registered inthis.methodsfromClassDeclaration.membersbeforemain()runs, so mutual recursion within the same class works. Each method call gets a freshEnvironment(null)— no closure over the caller, matching Java's static semantics.returnpropagates viaReturnSignal, caught incallMethod(). -
Task 6 —
StandardLibrary.js. Implement theSTDregistry (System.out.println/print, all listedMath.*methods, the listedStringinstance methods) andjavaToString()with Java's actual int-vs-double and null/boolean formatting rules. Files:StandardLibrary.js· Depends on: Task 5 Done (implemented alongside Tasks 3–5;StandardLibrary.jsis where theJavaValuewrapper and all constructors live).javaToStringhandles the int-vs-double distinction viajavaType:int → String(Math.trunc(v)),double → value includes '.' ? s : s + '.0'.Math.PIis exported asMATH_PI = mkDouble(Math.PI)and recognised as a field access inMemberExpression(not a call). String instance methods are keyed as'.length','.charAt', etc. inSTDand dispatched via theMemberExpressionpath inevaluateCall. Additional String methods beyond the doc's list —.startsWith,.endsWith,.isEmpty,.split,.toCharArray,.replace,.indexOf,.equalsIgnoreCase— added ahead of Task 6's spec since they appear in Unit 1 lesson content. -
Task 7 —
index.js: wire the pipeline. Implementrun(): Lexer → Parser → Evaluator, catchInterpreterErrorand map it to{ success, stderr, errorType, errorLine }, return the fullExecutionResultshape. Files:index.js· Depends on: Task 6 Done. Note: the public API shape in this doc's §"index.js" section (stdout,stderr,success,errorType,errorLine) differs from what was actually implemented:run()returns{ output: string, error: string | null }— simpler, and sufficient for whatSandboxEditorneeds. Theerrorstring is pre-formatted as"ErrorType(line N): message"so the UI can display it directly. If Task 8 needs the structured fields,index.jscan be extended without breaking callers. The doc's §"index.js" section should be treated as aspirational; the source is authoritative. -
Task 8 — Wire up
SandboxEditor.jsxandLessonCanvas.jsx. Make the Run button callrun(sourceCode), showstdout/stderrin their panels, diff againstexpectedOutput, and callonPass. Add thephase3?.executionModeconditional toLessonCanvas.jsxper §"Changes to LessonCanvas.jsx" and implementhandleSandboxPass. Files:SandboxEditor.jsx,SandboxEditor.css,LessonCanvas.jsx· Depends on: Task 7 Done. Build (npm run build) and full suite (206 tests) both green; lint clean on the three touched files (pre-existing lint errors elsewhere in the repo — XPDisplay.jsx, useLesson.js, Evaluator.js, Home.jsx, LessonPage.jsx, Shop.jsx, storageService.js, patternMatcher.js — are untouched and unrelated to this task). Responsibility split, deliberately different from ScaffoldEditor:SandboxEditorowns its own fail-path reactions (error/mismatch panels, Java-Chan's expression + dialogue on a wrong run) directly viauseLessonStore/useSound, the same direct-store-access patternPhase1SplitScreenalready uses forjournalStore. It only callsonPass()— matching this doc's literal 3-prop JSX example exactly — on a passing run;LessonCanvas's newhandleSandboxPassthen mirrors justhandleSubmitBlanks's success branch (record attempt, XP,completeLesson, celebrate, advance to Phase 4). One consequence: the attempt count drivingSandboxEditor's own hint/solution pacing (sameHINT_THRESHOLD/SOLUTION_THRESHOLD= 2/5 as the blanks flow) is a local run count for that mount, notprogressStore's persisted count — the persisted count (viarecordAttempt/getAttempts) is still what feeds the final XP calc on pass, same as blanks. Error panel shows the interpreter's raw"Type (line N): message"string as-is; this doc's §"Error Message Design" full voice-template rewrite (mapping each raw message to a Java-Chan-phrased line) needs the actual raw strings verified against real fixtures first, so it's deferred to when Task 9's fixtures land rather than guessed at now — verified via a throwaway vitest smoke check thatrun()on a missing-semicolon program actually returnsParseError (line N): Expected ';' after ..., matchingindex.js's documented shape. In the meantime the raw message (precise) plus a short Java-Chan reaction in thevalidation-feedbackstrip (encouraging) cover the same goal. Expected/Got diff on mismatch uses new.sandbox-editor__diff*classes inSandboxEditor.css; output/error/feedback panels reuse the existing.output-block/.error-block/.validation-feedbackclasses fromLessonCanvas.css, which the parent already imports. -
Task 9 — Milestone 1 fixtures and Definition of Done. Write fixtures for lessons 1.3–1.14 (each confirmed against a real JVM), confirm every one produces matching
stdout, and verify the rest of Milestone 1's Definition of Done in §"Milestone 1 — The Scripting Subset". Files:__tests__/fixtures/unit1/,Lexer.js,Parser.js,Evaluator.js,__tests__/evaluator.test.js· Depends on: Task 8 Done: 233 tests passing (up from 206). Three interpreter gaps discovered and filled during fixture authoring: (1) ternary operator (? :) —QUESTIONtoken added to Lexer,ternary()precedence level inserted betweenassignment()andlogicalOr()in Parser,TernaryExpressioncase added to Evaluator; (2) enhanced for-each (for (T item : collection)) — Parser'sparseForStatement()extended with lookahead that detectsType name :and buildsEnhancedForStatement;executeEnhancedFor()added to Evaluator, handlesbreak/continue/ReturnSignalcorrectly; (3) array initializer syntax ({1, 2, 3}/{{1,2},{3,4}}) —parseArrayInitializer()added to Parser'sprimary(),ArrayInitializerExpressioncase added to Evaluator. Ten new fixture files:while_loop.java,switch_statement.java,string_methods.java,math_methods.java,break_continue.java,type_casting.java,two_d_array.java,enhanced_for.java,multiple_methods.java,ternary.java— all produce output matching real JVM semantics. Milestone 1 Definition of Done: timeout guard ✓, error surface ✓, test coverage per AST node type ✓. Direction change: lesson migration plan dropped — see §"Direction Change: The Global Sandbox". -
Task 10 — Global Sandbox:
GlobalSandbox.jsx+ app-shell wiring. Build the floating sandbox panel and wire it into the app shell. See §"Global Sandbox — Design & Implementation Options" for the full spec and UI options — agree on Option A/B/C with the team before starting. The three sub-tasks below are the minimum deliverable; they may be split across sessions if needed.- 10a —
SandboxEditor.jsxprops update.expectedOutputandonPassare now optional. When both are absent (sandboxMode = true): only raw output or error is shown; diff panel, validation strip, and attempt counter are all hidden; Java-Chan is not notified (no expression/dialogue/sound side-effects — the sandbox is a judgement-free space); Run button is never disabled. Lesson mode (both props present) is completely unchanged.sandboxModeisexpectedOutput === undefined && onPass === undefined— explicitexpectedOutput=""still triggers lesson mode. Close button on the drawer also improved: visible border, enforced 32×32 min tap target, active scale feedback. - 10a.1 —
SandboxEditor.jsxeditor features. Four quality-of-life editor upgrades:- Syntax highlighting — transparent
<textarea>over a highlighted<pre>backdrop using the existingjavaHighlighter.tokenize()and.token-*classes fromCodeBlock.css. AhighlightCode()pre-pass in the component handles multi-line/* ... */block comments before the per-line tokenizer runs (javaHighlighter.jsitself is unchanged). Scroll is mirrored from textarea to backdrop viasyncScroll. - Bracket / quote auto-pair —
onKeyDownintercepts(,[,{,",'; inserts the closing counterpart and parks cursor between them. Typing a closing char over an existing one skips past it. Selecting text and typing an opener wraps the selection. - Auto-indent on Enter after
{— inserts newline + current line's indentation + one 4-space tab, then newline + indentation +}; cursor lands on the blank indented line. Plain Enter preserves the current line's indentation. - Tab key → 4 spaces —
preventDefaultkeeps focus in the editor; 4 spaces are spliced in at caret position. Files:src/components/lesson/SandboxEditor.jsx,src/components/lesson/SandboxEditor.css
- Syntax highlighting — transparent
- 10b —
GlobalSandbox.jsx+SandboxTrigger.jsx. The modal/drawer shell and the FAB trigger button.GlobalSandboxrendersSandboxEditorin lesson-free mode. Open/close state in a lightweight store slice or context. Editor content preserved across route changes within a session. - 10c — App-shell wiring. Render
SandboxTriggerandGlobalSandboxonce at the root level (inApp.jsxor equivalent) so they're available on every route including Home, Shop, and Lessons. Files:src/components/sandbox/GlobalSandbox.jsx,SandboxTrigger.jsx, their CSS,src/components/lesson/SandboxEditor.jsx(props change),App.jsx· Depends on: Task 9
- 10a —
After Task 10: The task queue becomes open-ended. Each new interpreter feature follows the same pattern — fixture, test, implementation, done — and gets its own task entry added here when it's scoped. The milestones in §"Build Phases & Milestones" give the broad direction; the task queue is where the specifics live. Add new tasks at the bottom of this list as they're planned, keeping the dependency chain clear.
Java-Chan's interpreter is a tree-walking Java subset interpreter written entirely in JavaScript, living in src/interpreter/, integrated additively into the existing app via the Global Sandbox — a floating panel available on every screen where students can write and run Java freely, without any lesson context.
Lessons are not migrated. The fill-in-the-blank validation (blankValidator.js, patternMatcher.js) remains the sole validation path for all current lessons. The executionMode field exists in the schema and SandboxEditor still works as a lesson component, but no lesson migration is planned.
The interpreter's roadmap is open-ended. Milestone 1 (the scripting subset) is complete. Milestone 2 (the Global Sandbox UI) is next. After that, the object system, collections, and whatever the students actually try to run come in rough priority order. Each addition follows the same fixture-first, test-driven process. There is no fixed endpoint.
The public API is a single function: run(sourceCode) → { output, error }. Everything else is internal. The rest of the app stays unchanged.
This document lives on the feature/java-interpreter branch and should be updated as implementation decisions are made. Major deviations from this plan — especially changes to the public API or the lesson JSON schema — should be noted here with a rationale before the code changes.