#include "Brainfuck.h" #include #include #include #include #include #include #include /* ── Built-in examples ────────────────────────────────────────────── */ struct BfExample { const char* name; const char* code; }; static const BfExample examples[] = { { "Hello World (built-in)", "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]" ">>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++." }, { "Fibonacci (built-in)", ">++++++++++>+>+[[+++++[>++++++++<-]>.<++++++[>--------<-]+<<<" "]>.>>[[-]<[>+<-]>>[<<+>+>-]<[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-" "[>+<-[>+<-[>+<-[>[-]>+>+<<<-[>+<-]]]]]]]]]]]+>>>]<<<]" }, { "Alphabet (built-in)", "+++++[>+++++++++++++<-]++++++++++++++++++++++++++[>.+<-]" }, { "Beer (built-in)", ">++++++++++[<++++++++++>-]<->>>>>+++[>+++>+++<<-]<<<<+<[>[>+" ">+<<-]>>[-<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]>>" ">>>>[[-]<++++++++++<->>]<-[>+>+<<-]>[<+>-]+>[[-]<->]<<<<<<<<" "<->>]<[>+>+<<-]>>[-<<+>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<" "<[>>+>+<<<-]>>>[-<<<+>>>]++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<" "<[>+<[-]]<[>>+<<[-]]>>[<<+>>[-]]<<<[>>+>+<<<-]>>>[-<<<+>>>]+" "+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<" "<+>>[-]]<<[[-]>>>++++++++[>>++++++<<-]>[<++++++++[>++++++<-]" ">.<++++++++[>------<-]>[<<+>>-]]>.<<++++++++[>>------<<-]<[-" ">>+<<]<++++++++[<++++>-]<.>+++++++[>+++++++++<-]>+++.<+++++[" ">+++++++++<-]>.+++++..--------.-------.++++++++++++++>>[>>>+" ">+<<<<-]>>>>[-<<<<+>>>>]>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<<" "[>>>+>+<<<<-]>>>>[-<<<<+>>>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>" ">]<<<[>>+<<[-]]>[>+<[-]]++>>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<" "+<[[-]>-<]>[<<<<<<<.>>>>>>>[-]]<<<<<<<<<.>>----.---------.<<" ".>>----.+++..+++++++++++++.[-]<<[-]]<[>+>+<<-]>>[-<<+>>]+>+<" "[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<[>>+>+<<<-]>>>[-<<<+>>>]++++" ">+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<<+>" ">[-]]<<[[-]>++++++++[<++++>-]<.>++++++++++[>+++++++++++<-]>+" ".-.<<.>>++++++.------------.---.<<.>++++++[>+++<-]>.<++++++[" ">----<-]>++.+++++++++++..[-]<<[-]++++++++++.[-]]<[>+>+<<-]>>" "[-<<+>>]+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++." ">+++++++++[>+++++++++<-]>+++.+++++++++++++.++++++++++.------" ".<++++++++[>>++++<<-]>>.<++++++++++.-.---------.>.<-.+++++++" "++++.++++++++.---------.>.<-------------.+++++++++++++.-----" "-----.>.<++++++++++++.---------------.<+++[>++++++<-]>..>.<-" "---------.+++++++++++.>.<<+++[>------<-]>-.+++++++++++++++++" ".---.++++++.-------.----------.[-]>[-]<<<.[-]]<[>+>+<<-]>>[-" "<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++.[" "-]<[-]>]<+<]" }, }; static constexpr int NUM_EXAMPLES = sizeof(examples) / sizeof(examples[0]); /* ── App handle for user data path ────────────────────────────────── */ static AppHandle s_appHandle = nullptr; static bool getScriptDir(char* buf, size_t bufSize) { if (!s_appHandle) return false; size_t size = bufSize; tt_app_get_user_data_path(s_appHandle, buf, &size); if (size == 0) return false; for (char* p = buf + 1; *p; ++p) { if (*p == '/') { *p = '\0'; mkdir(buf, 0755); *p = '/'; } } mkdir(buf, 0755); return true; } static char** scriptPaths = nullptr; static int scriptCount = 0; static int ciStrcmp(const char* a, const char* b) { while (*a && *b) { int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a; int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b; if (ca != cb) return ca - cb; a++; b++; } return (unsigned char)*a - (unsigned char)*b; } static void freeScriptPaths() { for (int i = 0; i < scriptCount; i++) { free(scriptPaths[i]); } free(scriptPaths); scriptPaths = nullptr; scriptCount = 0; } /* File-scope instance pointer for index-based and path-based callbacks */ static Brainfuck* g_instance = nullptr; /* ── VM Logic ─────────────────────────────────────────────────────── */ void Brainfuck::bfInit() { memset(&vm, 0, sizeof(BfVM)); } static int bfFindBracket(const char* code, int pos, int dir) { int depth = 1; while (depth > 0) { pos += dir; if (pos < 0 || code[pos] == '\0') return -1; if (code[pos] == '[') depth += dir; if (code[pos] == ']') depth -= dir; } return pos; } void Brainfuck::bfRun(const char* code) { int len = strlen(code); while (vm.pc < len && !vm.error) { vm.cycles++; if (vm.cycles > MAX_CYCLES) { vm.error = true; snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Halted after %d cycles (infinite loop?)", MAX_CYCLES); return; } char c = code[vm.pc]; switch (c) { case '>': vm.ptr++; if (vm.ptr >= TAPE_SIZE) { vm.error = true; snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer overflow (>%d)", TAPE_SIZE); return; } break; case '<': vm.ptr--; if (vm.ptr < 0) { vm.error = true; snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer underflow (<0)"); return; } break; case '+': vm.tape[vm.ptr]++; break; case '-': vm.tape[vm.ptr]--; break; case '.': if (vm.outLen < MAX_OUTPUT - 1) { vm.output[vm.outLen++] = (char)vm.tape[vm.ptr]; vm.output[vm.outLen] = '\0'; } break; case ',': vm.tape[vm.ptr] = 0; break; case '[': if (vm.tape[vm.ptr] == 0) { int target = bfFindBracket(code, vm.pc, 1); if (target < 0) { vm.error = true; snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched '[' at pos %d", vm.pc); return; } vm.pc = target; } break; case ']': if (vm.tape[vm.ptr] != 0) { int target = bfFindBracket(code, vm.pc, -1); if (target < 0) { vm.error = true; snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched ']' at pos %d", vm.pc); return; } vm.pc = target; } break; } vm.pc++; } } void Brainfuck::runCode(const char* code) { if (!outputTa) return; bfInit(); bfRun(code); constexpr int resultSize = MAX_OUTPUT + 128; char* result = (char*)malloc(resultSize); if (!result) { lv_textarea_set_text(outputTa, "Out of memory"); return; } int pos = 0; int remaining; remaining = resultSize - pos; if (remaining > 0) { int n = snprintf(result + pos, remaining, "> RUN (%d chars)\n", (int)strlen(code)); pos += (n < remaining) ? n : (remaining - 1); } if (vm.outLen > 0) { remaining = resultSize - pos; if (remaining > 0) { int n = snprintf(result + pos, remaining, "%s\n", vm.output); pos += (n < remaining) ? n : (remaining - 1); } } if (vm.error) { remaining = resultSize - pos; if (remaining > 0) { int n = snprintf(result + pos, remaining, "ERROR: %s\n", vm.errorMsg); pos += (n < remaining) ? n : (remaining - 1); } } else { remaining = resultSize - pos; if (remaining > 0) { int n = snprintf(result + pos, remaining, "OK (%d cycles)\n", vm.cycles); pos += (n < remaining) ? n : (remaining - 1); } } lv_textarea_set_text(outputTa, result); lv_obj_scroll_to_y(outputTa, LV_COORD_MAX, LV_ANIM_ON); free(result); } /* ── View management ──────────────────────────────────────────────── */ void Brainfuck::showMainView() { state = BfState::Main; if (examplesList) lv_obj_add_flag(examplesList, LV_OBJ_FLAG_HIDDEN); if (outputTa) lv_obj_remove_flag(outputTa, LV_OBJ_FLAG_HIDDEN); if (inputRow) lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_HIDDEN); if (clrBtn) lv_obj_remove_flag(clrBtn, LV_OBJ_FLAG_HIDDEN); } void Brainfuck::showExamplesView() { state = BfState::Examples; if (outputTa) lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN); if (inputRow) lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN); if (examplesList) lv_obj_remove_flag(examplesList, LV_OBJ_FLAG_HIDDEN); if (clrBtn) lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN); } /* ── Callbacks ────────────────────────────────────────────────────── */ void Brainfuck::onRunClicked(lv_event_t* e) { if (!g_instance || !g_instance->inputTa) return; const char* code = lv_textarea_get_text(g_instance->inputTa); if (code && code[0]) { g_instance->runCode(code); } } void Brainfuck::onClearClicked(lv_event_t* e) { if (!g_instance) return; if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, ""); if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, ""); } void Brainfuck::onExamplesClicked(lv_event_t* e) { if (!g_instance) return; if (g_instance->state == BfState::Examples) { g_instance->showMainView(); } else { g_instance->showExamplesView(); } } void Brainfuck::onExampleSelected(lv_event_t* e) { if (!g_instance) return; int idx = (int)(intptr_t)lv_event_get_user_data(e); if (idx >= 0 && idx < NUM_EXAMPLES) { if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, examples[idx].code); g_instance->showMainView(); g_instance->runCode(examples[idx].code); } } void Brainfuck::onFileSelected(lv_event_t* e) { if (!g_instance) return; const char* path = (const char*)lv_event_get_user_data(e); FILE* f = fopen(path, "rb"); if (!f) { if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Cannot open file"); g_instance->showMainView(); return; } fseek(f, 0, SEEK_END); long fsize = ftell(f); if (fsize <= 0 || fsize > 32768) { fclose(f); if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "File too large or empty"); g_instance->showMainView(); return; } fseek(f, 0, SEEK_SET); char* buf = (char*)malloc(fsize + 1); if (!buf) { fclose(f); if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Out of memory"); g_instance->showMainView(); return; } size_t bytesRead = fread(buf, 1, fsize, f); fclose(f); buf[bytesRead] = '\0'; if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, buf); g_instance->showMainView(); g_instance->runCode(buf); free(buf); } void Brainfuck::onInputReady(lv_event_t* e) { onRunClicked(e); } /* ── Script list building ─────────────────────────────────────────── */ void Brainfuck::buildScriptList(lv_obj_t* list) { freeScriptPaths(); for (int i = 0; i < NUM_EXAMPLES; i++) { lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_PLAY, examples[i].name); lv_obj_add_event_cb(btn, onExampleSelected, LV_EVENT_CLICKED, (void*)(intptr_t)i); } char scriptDir[256]; if (!getScriptDir(scriptDir, sizeof(scriptDir))) { lv_list_add_text(list, "No storage available for .bf files"); return; } DIR* dir = opendir(scriptDir); if (!dir) { char hint[300]; snprintf(hint, sizeof(hint), "Put .bf files in %s", scriptDir); lv_list_add_text(list, hint); return; } struct dirent* entry; while ((entry = readdir(dir)) != nullptr) { const char* name = entry->d_name; size_t len = strlen(name); if (len < 3) continue; const char* ext3 = &name[len - 3]; const char* ext2 = &name[len - 2]; if (ciStrcmp(ext3, ".bf") != 0 && ciStrcmp(ext2, ".b") != 0) { continue; } size_t pathLen = strlen(scriptDir) + 1 + len + 1; char* path = (char*)malloc(pathLen); if (!path) break; snprintf(path, pathLen, "%s/%s", scriptDir, name); char** tmp = (char**)realloc(scriptPaths, sizeof(char*) * (scriptCount + 1)); if (!tmp) { free(path); break; } scriptPaths = tmp; scriptPaths[scriptCount] = path; lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_FILE, name); lv_obj_add_event_cb(btn, onFileSelected, LV_EVENT_CLICKED, path); scriptCount++; } closedir(dir); if (scriptCount == 0) { lv_list_add_text(list, "No custom scripts found on storage"); } } /* ── Lifecycle ────────────────────────────────────────────────────── */ void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) { g_instance = this; s_appHandle = app; state = BfState::Examples; lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr); lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN); tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr); lv_obj_t* cont = lv_obj_create(parent); lv_obj_set_width(cont, LV_PCT(100)); lv_obj_set_flex_grow(cont, 1); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_all(cont, 4, 0); lv_obj_set_style_pad_gap(cont, 4, 0); lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_border_width(cont, 0, 0); outputTa = lv_textarea_create(cont); lv_textarea_set_text(outputTa, ""); lv_textarea_set_cursor_click_pos(outputTa, false); lv_obj_add_state(outputTa, LV_STATE_DISABLED); lv_obj_set_width(outputTa, LV_PCT(100)); lv_obj_set_flex_grow(outputTa, 1); lv_obj_set_style_text_font(outputTa, lv_font_get_default(), 0); lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN); inputRow = lv_obj_create(cont); lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(inputRow, 0, 0); lv_obj_set_style_pad_gap(inputRow, 4, 0); lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_border_width(inputRow, 0, 0); lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN); inputTa = lv_textarea_create(inputRow); lv_textarea_set_placeholder_text(inputTa, "++++++[>++++++++++<-]>+++++."); lv_textarea_set_one_line(inputTa, false); lv_obj_set_flex_grow(inputTa, 1); lv_obj_set_height(inputTa, 50); lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0); lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr); lv_obj_t* runBtn = lv_button_create(inputRow); lv_obj_t* runLbl = lv_label_create(runBtn); lv_label_set_text(runLbl, LV_SYMBOL_PLAY); lv_obj_add_event_cb(runBtn, onRunClicked, LV_EVENT_CLICKED, nullptr); examplesList = lv_list_create(cont); lv_obj_set_width(examplesList, LV_PCT(100)); lv_obj_set_flex_grow(examplesList, 1); buildScriptList(examplesList); } void Brainfuck::onHide(AppHandle app) { freeScriptPaths(); outputTa = nullptr; inputTa = nullptr; inputRow = nullptr; examplesList = nullptr; clrBtn = nullptr; g_instance = nullptr; s_appHandle = nullptr; }