1
0
Fork 0
forked from fun/fun

Fixed bug with wrong line numbers in error messages. Some examples refactoring and bug fixes. (0.39.15)

This commit is contained in:
Johannes Findeisen 2026-03-30 20:51:04 +02:00
commit 3d3c11496f
23 changed files with 265 additions and 114 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(fun VERSION 0.39.14 LANGUAGES C)
project(fun VERSION 0.39.15 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

View file

@ -18,7 +18,7 @@
* FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/sha1_demo.fun
*/
#include <crypt/sha1.fun> as sha1
include <crypt/sha1.fun>
print("=== SHA-1 demo ===")
@ -27,15 +27,15 @@ hex_abc = "616263" // "abc" in hex
empty = ""
// Hash ASCII string directly
d1 = sha1.SHA1().sha1_str(s)
d1 = SHA1().sha1_str(s)
print("SHA-1('abc') = " + d1)
// Hash pre-encoded hex bytes
d2 = sha1.SHA1().sha1_hex(hex_abc)
d2 = SHA1().sha1_hex(hex_abc)
print("SHA-1(616263 hex) = " + d2)
// Empty string
d3 = sha1.SHA1().sha1_str(empty)
d3 = SHA1().sha1_str(empty)
print("SHA-1('') = " + d3)
print("=== done ===")

View file

@ -75,7 +75,7 @@ main()
/* Expected when run with --repl-on-error (your version and line/ip may vary):
Runtime error: index out of range
(at ./examples/debug_reporting.fun:XX in crash, op INDEX_GET @ip YY)
(at ./examples/debug_reporting.fun:68 in crash, op INDEX_GET @ip 9)
Entering REPL due to runtime error (code 1)
Fun VERSION REPL
Type :help for commands. Submit an empty line to run.

View file

@ -150,14 +150,57 @@ fun str_repeat(s, count)
i = i + 1
return join(parts, "")
/*
// ASCII string to bytes (printable ASCII 0x20..0x7E)
// Temporarily disabled due to parser incompatibilities with certain string
// literals in this function on some environments. Re-enable after the
// language parser updates to support these cases.
// ASCII string to array of byte codes (0..255)
// For printable ASCII (0x20..0x7E) returns the exact code; for any other
// character it returns 0 as a fallback. This mirrors legacy helpers used by
// crypto libs that expect ASCII input.
fun string_to_bytes_ascii(s)
str = to_string(s)
out = []
number i = 0
// ASCII printable ranges buckets for quick lookup
P1 = " !\"#$%&'()*+,-./" // 32..47
P2 = "0123456789" // 48..57
P3 = ":;<=>?@" // 58..64
P4 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // 65..90
P5 = "[\\]^_`" // 91..96
P6 = "abcdefghijklmnopqrstuvwxyz" // 97..122
P7 = "{|}~" // 123..126
while (true)
ch = substr(str, i, 1)
if (typeof(ch) != "String" || ch == "")
break
number code = -1
idx = find(P1, ch)
if (idx >= 0)
code = 32 + idx
else
idx = find(P2, ch)
if (idx >= 0)
code = 48 + idx
else
idx = find(P3, ch)
if (idx >= 0)
code = 58 + idx
else
idx = find(P4, ch)
if (idx >= 0)
code = 65 + idx
else
idx = find(P5, ch)
if (idx >= 0)
code = 91 + idx
else
idx = find(P6, ch)
if (idx >= 0)
code = 97 + idx
else
idx = find(P7, ch)
if (idx >= 0)
code = 123 + idx
else
// non-printable -> 0
code = 0
push(out, code)
i = i + 1
return out
*/

View file

@ -51,6 +51,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* from parser_utils.c */
extern char *preprocess_includes_with_path(const char *src, const char *current_path);
/* ---- parser error state ---- */
static const char *g_current_source_path = NULL; /* for propagating filename into nested bytecodes */
@ -7572,8 +7574,8 @@ Bytecode *parse_file_to_bytecode(const char *path) {
return NULL;
}
/* Preprocess includes before compiling */
char *prep = preprocess_includes(src);
/* Preprocess includes before compiling (with path for accurate markers) */
char *prep = preprocess_includes_with_path(src, path);
const char *compile_src = prep ? prep : src;
size_t compile_len = strlen(compile_src);
@ -7604,6 +7606,43 @@ Bytecode *parse_file_to_bytecode(const char *path) {
if (g_has_error) {
int line = 1, col = 1;
calc_line_col(compile_src, compile_len, g_err_pos, &line, &col);
/* If the preprocessor injected an initial include marker line, compensate
* it in the outward-reported top-level line number so it matches the
* physical file. The marker may appear on the first line OR on the second
* line if a shebang was preserved as line 1. We therefore check the first
* non-shebang line for the marker and, if the error lies after that line,
* subtract exactly one from the reported line. */
{
const char *marker0 = "// __include_begin__: ";
size_t m0 = strlen(marker0);
/* Determine start of first logical line to examine (skip shebang) */
size_t start = 0;
if (compile_len >= 2 && compile_src[0] == '#' && compile_src[1] == '!') {
/* skip to end of shebang line (handle CR/LF/CRLF) */
while (start < compile_len && compile_src[start] != '\n' && compile_src[start] != '\r') start++;
if (start < compile_len && compile_src[start] == '\r') {
start++;
if (start < compile_len && compile_src[start] == '\n') start++;
} else if (start < compile_len && compile_src[start] == '\n') {
start++;
}
}
/* Find beginning and end of that (first non-shebang) line */
size_t ls = start;
size_t eol0 = ls;
while (eol0 < compile_len && compile_src[eol0] != '\n') eol0++;
if (ls + m0 <= compile_len && strncmp(compile_src + ls, marker0, m0) == 0) {
/* If the error is positioned after the synthetic marker line, reduce the outward line */
if (g_err_pos > eol0) {
if (line > 1) line -= 1;
}
}
}
g_err_line = line;
g_err_col = col;
@ -7611,6 +7650,7 @@ Bytecode *parse_file_to_bytecode(const char *path) {
const char *marker = "// __include_begin__: ";
size_t mlen = strlen(marker);
int inner_line = -1;
int base_line = 1;
char inc_path[512];
inc_path[0] = '\0';
/* scan backward to find last marker line */
@ -7622,16 +7662,40 @@ Bytecode *parse_file_to_bytecode(const char *path) {
ls--;
/* check if this line starts with marker */
if (ls + mlen <= compile_len && strncmp(compile_src + ls, marker, mlen) == 0) {
/* extract path until end-of-line */
/* Parse marker: path [as alias] [@line N] */
size_t p = ls + mlen;
size_t pe = p;
while (pe < compile_len && compile_src[pe] != '\n' && (pe - p) < sizeof(inc_path) - 1)
pe++;
memcpy(inc_path, compile_src + p, pe - p);
inc_path[pe - p] = '\0';
/* compute inner line as number of newlines from (pe+1) to error position */
size_t eol = p;
while (eol < compile_len && compile_src[eol] != '\n') eol++;
/* locate separators */
size_t pos_as = eol, pos_line = eol;
for (size_t t = p; t + 3 < eol; ++t) {
if (compile_src[t] == ' ' && strncmp(compile_src + t, " as ", 4) == 0) { pos_as = t; break; }
}
for (size_t t = p; t + 6 < eol; ++t) {
if (compile_src[t] == ' ' && strncmp(compile_src + t, " @line ", 7) == 0) { pos_line = t; break; }
}
size_t path_end = pos_as < pos_line ? pos_as : pos_line;
if (path_end < p) path_end = eol;
size_t copy = (path_end - p) < sizeof(inc_path) - 1 ? (path_end - p) : sizeof(inc_path) - 1;
memcpy(inc_path, compile_src + p, copy);
inc_path[copy] = '\0';
/* parse optional base line value */
base_line = 1;
if (pos_line < eol) {
size_t num_start = pos_line + 7;
while (num_start < eol && compile_src[num_start] == ' ') num_start++;
int v = 0;
while (num_start < eol && compile_src[num_start] >= '0' && compile_src[num_start] <= '9') {
v = v * 10 + (compile_src[num_start] - '0');
num_start++;
}
if (v > 0) base_line = v;
}
/* compute inner line as number of newlines from (eol+1) to error position */
int count = 1;
size_t q = (pe < compile_len && compile_src[pe] == '\n') ? (pe + 1) : pe;
size_t q = (eol < compile_len && compile_src[eol] == '\n') ? (eol + 1) : eol;
while (q < g_err_pos) {
if (compile_src[q] == '\n') count++;
q++;
@ -7645,22 +7709,24 @@ Bytecode *parse_file_to_bytecode(const char *path) {
}
if (inner_line > 0 && inc_path[0] != '\0') {
/* Adjust for shebang stripping in included file: if it starts with '#!' we
* removed that entire first line during preprocessing, so add +1 to map
* back to the physical file line number. */
{
FILE *sf = fopen(inc_path, "rb");
if (sf) {
int c1 = fgetc(sf);
int c2 = fgetc(sf);
if (c1 == '#' && c2 == '!') {
inner_line += 1;
}
fclose(sf);
}
/* Adjust for shebang in included physical file */
int shebang_adjust = 0;
FILE *sf = fopen(inc_path, "rb");
if (sf) {
int c1 = fgetc(sf);
int c2 = fgetc(sf);
if (c1 == '#' && c2 == '!') shebang_adjust = 1;
fclose(sf);
}
int mapped_inner = inner_line + (base_line - 1) + shebang_adjust;
/* If the include context points to the same top-level path, suppress the redundant trailer. */
if (path && strcmp(path, inc_path) == 0) {
fprintf(stderr, "Parse error %s:%d:%d: %s\n",
path, line, col, g_err_msg);
} else {
fprintf(stderr, "Parse error %s:%d:%d: %s (in %s:%d)\n",
path ? path : "<input>", line, col, g_err_msg, inc_path, mapped_inner);
}
fprintf(stderr, "Parse error %s:%d:%d: %s (in %s:%d)\n",
path ? path : "<input>", line, col, g_err_msg, inc_path, inner_line);
} else {
fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "<input>", line, col, g_err_msg);
}

View file

@ -499,7 +499,7 @@ static void collect_exports_top_level(const char *text, NameList *out) {
}
}
static char *preprocess_includes_internal(const char *src, int depth) {
static char *preprocess_includes_internal(const char *src, const char *current_path, int depth) {
if (!src) return NULL;
if (depth > 64) {
fprintf(stderr, "Include error: include nesting too deep\n");
@ -515,10 +515,44 @@ static char *preprocess_includes_internal(const char *src, int depth) {
size_t len = strlen(src);
StrBuf out;
sb_init(&out);
/* Preserve shebang on the very first line before inserting the initial marker. */
size_t shebang_end = 0;
int shebang_lines = 0;
if (src[0] == '#' && src[1] == '!') {
/* find end of line (handle CR, LF, or CRLF) */
size_t j = 0;
while (src[j] && src[j] != '\n' && src[j] != '\r') j++;
/* include line ending */
if (src[j] == '\r') {
j++;
if (src[j] == '\n') j++;
} else if (src[j] == '\n') {
j++;
}
/* count lines in shebang we are copying */
for (size_t t = 0; t < j; ++t) if (src[t] == '\n') shebang_lines++;
if (shebang_lines == 0) shebang_lines = 1; /* single shebang line without LF */
shebang_end = j;
sb_append_n(&out, src, shebang_end);
}
/* When we know the current file path, emit a leading marker so mapping
* can always recover the correct file for regions before any include. */
if (current_path && current_path[0]) {
sb_append(&out, "// __include_begin__: ");
sb_append(&out, current_path);
/* annotate physical base line in the original file */
char lb[32];
int base_line = 1 + (shebang_end ? shebang_lines : 0);
snprintf(lb, sizeof(lb), " @line %d", base_line);
sb_append(&out, lb);
sb_append(&out, "\n");
}
int in_line = 0, in_block = 0, in_sq = 0, in_dq = 0, esc = 0;
int bol = 1; /* beginning of line */
for (size_t i = 0; i < len;) {
for (size_t i = shebang_end; i < len;) {
char c = src[i];
/* Detect include directive at BOL, outside comments/strings */
@ -657,7 +691,7 @@ static char *preprocess_includes_internal(const char *src, int depth) {
startp = q;
}
char *inc_clean = strdup(startp);
char *exp = preprocess_includes_internal(inc_clean, depth + 1);
char *exp = preprocess_includes_internal(inc_clean, resolved, depth + 1);
free(inc);
free(inc_clean);
if (exp) {
@ -668,6 +702,7 @@ static char *preprocess_includes_internal(const char *src, int depth) {
sb_append(&out, " as ");
sb_append(&out, ns);
}
sb_append(&out, " @line 1");
sb_append(&out, "\n");
/* append expanded included content */
@ -675,6 +710,23 @@ static char *preprocess_includes_internal(const char *src, int depth) {
/* ensure included chunk ends with newline to preserve line structure */
if (out.len == 0 || out.buf[out.len - 1] != '\n') sb_append_ch(&out, '\n');
/* After including, if we know the parent file path, emit a marker to
* resume mapping to the including (parent) file for the subsequent text. */
if (current_path && current_path[0]) {
sb_append(&out, "// __include_begin__: ");
sb_append(&out, current_path);
/* compute resume line number in parent: next line after include directive */
char lb2[32];
/* crude estimate: resume at next physical line */
snprintf(lb2, sizeof(lb2), " @line %d", 0); /* will be patched below */
/* We need actual parent line number. Compute by scanning from start to 'i' */
int parent_line = 1 + (shebang_end ? shebang_lines : 0);
for (size_t tt = shebang_end; tt < k; ++tt) if (src[tt] == '\n') parent_line++;
snprintf(lb2, sizeof(lb2), " @line %d", parent_line);
sb_append(&out, lb2);
sb_append(&out, "\n");
}
free(exp);
}
}
@ -792,7 +844,12 @@ static char *preprocess_includes_internal(const char *src, int depth) {
}
char *preprocess_includes(const char *src) {
return preprocess_includes_internal(src, 0);
return preprocess_includes_internal(src, NULL, 0);
}
/* Variant with known current file path to allow precise resume markers. */
char *preprocess_includes_with_path(const char *src, const char *current_path) {
return preprocess_includes_internal(src, current_path, 0);
}
/*
@ -813,7 +870,7 @@ int map_expanded_line_to_include_path(const char *path, int line,
char *orig = read_file_all(path, &fsz);
if (!orig) return 0;
char *prep = preprocess_includes_internal(orig, 0);
char *prep = preprocess_includes_internal(orig, path, 0);
free(orig);
if (!prep) return 0;
@ -827,11 +884,9 @@ int map_expanded_line_to_include_path(const char *path, int line,
}
if (cur != line) { free(prep); return 0; }
/* scan backward to find last include marker line */
/* scan backward to find the nearest include/resume marker line */
const char *marker = "// __include_begin__: ";
const char *end_marker = "// __include_end__: ";
size_t mlen = strlen(marker);
size_t elen = strlen(end_marker);
size_t scan = pos;
while (scan > 0) {
/* find start of current line */
@ -839,46 +894,51 @@ int map_expanded_line_to_include_path(const char *path, int line,
while (ls > 0 && prep[ls - 1] != '\n') ls--;
/* check if this line starts with marker */
if (ls + mlen <= len && strncmp(prep + ls, marker, mlen) == 0) {
/* extract included path until EOL or " as " */
/* Parse marker line: path [as alias] [@line N] */
size_t p = ls + mlen;
size_t pe = p;
while (pe < len && prep[pe] != '\n' && !(prep[pe] == ' ' && pe + 3 < len && strncmp(prep + pe, " as ", 4) == 0)) pe++;
size_t copy = (pe - p) < (out_path_cap - 1) ? (pe - p) : (out_path_cap - 1);
size_t eol = p;
while (eol < len && prep[eol] != '\n') eol++;
/* find separators */
size_t pos_as = eol, pos_line = eol;
for (size_t t = p; t + 3 < eol; ++t) {
if (prep[t] == ' ' && strncmp(prep + t, " as ", 4) == 0) { pos_as = t; break; }
}
for (size_t t = p; t + 6 < eol; ++t) {
if (prep[t] == ' ' && strncmp(prep + t, " @line ", 7) == 0) { pos_line = t; break; }
}
size_t path_end = pos_as < pos_line ? pos_as : pos_line;
if (path_end < p) path_end = eol;
size_t copy = (path_end - p) < (out_path_cap - 1) ? (path_end - p) : (out_path_cap - 1);
memcpy(out_path, prep + p, copy);
out_path[copy] = '\0';
/* determine the span end by scanning forward with nesting to match the
* corresponding __include_end__ marker. Lines between begin and its
* matching end belong to this include. */
size_t q = pe; /* end of path segment on the begin line */
/* move to first content line after begin marker */
while (q < len && prep[q] != '\n') q++;
if (q < len && prep[q] == '\n') q++;
/* parse optional base line number */
int base_line = 1;
if (pos_line < eol) {
size_t num_start = pos_line + 7; /* after ' @line ' */
int v = 0;
while (num_start < eol && prep[num_start] == ' ') num_start++;
while (num_start < eol && prep[num_start] >= '0' && prep[num_start] <= '9') {
v = v * 10 + (prep[num_start] - '0');
num_start++;
}
if (v > 0) base_line = v;
}
/* forward scan to find matching end marker accounting for nested includes */
/* determine the span: from the end of this marker line to the start of the next marker line */
size_t q = eol;
if (q < len && prep[q] == '\n') q++;
size_t span_start = q;
size_t span_end = len; /* default to EOF if no end marker is found */
int depth = 0;
size_t span_end = len;
size_t fwd = q;
while (fwd < len) {
/* find start of current line */
/* find start of next line */
size_t ls2 = fwd;
if (ls2 > 0) {
while (ls2 > 0 && prep[ls2 - 1] != '\n') ls2--;
}
while (ls2 > 0 && prep[ls2 - 1] != '\n') ls2--;
if (ls2 + mlen <= len && strncmp(prep + ls2, marker, mlen) == 0) {
/* nested begin */
depth++;
} else if (ls2 + elen <= len && strncmp(prep + ls2, end_marker, elen) == 0) {
if (depth == 0) {
/* matching end for our begin */
span_end = ls2; /* end before this line */
break;
} else {
depth--;
}
span_end = ls2; /* region ends right before the next marker */
break;
}
/* advance to next line */
while (fwd < len && prep[fwd] != '\n') fwd++;
if (fwd < len && prep[fwd] == '\n') fwd++;
}
@ -894,39 +954,7 @@ int map_expanded_line_to_include_path(const char *path, int line,
int inner = 1;
size_t cnt = span_start;
while (cnt < pos) { if (prep[cnt] == '\n') inner++; cnt++; }
/* Adjust for stripped shebang in included file: if the physical file
* starts with "#!" (with or without a UTF-8 BOM), add +1 so the reported
* line matches what the user sees in the editor. */
do {
size_t inc_sz = 0;
char *inc_src = read_file_all(out_path, &inc_sz);
if (!inc_src) break;
const unsigned char *u = (const unsigned char *)inc_src;
size_t off = 0;
if (inc_sz >= 3 && u[0] == 0xEF && u[1] == 0xBB && u[2] == 0xBF) off = 3; /* UTF-8 BOM */
if (inc_sz >= off + 2 && inc_src[off] == '#' && inc_src[off + 1] == '!') {
inner += 1;
}
free(inc_src);
} while (0);
/* Adjust for shebang stripping: if the real included file starts with '#!',
* preprocessing removed that entire first line, so the visible "inner" line
* number in the expanded text is one less than the actual file line. */
int adjusted = inner;
if (copy > 2) {
FILE *sf = fopen(out_path, "rb");
if (sf) {
int c1 = fgetc(sf);
int c2 = fgetc(sf);
if (c1 == '#' && c2 == '!') {
adjusted += 1;
}
fclose(sf);
}
}
*out_line = adjusted;
*out_line = base_line + inner - 1;
free(prep);
return 1;
}

View file

@ -123,13 +123,27 @@ static int fun_vm_vfprintf(FILE *stream, const char *fmt, va_list ap) {
/* fallback to VM's last recorded line if no marker found */
if (line <= 0) line = g_active_vm->current_line > 0 ? g_active_vm->current_line : 1;
/* If this function was compiled from an include-expanded source, map to the included file */
if (sfile && line > 0) {
char mapped_path[1024];
int mapped_line = line;
if (map_expanded_line_to_include_path(sfile, line, mapped_path, sizeof(mapped_path), &mapped_line)) {
sfile = strdup(mapped_path); /* leak on purpose for simplicity; this is rare */
line = mapped_line;
/* Map expanded line back to the real included file.
* Important: OP_LINE operands refer to the expanded, top-level source produced by
* the preprocessor. Therefore we must pass the TOP-LEVEL script path to the mapper,
* not the current function's own source_file (which may point at an included file).
*/
if (line > 0) {
/* Attempt to retrieve the entry frame's source file as the true top-level path */
const char *top_path = NULL;
if (g_active_vm->fp >= 0) {
Frame *entry = &g_active_vm->frames[0];
if (entry && entry->fn && entry->fn->source_file) top_path = entry->fn->source_file;
}
/* Fallback: use current function's source when entry is unavailable */
if (!top_path) top_path = sfile;
if (top_path) {
char mapped_path[1024];
int mapped_line = line;
if (map_expanded_line_to_include_path(top_path, line, mapped_path, sizeof(mapped_path), &mapped_line)) {
sfile = strdup(mapped_path); /* leak acceptable on error paths */
line = mapped_line;
}
}
}
}