work async, js compiler

This commit is contained in:
ticvac 2025-05-07 22:51:08 +02:00
parent 4cb534838a
commit c6d280dfd8
4 changed files with 228 additions and 333 deletions

View file

@ -1,218 +0,0 @@
import sys
import re
import time
class Interpreter:
def __init__(self, commands_per_second=1_000_000, debug=False, comma_standart_input=True):
self.debug = debug
self.commands = ['+', '-', '>', '<', '.', ',', '[', ']']
self.mp = 0 # memory pointer
self.pc = 0 # program counter
self.data = [0] * 2
'''
chceme mít k dispozici i konec datového pole, (abychom mohli zavolat '<' na 0)
-> konec pole budeme mít jako self.data_end a kde jsme, poznáme podle self.is_at_end...
pak můžeme podle potřeby zvětšovat pole ze začátku nebo z konce
!poznámka autora: dalo by se také udělat s jedním polem, tak že bychom 'posouvali' index 0...
to ale nenapadlo, když jsem to psal, tak jsem to udělal takhle...
'''
self.data_end = [0] * 2 # tail is reversed
self.is_at_end = False
self.code = ""
self.commands_per_second = commands_per_second
self.MAX = 2**32 - 1
self.user_input = ""
self.user_input_index = 0
self.saved_output = ""
self.comma_standart_input = comma_standart_input
self.last_step = 0
def our_i32(self, value):
if value == -1:
return self.MAX
if value == self.MAX + 1:
return 0
return value
def load_code_from_input(self):
def expand_numbers(match):
count = int(match.group(1))
char = match.group(2)
return char * count
self.code = ""
code_filename = sys.argv[1]
with open(code_filename) as code_file:
for line in code_file:
line = line.strip()
line = line.split('//')[0].strip()
line = re.sub(r'(\d+)(.)', expand_numbers, line)
line = ''.join(filter(lambda c: c in self.commands, line))
self.code += line
def load_code_from_string(self, code):
def expand_numbers(match):
count = int(match.group(1))
char = match.group(2)
return char * count
self.code = ""
code = code.strip()
code = re.sub(r'(\d+)(.)', expand_numbers, code)
code = ''.join(filter(lambda c: c in self.commands, code))
self.code += code
def print_overwrite(self, message, prev_len=[0]):
sys.stdout.write('\r' + message + ' ' * max(prev_len[0] - len(message), 0))
sys.stdout.flush()
prev_len[0] = len(message)
def print_data(self, steps):
to_print = "step: " + str(steps) + " "
to_print += "mp: " + str(self.mp) + " | "
for i in range(len(self.data)):
to_print += str(self.data[i]) + " "
to_print += "|"
to_print += " end: | "
for i in range(len(self.data_end)).__reversed__():
to_print += str(self.data_end[i]) + " "
to_print += "|"
self.print_overwrite(to_print)
return to_print
def get_value_at_mp(self):
if not self.is_at_end:
return self.data[self.mp]
else:
return self.data_end[self.MAX - self.mp]
def set_value_at_mp(self, value):
if not self.is_at_end:
self.data[self.mp] = value
else:
self.data_end[self.MAX - self.mp] = value
def handle_plus(self):
self.set_value_at_mp(self.our_i32(self.get_value_at_mp() + 1))
def handle_minus(self):
self.set_value_at_mp(self.our_i32(self.get_value_at_mp() - 1))
def handle_greater(self):
temp = self.mp
self.mp = self.our_i32(self.mp + 1)
# kontrola, jesstli jsem ne přeskočil konec
if self.mp > self.MAX - len(self.data_end):
self.is_at_end = True
return
if temp > self.mp:
self.is_at_end = False
# rozšíření datového pole
if self.mp >= len(self.data):
self.data += [0] * (len(self.data))
def handle_less(self):
temp = self.mp
self.mp = self.our_i32(self.mp - 1)
# kontrola, jestli jsem ne přeskočil začátek
if self.mp < len(self.data):
self.is_at_end = False
return
if temp < self.mp:
self.is_at_end = True
if self.MAX - self.mp >= len(self.data_end):
self.data_end += [0] * (len(self.data_end))
def handle_dot(self):
sys.stdout.write(chr(self.get_value_at_mp()))
sys.stdout.flush()
self.saved_output += chr(self.get_value_at_mp())
def handle_comma(self):
if self.comma_standart_input:
input_char = sys.stdin.read(1)
else:
if self.user_input_index < len(self.user_input):
input_char = self.user_input[self.user_input_index]
self.user_input_index += 1
else:
input_char = None
if not input_char:
self.set_value_at_mp(0)
else:
self.set_value_at_mp(ord(input_char))
def handle_left_bracket(self):
if self.data[self.mp] == 0: # jump to the matching ]
open_brackets = 1
while open_brackets > 0:
self.pc += 1
if self.pc >= len(self.code):
raise SyntaxError("Unmatched \"[\"")
if self.code[self.pc] == "[":
open_brackets += 1
elif self.code[self.pc] == "]":
open_brackets -= 1
def handle_right_bracket(self):
if self.data[self.mp] != 0:
close_brackets = 1
while close_brackets > 0:
self.pc -= 1
if self.pc < 0:
raise SyntaxError("Unmatched \"]\"")
if self.code[self.pc] == "]":
close_brackets += 1
elif self.code[self.pc] == "[":
close_brackets -= 1
def execute_loaded_code(self):
steps = 0
print("|Executing code|")
while True:
if self.pc >= len(self.code):
break
com = self.code[self.pc]
if com == "+":
self.handle_plus()
elif com == "-":
self.handle_minus()
elif com == ">":
self.handle_greater()
elif com == "<":
self.handle_less()
elif com == ".":
self.handle_dot()
elif com == ",":
self.handle_comma()
elif com == "[":
self.handle_left_bracket()
elif com == "]":
self.handle_right_bracket()
# debug print
if self.debug:
self.print_data(steps)
time.sleep(1 / self.commands_per_second)
self.pc += 1
steps += 1
self.last_step = steps
print("\n|Execution done|")
'''
Usage:
python interpreter.py <code_file> < <user_input>
nastavte si DEBUG na True, pokud chcete vidět debugovací výstup, pak ale nebude správně fungovat "."...
nastavte si commands_per_second na 2, pokud chcete dva kroky za sekundu
'''
DEBUG = True
COMMANDS_PER_SECOND = 1000
if __name__ == "__main__":
interpreter = Interpreter(commands_per_second=COMMANDS_PER_SECOND, debug=DEBUG)
interpreter.load_code_from_input()
print("Loaded code:", interpreter.code, "length:", len(interpreter.code))
interpreter.execute_loaded_code()

