diff --git a/CMakeLists.txt b/CMakeLists.txt index d88bac1..ffd71d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.45 LANGUAGES C) +project(fun VERSION 0.37.48 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/error/byte_for_demo.fun b/examples/byte_for_demo.fun similarity index 100% rename from examples/error/byte_for_demo.fun rename to examples/byte_for_demo.fun diff --git a/examples/class_without_object.fun b/examples/class_without_object.fun new file mode 100755 index 0000000..d60ed75 --- /dev/null +++ b/examples/class_without_object.fun @@ -0,0 +1,22 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-12 + */ + +// Demonstrates the usage of functions in classes without initiating an object. + +include + +print(SHA256().sha256_hex("")) + +/* Expected output: +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +*/ diff --git a/examples/console_prompt.fun b/examples/console_prompt.fun new file mode 100755 index 0000000..938532e --- /dev/null +++ b/examples/console_prompt.fun @@ -0,0 +1,24 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-12 + */ + +// Demonstrates the the console prompt. + +include + +input = Console().prompt("fun> ") +print("You entered: " + input) + +/* Possible output: +fun> Fun! +You entered: Fun! +*/ diff --git a/examples/progress.fun b/examples/progress.fun new file mode 100755 index 0000000..82f754b --- /dev/null +++ b/examples/progress.fun @@ -0,0 +1,42 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-12 + */ + +// Demonstrates system library includes after installation to /usr/lib/fun + +// includes can also be done with a leading # like in C, but some programmers maybe like more clean code without a +// leading #. +include +include // for sleep() + +c = Console() + +print("Progress demo: 0..100") + +total = 100 +for i in range(0, total + 1) + c.progress(i, total, "Downloading") + sleep(30) // milliseconds + +print("\nMultiple phases demo") + +// Phase 1 +phase_total = 40 +for i in range(0, phase_total + 1) + c.progress(i, phase_total, "Phase 1") + sleep(20) + +// Phase 2 +phase_total = 60 +for i in range(0, phase_total + 1) + c.progress(i, phase_total, "Phase 2") + sleep(15) diff --git a/examples/progress_inline.fun b/examples/progress_inline.fun new file mode 100644 index 0000000..42cb3b8 --- /dev/null +++ b/examples/progress_inline.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-01-13 + */ + +// Realtime progress bar demo without printing extra newlines. +// It only updates a single console line in-place. The progress() +// helper emits one trailing newline automatically at 100%. + +include +include // for sleep() + +c = Console() + +total = 100 +for i in range(0, total + 1) + c.progress(i, total, "Downloading") + sleep(20) // milliseconds + +// Done. No additional prints/newlines here; progress() already +// finalized the line when it reached 100%. diff --git a/lib/io/console.fun b/lib/io/console.fun index e2a0cd0..c7f76ba 100644 --- a/lib/io/console.fun +++ b/lib/io/console.fun @@ -1,8 +1,18 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + /* * Console utilities: prompt, ask, and yes/no helpers built on input(). */ #include +#include class Console() // Print a prompt and read a line (no trailing newline) @@ -38,3 +48,66 @@ class Console() else if (a == "n" || a == "no") return 0 // otherwise loop again + + // Internal: best-effort terminal columns via `tput cols` (fallback to 80) + fun term_cols(this) + // Use builtin proc_run directly to avoid any method dispatch quirks + r = proc_run("tput cols") + if (r["code"] == 0) + s = str_trim(r["out"]) // trim trailing newline + if (len(s) > 0) + n = to_number(s) + if (n > 0) + return n + // Fallback if tput not available or not a TTY + return 80 + + // Draw/update a full-width progress bar on a single console line. + // - current: Number (0..total) + // - total: Number (>0) + // - label: Optional text shown left of the bar + // Returns the integer percent [0..100]. Adds a newline automatically at 100%. + fun progress(this, current, total, label) + cur = to_number(current) + tot = to_number(total) + if (tot <= 0) + tot = 1 + pct = to_number((cur * 100) / tot) + if (pct < 0) + pct = 0 + if (pct > 100) + pct = 100 + + cols = this.term_cols() + + lbl = to_string(label) + if (len(lbl) > 0) + prefix = lbl + " " + else + prefix = "" + + perc_str = to_string(pct) + "%" + // We render: [====....] plus percent; keep the whole line within terminal width + // Compute bar width: terminal cols minus prefix, brackets, space, and percent. + fixed = len(prefix) + 2 + 1 + len(perc_str) // [] + space + percent + bar_w = cols - fixed + if (bar_w < 10) + bar_w = 10 // minimal bar width for visibility + + filled = to_number((bar_w * pct) / 100) + if (filled < 0) + filled = 0 + if (filled > bar_w) + filled = bar_w + + bar = "[" + str_repeat("=", filled) + str_repeat(" ", bar_w - filled) + "]" + line = prefix + bar + " " + perc_str + + // In-place update: CR + full-width content (we already fit to cols), no newline + echo("\r" + line) + + // When done, end the line cleanly + if (cur >= tot) + print("") + + return pct diff --git a/src/repl.c b/src/repl.c index 9e3c481..215a38f 100644 --- a/src/repl.c +++ b/src/repl.c @@ -860,6 +860,8 @@ static void show_repl_help(void) { printf(" :frame N Select frame N for :locals/:list/:disas (default: top)\n"); printf(" :list [±K] Show K lines of source around current frame line (default 5)\n"); printf(" :disas [±N] Disassemble around current frame ip (default 5)\n"); + printf(" :disasm WHAT [off [len]] [to ] Hexdump VM memory region to screen or file\n"); + printf(" WHAT = code | stack | globals | consts\n"); printf(" :stack [N] Show top N (default all) stack values\n"); printf(" :top Show the top of the VM stack\n"); printf(" :locals [FRAME] Show locals of frame (default: selected frame)\n"); @@ -898,6 +900,33 @@ static int write_entire_file(const char *path, const char *data, size_t len) { return n == len; } +/* ---------- Hexdump helper ---------- */ +static void hexdump_to(FILE *out, const unsigned char *data, size_t len, size_t base_off) { + if (!out || !data || len == 0) return; + const size_t width = 16; + char ascii[17]; + ascii[16] = '\0'; + for (size_t i = 0; i < len; i += width) { + size_t chunk = (len - i < width) ? (len - i) : width; + /* address */ + fprintf(out, "%08zx ", base_off + i); + /* hex bytes */ + for (size_t j = 0; j < width; ++j) { + if (j < chunk) { + fprintf(out, "%02x ", data[i + j]); + unsigned char c = data[i + j]; + ascii[j] = (c >= 32 && c <= 126) ? (char)c : '.'; + } else { + fputs(" ", out); + ascii[j] = ' '; + } + if (j == 7) fputc(' ', out); /* extra space between 8-byte halves */ + } + ascii[chunk < width ? chunk : width] = '\0'; + fprintf(out, " |%s|\n", ascii); + } +} + static void print_last_n_lines(const char *path, int n) { if (n <= 0) n = 50; size_t flen = 0; @@ -1388,16 +1417,145 @@ int fun_run_repl(VM *vm) { if (idx < 0) { printf("(no current frame)\n"); continue; } Frame *f = &vm->frames[idx]; if (!f->fn) { printf("(no function)\n"); continue; } - int curip = f->ip - 1; if (curip < 0) curip = 0; + + /* Ensure we have a valid instruction buffer */ + if (f->fn->instr_count <= 0 || f->fn->instructions == NULL) { + printf("(no instructions)\n"); + continue; + } + + int count = f->fn->instr_count; + int curip = f->ip - 1; + if (curip < 0) curip = 0; + if (curip >= count) curip = count - 1; + int from = curip - n; if (from < 0) from = 0; - int to = curip + n; if (to >= f->fn->instr_count) to = f->fn->instr_count - 1; + int to = curip + n; if (to >= count) to = count - 1; + if (to < from) { /* nothing to show */ continue; } + for (int i = from; i <= to; ++i) { Instruction ins = f->fn->instructions[i]; - const char *opname = (ins.op >= 0 && ins.op < (int)(sizeof(opcode_names)/sizeof(opcode_names[0]))) - ? opcode_names[ins.op] : "???"; + const char *opname = opcode_is_valid(ins.op) ? opcode_names[ins.op] : "???"; printf("%c %6d: %-14s %d\n", (i == curip ? '>' : ' '), i, opname, ins.operand); } continue; + } else if (strcmp(cmd, "disasm") == 0) { + /* Syntax: :disasm WHAT [off [len]] [to ] + WHAT: code | stack | globals | consts */ + const char *p = lstrip(arg); + if (!p || !*p) { + printf("Usage: :disasm WHAT [off [len]] [to ]\n"); + printf(" WHAT = code | stack | globals | consts\n"); + continue; + } + + char what[32]; + int consumed = 0; + if (sscanf(p, "%31s %n", what, &consumed) != 1) { + printf("Usage: :disasm WHAT [off [len]] [to ]\n"); + continue; + } + p += consumed; + + size_t off = 0; + size_t len = (size_t)-1; /* default later to clamp */ + + /* parse optional off */ + while (*p == ' ' || *p == '\t') p++; + if (*p && (isdigit((unsigned char)*p))) { + char *endp = NULL; + long long v = strtoll(p, &endp, 10); + if (endp && endp != p && v >= 0) { + off = (size_t)v; + p = endp; + } + } + /* parse optional len */ + while (*p == ' ' || *p == '\t') p++; + if (*p && (isdigit((unsigned char)*p))) { + char *endp = NULL; + long long v = strtoll(p, &endp, 10); + if (endp && endp != p && v >= 0) { + len = (size_t)v; + p = endp; + } + } + + /* optional: to */ + while (*p == ' ' || *p == '\t') p++; + int to_file = 0; + char path[PATH_MAX]; + path[0] = '\0'; + if (strncmp(p, "to", 2) == 0 && (p[2] == '\0' || isspace((unsigned char)p[2]))) { + p += 2; + while (*p == ' ' || *p == '\t') p++; + if (!*p) { printf("Missing after 'to'\n"); continue; } + /* read until whitespace end or end of string; simple paths without spaces */ + size_t i = 0; + while (*p && !isspace((unsigned char)*p) && i + 1 < sizeof(path)) path[i++] = *p++; + path[i] = '\0'; + if (path[0] == '\0') { printf("Invalid file path\n"); continue; } + to_file = 1; + } + + const unsigned char *base = NULL; + size_t total = 0; + int ok_region = 1; + + if (strcmp(what, "code") == 0) { + int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp; + if (idx < 0) { printf("(no current frame)\n"); continue; } + Frame *fr = &vm->frames[idx]; + if (!fr->fn || fr->fn->instr_count <= 0 || fr->fn->instructions == NULL) { + printf("(no code to dump)\n"); continue; + } + base = (const unsigned char*)fr->fn->instructions; + total = (size_t)fr->fn->instr_count * sizeof(Instruction); + } else if (strcmp(what, "consts") == 0) { + int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp; + if (idx < 0) { printf("(no current frame)\n"); continue; } + Frame *fr = &vm->frames[idx]; + if (!fr->fn || fr->fn->const_count <= 0 || fr->fn->constants == NULL) { + printf("(no constants to dump)\n"); continue; + } + base = (const unsigned char*)fr->fn->constants; + total = (size_t)fr->fn->const_count * sizeof(Value); + } else if (strcmp(what, "stack") == 0) { + int count = vm->sp + 1; + if (count <= 0) { printf("(stack empty)\n"); continue; } + base = (const unsigned char*)vm->stack; + total = (size_t)count * sizeof(Value); + } else if (strcmp(what, "globals") == 0) { + base = (const unsigned char*)vm->globals; + total = (size_t)MAX_GLOBALS * sizeof(Value); + } else { + ok_region = 0; + } + + if (!ok_region) { + printf("Unknown region '%s'. Use one of: code, stack, globals, consts\n", what); + continue; + } + if (!base || total == 0) { printf("(nothing to dump)\n"); continue; } + + if (off >= total) { printf("(empty range: offset beyond end)\n"); continue; } + size_t avail = total - off; + if (len == (size_t)-1 || len == 0 || len > avail) { + /* default to min(256, avail) */ + len = avail < 256 ? avail : 256; + } + + if (!to_file) { + printf("Hexdump %s: total=%zu, offset=%zu, len=%zu\n", what, total, off, len); + hexdump_to(stdout, base + off, len, off); + } else { + FILE *fout = fopen(path, "wb"); + if (!fout) { printf("Failed to open '%s' for writing\n", path); continue; } + hexdump_to(fout, base + off, len, off); + fclose(fout); + printf("Wrote hexdump (%zu bytes from %s) to %s\n", len, what, path); + } + continue; } else if (strcmp(cmd, "printv") == 0) { const char *spec = lstrip(arg); if (!spec || !*spec) { printf("Usage: :printv local[i] | stack[i] | global[i]\n"); continue; } diff --git a/src/vm.c b/src/vm.c index f21d110..61e347f 100644 --- a/src/vm.c +++ b/src/vm.c @@ -503,10 +503,6 @@ void vm_print_output(VM *vm) { printf("\n"); } } - /* If the last item was partial (from echo), terminate the line for cleanliness */ - if (vm->output_count > 0 && vm->output_is_partial[vm->output_count - 1]) { - printf("\n"); - } } void vm_run(VM *vm, Bytecode *entry) { @@ -853,6 +849,18 @@ void vm_run(VM *vm, Bytecode *entry) { } break; } + + /* Stream console output in realtime for scripts: + * When PRINT/ECHO pushed items into the VM's output buffer, flush them + * immediately to stdout and clear the buffer to avoid end-of-run bursts. + * This keeps REPL compatibility (REPL still prints after each submit), + * while regular script execution shows progress bars live. + */ + if (inst.op == OP_PRINT || inst.op == OP_ECHO) { + vm_print_output(vm); + vm_clear_output(vm); + fflush(stdout); + } } g_active_vm = NULL; } diff --git a/src/vm/echo.c b/src/vm/echo.c index 0a18101..b29d8e6 100644 --- a/src/vm/echo.c +++ b/src/vm/echo.c @@ -1,4 +1,13 @@ -/** +/* +* This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + + /** * Implements OP_ECHO: print top-of-stack value without trailing newline. * Now stores the value into the VM's output buffer and marks it as partial, * so the CLI can render echo output together with following print output.