Skip to content

Commit 0cc0bac

Browse files
Add configurable fail-safe controls to brainfuck interpreter (#104)
- add a fail-safe settings card with configurable timeout, tape length limit, and a disable toggle - persist fail-safe preferences in localStorage and apply them on load - enforce the configurable guard rails during program execution with clearer error reporting ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_69514885fabc8325a1f8cd2e84dcac95)
1 parent 8f2f56b commit 0cc0bac

1 file changed

Lines changed: 116 additions & 14 deletions

File tree

brainfuck-interpreter.html

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,34 @@ <h2 style="margin: 0;">Output</h2>
168168
</div>
169169
</div>
170170
</section>
171+
172+
<section class="surface tool-card">
173+
<div class="card-header">
174+
<h2 style="margin: 0;">Fail-safe settings</h2>
175+
<p style="margin: 0; color: var(--tx-2); max-width: 48ch;">Prevent runaway programs by stopping execution after a
176+
time limit or when the tape grows too large.</p>
177+
</div>
178+
179+
<div class="content-flow" style="--flow-space: 0.75rem;">
180+
<label class="content-flow" style="--flow-space: 0.25rem;">
181+
<span>Timeout (seconds)</span>
182+
<input id="failsafe-timeout" type="number" min="0" step="1" value="3" style="max-width: 200px;">
183+
<small class="input-help">Stops execution after this many seconds. Use 0 for no timeout.</small>
184+
</label>
185+
186+
<label class="content-flow" style="--flow-space: 0.25rem;">
187+
<span>Tape length limit (cells)</span>
188+
<input id="failsafe-tape-limit" type="number" min="0" step="1" value="0" style="max-width: 200px;">
189+
<small class="input-help">Interrupts execution if the tape exceeds this length. Use 0 to allow unlimited
190+
growth.</small>
191+
</label>
192+
193+
<label style="display: inline-flex; gap: 0.5rem; align-items: center;">
194+
<input id="failsafe-disabled" type="checkbox">
195+
<span>Disable the fail-safe entirely</span>
196+
</label>
197+
</div>
198+
</section>
171199
</main>
172200

173201
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.15/lib/codemirror.min.js"></script>
@@ -184,6 +212,9 @@ <h2 style="margin: 0;">Output</h2>
184212
const bfInput = document.getElementById('bf-input');
185213
const inputHelp = document.getElementById('input-help');
186214
const asciiModeRadio = document.getElementById('mode-ascii');
215+
const failsafeTimeoutInput = document.getElementById('failsafe-timeout');
216+
const failsafeTapeLimitInput = document.getElementById('failsafe-tape-limit');
217+
const failsafeDisabledInput = document.getElementById('failsafe-disabled');
187218

188219
const textarea = document.getElementById('code-input');
189220
const editor = CodeMirror.fromTextArea(textarea, {
@@ -195,11 +226,17 @@ <h2 style="margin: 0;">Output</h2>
195226
});
196227

197228
const STORAGE_KEY = 'brainfuck-interpreter-code';
198-
199-
class BrainfuckTimeoutError extends Error {
229+
const FAILSAFE_STORAGE_KEY = 'brainfuck-interpreter-failsafe-settings';
230+
const DEFAULT_FAILSAFE_SETTINGS = {
231+
timeoutSeconds: 3,
232+
tapeLengthLimit: 0,
233+
disabled: false,
234+
};
235+
236+
class BrainfuckFailSafeError extends Error {
200237
constructor(message, debugInfo) {
201238
super(message);
202-
this.name = 'BrainfuckTimeoutError';
239+
this.name = 'BrainfuckFailSafeError';
203240
this.debugInfo = debugInfo;
204241
}
205242
}
@@ -333,7 +370,54 @@ <h2 style="margin: 0;">Output</h2>
333370
return `${leftEllipsis ? '… ' : ''}${cells.join(' ')}${rightEllipsis ? ' …' : ''}`;
334371
}
335372

