1
0
Fork 0
forked from fun/fun
fun/src/repl.c

2263 lines
70 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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
*/
/**
* @file repl.c
* @brief Interactive ReadEvalPrint Loop (REPL) for the Fun language.
*
* Provides line editing, history, simple completion for :load paths and
* stdlib identifiers, multi-line input heuristics, and a command interface
* (e.g. :help, :env, :load). Built only when FUN_WITH_REPL is defined.
*/
#include "repl.h"
#include "bytecode.h"
#include "parser.h"
#include "value.h"
#include "vm.h"
/* include mapping helper from parser_utils.c */
extern int map_expanded_line_to_include_path(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line);
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifndef _WIN32
#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#endif
#ifdef FUN_WITH_REPL
#ifndef FUN_VERSION
#define FUN_VERSION "0.0.0-dev"
#endif
/* --------- REPL history (in-memory + file integration) ---------- */
enum { RL_HIST_MAX = 1000 };
static char *rl_hist[RL_HIST_MAX];
static int rl_count = 0;
/* last history entry (or NULL) */
/**
* @brief Return the last history line or NULL if history is empty.
*/
static const char *rl_hist_last(void) {
if (rl_count <= 0) return NULL;
return rl_hist[rl_count - 1];
}
/* add one line to in-memory history (without trailing newline), dedup consecutive */
/**
* @brief Append a non-empty line to in-memory history, de-duplicating
* consecutive duplicates. Trailing newlines are stripped.
*/
static void rl_hist_add(const char *s) {
if (!s) return;
size_t n = strlen(s);
/* strip single trailing newline if present */
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r'))
n--;
if (n == 0) return;
/* dedupe consecutive identical */
const char *last = rl_hist_last();
if (last && strncmp(last, s, n) == 0 && last[n] == '\0') return;
/* allocate and copy */
char *cpy = (char *)malloc(n + 1);
if (!cpy) return;
memcpy(cpy, s, n);
cpy[n] = '\0';
if (rl_count == RL_HIST_MAX) {
free(rl_hist[0]);
memmove(&rl_hist[0], &rl_hist[1], sizeof(rl_hist[0]) * (RL_HIST_MAX - 1));
rl_count--;
}
rl_hist[rl_count++] = cpy;
}
/* preload history from file (one line per entry) */
/**
* @brief Load history entries from a file (one line per entry).
*/
static void rl_hist_load_file(const char *path) {
if (!path) return;
FILE *f = fopen(path, "r");
if (!f) return;
char line[4096];
while (fgets(line, sizeof(line), f)) {
rl_hist_add(line);
}
fclose(f);
}
#ifndef _WIN32
static struct termios g_orig_tios;
static int g_raw_enabled = 0;
/**
* @brief Disable terminal raw mode if previously enabled.
*/
static void repl_disable_raw(void) {
if (g_raw_enabled) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_orig_tios);
g_raw_enabled = 0;
}
}
/**
* @brief Enable minimal terminal raw mode for interactive input.
* @return 1 on success, 0 otherwise.
*/
static int repl_enable_raw(void) {
if (!isatty(STDIN_FILENO)) return 0;
if (g_raw_enabled) return 1;
if (tcgetattr(STDIN_FILENO, &g_orig_tios) == -1) return 0;
struct termios raw = g_orig_tios;
raw.c_lflag &= ~(ICANON | ECHO);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) return 0;
g_raw_enabled = 1;
return 1;
}
/* return 1 if path is a directory, else 0 */
/**
* @brief Return 1 if path refers to a directory, 0 otherwise.
*/
static int is_dir_path(const char *path) {
struct stat st;
if (stat(path, &st) != 0) return 0;
return S_ISDIR(st.st_mode);
}
/* Compute longest common prefix of a set of strings (starting from offset base_len) */
/**
* @brief Longest common suffix length among names[i] starting at base_len.
*/
static size_t lcp_suffix(const char **names, int count, size_t base_len) {
if (count <= 0) return base_len;
size_t lcp = (size_t)-1;
for (int i = 0; i < count; ++i) {
size_t nlen = strlen(names[i]);
size_t cur = 0;
/* compare suffix part starting at base_len */
while (base_len + cur < nlen) {
char c = names[i][base_len + cur];
int ok = 1;
for (int j = 0; j < count; ++j) {
size_t jlen = strlen(names[j]);
if (base_len + cur >= jlen || names[j][base_len + cur] != c) {
ok = 0;
break;
}
}
if (!ok) break;
cur++;
}
if (lcp == (size_t)-1 || cur < lcp) lcp = cur;
}
return base_len + (lcp == (size_t)-1 ? 0 : lcp);
}
/* Word-jump helpers (used by Ctrl+Left/Right in the REPL editor).
* Words are runs of non-space characters; separators are spaces.
*/
/**
* @brief Move cursor left by one word.
*/
static void rl_word_left(const char *out, size_t len, size_t *pos) {
(void)len;
if (!out || !pos) return;
if (*pos == 0) return;
while (*pos > 0 && out[*pos - 1] == ' ')
(*pos)--;
while (*pos > 0 && out[*pos - 1] != ' ')
(*pos)--;
}
/**
* @brief Move cursor right by one word.
*/
static void rl_word_right(const char *out, size_t len, size_t *pos) {
if (!out || !pos) return;
if (*pos >= len) return;
while (*pos < len && out[*pos] != ' ')
(*pos)++;
while (*pos < len && out[*pos] == ' ')
(*pos)++;
}
/* Expand file path for path-taking REPL commands (e.g., :load, :run) in-place; returns 1 if buffer changed (redraw) */
/**
* @brief Complete a :load file path in-place within the buffer.
* @return 1 if buffer changed, 2 if a menu was printed, 0 otherwise.
*/
static int complete_load_path(char *buf, size_t *len_io) {
size_t len = *len_io;
if (len < 3) return 0; /* minimally ":x" */
const char *p = buf;
while (*p == ' ')
p++;
if (*p != ':') return 0;
p++;
/* Parse command token (letters only) */
const char *cmd_start = p;
while (*p && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')))
p++;
size_t cmd_len = (size_t)(p - cmd_start);
if (cmd_len == 0) return 0;
/* Accept both full and short aliases for :load and :run */
int is_supported = 0;
if ((cmd_len == 4 && strncmp(cmd_start, "load", 4) == 0) ||
(cmd_len == 2 && strncmp(cmd_start, "lo", 2) == 0) ||
(cmd_len == 3 && strncmp(cmd_start, "run", 3) == 0) ||
(cmd_len == 2 && strncmp(cmd_start, "ru", 2) == 0)) {
is_supported = 1;
}
if (!is_supported) return 0;
while (*p == ' ' || *p == '\t')
p++;
size_t arg_off = (size_t)(p - buf);
if (arg_off > len) return 0;
char prefix[PATH_MAX];
size_t plen = 0;
if (len - arg_off >= sizeof(prefix))
plen = sizeof(prefix) - 1;
else
plen = len - arg_off;
memcpy(prefix, buf + arg_off, plen);
prefix[plen] = '\0';
char expanded[PATH_MAX];
if (prefix[0] == '~') {
const char *home = getenv("HOME");
if (!home) home = getenv("USERPROFILE");
if (home)
snprintf(expanded, sizeof(expanded), "%s%s", home, prefix + 1);
else
snprintf(expanded, sizeof(expanded), "%s", prefix);
} else {
snprintf(expanded, sizeof(expanded), "%s", prefix);
}
char dirpart[PATH_MAX], base[PATH_MAX];
const char *slash = strrchr(expanded, '/');
if (slash) {
size_t dlen = (size_t)(slash - expanded);
if (dlen == 0) {
strcpy(dirpart, "/");
} else {
memcpy(dirpart, expanded, dlen);
dirpart[dlen] = '\0';
}
snprintf(base, sizeof(base), "%s", slash + 1);
} else {
strcpy(dirpart, ".");
snprintf(base, sizeof(base), "%s", expanded);
}
DIR *dp = opendir(dirpart);
if (!dp) {
fputc('\a', stdout);
fflush(stdout);
return 0;
}
const char *names[1024];
char storage[1024][NAME_MAX + 1];
int count = 0;
struct dirent *de;
size_t blen = strlen(base);
while ((de = readdir(dp)) != NULL) {
if (blen == 0 || strncmp(de->d_name, base, blen) == 0) {
if (count < (int)(sizeof(names) / sizeof(names[0]))) {
snprintf(storage[count], sizeof(storage[count]), "%s", de->d_name);
names[count] = storage[count];
count++;
}
}
}
closedir(dp);
if (count == 0) {
fputc('\a', stdout);
fflush(stdout);
return 0;
}
size_t new_pref_len = lcp_suffix(names, count, blen);
int changed = 0;
char completed[PATH_MAX];
if (slash) {
char head[PATH_MAX];
memcpy(head, expanded, (size_t)(slash - expanded + 1));
head[slash - expanded + 1] = '\0';
strncpy(completed, head, sizeof(completed));
} else {
snprintf(completed, sizeof(completed), "%s", "");
}
strncat(completed, base, sizeof(completed) - strlen(completed) - 1);
if (new_pref_len > blen) {
strncat(completed, names[0] + blen, (new_pref_len - blen));
changed = 1;
} else if (count == 1) {
size_t extra = strlen(names[0]) - blen;
strncat(completed, names[0] + blen, extra);
changed = 1;
}
if (count == 1) {
char test[PATH_MAX];
if (slash)
snprintf(test, sizeof(test), "%.*s/%s", (int)(slash - expanded), expanded, names[0]);
else
snprintf(test, sizeof(test), "%s", names[0]);
if (is_dir_path(test)) {
strncat(completed, "/", sizeof(completed) - strlen(completed) - 1);
}
}
if (!changed && count > 1) {
putchar('\n');
for (int i = 0; i < count; ++i) {
fputs(names[i], stdout);
fputc(((i + 1) % 6 == 0) ? '\n' : '\t', stdout);
}
if (count % 6 != 0) putchar('\n');
fflush(stdout);
return 0;
}
size_t comp_len = strlen(completed);
if (arg_off + comp_len >= PATH_MAX - 1) comp_len = PATH_MAX - 2 - arg_off;
memcpy(buf + arg_off, completed, comp_len);
*len_io = arg_off + comp_len;
buf[*len_io] = '\0';
return 1;
}
/* ---------- Stdlib symbol completion ---------- */
static char **g_std_syms = NULL;
static int g_std_syms_count = 0;
static int g_std_syms_cap = 0;
/**
* @brief Return 1 if c can start an identifier.
*/
static int is_ident_start(int c) {
return (c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
}
/**
* @brief Return 1 if c is a valid identifier continuation character.
*/
static int is_ident_char(int c) {
return is_ident_start(c) || (c >= '0' && c <= '9');
}
/**
* @brief Add a stdlib symbol name to the completion table.
*/
static void std_syms_add(const char *name) {
if (!name || !*name) return;
/* dedupe */
for (int i = 0; i < g_std_syms_count; ++i) {
if (strcmp(g_std_syms[i], name) == 0) return;
}
if (g_std_syms_count == g_std_syms_cap) {
int ncap = g_std_syms_cap == 0 ? 256 : g_std_syms_cap * 2;
char **nn = (char **)realloc(g_std_syms, (size_t)ncap * sizeof(char *));
if (!nn) return;
g_std_syms = nn;
g_std_syms_cap = ncap;
}
size_t n = strlen(name);
char *cpy = (char *)malloc(n + 1);
if (!cpy) return;
memcpy(cpy, name, n + 1);
g_std_syms[g_std_syms_count++] = cpy;
}
/**
* @brief Scan a .fun file and collect top-level definitions for completion.
*/
static void scan_symbols_from_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return;
char line[4096];
while (fgets(line, sizeof(line), f)) {
const char *p = line;
while (*p == ' ' || *p == '\t')
p++;
/* Patterns: fun NAME( ; class NAME ; const NAME ; var NAME */
const char *kw = NULL;
if (strncmp(p, "fun ", 4) == 0) {
kw = "fun";
p += 4;
} else if (strncmp(p, "class ", 6) == 0) {
kw = "class";
p += 6;
} else if (strncmp(p, "const ", 6) == 0) {
kw = "const";
p += 6;
} else if (strncmp(p, "var ", 4) == 0) {
kw = "var";
p += 4;
}
if (!kw) continue;
while (*p == ' ' || *p == '\t')
p++;
if (!is_ident_start((unsigned char)*p)) continue;
char name[256];
size_t ni = 0;
while (is_ident_char((unsigned char)*p) && ni + 1 < sizeof(name)) {
name[ni++] = *p++;
}
name[ni] = '\0';
if (ni > 0) std_syms_add(name);
}
fclose(f);
}
/**
* @brief Recursively scan a directory tree for .fun files.
*/
static void scan_dir_recursive(const char *dir) {
DIR *dp = opendir(dir);
if (!dp) return;
struct dirent *de;
char path[PATH_MAX];
while ((de = readdir(dp)) != NULL) {
const char *n = de->d_name;
if (strcmp(n, ".") == 0 || strcmp(n, "..") == 0) continue;
snprintf(path, sizeof(path), "%s/%s", dir, n);
struct stat st;
if (stat(path, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
scan_dir_recursive(path);
} else {
size_t L = strlen(n);
if (L >= 4 && strcmp(n + (L - 4), ".fun") == 0) {
scan_symbols_from_file(path);
}
}
}
closedir(dp);
}
/**
* @brief Populate stdlib completion symbols from a base directory.
*/
static void load_stdlib_symbols(const char *libdir) {
if (!libdir || !*libdir) return;
scan_dir_recursive(libdir);
}
/**
* @brief Free all memory held by stdlib completion symbols.
*/
static void free_stdlib_symbols(void) {
for (int i = 0; i < g_std_syms_count; ++i)
free(g_std_syms[i]);
free(g_std_syms);
g_std_syms = NULL;
g_std_syms_count = g_std_syms_cap = 0;
}
/* Complete the trailing identifier in 'buf' using stdlib symbols.
Returns 1 if buffer changed, 2 if a menu was printed, 0 otherwise. */
/**
* @brief Complete the trailing identifier in buf from stdlib symbols.
* @return 1 if buffer changed, 2 if menu printed, 0 otherwise.
*/
static int complete_stdlib_ident(char *buf, size_t *len_io, size_t *pos_io, size_t cap) {
size_t len = *len_io;
size_t pos = *pos_io;
if (pos != len) return 0; /* complete only at end */
if (len == 0) return 0;
/* find start of identifier */
size_t start = len;
while (start > 0 && is_ident_char((unsigned char)buf[start - 1]))
start--;
if (start == len) return 0;
const char *prefix = buf + start;
size_t plen = len - start;
/* collect matches */
const char *matches[1024];
int mcount = 0;
for (int i = 0; i < g_std_syms_count && mcount < (int)(sizeof(matches) / sizeof(matches[0])); ++i) {
if (strncmp(g_std_syms[i], prefix, plen) == 0) {
matches[mcount++] = g_std_syms[i];
}
}
if (mcount == 0) {
fputc('\a', stdout);
fflush(stdout);
return 0;
}
/* compute longest common extension */
size_t lcp = (size_t)-1;
for (int i = 0; i < mcount; ++i) {
size_t nlen = strlen(matches[i]);
size_t cur = 0;
while (plen + cur < nlen) {
char c = matches[i][plen + cur];
int ok = 1;
for (int j = 0; j < mcount; ++j) {
size_t jlen = strlen(matches[j]);
if (plen + cur >= jlen || matches[j][plen + cur] != c) {
ok = 0;
break;
}
}
if (!ok) break;
cur++;
}
if (lcp == (size_t)-1 || cur < lcp) lcp = cur;
}
size_t new_pref_len = plen + (lcp == (size_t)-1 ? 0 : lcp);
/* build completion text */
char comp[512];
size_t write_len = 0;
if (mcount == 1) {
snprintf(comp, sizeof(comp), "%s", matches[0]);
write_len = strlen(comp);
} else if (new_pref_len > plen) {
strncpy(comp, prefix, plen);
comp[plen] = '\0';
strncat(comp, matches[0] + plen, new_pref_len - plen);
write_len = strlen(comp);
} else {
/* list candidates */
putchar('\n');
for (int i = 0; i < mcount; ++i) {
fputs(matches[i], stdout);
fputc(((i + 1) % 6 == 0) ? '\n' : '\t', stdout);
}
if (mcount % 6 != 0) putchar('\n');
fflush(stdout);
return 2;
}
/* replace tail from 'start' with comp */
if (start + write_len >= cap) write_len = cap - 1 - start;
memmove(buf + start, comp, write_len);
*len_io = start + write_len;
buf[*len_io] = '\0';
*pos_io = *len_io;
return 1;
}
/* Read one line with prompt, handling backspace, Up/Down history and :load path completion (now with multi-line editing via Ctrl+O) */
/**
* @brief Read a line with basic line-editing and history support.
* @param out Output buffer to receive the line (NUL-terminated).
* @param out_cap Capacity of the output buffer.
* @param prompt Prompt string to display (may be NULL).
* @return 1 on success, 0 on EOF/error.
*/
static int read_line_edit(char *out, size_t out_cap, const char *prompt) {
#ifdef _WIN32
if (prompt) fputs(prompt, stdout), fflush(stdout);
if (!fgets(out, (int)out_cap, stdin)) return 0;
return 1;
#else
if (prompt) fputs(prompt, stdout), fflush(stdout);
if (!repl_enable_raw()) {
if (!fgets(out, (int)out_cap, stdin)) return 0;
return 1;
}
size_t len = 0;
size_t pos = 0;
int hist_pos = rl_count;
char saved_current[4096];
int has_saved = 0;
/* How many terminal rows were drawn in the previous repaint (prompt + lines in 'out').
We assume no automatic wrapping (only explicit '\n'). */
static int prev_rows = 1;
#define RL_REDRAW() \
do { \
/* Count explicit newlines to estimate rows to draw (prompt line + content lines) */ \
int rows = 1; \
int has_nl = 0; \
for (size_t __i = 0; __i < len; ++__i) { \
if (out[__i] == '\n') { \
rows++; \
has_nl = 1; \
} \
} \
/* Move cursor to the start (top) of the previous render block */ \
if (prev_rows > 1) { \
fprintf(stdout, "\x1b[%dA", prev_rows - 1); \
} \
fputc('\r', stdout); \
/* Repaint prompt + content */ \
if (prompt) fputs(prompt, stdout); \
fwrite(out, 1, len, stdout); \
/* Clear everything below the current cursor (removes remnants of older, longer renders) */ \
fputs("\x1b[J", stdout); \
/* Cursor policy: keep at end for multi-line; for single-line respect pos */ \
if (!has_nl && pos < len) { \
size_t back = (size_t)(len - pos); \
if (back > 0) fprintf(stdout, "\x1b[%zuD", back); \
} else { \
pos = len; \
} \
prev_rows = rows; \
fflush(stdout); \
} while (0)
for (;;) {
int ch = getchar();
if (ch == EOF) {
repl_disable_raw();
return 0;
}
if (ch == '\r' || ch == '\n') {
fputc('\n', stdout);
if (len + 1 < out_cap) {
out[len++] = '\n';
out[len] = '\0';
} else {
out[len] = '\0';
}
rl_hist_add(out);
repl_disable_raw();
return 1;
} else if (ch == 27) {
int c1 = getchar();
if (c1 == '[') {
char params[16];
int pi = 0;
int final = 0;
for (;;) {
int cx = getchar();
if (cx == EOF) break;
if ((cx >= 'A' && cx <= 'Z') || (cx >= 'a' && cx <= 'z') || cx == '~') {
final = cx;
break;
}
if (pi + 1 < (int)sizeof(params)) {
params[pi++] = (char)cx;
params[pi] = '\0';
}
}
int ctrl = 0;
if (pi > 0) {
if (strstr(params, ";5") != NULL || strcmp(params, "5") == 0 || strstr(params, "1;5") != NULL) {
ctrl = 1;
}
}
/* disallow fine-grained horizontal movement in multi-line mode (cursor stays at end) */
int has_nl = 0;
for (size_t __i = 0; __i < len; ++__i) {
if (out[__i] == '\n') {
has_nl = 1;
break;
}
}
if (final == 'A') {
if (!has_saved) {
size_t sl = len < sizeof(saved_current) - 1 ? len : sizeof(saved_current) - 1;
memcpy(saved_current, out, sl);
saved_current[sl] = '\0';
has_saved = 1;
}
if (hist_pos > 0) {
hist_pos--;
const char *h = rl_hist[hist_pos];
size_t hl = strlen(h);
if (hl >= out_cap) hl = out_cap - 1;
memcpy(out, h, hl);
out[hl] = '\0';
len = hl;
pos = len;
RL_REDRAW();
} else {
fputc('\a', stdout);
fflush(stdout);
}
} else if (final == 'B') {
if (hist_pos < rl_count) {
hist_pos++;
if (hist_pos == rl_count) {
if (has_saved) {
size_t hl = strlen(saved_current);
if (hl >= out_cap) hl = out_cap - 1;
memcpy(out, saved_current, hl);
out[hl] = '\0';
len = hl;
} else {
len = 0;
out[0] = '\0';
}
} else {
const char *h = rl_hist[hist_pos];
size_t hl = strlen(h);
if (hl >= out_cap) hl = out_cap - 1;
memcpy(out, h, hl);
out[hl] = '\0';
len = hl;
}
pos = len;
RL_REDRAW();
} else {
fputc('\a', stdout);
fflush(stdout);
}
} else if (final == 'C') {
if (has_nl) {
fputc('\a', stdout);
fflush(stdout);
pos = len;
RL_REDRAW();
} else if (ctrl) {
size_t old = pos;
rl_word_right(out, len, &pos);
if (pos != old)
RL_REDRAW();
else {
fputc('\a', stdout);
fflush(stdout);
}
} else {
if (pos < len) {
pos++;
fputs("\x1b[C", stdout);
fflush(stdout);
} else {
fputc('\a', stdout);
fflush(stdout);
}
}
} else if (final == 'D') {
if (has_nl) {
fputc('\a', stdout);
fflush(stdout);
pos = len;
RL_REDRAW();
} else if (ctrl) {
size_t old = pos;
rl_word_left(out, len, &pos);
if (pos != old)
RL_REDRAW();
else {
fputc('\a', stdout);
fflush(stdout);
}
} else {
if (pos > 0) {
pos--;
fputs("\x1b[D", stdout);
fflush(stdout);
} else {
fputc('\a', stdout);
fflush(stdout);
}
}
} else {
/* ignore other CSI sequences */
}
}
} else if (ch == 127 || ch == 8) {
if (pos > 0) {
memmove(out + pos - 1, out + pos, len - pos);
len--;
pos--;
out[len] = '\0';
RL_REDRAW();
} else {
fputc('\a', stdout);
fflush(stdout);
}
} else if (ch == '\t') {
if (pos != len) {
fputc('\a', stdout);
fflush(stdout);
} else {
out[len] = '\0';
size_t newlen = len;
int changed = complete_load_path(out, &newlen);
if (changed) {
len = newlen;
pos = len;
} else {
/* try stdlib symbol completion */
int res = complete_stdlib_ident(out, &newlen, &pos, out_cap);
if (res == 1) {
len = newlen;
} else if (res == 2) {
/* menu printed — do not modify buffer, just redraw prompt+line */
} else {
fputc('\a', stdout);
fflush(stdout);
}
}
RL_REDRAW();
}
} else if (ch == 15) { /* Ctrl+O -> insert newline at cursor (multi-line editing) */
if (len + 1 < out_cap) {
memmove(out + pos + 1, out + pos, len - pos);
out[pos] = '\n';
len++;
pos++;
out[len] = '\0';
hist_pos = rl_count;
RL_REDRAW();
} else {
fputc('\a', stdout);
fflush(stdout);
}
} else if (ch >= 32 && ch <= 126) {
if (len + 1 < out_cap) {
memmove(out + pos + 1, out + pos, len - pos);
out[pos] = (char)ch;
len++;
pos++;
out[len] = '\0';
hist_pos = rl_count;
RL_REDRAW();
} else {
fputc('\a', stdout);
fflush(stdout);
}
} else {
/* ignore other control characters */
}
}
#endif
}
#endif /* !_WIN32 */
/* ---------- Small utilities ---------- */
static int is_blank_line(const char *s) {
/**
* @brief Return 1 if the line consists only of whitespace.
*/
for (const char *p = s; *p; ++p) {
if (*p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') return 0;
}
return 1;
}
/**
* @brief Advance pointer past leading spaces and tabs.
*/
static const char *lstrip(const char *s) {
while (*s == ' ' || *s == '\t')
s++;
return s;
}
/**
* @brief Heuristic: check if a line ends with an operator that suggests continuation.
*/
static int ends_with_opener(const char *line) {
size_t n = strlen(line);
while (n > 0 && (line[n - 1] == ' ' || line[n - 1] == '\t' || line[n - 1] == '\r' || line[n - 1] == '\n'))
n--;
if (n == 0) return 0;
char c = line[n - 1];
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' ||
c == '<' || c == '>' || c == '=' || c == '!' || c == '&' || c == '|' || c == ',') {
return 1;
}
return 0;
}
/* Compute how many indentation levels (2 spaces per level) are still open. */
/**
* @brief Compute number of open indentation blocks (2 spaces per level).
*/
static int compute_open_indent_blocks(const char *buf) {
int in_block_comment = 0;
int open = 0;
int have_baseline = 0;
int cur = 0;
const char *p = buf;
while (*p) {
const char *line = p;
while (*p && *p != '\n')
p++;
const char *line_end = p;
if (*p == '\n') p++;
if (in_block_comment) {
const char *q = line;
while (q < line_end) {
if (q + 1 < line_end && q[0] == '*' && q[1] == '/') {
in_block_comment = 0;
q += 2;
break;
}
q++;
}
if (in_block_comment) continue;
}
const char *s = line;
int spaces = 0;
while (s < line_end && *s == ' ') {
spaces++;
s++;
}
while (s < line_end && *s == '\t') {
s++;
}
const char *t = s;
if (t >= line_end) continue;
if ((t + 1) <= line_end && t[0] == '/' && (t + 1 < line_end && (t[1] == '/' || t[1] == '*'))) {
if (t[1] == '/') {
continue;
} else if (t[1] == '*') {
in_block_comment = 1;
continue;
}
}
int lvl = spaces / 2;
if (!have_baseline) {
cur = lvl;
have_baseline = 1;
continue;
}
if (lvl > cur) {
open += (lvl - cur);
} else if (lvl < cur) {
int dec = (cur - lvl);
if (dec > open)
open = 0;
else
open -= dec;
}
cur = lvl;
}
return open;
}
/* Detect if current buffer looks incomplete. */
/**
* @brief Heuristic to determine if the current buffer likely needs another line.
* Looks for open quotes/escapes or unmatched indentation.
*/
static int buffer_looks_incomplete(const char *buf) {
int in_single = 0, in_double = 0, escape = 0;
int in_block_comment = 0, in_line_comment = 0;
int paren = 0;
const char *p = buf;
const char *last_sig_line = NULL;
while (*p) {
char c = *p;
if (in_line_comment) {
if (c == '\n') in_line_comment = 0;
p++;
continue;
}
if (in_block_comment) {
if (c == '*' && p[1] == '/') {
in_block_comment = 0;
p += 2;
continue;
}
p++;
continue;
}
if (!in_single && !in_double) {
if (c == '/' && p[1] == '/') {
in_line_comment = 1;
p += 2;
continue;
}
if (c == '/' && p[1] == '*') {
in_block_comment = 1;
p += 2;
continue;
}
}
if (in_single) {
if (!escape && c == '\\') {
escape = 1;
p++;
continue;
}
if (!escape && c == '\'') {
in_single = 0;
p++;
continue;
}
escape = 0;
p++;
continue;
} else if (in_double) {
if (!escape && c == '\\') {
escape = 1;
p++;
continue;
}
if (!escape && c == '"') {
in_double = 0;
p++;
continue;
}
escape = 0;
p++;
continue;
} else {
if (c == '\'') {
in_single = 1;
p++;
continue;
}
if (c == '"') {
in_double = 1;
p++;
continue;
}
if (c == '(') {
paren++;
p++;
continue;
}
if (c == ')') {
if (paren > 0) paren--;
p++;
continue;
}
}
if (c == '\n') {
const char *q = p + 1;
while (*q == ' ' || *q == '\t')
q++;
if (*q && *q != '\n') last_sig_line = q;
}
p++;
}
if (!last_sig_line) {
const char *q = buf;
const char *candidate = NULL;
while (*q) {
const char *line_start = q;
while (*q && *q != '\n')
q++;
const char *t = line_start;
while (*t == ' ' || *t == '\t')
t++;
if (*t && *t != '\n' && *t != '\r') candidate = t;
if (*q == '\n') q++;
}
last_sig_line = candidate;
}
if (in_single || in_double || in_block_comment || paren > 0) return 1;
if (last_sig_line) {
if (strncmp(lstrip(last_sig_line), "if", 2) == 0 ||
strncmp(lstrip(last_sig_line), "else", 4) == 0 ||
strncmp(lstrip(last_sig_line), "while", 5) == 0 ||
strncmp(lstrip(last_sig_line), "for", 3) == 0 ||
strncmp(lstrip(last_sig_line), "fun", 3) == 0) {
return 1;
}
if (ends_with_opener(last_sig_line)) return 1;
}
return 0;
}
/**
* @brief Print REPL help with available commands.
*/
static void show_repl_help(void) {
printf("Commands:\n");
printf(" :help | :h Show this help\n");
printf(" :quit | :q | :exit Exit the REPL\n");
printf(" :reset | :re Reset VM state (clears globals)\n");
printf(" :dump | :du | :globals | :gl Dump current globals\n");
printf(" :globals [pattern] Dump globals filtering by value substring\n");
printf(" :vars | :v [pattern] Alias for :globals\n");
printf(" :clear | :cl Clear current input buffer\n");
printf(" :print | :pr Show current buffer\n");
printf(" :run | :ru [file] Execute current buffer or the given file immediately\n");
printf(" :profile | :pf Execute buffer and show timing + instruction count\n");
printf(" :save | :sa <file> Save current buffer to file\n");
printf(" :load | :lo <file> Load file into buffer (does not run)\n");
printf(" :paste | :pa [run] Enter paste mode; end with a single '.' line (optional 'run')\n");
printf(" :history | :hi [N] Show last N lines of history (default 50)\n");
printf(" :time | :ti on|off|toggle Toggle/enable/disable timing\n");
printf(" :env | :en [NAME[=VALUE]] Get or set environment variable\n");
printf(" :backtrace | :bt | :ba Show backtrace of VM frames (most recent first)\n");
printf(" :frame | :fr N Select frame N for :locals/:list/:disasm (default: top)\n");
printf(" :list | :li [±K] Show K lines of source around current frame line (default 5)\n");
printf(" :disasm | :di [±N] Disassemble around current frame ip (default 5)\n");
printf(" :mdump | :md WHAT [offset [len]] [raw] [to <file>] Dump VM memory region\n");
printf(" WHAT = code | stack | globals | consts\n");
printf(" 'raw' writes binary bytes instead of a formatted hexdump\n");
printf(" :stack | :st [N] Show top N (default all) stack values\n");
printf(" :top | :to Show the top of the VM stack\n");
printf(" :locals | :lc [FRAME] Show locals of frame (default: selected frame)\n");
printf(" :printv | :pv WHAT Print value: local[i] | stack[i] | global[i]\n");
printf(" :break | :br [file:]line Set a breakpoint (default file = current frame file)\n");
printf(" :info | :in breaks List breakpoints\n");
printf(" :delete | :de ID Delete breakpoint by ID\n");
printf(" :clear breaks | :cb Remove all breakpoints\n");
printf(" :cont | :co Continue execution (exit REPL if in debug stop)\n");
printf(" :step | :sp Step one instruction\n");
printf(" :next | :ne Step over (current frame)\n");
printf(" :finish | :fi Run until the current frame returns\n");
}
/**
* @brief Read a file fully into a newly allocated buffer.
* @return Buffer on success (caller frees), or NULL on error.
*/
static char *read_entire_file(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long sz = ftell(f);
if (sz < 0) {
fclose(f);
return NULL;
}
rewind(f);
char *buf = (char *)malloc((size_t)sz + 1);
if (!buf) {
fclose(f);
return NULL;
}
size_t n = fread(buf, 1, (size_t)sz, f);
fclose(f);
buf[n] = '\0';
if (out_len) *out_len = n;
return buf;
}
/**
* @brief Write a buffer to a file path, replacing existing contents.
* @return 1 on success, 0 on error.
*/
static int write_entire_file(const char *path, const char *data, size_t len) {
FILE *f = fopen(path, "wb");
if (!f) return 0;
size_t n = fwrite(data, 1, len, f);
fclose(f);
return n == len;
}
/* ---------- REPL command matching helper ---------- */
/**
* @brief Return 1 if cmd equals any non-NULL entry in names[] (terminated by NULL).
*/
static int cmd_is_one_of(const char *cmd, const char *const names[]) {
if (!cmd || !*cmd) return 0;
for (int i = 0; names[i] != NULL; ++i) {
if (strcmp(cmd, names[i]) == 0) return 1;
}
return 0;
}
/* ---------- Hexdump helper ---------- */
/**
* @brief Dump a byte buffer in hex to a FILE*, with offsets.
*/
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);
}
}
/**
* @brief Print the last n lines of a file to stdout (tail-like).
*/
static void print_last_n_lines(const char *path, int n) {
if (n <= 0) n = 50;
size_t flen = 0;
char *content = read_entire_file(path, &flen);
if (!content) {
printf("No history available.\n");
return;
}
int lines = 0;
for (size_t i = flen; i > 0; --i) {
if (content[i - 1] == '\n') {
lines++;
if (lines > n) {
content[i] = '\0';
printf("%s", content + i);
free(content);
return;
}
}
}
printf("%s", content);
free(content);
}
/**
* @brief Append multi-line buffer to history file, one line per entry.
*/
static void append_history(FILE *hist, const char *buffer) {
if (!hist || !buffer) return;
fputs(buffer, hist);
if (buffer[0] && buffer[strlen(buffer) - 1] != '\n') fputc('\n', hist);
fflush(hist);
}
/* ---------- Env helpers ---------- */
/**
* @brief Print usage for :env commands.
*/
static void env_show_usage(void) {
printf("Usage:\n");
printf(" :env NAME Show environment variable NAME\n");
printf(" :env NAME=VALUE Set environment variable NAME to VALUE\n");
printf(" :env Show this usage\n");
}
/**
* @brief Show the value of an environment variable.
*/
static void env_get(const char *name) {
const char *v = getenv(name);
if (v)
printf("%s=%s\n", name, v);
else
printf("%s is not set\n", name);
}
/**
* @brief Set or unset (when value is NULL) an environment variable.
*/
static void env_set(const char *name, const char *value) {
#ifdef _WIN32
if (_putenv_s(name, value ? value : "") != 0) {
printf("Failed to set %s\n", name);
}
#else
if (setenv(name, value ? value : "", 1) != 0) {
printf("Failed to set %s\n", name);
}
#endif
}
/* ---------- REPL Entry ---------- */
/**
* @brief Run the interactive Fun REPL session.
* @param vm Initialized VM to execute user input within.
* @return Exit status code for the REPL.
*/
int fun_run_repl(VM *vm) {
int repl_timing = 0;
int selected_frame = -1; /* -1 means use current top frame */
printf("Fun %s REPL\n", FUN_VERSION);
printf("Type :help for commands. Submit an empty line to run.\n");
/* Load stdlib symbols for completion */
{
char libdir[PATH_MAX];
const char *envlib = getenv("FUN_LIB_DIR");
if (envlib && *envlib) {
snprintf(libdir, sizeof(libdir), "%s", envlib);
} else {
#ifdef DEFAULT_LIB_DIR
snprintf(libdir, sizeof(libdir), "%s", DEFAULT_LIB_DIR);
#else
snprintf(libdir, sizeof(libdir), "%s", "lib");
#endif
}
/* strip trailing slash if present, not strictly necessary */
size_t L = strlen(libdir);
if (L > 1 && libdir[L - 1] == '/') libdir[L - 1] = '\0';
load_stdlib_symbols(libdir);
}
char *buffer = NULL;
size_t bufcap = 0;
size_t buflen = 0;
/* History setup */
char hist_path[1024];
FILE *hist = NULL;
const char *home = getenv("HOME");
if (!home) home = getenv("USERPROFILE");
if (home) {
snprintf(hist_path, sizeof(hist_path), "%s/.fun_history", home);
rl_hist_load_file(hist_path);
hist = fopen(hist_path, "a+");
} else {
snprintf(hist_path, sizeof(hist_path), ".fun_history");
rl_hist_load_file(hist_path);
hist = fopen(hist_path, "a+");
}
for (;;) {
if (buflen + 1 > bufcap) {
size_t newcap = bufcap == 0 ? 1024 : bufcap * 2;
while (newcap < buflen + 1)
newcap *= 2;
buffer = (char *)realloc(buffer, newcap);
bufcap = newcap;
}
buffer[buflen] = '\0';
int indent_debt = (buflen > 0) ? compute_open_indent_blocks(buffer) : 0;
char prompt[64];
if (buflen == 0) {
snprintf(prompt, sizeof(prompt), "fun> ");
} else if (indent_debt > 0) {
snprintf(prompt, sizeof(prompt), "...%d> ", indent_debt);
} else {
snprintf(prompt, sizeof(prompt), "... ");
}
char line[4096];
#ifndef _WIN32
if (!read_line_edit(line, sizeof(line), prompt)) {
puts("");
break; // EOF
}
#else
fputs(prompt, stdout);
fflush(stdout);
if (!fgets(line, sizeof(line), stdin)) {
puts("");
break;
}
#endif
if (hist) {
const char *lp = line;
int only_nl = 1;
while (*lp) {
if (*lp != '\n' && *lp != '\r') {
only_nl = 0;
break;
}
lp++;
}
if (!only_nl) append_history(hist, line);
}
if (line[0] == ':') {
char cmd[64] = {0};
char arg[2048] = {0};
sscanf(line, ":%63s %2047[^\n]", cmd, arg);
if (cmd_is_one_of(cmd, (const char *[]){"quit", "q", "qu", "exit", NULL})) {
break;
} else if (cmd_is_one_of(cmd, (const char *[]){"help", "h", NULL})) {
show_repl_help();
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"reset", "re", NULL})) {
vm_reset(vm);
printf("VM state reset.\n");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"dump", "du", NULL})) {
vm_dump_globals(vm);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"globals", "vars", "gl", "v", "va", NULL})) {
const char *pattern = lstrip(arg);
int filtered = (pattern && *pattern);
printf("=== globals%s%s ===\n", filtered ? " matching '" : "", filtered ? pattern : "");
if (filtered) printf("'\n");
for (int i = 0; i < MAX_GLOBALS; ++i) {
if (vm->globals[i].type == VAL_NIL) continue;
char *sv = value_to_string_alloc(&vm->globals[i]);
if (!filtered || (sv && strstr(sv, pattern))) {
printf("[%d] %s\n", i, sv ? sv : "nil");
}
free(sv);
}
printf("===============\n");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"clear", "cl", NULL})) {
buflen = 0;
printf("(buffer cleared)\n");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"print", "pr", NULL})) {
if (buflen == 0)
printf("(buffer empty)\n");
else {
if (buflen >= bufcap) {
buffer = (char *)realloc(buffer, buflen + 1);
bufcap = buflen + 1;
}
buffer[buflen] = '\0';
printf("%s", buffer);
if (buflen > 0 && buffer[buflen - 1] != '\n') printf("\n");
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"run", "ru", "profile", "pf", NULL})) {
int is_profile = cmd_is_one_of(cmd, (const char *[]){"profile", "pf", NULL});
// 'arg' is a fixed-size local array, so its address is always non-null.
// Only check whether it contains a non-empty string.
int from_file = (arg[0] != '\0');
const char *src = NULL;
char *filebuf = NULL;
if (from_file) {
size_t flen = 0;
filebuf = read_entire_file(arg, &flen);
if (!filebuf) {
printf("Failed to load '%s'\n", arg);
continue;
}
src = filebuf;
} else {
if (buflen == 0) {
printf("(buffer empty)\n");
continue;
}
if (buflen + 1 > bufcap) {
buffer = (char *)realloc(buffer, buflen + 1);
bufcap = buflen + 1;
}
buffer[buflen] = '\0';
src = buffer;
}
clock_t t_parse0 = 0, t_parse1 = 0, t_run0 = 0, t_run1 = 0;
if (is_profile) t_parse0 = clock();
Bytecode *bc = parse_string_to_bytecode(src);
if (is_profile) t_parse1 = clock();
if (bc) {
if (repl_timing || is_profile) t_run0 = clock();
vm_run(vm, bc);
if (repl_timing || is_profile) t_run1 = clock();
if (is_profile) {
double ms_parse = (double)(t_parse1 - t_parse0) * 1000.0 / (double)CLOCKS_PER_SEC;
double ms_run = (double)(t_run1 - t_run0) * 1000.0 / (double)CLOCKS_PER_SEC;
printf("[profile] parse: %.2f ms, run: %.2f ms, total: %.2f ms, instr: %lld\n",
ms_parse, ms_run, ms_parse + ms_run, vm->instr_count);
} else if (repl_timing) {
double ms = (double)(t_run1 - t_run0) * 1000.0 / (double)CLOCKS_PER_SEC;
printf("[time] %.2f ms\n", ms);
}
vm_print_output(vm);
vm_clear_output(vm);
bytecode_free(bc);
if (!from_file) append_history(hist, buffer);
} else {
int line_no = 0, col_no = 0;
char emsg[256];
if (parser_last_error(emsg, sizeof(emsg), &line_no, &col_no)) {
printf("Parse error at %d:%d: %s\n", line_no, col_no, emsg);
int cur_line = 1;
const char *p = src ? src : buffer;
while (*p && cur_line < line_no) {
if (*p == '\n') cur_line++;
p++;
}
const char *line_start = p;
while (*p && *p != '\n')
p++;
fwrite(line_start, 1, (size_t)(p - line_start), stdout);
printf("\n");
for (int i = 1; i < col_no; ++i)
putchar(' ');
printf("^\n");
#ifdef FUN_DEBUG
if (hist && !from_file) {
fprintf(hist, "// ERROR %d:%d: %s\n", line_no, col_no, emsg);
fflush(hist);
}
#endif
} else {
printf("Parse error.\n");
#ifdef FUN_DEBUG
if (hist && !from_file) {
fprintf(hist, "// ERROR: parse error\n");
fflush(hist);
}
#endif
}
}
if (from_file) {
free(filebuf);
} else {
buflen = 0; /* keep behavior: clear buffer only when running current buffer */
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"save", "sa", NULL})) {
if (arg[0] == '\0') {
printf("Usage: :save <file>\n");
continue;
}
if (buflen == 0) {
printf("(buffer empty)\n");
continue;
}
if (!write_entire_file(arg, buffer, buflen)) {
printf("Failed to save to '%s'\n", arg);
} else {
printf("Saved %zu bytes to '%s'\n", buflen, arg);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"load", "lo", NULL})) {
if (arg[0] == '\0') {
printf("Usage: :load <file>\n");
continue;
}
size_t flen = 0;
char *filebuf = read_entire_file(arg, &flen);
if (!filebuf) {
printf("Failed to load '%s'\n", arg);
continue;
}
if (flen + 1 > bufcap) {
size_t newcap = flen + 1;
buffer = (char *)realloc(buffer, newcap);
bufcap = newcap;
}
memcpy(buffer, filebuf, flen);
buflen = flen;
buffer[buflen] = '\0';
free(filebuf);
printf("Loaded %zu bytes into buffer. Use :run or submit an empty line to execute.\n", buflen);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"paste", "pa", NULL})) {
int run_after = 0;
const char *opt = lstrip(arg);
if (opt && (strcmp(opt, "run") == 0 || strcmp(opt, "exec") == 0)) run_after = 1;
printf("(paste mode: end with single '.' line)%s\n", run_after ? " [will run]" : "");
for (;;) {
fputs("... paste> ", stdout);
fflush(stdout);
char pline[8192];
if (!fgets(pline, sizeof(pline), stdin)) {
puts("");
break;
}
if ((strcmp(pline, ".\n") == 0) || (strcmp(pline, ".\r\n") == 0) || (strcmp(pline, ".") == 0)) {
break;
}
size_t pl = strlen(pline);
if (buflen + pl + 1 > bufcap) {
size_t newcap = bufcap == 0 ? 1024 : bufcap * 2;
while (newcap < buflen + pl + 1)
newcap *= 2;
buffer = (char *)realloc(buffer, newcap);
bufcap = newcap;
}
memcpy(buffer + buflen, pline, pl);
buflen += pl;
}
if (run_after) {
if (buflen + 1 > bufcap) {
buffer = (char *)realloc(buffer, buflen + 1);
bufcap = buflen + 1;
}
buffer[buflen] = '\0';
Bytecode *bc = parse_string_to_bytecode(buffer);
if (bc) {
clock_t t0 = clock();
vm_run(vm, bc);
clock_t t1 = clock();
double ms = (double)(t1 - t0) * 1000.0 / (double)CLOCKS_PER_SEC;
printf("[time] %.2f ms\n", ms);
vm_print_output(vm);
vm_clear_output(vm);
bytecode_free(bc);
append_history(hist, buffer);
} else {
int line_no = 0, col_no = 0;
char emsg[256];
if (parser_last_error(emsg, sizeof(emsg), &line_no, &col_no)) {
printf("Parse error at %d:%d: %s\n", line_no, col_no, emsg);
#ifdef FUN_DEBUG
if (hist) {
fprintf(hist, "// ERROR %d:%d: %s\n", line_no, col_no, emsg);
fflush(hist);
}
#endif
} else {
printf("Parse error.\n");
#ifdef FUN_DEBUG
if (hist) {
fprintf(hist, "// ERROR: parse error\n");
fflush(hist);
}
#endif
}
}
buflen = 0;
} else {
printf("(pasted %zu bytes into buffer)\n", buflen);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"history", "hi", NULL})) {
int n = 50;
if (arg[0] != '\0') n = atoi(arg);
if (n <= 0) n = 50;
print_last_n_lines(hist_path, n);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"time", "ti", NULL})) {
if (strcmp(lstrip(arg), "on") == 0)
repl_timing = 1;
else if (strcmp(lstrip(arg), "off") == 0)
repl_timing = 0;
else if (strcmp(lstrip(arg), "toggle") == 0)
repl_timing = !repl_timing;
else {
printf("Usage: :time on|off|toggle (currently %s)\n", repl_timing ? "on" : "off");
continue;
}
printf("Timing %s\n", repl_timing ? "enabled" : "disabled");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"env", "en", NULL})) {
const char *spec = lstrip(arg);
if (!spec || *spec == '\0') {
env_show_usage();
continue;
}
const char *eq = strchr(spec, '=');
if (!eq) {
env_get(spec);
} else {
char name[256];
size_t nlen = (size_t)(eq - spec);
if (nlen >= sizeof(name)) nlen = sizeof(name) - 1;
memcpy(name, spec, nlen);
name[nlen] = '\0';
const char *val = eq + 1;
env_set(name, val);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"backtrace", "bt", "ba", NULL})) {
if (vm->fp < 0) {
printf("(no frames)\n");
continue;
}
printf("Backtrace (most recent call first):\n");
for (int i = vm->fp; i >= 0; --i) {
Frame *f = &vm->frames[i];
const char *fname = (f->fn && f->fn->name) ? f->fn->name : "<entry>";
const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : "<unknown>";
int ip = f->ip - 1;
printf(" #%d %s at %s ip=%d line=%d\n", i, fname, sfile, ip, vm->current_line);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"stack", "st", NULL})) {
int n = -1;
const char *p = lstrip(arg);
if (p && *p) n = atoi(p);
int count = vm->sp + 1;
if (count <= 0) {
printf("(stack empty)\n");
continue;
}
int start = 0;
if (n > 0 && n < count) start = count - n;
printf("Stack size=%d\n", count);
for (int i = start; i < count; ++i) {
char *sv = value_to_string_alloc(&vm->stack[i]);
printf("[%d] %s\n", i, sv ? sv : "nil");
free(sv);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"locals", "lc", NULL})) {
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
const char *p = lstrip(arg);
if (p && *p) {
int v = atoi(p);
if (v >= 0 && v <= vm->fp) idx = v;
}
if (idx < 0) {
printf("(no current frame)\n");
continue;
}
Frame *f = &vm->frames[idx];
const char *fname = (f->fn && f->fn->name) ? f->fn->name : "<entry>";
printf("Locals in frame #%d (%s):\n", idx, fname);
int any = 0;
for (int i = 0; i < MAX_FRAME_LOCALS; ++i) {
if (f->locals[i].type != VAL_NIL) {
char *sv = value_to_string_alloc(&f->locals[i]);
printf(" %d: %s\n", i, sv ? sv : "nil");
free(sv);
any = 1;
}
}
if (!any) printf(" (no non-nil locals)\n");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"frame", "fr", NULL})) {
const char *p = lstrip(arg);
if (!p || !*p) {
printf("Usage: :frame N\n");
continue;
}
int v = atoi(p);
if (v < 0 || v > vm->fp) {
printf("Invalid frame index. Current top is %d\n", vm->fp);
continue;
}
selected_frame = v;
Frame *f = &vm->frames[selected_frame];
const char *fname = (f->fn && f->fn->name) ? f->fn->name : "<entry>";
const char *sfile = (f->fn && f->fn->source_file) ? f->fn->source_file : "<unknown>";
printf("Selected frame #%d: %s (%s)\n", selected_frame, fname, sfile);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"list", "li", NULL})) {
int k = 5;
const char *p = lstrip(arg);
if (p && *p) k = atoi(p);
if (k <= 0) k = 5;
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idx < 0) {
printf("(no current frame)\n");
continue;
}
Frame *f = &vm->frames[idx];
if (!f->fn || !f->fn->source_file) {
printf("(no source info)\n");
continue;
}
/* derive current line for this frame by scanning LINE markers up to ip-1 */
int line = vm->current_line;
int upto = f->ip - 1;
if (upto < 0) upto = 0;
for (int i = 0; i <= upto && i < f->fn->instr_count; ++i) {
Instruction ins = f->fn->instructions[i];
if (ins.op == OP_LINE) line = ins.operand;
}
const char *path = f->fn->source_file;
/* Map to included file if the current line belongs to an included chunk */
char mapped_path[1024];
int mapped_line = line;
if (map_expanded_line_to_include_path(path, line, mapped_path, sizeof(mapped_path), &mapped_line)) {
path = mapped_path;
line = mapped_line;
}
size_t flen = 0;
char *src = read_entire_file(path, &flen);
if (!src) {
printf("Unable to read %s\n", path);
continue;
}
int start = line - k;
if (start < 1) start = 1;
int end = line + k;
int cur = 1;
const char *s = src;
while (*s && cur <= end) {
const char *ls = s;
while (*s && *s != '\n')
s++;
int print = (cur >= start && cur <= end);
if (print) {
printf("%c %5d | ", (cur == line ? '>' : ' '), cur);
fwrite(ls, 1, (size_t)(s - ls), stdout);
printf("\n");
}
if (*s == '\n') s++;
cur++;
}
free(src);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"disasm", "disassemble", "di", NULL})) {
int n = 5;
const char *p = lstrip(arg);
if (p && *p) n = atoi(p);
if (n <= 0) n = 5;
int idx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idx < 0) {
printf("(no current frame)\n");
continue;
}
Frame *f = &vm->frames[idx];
if (!f->fn) {
printf("(no function)\n");
continue;
}
/* 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 >= 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 = opcode_is_valid(ins.op) ? opcode_names[ins.op] : "???";
printf("%c %6d: %-14s %d\n", (i == curip ? '>' : ' '), i, opname, ins.operand);
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"mdump", "md", NULL})) {
/* Syntax: :mdump WHAT [offset [len]] [raw] [to <file>]
WHAT: code | stack | globals | consts
'raw' writes binary bytes instead of a formatted hexdump */
const char *p = lstrip(arg);
if (!p || !*p) {
printf("Usage: :mdump WHAT [offset [len]] [raw] [to <file>]\n");
printf(" WHAT = code | stack | globals | consts\n");
printf(" 'raw' writes binary bytes instead of a formatted hexdump\n");
continue;
}
char what[32];
int consumed = 0;
if (sscanf(p, "%31s %n", what, &consumed) != 1) {
printf("Usage: :mdump WHAT [offset [len]] [raw] [to <file>]\n");
continue;
}
p += consumed;
size_t off = 0;
size_t len = (size_t)-1; /* default later to clamp */
int want_raw = 0; /* output raw bytes instead of hexdump */
/* 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: 'raw' keyword */
while (*p == ' ' || *p == '\t')
p++;
if (strncmp(p, "raw", 3) == 0 && (p[3] == '\0' || isspace((unsigned char)p[3]))) {
want_raw = 1;
p += 3;
}
/* 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) {
if (want_raw) {
/* Write raw bytes directly to stdout with no header */
fwrite(base + off, 1, len, stdout);
fflush(stdout);
} else {
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;
}
if (want_raw) {
fwrite(base + off, 1, len, fout);
fclose(fout);
printf("Wrote raw bytes (%zu from %s) to %s\n", len, what, path);
} else {
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 (cmd_is_one_of(cmd, (const char *[]){"printv", "pv", NULL})) {
const char *spec = lstrip(arg);
if (!spec || !*spec) {
printf("Usage: :printv local[i] | stack[i] | global[i]\n");
continue;
}
int idx = -1;
if (sscanf(spec, "local[%d]", &idx) == 1) {
int fidx = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (fidx < 0 || idx < 0 || idx >= MAX_FRAME_LOCALS) {
printf("(out of range)\n");
continue;
}
Frame *f = &vm->frames[fidx];
char *sv = value_to_string_alloc(&f->locals[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else if (sscanf(spec, "stack[%d]", &idx) == 1) {
if (idx < 0 || idx > vm->sp) {
printf("(out of range)\n");
continue;
}
char *sv = value_to_string_alloc(&vm->stack[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else if (sscanf(spec, "global[%d]", &idx) == 1) {
if (idx < 0 || idx >= MAX_GLOBALS) {
printf("(out of range)\n");
continue;
}
char *sv = value_to_string_alloc(&vm->globals[idx]);
printf("%s\n", sv ? sv : "nil");
free(sv);
} else {
printf("Usage: :printv local[i] | stack[i] | global[i]\n");
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"top", "to", NULL})) {
if (vm->sp < 0) {
printf("(stack empty)\n");
continue;
}
char *sv = value_to_string_alloc(&vm->stack[vm->sp]);
printf("%s\n", sv ? sv : "nil");
free(sv);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"break", "br", NULL})) {
const char *p = lstrip(arg);
if (!p || !*p) {
printf("Usage: :break [file:]line\n");
continue;
}
const char *colon = strchr(p, ':');
char filebuf[1024];
int line = 0;
if (colon) {
size_t fl = (size_t)(colon - p);
if (fl >= sizeof(filebuf)) fl = sizeof(filebuf) - 1;
memcpy(filebuf, p, fl);
filebuf[fl] = '\0';
line = atoi(colon + 1);
} else {
int idxf = (selected_frame >= 0 && selected_frame <= vm->fp) ? selected_frame : vm->fp;
if (idxf < 0) {
printf("(no current frame)\n");
continue;
}
Frame *f = &vm->frames[idxf];
const char *sf = (f->fn && f->fn->source_file) ? f->fn->source_file : NULL;
if (!sf) {
printf("(no current source file)\n");
continue;
}
snprintf(filebuf, sizeof(filebuf), "%s", sf);
line = atoi(p);
}
if (line <= 0) {
printf("Invalid line\n");
continue;
}
int id = vm_debug_add_breakpoint(vm, filebuf, line);
if (id >= 0)
printf("Breakpoint %d set at %s:%d\n", id, filebuf, line);
else
printf("Failed to set breakpoint\n");
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"info", "in", NULL})) {
const char *what = lstrip(arg);
if (what && strcmp(what, "breaks") == 0) {
vm_debug_list_breakpoints(vm);
} else {
printf("Usage: :info breaks\n");
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"delete", "de", NULL})) {
int id = atoi(lstrip(arg));
if (vm_debug_delete_breakpoint(vm, id))
printf("Deleted breakpoint %d\n", id);
else
printf("No such breakpoint %d\n", id);
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"cb", NULL})) {
vm_debug_clear_breakpoints(vm);
printf("Cleared all breakpoints\n");
continue;
} else if (strcmp(cmd, "clear") == 0) {
const char *what = lstrip(arg);
if (what && strcmp(what, "breaks") == 0) {
vm_debug_clear_breakpoints(vm);
printf("Cleared all breakpoints\n");
} else {
printf("Usage: :clear breaks\n");
}
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"cont", "continue", "co", NULL})) {
vm_debug_request_continue(vm);
printf("Continuing...\n");
if (vm->on_error_repl) return 0; /* exit REPL to continue execution */
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"step", "sp", NULL})) {
vm_debug_request_step(vm);
printf("Stepping one instruction...\n");
if (vm->on_error_repl) return 0;
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"next", "ne", NULL})) {
vm_debug_request_next(vm);
printf("Stepping over...\n");
if (vm->on_error_repl) return 0;
continue;
} else if (cmd_is_one_of(cmd, (const char *[]){"finish", "fi", NULL})) {
vm_debug_request_finish(vm);
printf("Running until current frame returns...\n");
if (vm->on_error_repl) return 0;
continue;
} else {
printf("Unknown command. Use :help\n");
continue;
}
}
if (is_blank_line(line)) {
if (buflen == 0) continue;
if (buflen + 1 > bufcap) {
buffer = (char *)realloc(buffer, buflen + 1);
bufcap = buflen + 1;
}
buffer[buflen] = '\0';
int indent_debt2 = compute_open_indent_blocks(buffer);
if (buffer_looks_incomplete(buffer) || indent_debt2 > 0) {
if (indent_debt2 > 0) {
printf("(incomplete, open block indent +%d)\n", indent_debt2);
} else {
printf("(incomplete, continue typing)\n");
}
continue;
}
Bytecode *bc = parse_string_to_bytecode(buffer);
if (bc) {
clock_t t0 = 0, t1 = 0;
if (repl_timing) t0 = clock();
vm_run(vm, bc);
if (repl_timing) {
t1 = clock();
double ms = (double)(t1 - t0) * 1000.0 / (double)CLOCKS_PER_SEC;
printf("[time] %.2f ms\n", ms);
}
vm_print_output(vm);
vm_clear_output(vm);
bytecode_free(bc);
append_history(hist, buffer);
} else {
int line_no = 0, col_no = 0;
char emsg[256];
if (parser_last_error(emsg, sizeof(emsg), &line_no, &col_no)) {
printf("Parse error at %d:%d: %s\n", line_no, col_no, emsg);
int cur_line = 1;
const char *p = buffer;
while (*p && cur_line < line_no) {
if (*p == '\n') cur_line++;
p++;
}
const char *line_start = p;
while (*p && *p != '\n')
p++;
fwrite(line_start, 1, (size_t)(p - line_start), stdout);
printf("\n");
for (int i = 1; i < col_no; ++i)
putchar(' ');
printf("^\n");
#ifdef FUN_DEBUG
if (hist) {
fprintf(hist, "// ERROR %d:%d: %s\n", line_no, col_no, emsg);
fflush(hist);
}
#endif
} else {
printf("Parse error.\n");
#ifdef FUN_DEBUG
if (hist) {
fprintf(hist, "// ERROR: parse error\n");
fflush(hist);
}
#endif
}
}
buflen = 0;
continue;
}
size_t linelen = strlen(line);
if (buflen + linelen + 1 > bufcap) {
size_t newcap = bufcap == 0 ? 1024 : bufcap * 2;
while (newcap < buflen + linelen + 1)
newcap *= 2;
buffer = (char *)realloc(buffer, newcap);
bufcap = newcap;
}
memcpy(buffer + buflen, line, linelen);
buflen += linelen;
}
if (hist) fclose(hist);
free_stdlib_symbols();
free(buffer);
return 0;
}
#endif /* FUN_WITH_REPL */