1
0
Fork 0
forked from fun/fun

Compare commits

...

3 commits

10 changed files with 375 additions and 10 deletions

View file

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

View file

@ -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 <you@hanez.org>
* 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 <crypt/sha256.fun>
print(SHA256().sha256_hex(""))
/* Expected output:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
*/

24
examples/console_prompt.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <io/console.fun>
input = Console().prompt("fun> ")
print("You entered: " + input)
/* Possible output:
fun> Fun!
You entered: Fun!
*/

42
examples/progress.fun Executable file
View file

@ -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 <you@hanez.org>
* 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 <io/console.fun>
include <utils/datetime.fun> // 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)

View file

@ -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 <io/console.fun>
include <utils/datetime.fun> // 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%.

View file

@ -1,8 +1,18 @@
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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 <strings.fun>
#include <io/process.fun>
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

View file

@ -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 <file>] 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 <file>]
WHAT: code | stack | globals | consts */
const char *p = lstrip(arg);
if (!p || !*p) {
printf("Usage: :disasm WHAT [off [len]] [to <file>]\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 <file>]\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 <file> */
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 <file> 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; }

View file

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

View file

@ -1,4 +1,13 @@
/**
/*
* This file is part of the Fun programming language.
* https://fun-lang.xyz/
*
* Copyright 2025 Johannes Findeisen <you@hanez.org>
* 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.