@@ -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