View file

@ -33,6 +33,14 @@
margin-top: 1rem;
}
.btn:hover:not([disabled]) { background: var(--primary-light); }
.btn-stop {
background: #ef4444; color: #fff; font-weight: 600; cursor: pointer;
transition: background 0.2s ease;
margin-top: 1rem; padding: 0.75rem 1.5rem; border: none; border-radius: 0.375rem;
display: inline-block; text-decoration: none;
margin-left: 1rem;
}
.btn-stop:hover:not([disabled]) { background: #dc2626; }
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }
.spinner {
border: 2px solid #f3f3f3;
@ -66,6 +74,13 @@
top: 0; left: 0; width: 100%; height: 100%; opacity: 0;
cursor: pointer;
}
.author {
text-align: center;
font-size: 0.8rem;
color: var(--text);
margin-top: 1rem;
line-height: 2em;
}
@media screen and (max-width: 768px) {
.flex { flex-direction: column; }
.flex > div { width: 100%; }
@ -74,7 +89,7 @@
</head>
<body>
<div class="container">
<h1>Brainfuck Compiler</h1>
<h1>Brainfuck Compiler for M&M</h1>
<form id="bf-form" class="card">
<div class="flex">
<div>
@ -95,20 +110,209 @@
</div>
</div>
<button type="button" id="run-btn" class="btn">Run</button>
<button type="button" id="stop-btn" class="btn-stop" style="display: none;">Stop</button>
</form>
<div class="card">
<label for="output">Output</label>
<div id="output"></div>
<pre id="output"></pre>
</div>
<div class="card">
<label for="last_state">State</label>
<div id="last_state"></div>
<pre id="last_state"></pre>
</div>
<div class="author">Created by TicVac 2025<br>vaclav.tichy.mam@gmail.com</div>
</div>
<script>
let abortSignal = false;
const outElement = document.getElementById('output');
const lastStateElement = document.getElementById('last_state');
class Interpreter {
constructor({ commandsPerSecond = 1_000_000_000, debug = false} = {}) {
this.debug = debug;
this.commands = ['+', '-', '>', '<', '.', ',', '[', ']'];
this.mp = 0; // memory pointer
this.pc = 0; // program counter
this.data = [0, 0];
this.dataEnd = [0, 0]; // tail reversed
this.isAtEnd = false;
this.code = "";
this.commandsPerSecond = commandsPerSecond;
this.MAX = 2 ** 32 - 1;
this.userInput = "";
this.userInputIndex = 0;
this.savedOutput = "";
}
ourI32(value) {
if (value === -1) return this.MAX;
if (value === this.MAX + 1) return 0;
return value;
}
loadCodeFromString(code) {
const expandNumbers = (match, count, char) => {
return char.repeat(parseInt(count, 10));
};
let cleaned = code.trim();
cleaned = cleaned.replace(/(\d+)(.)/g, expandNumbers);
cleaned = cleaned.split('').filter(c => this.commands.includes(c)).join('');
this.code = cleaned;
}
getValueAtMp() {
if (!this.isAtEnd) {
return this.data[this.mp];
} else {
return this.dataEnd[this.MAX - this.mp];
}
}
setValueAtMp(value) {
if (!this.isAtEnd) {
this.data[this.mp] = value;
} else {
this.dataEnd[this.MAX - this.mp] = value;
}
}
handlePlus() {
this.setValueAtMp(this.ourI32(this.getValueAtMp() + 1));
}
handleMinus() {
this.setValueAtMp(this.ourI32(this.getValueAtMp() - 1));
}
handleGreater() {
const temp = this.mp;
this.mp = this.ourI32(this.mp + 1);
if (this.mp > this.MAX - this.dataEnd.length) {
this.isAtEnd = true;
return;
}
if (temp > this.mp) {
this.isAtEnd = false;
}
if (this.mp >= this.data.length) {
this.data = this.data.concat(new Array(this.data.length).fill(0));
}
}
handleLess() {
const temp = this.mp;
this.mp = this.ourI32(this.mp - 1);
if (this.mp < this.data.length) {
this.isAtEnd = false;
return;
}
if (temp < this.mp) {
this.isAtEnd = true;
}
if (this.MAX - this.mp >= this.dataEnd.length) {
this.dataEnd = this.dataEnd.concat(new Array(this.dataEnd.length).fill(0));
}
}
handleDot() {
const ch = String.fromCharCode(this.getValueAtMp());
this.savedOutput += ch;
}
handleComma() {
let inputChar;
if (this.commaStandardInput) {
// Standard input ignored in this variant
inputChar = null;
} else {
if (this.userInputIndex < this.userInput.length) {
inputChar = this.userInput[this.userInputIndex++];
} else {
inputChar = null;
}
}
if (!inputChar) {
this.setValueAtMp(0);
} else {
this.setValueAtMp(inputChar.charCodeAt(0));
}
}
handleLeftBracket() {
if (this.getValueAtMp() === 0) {
let openBrackets = 1;
while (openBrackets > 0) {
this.pc++;
if (this.pc >= this.code.length) {
throw new SyntaxError('Unmatched "["');
}
if (this.code[this.pc] === '[') openBrackets++;
if (this.code[this.pc] === ']') openBrackets--;
}
}
}
handleRightBracket() {
if (this.getValueAtMp() !== 0) {
let closeBrackets = 1;
while (closeBrackets > 0) {
this.pc--;
if (this.pc < 0) {
throw new SyntaxError('Unmatched "]"');
}
if (this.code[this.pc] === ']') closeBrackets++;
if (this.code[this.pc] === '[') closeBrackets--;
}
}
}
getState() {
const dataMain = this.data.join(' ');
const dataTail = [...this.dataEnd].reverse().join(' ');
return `step: ${this.steps} mp: ${this.mp} | ${dataMain} | end: | ${dataTail} |`;
}
async execute({ codeString, userInput = '', callback } = {}) {
this.loadCodeFromString(codeString);
this.userInput = userInput;
let steps = 0;
let breath = 100;
while (this.pc < this.code.length) {
const com = this.code[this.pc];
switch (com) {
case '+': this.handlePlus(); break;
case '-': this.handleMinus(); break;
case '>': this.handleGreater(); break;
case '<': this.handleLess(); break;
case '.': this.handleDot(); break;
case ',': this.handleComma(); break;
case '[': this.handleLeftBracket(); break;
case ']': this.handleRightBracket(); break;
}
this.pc++;
steps++;
if (steps % breath === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
if (abortSignal) {
console.log("Execution aborted");
this.savedOutput = "Execution aborted";
break;
}
outElement.textContent = this.savedOutput;
lastStateElement.textContent = this.getState();
}
callback(this.savedOutput, this.getState());
return this.savedOutput;
}
}
const stopBtn = document.getElementById('stop-btn');
// Setup drop zones with filename display
function setupDropZone(dropZoneId, textareaId, textId) {
const dropZone = document.getElementById(dropZoneId);
@ -146,63 +350,14 @@
setupDropZone('code-drop', 'code', 'code-drop-text');
setupDropZone('input-drop', 'input', 'input-drop-text');
// Brainfuck Interpreter unchanged
function runBrainfuck(code, input) {
const tape = Array(30000).fill(0);
let ptr = 0, inputPtr = 0, output = '';
const loopStack = [];
for (let i = 0; i < code.length; i++) {
switch(code[i]) {
case '>': ptr++; break;
case '<': ptr--; break;
case '+': tape[ptr] = (tape[ptr] + 1) % 256; break;
case '-': tape[ptr] = (tape[ptr] + 255) % 256; break;
case '.': output += String.fromCharCode(tape[ptr]); break;
case ',': tape[ptr] = inputPtr < input.length ? input.charCodeAt(inputPtr++) : 0; break;
case '[':
if (tape[ptr] === 0) {
let open = 1;
while (open) {
i++;
if (code[i] === '[') open++;
if (code[i] === ']') open--;
}
} else {
loopStack.push(i);
}
break;
case ']':
if (tape[ptr] !== 0) { i = loopStack[loopStack.length - 1]; }
else { loopStack.pop(); }
break;
default: break;
}
}
return output;
}
function formatForHTML(text, tabWidth = 4, spaces = true) {
// 1) Escape &, <, >, " and '
let s = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
// 2) Convert tabs → a run of non-breaking spaces
const nbspTab = "&nbsp;".repeat(tabWidth);
s = s.replace(/\t/g, nbspTab);
// 3) Optionally convert every single space → &nbsp;
if (spaces) {
s = s.replace(/ /g, "&nbsp;");
}
// 4) Convert newlines → <br>
s = s.replace(/\r?\n/g, "<br>\n");
return s;
function random_js(out, state) {
console.log("random_js");
const btn = document.getElementById('run-btn');
btn.removeChild(btn.firstChild);
document.getElementById('output').textContent = out;
document.getElementById('last_state').textContent = state;
btn.disabled = false;
stopBtn.style.display = 'none';
}
// Run button handler with spinner prepend
@ -211,25 +366,23 @@
const code = document.getElementById('code').value;
const input = document.getElementById('input').value;
if (btn.disabled) return;
outElement.textContent = "";
lastStateElement.textContent = "";
abortSignal = false;
stopBtn.style.display = 'inline-block';
btn.disabled = true;
const spinner = document.createElement('div'); spinner.className = 'spinner';
btn.insertBefore(spinner, btn.firstChild);
const url = '/brainfuck/process_code?code=' + encodeURIComponent(code) + "&user_input=" + encodeURIComponent(input);
fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
.then(response => response.json())
.then(data => {
// escape HTML entities in output
console.log(data);
document.getElementById('output').textContent = data.output;
document.getElementById('last_state').textContent = data.end_state;
document.getElementById('output').innerHTML = formatForHTML(data.output);
})
.catch(err => console.error(err))
.finally(() => {
btn.disabled = false;
btn.removeChild(spinner);
});
// execution start
const interpreter = new Interpreter({ commandsPerSecond: 1_000_000_000, debug: false });
let out = interpreter.execute({ codeString: code, userInput: input, callback: random_js });
});
stopBtn.addEventListener('click', () => {
console.log("Stop button clicked");
abortSignal = true;
});
</script>
</body>

View file

@ -3,6 +3,5 @@ from . import views
urlpatterns = [
path('', views.index),
path('process_code', views.process_code),
]

View file

@ -7,42 +7,3 @@ from typing import Dict
def index(request):
args = {}
return TemplateResponse(request, 'brainfuck/index.html', args)
def process_code(request):
qs = request.META['QUERY_STRING']
params: Dict[str, str] = {}
for pair in qs.split('&'):
if not pair:
continue
key, sep, val = pair.partition('=')
# decode key (still treat '+'→space in parameter names)
decoded_key = unquote_plus(key)
# decode value: unquote (leaves '+' intact), then percent-decode
decoded_val = unquote(val)
params[decoded_key] = decoded_val
print('params:', params)
code = params.get('code', '')
input_data = params.get('user_input', '')
# interpreter
interpreter = Interpreter(commands_per_second=1_000_000_000_000, debug=True, comma_standart_input=False)
interpreter.user_input = input_data
interpreter.load_code_from_string(code)
# TODO kill the process if it takes too long?
try:
interpreter.execute_loaded_code()
out = interpreter.saved_output
except Exception as e:
print('Error:', e)
out = 'Error: Code execution failed - ' + str(e)
end_state = interpreter.print_data(interpreter.last_step)
return JSONResponse({
'code': code,
'input_data': input_data,
'output': out,
'end_state': end_state,
})