Skip to content

Commit 16223f3

Browse files
Add brainfuck editor and interpreter tool (#92)
- add a CodeMirror-based UI card for writing and running brainfuck programs with configurable input modes - implement a browser brainfuck interpreter that wraps cell values, extends the tape, and handles EOF input as zero while showing ASCII and byte outputs - document the new brainfuck interpreter tool ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_694497386eb483258093b08c62c86245)
1 parent 8fae462 commit 16223f3

2 files changed

Lines changed: 331 additions & 0 deletions

File tree

brainfuck-interpreter.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Write and run brainfuck programs in the browser with a CodeMirror editor. Provide input as ASCII or comma-separated integers, then view the ASCII output and raw byte values after execution.

brainfuck-interpreter.html

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Brainfuck Editor & Interpreter</title>
8+
<link rel="stylesheet" href="styles.css">
9+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.min.css">
10+
<style>
11+
body {
12+
max-width: 1000px;
13+
margin: 0 auto;
14+
padding: clamp(1.75rem, 3vw, 2.5rem) clamp(1.25rem, 3vw, 2.5rem) clamp(3rem, 5vw, 4rem);
15+
}
16+
17+
main {
18+
display: grid;
19+
gap: clamp(1.25rem, 2.5vw, 1.75rem);
20+
}
21+
22+
.tool-card {
23+
padding: clamp(1.25rem, 3vw, 2rem);
24+
}
25+
26+
.card-header {
27+
display: flex;
28+
justify-content: space-between;
29+
align-items: center;
30+
gap: 1rem;
31+
flex-wrap: wrap;
32+
margin-bottom: 0.75rem;
33+
}
34+
35+
.card-header h2 {
36+
margin: 0;
37+
}
38+
39+
.CodeMirror {
40+
height: 420px;
41+
border: 1px solid var(--ui-3);
42+
border-radius: var(--radius-md);
43+
font-family: var(--font-mono);
44+
font-size: 15px;
45+
}
46+
47+
.mode-toggle {
48+
display: inline-flex;
49+
gap: 0.5rem;
50+
align-items: center;
51+
flex-wrap: wrap;
52+
}
53+
54+
.mode-toggle label {
55+
display: inline-flex;
56+
gap: 0.35rem;
57+
align-items: center;
58+
cursor: pointer;
59+
}
60+
61+
.input-help {
62+
color: var(--tx-2);
63+
font-size: 0.95rem;
64+
margin-top: 0.35rem;
65+
}
66+
67+
.output-grid {
68+
display: grid;
69+
gap: 0.9rem;
70+
}
71+
72+
.output-block {
73+
background: var(--bg);
74+
border: 1px solid var(--ui-2);
75+
border-radius: var(--radius-sm);
76+
padding: 0.75rem 0.9rem;
77+
min-height: 80px;
78+
white-space: pre-wrap;
79+
word-break: break-word;
80+
}
81+
82+
.status {
83+
margin-top: 0.5rem;
84+
font-size: 0.98rem;
85+
color: var(--tx-2);
86+
}
87+
88+
.status.error {
89+
color: var(--re);
90+
}
91+
92+
@media (max-width: 720px) {
93+
body {
94+
padding: 1.4rem 1rem 2.5rem;
95+
}
96+
97+
.CodeMirror {
98+
height: 320px;
99+
}
100+
}
101+
</style>
102+
</head>
103+
104+
<body>
105+
<header class="page-header">
106+
<a class="site-link" href="https://tools.mathspp.com/" aria-label="Back to tools.mathspp.com">← tools.mathspp.com</a>
107+
<h1>Brainfuck Editor & Interpreter</h1>
108+
<p class="lead">Write brainfuck code with a CodeMirror editor, feed it ASCII or comma-separated input, and run it
109+
directly in your browser.</p>
110+
</header>
111+
112+
<main>
113+
<section class="surface tool-card">
114+
<div class="card-header">
115+
<h2 class="content-flow" style="--flow-space: 0.1rem;">
116+
<span>Program</span>
117+
<small style="color: var(--tx-2); font-weight: 500;">Run executes the code below</small>
118+
</h2>
119+
<button type="button" id="run-button">Run</button>
120+
</div>
121+
122+
<label class="sr-only" for="code-input">Brainfuck code</label>
123+
<textarea id="code-input" name="code-input"></textarea>
124+
125+
<div class="content-flow" style="--flow-space: 0.35rem; margin-top: 1rem;">
126+
<div class="mode-toggle" role="group" aria-label="Input mode">
127+
<label>
128+
<input type="radio" name="input-mode" value="ascii" id="mode-ascii" checked>
129+
ASCII
130+
</label>
131+
<label>
132+
<input type="radio" name="input-mode" value="numbers" id="mode-numbers">
133+
Comma-separated integers
134+
</label>
135+
</div>
136+
<label for="bf-input">Program input</label>
137+
<input type="text" id="bf-input" name="bf-input" placeholder="Type characters or numbers matching the selected input mode">
138+
<p class="input-help" id="input-help">ASCII mode sends each character as its byte value.</p>
139+
</div>
140+
141+
<p class="status" id="status" role="status" aria-live="polite"></p>
142+
</section>
143+
144+
<section class="surface tool-card">
145+
<div class="card-header" style="margin-bottom: 0.35rem;">
146+
<h2 style="margin: 0;">Output</h2>
147+
</div>
148+
<div class="output-grid">
149+
<div>
150+
<div class="input-help">ASCII</div>
151+
<div class="output-block" id="output-ascii">(no output)</div>
152+
</div>
153+
<div>
154+
<div class="input-help">Byte values</div>
155+
<div class="output-block" id="output-bytes">(no output)</div>
156+
</div>
157+
</div>
158+
</section>
159+
</main>
160+
161+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.min.js"></script>
162+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/mode/javascript/javascript.min.js"></script>
163+
<script>
164+
(function () {
165+
const runButton = document.getElementById('run-button');
166+
const statusEl = document.getElementById('status');
167+
const outputAscii = document.getElementById('output-ascii');
168+
const outputBytes = document.getElementById('output-bytes');
169+
const bfInput = document.getElementById('bf-input');
170+
const inputHelp = document.getElementById('input-help');
171+
const asciiModeRadio = document.getElementById('mode-ascii');
172+
173+
const textarea = document.getElementById('code-input');
174+
const editor = CodeMirror.fromTextArea(textarea, {
175+
mode: 'text/plain',
176+
lineNumbers: true,
177+
indentUnit: 2,
178+
lineWrapping: true,
179+
viewportMargin: Infinity,
180+
});
181+
182+
const sampleProgram = `
183+
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.
184+
>++.<<+++++++++++++++.>.+++.------.--------.>+.>.
185+
`.trim();
186+
editor.setValue(sampleProgram);
187+
188+
function updateStatus(message, isError = false) {
189+
statusEl.textContent = message;
190+
statusEl.classList.toggle('error', isError);
191+
}
192+
193+
function clampByte(value) {
194+
return ((value % 256) + 256) % 256;
195+
}
196+
197+
function parseInputs(rawValue, mode) {
198+
if (!rawValue) return [];
199+
200+
if (mode === 'ascii') {
201+
return Array.from(rawValue, (char) => clampByte(char.charCodeAt(0)));
202+
}
203+
204+
return rawValue.split(',')
205+
.map(part => part.trim())
206+
.filter(Boolean)
207+
.map((part, index) => {
208+
const numeric = Number(part);
209+
if (!Number.isFinite(numeric)) {
210+
throw new Error(`Input ${index + 1} is not a valid number: "${part}"`);
211+
}
212+
return clampByte(numeric);
213+
});
214+
}
215+
216+
function buildJumpMap(program) {
217+
const stack = [];
218+
const map = new Map();
219+
220+
program.split('').forEach((char, index) => {
221+
if (char === '[') {
222+
stack.push(index);
223+
} else if (char === ']') {
224+
const start = stack.pop();
225+
if (start === undefined) {
226+
throw new Error(`Unmatched ']' at position ${index + 1}`);
227+
}
228+
map.set(start, index);
229+
map.set(index, start);
230+
}
231+
});
232+
233+
if (stack.length) {
234+
throw new Error(`Unmatched '[' at position ${stack[stack.length - 1] + 1}`);
235+
}
236+
237+
return map;
238+
}
239+
240+
function runProgram(program, inputs) {
241+
const commands = program.replace(/[^\>\<\+\-\.\,\[\]]/g, '');
242+
const jumpMap = buildJumpMap(commands);
243+
244+
const tape = [0];
245+
let pointer = 0;
246+
let inputIndex = 0;
247+
let ip = 0;
248+
const output = [];
249+
250+
while (ip < commands.length) {
251+
const instruction = commands[ip];
252+
switch (instruction) {
253+
case '>':
254+
pointer += 1;
255+
if (pointer === tape.length) {
256+
tape.push(0);
257+
}
258+
break;
259+
case '<':
260+
pointer = Math.max(0, pointer - 1);
261+
break;
262+
case '+':
263+
tape[pointer] = clampByte(tape[pointer] + 1);
264+
break;
265+
case '-':
266+
tape[pointer] = clampByte(tape[pointer] - 1);
267+
break;
268+
case '.':
269+
output.push(tape[pointer]);
270+
break;
271+
case ',':
272+
tape[pointer] = inputIndex < inputs.length ? inputs[inputIndex++] : 0;
273+
break;
274+
case '[':
275+
if (tape[pointer] === 0) {
276+
ip = jumpMap.get(ip);
277+
}
278+
break;
279+
case ']':
280+
if (tape[pointer] !== 0) {
281+
ip = jumpMap.get(ip);
282+
}
283+
break;
284+
}
285+
ip += 1;
286+
}
287+
288+
return output;
289+
}
290+
291+
function renderOutput(bytes) {
292+
const ascii = String.fromCharCode(...bytes);
293+
outputAscii.textContent = ascii || '(no output)';
294+
outputBytes.textContent = bytes.length ? bytes.join(', ') : '(no output)';
295+
}
296+
297+
function updateInputHelp() {
298+
if (asciiModeRadio.checked) {
299+
inputHelp.textContent = 'ASCII mode sends each character as its byte value.';
300+
bfInput.placeholder = 'Type the exact characters your program should consume';
301+
} else {
302+
inputHelp.textContent = 'Number mode expects comma-separated integers; values wrap within 0-255.';
303+
bfInput.placeholder = 'e.g. 65, 66, 67 for A, B, C';
304+
}
305+
}
306+
307+
runButton.addEventListener('click', () => {
308+
try {
309+
const mode = asciiModeRadio.checked ? 'ascii' : 'numbers';
310+
const inputs = parseInputs(bfInput.value, mode);
311+
const program = editor.getValue();
312+
const output = runProgram(program, inputs);
313+
renderOutput(output);
314+
updateStatus(`Program ran successfully. Output length: ${output.length} byte${output.length === 1 ? '' : 's'}.`);
315+
} catch (error) {
316+
updateStatus(error.message, true);
317+
outputAscii.textContent = '(no output)';
318+
outputBytes.textContent = '(no output)';
319+
}
320+
});
321+
322+
asciiModeRadio.addEventListener('change', updateInputHelp);
323+
document.getElementById('mode-numbers').addEventListener('change', updateInputHelp);
324+
updateInputHelp();
325+
updateStatus('Ready to run. Tape starts at 0 and overflows/underflows within 0-255.');
326+
})();
327+
</script>
328+
</body>
329+
330+
</html>

0 commit comments

Comments
 (0)