336-
function runProgram(program, inputs) {
373+
function getStoredFailsafeSettings() {
374+
try {
375+
const raw = localStorage.getItem(FAILSAFE_STORAGE_KEY);
376+
if (!raw) return { ...DEFAULT_FAILSAFE_SETTINGS };
377+
const parsed = JSON.parse(raw);
378+
return sanitizeFailsafeSettings(parsed);
379+
} catch (error) {
380+
return { ...DEFAULT_FAILSAFE_SETTINGS };
381+
}
382+
}
383+
384+
function sanitizeFailsafeSettings(settings) {
385+
const timeoutSeconds = Number.isFinite(settings.timeoutSeconds)
386+
? Math.max(0, Math.floor(settings.timeoutSeconds))
387+
: DEFAULT_FAILSAFE_SETTINGS.timeoutSeconds;
388+
const tapeLengthLimit = Number.isFinite(settings.tapeLengthLimit)
389+
? Math.max(0, Math.floor(settings.tapeLengthLimit))
390+
: DEFAULT_FAILSAFE_SETTINGS.tapeLengthLimit;
391+
const disabled = Boolean(settings.disabled);
392+
393+
return { timeoutSeconds, tapeLengthLimit, disabled };
394+
}
395+
396+
function persistFailsafeSettings(settings) {
397+
try {
398+
localStorage.setItem(FAILSAFE_STORAGE_KEY, JSON.stringify(settings));
399+
} catch (error) {
400+
// Ignore persistence errors and continue.
401+
}
402+
}
403+
404+
function applyFailsafeSettings(settings) {
405+
failsafeTimeoutInput.value = settings.timeoutSeconds;
406+
failsafeTapeLimitInput.value = settings.tapeLengthLimit;
407+
failsafeDisabledInput.checked = settings.disabled;
408+
}
409+
410+
function collectFailsafeSettingsFromInputs() {
411+
const settings = sanitizeFailsafeSettings({
412+
timeoutSeconds: Number(failsafeTimeoutInput.value),
413+
tapeLengthLimit: Number(failsafeTapeLimitInput.value),
414+
disabled: failsafeDisabledInput.checked,
415+
});
416+
persistFailsafeSettings(settings);
417+
return settings;
418+
}
419+
420+
function runProgram(program, inputs, failsafeSettings) {
337421
const commands = program.replace(/[^\>\<\+\-\.\,\[\]]/g, '');
338422
const jumpMap = buildJumpMap(commands);
339423

@@ -342,17 +426,21 @@ <h2 style="margin: 0;">Output</h2>
342426
let inputIndex = 0;
343427
let ip = 0;
344428
const output = [];
345-
const timeoutMs = 3000;
429+
const timeoutMs = failsafeSettings.timeoutSeconds * 1000;
346430
const startTime = performance.now();
347431

432+
function enforceFailSafe(message) {
433+
const debugInfo = [
434+
`Tape length: ${tape.length}`,
435+
`Pointer position: ${pointer}`,
436+
`Tape window: ${formatTapeWindow(tape, pointer)}`,
437+
].join('\n');
438+
throw new BrainfuckFailSafeError(message, debugInfo);
439+
}
440+
348441
while (ip < commands.length) {
349-
if (performance.now() - startTime > timeoutMs) {
350-
const debugInfo = [
351-
`Tape length: ${tape.length}`,
352-
`Pointer position: ${pointer}`,
353-
`Tape window: ${formatTapeWindow(tape, pointer)}`,
354-
].join('\n');
355-
throw new BrainfuckTimeoutError('Execution timed out after 3 seconds.', debugInfo);
442+
if (!failsafeSettings.disabled && failsafeSettings.timeoutSeconds > 0 && performance.now() - startTime > timeoutMs) {
443+
enforceFailSafe(`Execution timed out after ${failsafeSettings.timeoutSeconds} second${failsafeSettings.timeoutSeconds === 1 ? '' : 's'}.`);
356444
}
357445

358446
const instruction = commands[ip];
@@ -391,6 +479,10 @@ <h2 style="margin: 0;">Output</h2>
391479
}
392480
break;
393481
}
482+
483+
if (!failsafeSettings.disabled && failsafeSettings.tapeLengthLimit > 0 && tape.length > failsafeSettings.tapeLengthLimit) {
484+
enforceFailSafe(`Execution stopped because the tape exceeded ${failsafeSettings.tapeLengthLimit} cell${failsafeSettings.tapeLengthLimit === 1 ? '' : 's'}.`);
485+
}
394486
ip += 1;
395487
}
396488

@@ -418,11 +510,11 @@ <h2 style="margin: 0;">Output</h2>
418510
const mode = asciiModeRadio.checked ? 'ascii' : 'numbers';
419511
const inputs = parseInputs(bfInput.value, mode);
420512
const program = editor.getValue();
421-
const output = runProgram(program, inputs);
513+
const output = runProgram(program, inputs, collectFailsafeSettingsFromInputs());
422514
renderOutput(output);
423515
updateStatus(`Program ran successfully. Output length: ${output.length} byte${output.length === 1 ? '' : 's'}.`);
424516
} catch (error) {
425-
const debugDetails = error instanceof BrainfuckTimeoutError && error.debugInfo
517+
const debugDetails = error instanceof BrainfuckFailSafeError && error.debugInfo
426518
? `${error.message}\n\nDebug info:\n${error.debugInfo}`
427519
: error.message;
428520
updateStatus(debugDetails, true);
@@ -464,6 +556,16 @@ <h2 style="margin: 0;">Output</h2>
464556
document.getElementById('mode-numbers').addEventListener('change', updateInputHelp);
465557
updateInputHelp();
466558

559+
applyFailsafeSettings(getStoredFailsafeSettings());
560+
[failsafeTimeoutInput, failsafeTapeLimitInput].forEach((input) => {
561+
input.addEventListener('input', () => {
562+
collectFailsafeSettingsFromInputs();
563+
});
564+
});
565+
failsafeDisabledInput.addEventListener('change', () => {
566+
collectFailsafeSettingsFromInputs();
567+
});
568+
467569
const initialStatus = programFromHash !== null
468570
? 'Loaded program from URL and saved to local storage.'
469571
: hadStoredProgram

0 commit comments

Comments
 (0)