diff --git a/CMakeLists.txt b/CMakeLists.txt index f794906..34d9fb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/examples/net/http_mt_server.fun b/examples/blocking/net/http_mt_server.fun similarity index 100% rename from examples/net/http_mt_server.fun rename to examples/blocking/net/http_mt_server.fun diff --git a/examples/net/http_mt_server_cgi.fun b/examples/blocking/net/http_mt_server_cgi.fun similarity index 100% rename from examples/net/http_mt_server_cgi.fun rename to examples/blocking/net/http_mt_server_cgi.fun diff --git a/examples/net/http_server.fun b/examples/blocking/net/http_server.fun similarity index 100% rename from examples/net/http_server.fun rename to examples/blocking/net/http_server.fun diff --git a/examples/net/http_server_cgi.fun b/examples/blocking/net/http_server_cgi.fun similarity index 100% rename from examples/net/http_server_cgi.fun rename to examples/blocking/net/http_server_cgi.fun diff --git a/examples/net/http_server_cgi_lib.fun b/examples/blocking/net/http_server_cgi_lib.fun similarity index 100% rename from examples/net/http_server_cgi_lib.fun rename to examples/blocking/net/http_server_cgi_lib.fun diff --git a/examples/net/http_server_test.fun b/examples/blocking/net/http_server_test.fun similarity index 100% rename from examples/net/http_server_test.fun rename to examples/blocking/net/http_server_test.fun diff --git a/examples/net/http_static_server.fun b/examples/blocking/net/http_static_server.fun similarity index 100% rename from examples/net/http_static_server.fun rename to examples/blocking/net/http_static_server.fun diff --git a/examples/net/tcp_echo_client.fun b/examples/blocking/net/tcp_echo_client.fun similarity index 100% rename from examples/net/tcp_echo_client.fun rename to examples/blocking/net/tcp_echo_client.fun diff --git a/examples/net/tcp_echo_server.fun b/examples/blocking/net/tcp_echo_server.fun similarity index 100% rename from examples/net/tcp_echo_server.fun rename to examples/blocking/net/tcp_echo_server.fun diff --git a/examples/extra/tcp_echo_server.fun b/examples/blocking/tcp_echo_server.fun similarity index 100% rename from examples/extra/tcp_echo_server.fun rename to examples/blocking/tcp_echo_server.fun diff --git a/examples/extra/tcp_echo_server_class.fun b/examples/blocking/tcp_echo_server_class.fun similarity index 100% rename from examples/extra/tcp_echo_server_class.fun rename to examples/blocking/tcp_echo_server_class.fun diff --git a/examples/extra/tk_file_manager.fun b/examples/blocking/tk_file_manager.fun similarity index 100% rename from examples/extra/tk_file_manager.fun rename to examples/blocking/tk_file_manager.fun diff --git a/examples/extra/tk_hello.fun b/examples/blocking/tk_hello.fun similarity index 100% rename from examples/extra/tk_hello.fun rename to examples/blocking/tk_hello.fun diff --git a/examples/extra/tk_testing.fun b/examples/blocking/tk_testing.fun similarity index 100% rename from examples/extra/tk_testing.fun rename to examples/blocking/tk_testing.fun diff --git a/examples/io/await_http_client.fun b/examples/broken/await_http_client.fun similarity index 100% rename from examples/io/await_http_client.fun rename to examples/broken/await_http_client.fun diff --git a/examples/crypto/sha1_demo.fun b/examples/crypto/sha1_demo.fun index 1298cd0..25b8cf9 100755 --- a/examples/crypto/sha1_demo.fun +++ b/examples/crypto/sha1_demo.fun @@ -18,7 +18,7 @@ * FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/sha1_demo.fun */ -#include as sha1 +include 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 ===") diff --git a/examples/error/debug_reporting.fun b/examples/error/debug_reporting.fun index 4daa009..3935d44 100755 --- a/examples/error/debug_reporting.fun +++ b/examples/error/debug_reporting.fun @@ -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. diff --git a/examples/extra/sqlite_example.fun b/examples/sqlite_example.fun similarity index 100% rename from examples/extra/sqlite_example.fun rename to examples/sqlite_example.fun diff --git a/lib/strings.fun b/lib/strings.fun index e12ce22..20f1bf8 100644 --- a/lib/strings.fun +++ b/lib/strings.fun @@ -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 -*/ diff --git a/src/parser.c b/src/parser.c index 2490527..3b1cae2 100644 --- a/src/parser.c +++ b/src/parser.c @@ -51,6 +51,8 @@ #include #include #include +/* 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 : "", line, col, g_err_msg, inc_path, mapped_inner); } - fprintf(stderr, "Parse error %s:%d:%d: %s (in %s:%d)\n", - path ? path : "", line, col, g_err_msg, inc_path, inner_line); } else { fprintf(stderr, "Parse error %s:%d:%d: %s\n", path ? path : "", line, col, g_err_msg); } diff --git a/src/parser_utils.c b/src/parser_utils.c index e0c80f2..d3e45b9 100644 --- a/src/parser_utils.c +++ b/src/parser_utils.c @@ -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; } diff --git a/src/vm.c b/src/vm.c index 27490cc..51468ce 100644 --- a/src/vm.c +++ b/src/vm.c @@ -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; + } } } }