The old terminal-based REPL approach had critical issues:
- Hard-coded delays (500ms, 1000ms) that don't account for network latency
- Silent failures - no error detection or user feedback
- Race conditions - multiple commands could queue incorrectly
- State inconsistency - modules marked loaded even on failure
- Cross-platform issues - timing varies on different systems
// Old utils/repl.ts
export class ReplManager {
private terminals: Map<string, vscode.Terminal> = new Map();
async loadModule(workspacePath, modulePath) {
const terminal = await this.getOrCreateRepl(workspacePath);
terminal.sendText(`:load "${modulePath}"`);
// ❌ PROBLEM: Hard-coded delay with no guarantee
await new Promise(resolve => setTimeout(resolve, 1000));
// No verification that module actually loaded!
}
async evaluateInRepl(workspacePath, code) {
// ❌ PROBLEM: More hard-coded delays
await new Promise(resolve => setTimeout(resolve, 200));
terminal.sendText(code);
await new Promise(resolve => setTimeout(resolve, 300));
// Still no way to know if evaluation succeeded!
}
}Issues:
- Delays are arbitrary (200ms? 1000ms? What if the computer is slow?)
- No output parsing - can't detect errors
- Terminal visibility required - clutters workspace
- No promise-based API - can't handle async properly
- State updates blindly without checking success
// New ghci/GhciReplManager.ts
export class GhciReplManager {
private ghci: ChildProcessWithoutNullStreams;
private commandQueue: GhciCommand[] = [];
private PROMPT_REGEX = />\s*$/;
private ERROR_REGEX = /(error:|Failed,|Could not find module|parse error|Variable not in scope)/i;
async loadModule(filePath, moduleName, loadedModules) {
try {
// ✅ SOLUTION: Wait for actual GHCi prompt (event-driven)
const output = await this.runCommand(`:load ${filePath}`);
// ✅ SOLUTION: Update state ONLY on success
loadedModules.add(moduleName);
vscode.window.showInformationMessage(`Loaded ${moduleName}`);
return output;
} catch (err) {
// ✅ SOLUTION: Proper error feedback
vscode.window.showErrorMessage(`Failed to load ${moduleName}:\n${err}`);
throw err;
}
}
async evaluate(expression: string): Promise<string> {
try {
// ✅ SOLUTION: Promise-based, no delays needed
const output = await this.runCommand(expression);
return output; // Output is parsed and verified
} catch (err) {
vscode.window.showErrorMessage(`Evaluation failed: ${err}`);
throw err;
}
}
}// extension.ts (OLD)
const repl = await replManager.getOrCreateRepl(workspacePath);
repl.show(); // Terminal must be visible!
// Load with unknown timing
await replManager.loadModule(workspacePath, documentUri.fsPath);
// Add arbitrary delay hoping module is loaded
await new Promise(resolve => setTimeout(resolve, 500));
// Evaluate with no error checking
await replManager.evaluateInRepl(workspacePath, actualFunctionName);
// Add another delay hoping evaluation is done
await new Promise(resolve => setTimeout(resolve, 300));
// Hope everything worked... 😅
vscode.window.showInformationMessage(`Function '${originalFunctionName}' executed`);Problems:
- No error detection
- Multiple hard-coded delays
- Terminal visibility requirement
- No way to verify execution
- Race conditions possible
// extension.ts (NEW)
try {
// Load module - waits for prompt, detects errors
await ghciRepl.loadModule(filePath, moduleName, loadedModules);
// Evaluate - Promise resolves when GHCi responds
const result = await ghciRepl.evaluate(actualFunctionName);
// If we get here, it definitely succeeded
outputChannel.appendLine(`Result:\n${result}`);
vscode.window.showInformationMessage(`Function '${originalFunctionName}' executed`);
} catch (error) {
// Error handling is automatic and detailed
vscode.window.showErrorMessage(`Error: ${error}`);
outputChannel.appendLine(`[ERROR] ${error}`);
}Benefits:
- ✅ Automatic error detection
- ✅ No arbitrary delays
- ✅ Promise-based async/await
- ✅ Proper error handling
- ✅ State safety guaranteed
$ ghci
GHCi, version 9.2.5: https://www.haskell.org/ghc/
:load "Main.hs"
[1 of 1] Compiling Main ( Main.hs, interpreted )
Main>
-- ❌ OLD CODE: Just sends text, doesn't check response
GHCi process spawned
[GHCi] GHCi, version 9.2.5: https://www.haskell.org/ghc/
[GHCi] :load "Main.hs"
[GHCi] [1 of 1] Compiling Main ( Main.hs, interpreted )
[GHCi]
[GHCi] Main>
✓ Loaded Main successfully ← State updated only here
[GHCi] Main> main
[GHCi] Hello, World!
[GHCi] Main>
✓ Evaluation complete ← Prompt detected, result captured
$ ghci
GHCi, version 9.2.5
:load "Foo.hs"
<interactive>:1:1: error: parse error in input 'load'
-- ❌ OLD: Code doesn't notice the error
-- User sees: "Function executed" but nothing actually ran
[GHCi] :load "Foo.hs"
[GHCi] <interactive>:1:1: error: parse error in input 'load'
✗ Failed to load Foo:
<interactive>:1:1: error: parse error in input 'load'
-- ✅ NEW: Error regex matches and rejects promise
-- User sees: Error notification with details
// State remains unchanged (module not added to loadedModules)
Command 1: :load Main.hs ← sent at 0ms
Wait 1000ms...
Command 2: main ← sent at 1000ms
Wait 300ms...
Return success
-- Problem: If Command 1 takes 1500ms, Command 2 runs before Main is loaded!
Queue: [:load Main.hs, main]
Send: :load Main.hs
Wait for prompt: >
✓ Prompt detected
Process: main
Send: main
Wait for prompt: >
✓ Prompt detected, return result
-- No race conditions possible
-- Commands execute in guaranteed order
// Each command waits for actual completion
| Metric | Old | New |
|---|---|---|
| Hard-coded delays | 3+ | 0 |
| Error detection | None | Yes (regex) |
| Promise-based | No | Yes |
| State safety | No | Yes |
| Race condition risk | High | None |
| Lines of code (REPL) | ~80 | ~288 |
| Functionality gain | Limited | Complete |
User clicks "Load Module" on Main.hs
BEFORE:
1. Send :load command to terminal
2. Wait 1000ms (hope it's enough)
3. Assume success, mark as loaded
4. User may see "success" but module failed to load
AFTER:
1. Queue :load command
2. Read output stream
3. Wait for GHCi prompt (actual confirmation)
4. Parse output for errors
5. If error detected, reject and don't update state
6. If success, update state and notify user
File has syntax error: let x =
BEFORE:
$ ghci
:load "Bad.hs"
[1 of 1] Compiling Main ( Main.hs, interpreted )
Bad.hs:1:10: error: Parse error in input 'let'
>
-- ❌ Code: Still returns success, marks as loaded
AFTER:
[GHCi] :load "Bad.hs"
[GHCi] Bad.hs:1:10: error: Parse error in input 'let'
[GHCi] >
-- ✅ ERROR_REGEX matches "error:"
// Promise rejects with error message
// State unchanged (module NOT added to loadedModules)
BEFORE:
$ ghci
:load Main
[assumes success]
main
[waits 300ms]
return control
[user doesn't see output]
AFTER:
[GHCi] :load Main
[GHCi] Main>
✓ Load confirmed
[GHCi] main
[GHCi] Result: "Hello"
[GHCi] Main>
✓ Result captured and logged
[User sees result in output channel]
| Problem | Old | New |
|---|---|---|
| Timing | Hard-coded ❌ | Event-driven ✅ |
| Errors | Undetected ❌ | Auto-parsed ✅ |
| State | Unsafe ❌ | Safe ✅ |
| Race conditions | Possible ❌ | Prevented ✅ |
| Terminal dependency | Required ❌ | Not needed ✅ |
| User feedback | Minimal ❌ | Detailed ✅ |
| API quality | Terminal-based ❌ | Promise-based ✅ |
| Cross-platform | Fragile ❌ | Robust ✅ |
Analysis Date: January 9, 2026
Result: ✅ Complete architectural upgrade