From 53ccdbda104e87c8d88df89143cc3b56a07ccdbb Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 30 Mar 2026 19:10:29 +0200 Subject: [PATCH] Fixed bug with wrong line numbers in error messages. Fixed permissions on examples. (0.39.14) --- CMakeLists.txt | 24 ++- examples/arrays/arrays.fun | 0 examples/arrays/arrays_advanced.fun | 0 examples/arrays/arrays_iter.fun | 0 examples/basics/boolean_decl.fun | 0 examples/basics/booleans.fun | 0 examples/basics/builtins_conversions.fun | 0 examples/cli/cli_parse_example.fun | 0 examples/crypto/libressl_md5.fun | 0 examples/crypto/libressl_ripemd160.fun | 0 examples/crypto/libressl_sha256.fun | 0 examples/crypto/libressl_sha512.fun | 0 examples/error/include_line_mapping_test.fun | 34 ++++ examples/extra/notcurses_menu.fun | 0 examples/extra/notcurses_progress.fun | 0 examples/io/await_http_client.fun | 11 ++ examples/io/file_io.fun | 0 examples/snippets/include_namespace.fun | 0 examples/snippets/maps_iterate.fun | 0 examples/strings/base64_demo.fun | 0 lib/async/scheduler.fun | 11 ++ lib/test/include_divzero.fun | 20 +++ src/parser.c | 22 ++- src/parser_utils.c | 171 ++++++++++++++++--- src/repl.c | 10 ++ src/vm.c | 87 +--------- 26 files changed, 274 insertions(+), 116 deletions(-) mode change 100644 => 100755 examples/arrays/arrays.fun mode change 100644 => 100755 examples/arrays/arrays_advanced.fun mode change 100644 => 100755 examples/arrays/arrays_iter.fun mode change 100644 => 100755 examples/basics/boolean_decl.fun mode change 100644 => 100755 examples/basics/booleans.fun mode change 100644 => 100755 examples/basics/builtins_conversions.fun mode change 100644 => 100755 examples/cli/cli_parse_example.fun mode change 100644 => 100755 examples/crypto/libressl_md5.fun mode change 100644 => 100755 examples/crypto/libressl_ripemd160.fun mode change 100644 => 100755 examples/crypto/libressl_sha256.fun mode change 100644 => 100755 examples/crypto/libressl_sha512.fun create mode 100755 examples/error/include_line_mapping_test.fun mode change 100644 => 100755 examples/extra/notcurses_menu.fun mode change 100644 => 100755 examples/extra/notcurses_progress.fun mode change 100644 => 100755 examples/io/await_http_client.fun mode change 100644 => 100755 examples/io/file_io.fun mode change 100644 => 100755 examples/snippets/include_namespace.fun mode change 100644 => 100755 examples/snippets/maps_iterate.fun mode change 100644 => 100755 examples/strings/base64_demo.fun create mode 100644 lib/test/include_divzero.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index d213914..f794906 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.39.13 LANGUAGES C) +project(fun VERSION 0.39.14 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -401,6 +401,28 @@ if(BUILD_TESTING) fun_add_example_test(crypto_crc32 examples/crypto/crc32_example.fun) fun_add_example_test(crypto_crc32c examples/crypto/crc32c_example.fun) fun_add_example_test(crypto_aes256 examples/crypto/aes256.fun) + + # Include-line mapping regression test: + # This script intentionally triggers a runtime error inside an included file. + # The VM augments the error with a precise (file:line) from the included file. + # We assert that stderr/stdout contain the correct mapped location and that the + # program exits with a non-zero status (WILL_FAIL). + set(_inc_map_script "${CMAKE_SOURCE_DIR}/examples/error/include_line_mapping_test.fun") + if(EXISTS "${_inc_map_script}") + add_test(NAME include_line_mapping + COMMAND $ "${_inc_map_script}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + # Ensure the interpreter resolves <...> includes to the repo lib/ + set_tests_properties(include_line_mapping PROPERTIES + ENVIRONMENT "FUN_LIB_DIR=${CMAKE_SOURCE_DIR}/lib" + # The runtime error message should contain the mapped file path and line 7 + PASS_REGULAR_EXPRESSION "lib/test/include_divzero.fun:7" + WILL_FAIL TRUE + ) + else() + message(WARNING "include_line_mapping_test.fun not found; skipping include_line_mapping CTest") + endif() endif() # Install rules diff --git a/examples/arrays/arrays.fun b/examples/arrays/arrays.fun old mode 100644 new mode 100755 diff --git a/examples/arrays/arrays_advanced.fun b/examples/arrays/arrays_advanced.fun old mode 100644 new mode 100755 diff --git a/examples/arrays/arrays_iter.fun b/examples/arrays/arrays_iter.fun old mode 100644 new mode 100755 diff --git a/examples/basics/boolean_decl.fun b/examples/basics/boolean_decl.fun old mode 100644 new mode 100755 diff --git a/examples/basics/booleans.fun b/examples/basics/booleans.fun old mode 100644 new mode 100755 diff --git a/examples/basics/builtins_conversions.fun b/examples/basics/builtins_conversions.fun old mode 100644 new mode 100755 diff --git a/examples/cli/cli_parse_example.fun b/examples/cli/cli_parse_example.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/libressl_md5.fun b/examples/crypto/libressl_md5.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/libressl_ripemd160.fun b/examples/crypto/libressl_ripemd160.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/libressl_sha256.fun b/examples/crypto/libressl_sha256.fun old mode 100644 new mode 100755 diff --git a/examples/crypto/libressl_sha512.fun b/examples/crypto/libressl_sha512.fun old mode 100644 new mode 100755 diff --git a/examples/error/include_line_mapping_test.fun b/examples/error/include_line_mapping_test.fun new file mode 100755 index 0000000..3274cc3 --- /dev/null +++ b/examples/error/include_line_mapping_test.fun @@ -0,0 +1,34 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-30 + */ + +/* + Test: error location in included files shows correct file:line + + How to run: + - From repo root: ./build_debug/fun examples/error/include_line_mapping_test.fun + + Expected: Runtime error message suffix contains the included file path + (lib/test/include_divzero.fun) and the exact line number of + the failing operation inside that file (see lib file below). +*/ + +// Use angle-bracket include so resolution tries FUN_LIB_DIR, DEFAULT_LIB_DIR, +// and finally the project-local lib/ directory. This makes the example work +// from the repository without extra setup. +#include + +fun main() + // Trigger the division by zero inside the included file + boom() + +main() diff --git a/examples/extra/notcurses_menu.fun b/examples/extra/notcurses_menu.fun old mode 100644 new mode 100755 diff --git a/examples/extra/notcurses_progress.fun b/examples/extra/notcurses_progress.fun old mode 100644 new mode 100755 diff --git a/examples/io/await_http_client.fun b/examples/io/await_http_client.fun old mode 100644 new mode 100755 index 9a57a88..ab89831 --- a/examples/io/await_http_client.fun +++ b/examples/io/await_http_client.fun @@ -1,5 +1,16 @@ #!/usr/bin/env fun +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-29 + */ + /* * Async-style HTTP GET using lib/async/scheduler.fun helpers * diff --git a/examples/io/file_io.fun b/examples/io/file_io.fun old mode 100644 new mode 100755 diff --git a/examples/snippets/include_namespace.fun b/examples/snippets/include_namespace.fun old mode 100644 new mode 100755 diff --git a/examples/snippets/maps_iterate.fun b/examples/snippets/maps_iterate.fun old mode 100644 new mode 100755 diff --git a/examples/strings/base64_demo.fun b/examples/strings/base64_demo.fun old mode 100644 new mode 100755 diff --git a/lib/async/scheduler.fun b/lib/async/scheduler.fun index db7bf59..6869402 100644 --- a/lib/async/scheduler.fun +++ b/lib/async/scheduler.fun @@ -1,3 +1,14 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-30 + */ + /* * Cooperative asyncio helpers (library-level) for Fun * diff --git a/lib/test/include_divzero.fun b/lib/test/include_divzero.fun new file mode 100644 index 0000000..12e9878 --- /dev/null +++ b/lib/test/include_divzero.fun @@ -0,0 +1,20 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-03-30 + */ + +// Minimal test library to trigger a runtime error at a precise line + +fun boom() + // The division by zero MUST be on this exact line (line 14) + x = 1 / 0 + return x + +fun ok() + return 42 diff --git a/src/parser.c b/src/parser.c index e964b28..2490527 100644 --- a/src/parser.c +++ b/src/parser.c @@ -6035,7 +6035,7 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, int current_indent) { while (*pos < len) { if (g_has_error) return; - size_t line_start = *pos; + size_t line_start = *pos; /* remember raw line start for dedent backtracking */ int indent = 0; if (!read_line_start(src, len, pos, &indent)) { /* EOF or error */ @@ -6053,10 +6053,12 @@ static void parse_block(Bytecode *bc, const char *src, size_t len, size_t *pos, } /* at same indent -> parse statement */ - /* Insert a line marker for better runtime error reporting */ + /* Insert a line marker for better runtime error reporting. Use the current + * position (start of the actual statement) returned by read_line_start(), + * not the pre-scan position which may point to a comment/blank line. */ { int stmt_line = 1, stmt_col = 1; - calc_line_col(src, len, line_start, &stmt_line, &stmt_col); + calc_line_col(src, len, *pos, &stmt_line, &stmt_col); bytecode_add_instruction(bc, OP_LINE, stmt_line); } @@ -7643,6 +7645,20 @@ 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); + } + } fprintf(stderr, "Parse error %s:%d:%d: %s (in %s:%d)\n", path ? path : "", line, col, g_err_msg, inc_path, inner_line); } else { diff --git a/src/parser_utils.c b/src/parser_utils.c index 7a97bf1..e0c80f2 100644 --- a/src/parser_utils.c +++ b/src/parser_utils.c @@ -661,17 +661,6 @@ static char *preprocess_includes_internal(const char *src, int depth) { free(inc); free(inc_clean); if (exp) { - /* If alias requested, initialize namespace map before including content */ - if (ns[0] != '\0') { - /* Announce alias so the parser can treat dot-call without implicit 'this' */ - sb_append(&out, "// __ns_alias__: "); - sb_append(&out, ns); - sb_append(&out, "\n"); - - sb_append(&out, ns); - sb_append(&out, " = {}\n"); - } - /* mark file origin for better error messages */ sb_append(&out, "// __include_begin__: "); sb_append(&out, resolved); @@ -686,22 +675,6 @@ 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'); - /* If alias is present, export top-level fun/class into alias map */ - if (ns[0] != '\0') { - NameList nl; - nl_init(&nl); - collect_exports_top_level(exp, &nl); - for (int ei = 0; ei < nl.count; ++ei) { - sb_append(&out, ns); - sb_append(&out, "."); - sb_append(&out, nl.names[ei]); - sb_append(&out, " = "); - sb_append(&out, nl.names[ei]); - sb_append(&out, "\n"); - } - nl_free(&nl); - } - free(exp); } } @@ -822,6 +795,150 @@ char *preprocess_includes(const char *src) { return preprocess_includes_internal(src, 0); } +/* + * Public helper: map a line number in the include-expanded top-level source file + * back to the original included file path and inner line number, using the + * `// __include_begin__: [ as ]` markers injected by the + * preprocessor. Returns 1 on success and fills out_path/out_line; 0 otherwise. + */ +int map_expanded_line_to_include_path(const char *path, int line, + char *out_path, size_t out_path_cap, + int *out_line) { + if (!path || line <= 0 || !out_path || out_path_cap == 0 || !out_line) return 0; + out_path[0] = '\0'; + *out_line = line; + + /* read original top-level file */ + size_t fsz = 0; + char *orig = read_file_all(path, &fsz); + if (!orig) return 0; + + char *prep = preprocess_includes_internal(orig, 0); + free(orig); + if (!prep) return 0; + + /* find start offset of requested 1-based line */ + size_t len = strlen(prep); + size_t pos = 0; + int cur = 1; + while (pos < len && cur < line) { + if (prep[pos] == '\n') cur++; + pos++; + } + if (cur != line) { free(prep); return 0; } + + /* scan backward to find last include 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 */ + size_t ls = scan; + 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 " */ + 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); + 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++; + + /* forward scan to find matching end marker accounting for nested includes */ + size_t span_start = q; + size_t span_end = len; /* default to EOF if no end marker is found */ + int depth = 0; + size_t fwd = q; + while (fwd < len) { + /* find start of current line */ + size_t ls2 = fwd; + if (ls2 > 0) { + 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--; + } + } + /* advance to next line */ + while (fwd < len && prep[fwd] != '\n') fwd++; + if (fwd < len && prep[fwd] == '\n') fwd++; + } + + /* If the requested position is not within [span_start, span_end), this + * begin marker does not apply; continue scanning backward. */ + if (!(pos >= span_start && pos < span_end)) { + /* not inside this include span */ + goto next_scan_back; + } + + /* compute inner line as number of newlines from span_start to current pos */ + 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; + free(prep); + return 1; + } + if (ls == 0) break; +next_scan_back: + scan = (ls > 0) ? (ls - 1) : 0; + } + + free(prep); + return 0; +} + /* Float literal parser: supports decimal and scientific notation. Returns parsed double and advances pos on success. */ static double parse_float_literal_value(const char *src, size_t len, size_t *pos, int *ok) { size_t p = *pos; diff --git a/src/repl.c b/src/repl.c index 29de060..c8732e3 100644 --- a/src/repl.c +++ b/src/repl.c @@ -20,6 +20,9 @@ #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 #include #include @@ -1662,6 +1665,13 @@ int fun_run_repl(VM *vm) { 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) { diff --git a/src/vm.c b/src/vm.c index e32fd04..27490cc 100644 --- a/src/vm.c +++ b/src/vm.c @@ -81,7 +81,7 @@ /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); -static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); +extern int map_expanded_line_to_include_path(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line); /* Threading internals (registry and platform glue) */ #include "vm/os/thread_common.c" @@ -127,7 +127,7 @@ static int fun_vm_vfprintf(FILE *stream, const char *fmt, va_list ap) { if (sfile && line > 0) { char mapped_path[1024]; int mapped_line = line; - if (map_expanded_line_to_include(sfile, line, mapped_path, sizeof(mapped_path), &mapped_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; } @@ -153,89 +153,6 @@ static int fun_vm_fprintf(FILE *stream, const char *fmt, ...) { return r; } -/* Helper: try to map expanded source line to included file:line using preprocessor markers */ -static int map_expanded_line_to_include(const char *path, int line, char *out_path, size_t out_path_cap, int *out_line) { - if (!path || line <= 0 || !out_path || out_path_cap == 0 || !out_line) return 0; - out_path[0] = '\0'; - *out_line = line; - - /* read original file */ - FILE *f = fopen(path, "rb"); - if (!f) return 0; - fseek(f, 0, SEEK_END); - long sz = ftell(f); - if (sz < 0) { - fclose(f); - return 0; - } - rewind(f); - char *buf = (char *)malloc((size_t)sz + 1); - if (!buf) { - fclose(f); - return 0; - } - size_t n = fread(buf, 1, (size_t)sz, f); - fclose(f); - buf[n] = '\0'; - - char *prep = preprocess_includes(buf); - free(buf); - if (!prep) return 0; - - /* find start offset of requested 1-based line */ - size_t len = strlen(prep); - size_t pos = 0; - int cur = 1; - while (pos < len && cur < line) { - if (prep[pos] == '\n') cur++; - pos++; - } - if (cur != line) { - free(prep); - return 0; - } - - /* scan backward to find last include marker line */ - const char *marker = "// __include_begin__: "; - size_t mlen = strlen(marker); - size_t scan = pos; - while (scan > 0) { - /* find start of this line */ - size_t ls = scan; - while (ls > 0 && prep[ls - 1] != '\n') - ls--; - /* check marker */ - if (ls + mlen <= len && strncmp(prep + ls, marker, mlen) == 0) { - /* extract included path up to EOL or ' as ' */ - 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); - memcpy(out_path, prep + p, copy); - out_path[copy] = '\0'; - /* compute inner line as number of newlines from end of marker line to current pos */ - int inner = 1; - size_t q = pe; - /* skip to next line start */ - while (q < len && prep[q] != '\n') - q++; - if (q < len && prep[q] == '\n') q++; - while (q < pos) { - if (prep[q] == '\n') inner++; - q++; - } - *out_line = inner; - free(prep); - return 1; - } - if (ls == 0) break; - scan = (ls > 0) ? (ls - 1) : 0; - } - - free(prep); - return 0; -} /* Redirect fprintf within this translation unit so opcode handlers use our wrapper */ #define fprintf fun_vm_fprintf