From adcd421456d50a26e50f1caef933caf539c3b8e5 Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Thu, 15 Jan 2026 12:53:08 +0800 Subject: [PATCH] checker: add error message call stack support (requested by #16127, #24575, etc) (#26356) --- vlib/v/ast/ast.v | 7 +-- vlib/v/checker/check_types.v | 1 + vlib/v/checker/checker.v | 28 +++++++---- vlib/v/checker/comptime.v | 16 ++++++- vlib/v/checker/errors.v | 46 ++++++++++++------- vlib/v/checker/fn.v | 2 + .../tests/compile_error_call_position.out | 13 ++++++ .../tests/compile_error_call_position.vv | 11 +++++ .../tests/compile_error_explicit_type.out | 14 ++++++ .../tests/compile_error_explicit_type.vv | 14 ++++++ ...ng_working_with_a_custom_compile_error.out | 6 +++ .../comptime_else_compile_error_no_return.out | 8 +++- .../tests/sync_stdatomic_compile_err.out | 5 ++ .../checker/tests/template_call_position.out | 14 ++++++ .../v/checker/tests/template_call_position.vv | 5 ++ .../tests/template_call_position_test.txt | 3 ++ vlib/v/errors/errors.v | 16 +++++-- vlib/v/parser/comptime.v | 8 ++++ vlib/v/util/errors.v | 13 ++++++ 19 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 vlib/v/checker/tests/compile_error_call_position.out create mode 100644 vlib/v/checker/tests/compile_error_call_position.vv create mode 100644 vlib/v/checker/tests/compile_error_explicit_type.out create mode 100644 vlib/v/checker/tests/compile_error_explicit_type.vv create mode 100644 vlib/v/checker/tests/template_call_position.out create mode 100644 vlib/v/checker/tests/template_call_position.vv create mode 100644 vlib/v/checker/tests/template_call_position_test.txt diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 8b196c88a..c375f9a75 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1108,9 +1108,10 @@ pub mut: imported_symbols map[string]string // used for `import {symbol}`, it maps symbol => module.symbol imported_symbols_trie token.KeywordsMatcherTrie // constructed from imported_symbols, to accelerate presense checks imported_symbols_used map[string]bool - errors []errors.Error // all the checker errors in the file - warnings []errors.Warning // all the checker warnings in the file - notices []errors.Notice // all the checker notices in the file + errors []errors.Error // all the checker errors in the file + warnings []errors.Warning // all the checker warnings in the file + notices []errors.Notice // all the checker notices in the file + call_stack []errors.CallStackItem // call stack for this file (used for template errors) generic_fns []&FnDecl global_labels []string // from `asm { .globl labelname }` template_paths []string // all the .html/.md files that were processed with $tmpl diff --git a/vlib/v/checker/check_types.v b/vlib/v/checker/check_types.v index bce0b0874..61ff40552 100644 --- a/vlib/v/checker/check_types.v +++ b/vlib/v/checker/check_types.v @@ -1282,6 +1282,7 @@ fn (mut c Checker) infer_fn_generic_types(func &ast.Fn, mut node ast.CallExpr) { if c.table.register_fn_concrete_types(func.fkey(), inferred_types) { c.need_recheck_generic_fns = true } + c.generic_call_positions[c.build_generic_call_key(func.fkey(), inferred_types)] = node.pos } // is_contains_any_kind_of_pointer check that the type and submember types(arrays, fixed arrays, maps, struct fields, and so on) diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 4b97942e6..b2704ec07 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -140,15 +140,16 @@ mut: inside_assign bool // doing_line_info int // a quick single file run when called with v -line-info (contains line nr to inspect) // doing_line_path string // same, but stores the path being parsed - is_index_assign bool - comptime_call_pos int // needed for correctly checking use before decl for templates - goto_labels map[string]ast.GotoLabel // to check for unused goto labels - enum_data_type ast.Type - field_data_type ast.Type - variant_data_type ast.Type - fn_return_type ast.Type - orm_table_fields map[string][]ast.StructField // known table structs - short_module_names []string // to check for function names colliding with module functions + is_index_assign bool + comptime_call_pos int // needed for correctly checking use before decl for templates + generic_call_positions map[string]token.Pos // map from generic function key to call position + goto_labels map[string]ast.GotoLabel // to check for unused goto labels + enum_data_type ast.Type + field_data_type ast.Type + variant_data_type ast.Type + fn_return_type ast.Type + orm_table_fields map[string][]ast.StructField // known table structs + short_module_names []string // to check for function names colliding with module functions v_current_commit_hash string // same as old C.V_CURRENT_COMMIT_HASH assign_stmt_attr string // for `x := [1,2,3] @[freed]` @@ -187,6 +188,15 @@ pub fn new_checker(table &ast.Table, pref_ &pref.Preferences) &Checker { return checker } +// build_generic_call_key builds a key for tracking generic function call positions +fn (c &Checker) build_generic_call_key(fkey string, concrete_types []ast.Type) string { + mut types_str := '' + for typ in concrete_types { + types_str += c.table.type_to_str(typ) + ',' + } + return '${fkey}[${types_str}]' +} + fn (mut c Checker) reset_checker_state_at_start_of_new_file() { c.expected_type = ast.void_type c.expected_or_type = ast.void_type diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v index 9fe870b51..fef603098 100644 --- a/vlib/v/checker/comptime.v +++ b/vlib/v/checker/comptime.v @@ -8,6 +8,7 @@ import v.pref import v.util import v.pkgconfig import v.type_resolver +import v.errors import strings fn (mut c Checker) comptime_call(mut node ast.ComptimeCall) ast.Type { @@ -15,7 +16,20 @@ fn (mut c Checker) comptime_call(mut node ast.ComptimeCall) ast.Type { node.left_type = c.expr(mut node.left) } if node.kind == .compile_error { - c.error(c.comptime_call_msg(node), node.pos) + // Add call stack information for `$compile_error()` + mut call_stack := []errors.CallStackItem{} + // Only add call stack if we're inside a function (not at module level) + if c.fn_level > 0 && c.table.cur_fn != unsafe { nil } { + call_key := c.build_generic_call_key(c.table.cur_fn.fkey(), c.table.cur_concrete_types) + pos := c.generic_call_positions[call_key] or { c.table.cur_fn.name_pos } + // Use the file path from the position, not the current file + file_path := if pos.file_idx >= 0 { c.table.filelist[pos.file_idx] } else { c.file.path } + call_stack << errors.CallStackItem{ + file_path: file_path + pos: pos + } + } + c.error(c.comptime_call_msg(node), node.pos, call_stack: call_stack) return ast.void_type } else if node.kind == .compile_warn { c.warn(c.comptime_call_msg(node), node.pos) diff --git a/vlib/v/checker/errors.v b/vlib/v/checker/errors.v index bf73cf52e..6e06dd227 100644 --- a/vlib/v/checker/errors.v +++ b/vlib/v/checker/errors.v @@ -28,9 +28,15 @@ fn (mut c Checker) add_instruction_for_result_type() { c.table.cur_fn.return_type_pos) } -fn (mut c Checker) warn(s string, pos token.Pos) { +@[params] +pub struct MessageOptions { +pub: + call_stack []errors.CallStackItem +} + +fn (mut c Checker) warn(s string, pos token.Pos, options MessageOptions) { allow_warnings := !(c.pref.is_prod || c.pref.warns_are_errors) // allow warnings only in dev builds - c.warn_or_error(s, pos, allow_warnings) + c.warn_or_error(s, pos, allow_warnings, options) } fn (mut c Checker) warn_alloc(s string, pos token.Pos) { @@ -43,7 +49,7 @@ fn (mut c Checker) warn_alloc(s string, pos token.Pos) { } } -fn (mut c Checker) error(message string, pos token.Pos) { +fn (mut c Checker) error(message string, pos token.Pos, options MessageOptions) { if (c.pref.translated || c.file.is_translated) && message.starts_with('mismatched types') { // TODO: move this return @@ -61,17 +67,17 @@ fn (mut c Checker) error(message string, pos token.Pos) { } msg += ' (veb action: ${veb_action[..j]})' } - c.warn_or_error(msg, pos, false) + c.warn_or_error(msg, pos, false, options) } -fn (mut c Checker) fatal(message string, pos token.Pos) { +fn (mut c Checker) fatal(message string, pos token.Pos, options MessageOptions) { if (c.pref.translated || c.file.is_translated) && message.starts_with('mismatched types') { // TODO: move this return } msg := message.replace('`Array_', '`[]') c.pref.fatal_errors = true - c.warn_or_error(msg, pos, false) + c.warn_or_error(msg, pos, false, options) } fn (mut c Checker) note(message string, pos token.Pos) { @@ -108,7 +114,7 @@ fn (mut c Checker) note(message string, pos token.Pos) { } } -fn (mut c Checker) warn_or_error(message string, pos token.Pos, warn bool) { +fn (mut c Checker) warn_or_error(message string, pos token.Pos, warn bool, options MessageOptions) { if !warn { $if checker_exit_on_first_error ? { eprintln('\n\n>> checker error: ${message}, pos: ${pos}') @@ -148,12 +154,19 @@ fn (mut c Checker) warn_or_error(message string, pos token.Pos, warn bool) { return } if !warn { + // Use provided call_stack or fall back to file.call_stack + actual_call_stack := if options.call_stack.len > 0 { + options.call_stack + } else { + c.file.call_stack + } if c.pref.fatal_errors { util.show_compiler_message('error:', errors.CompilerMessage{ - pos: pos - file_path: file_path - message: message - details: details + pos: pos + file_path: file_path + message: message + details: details + call_stack: actual_call_stack }) exit(1) } @@ -167,11 +180,12 @@ fn (mut c Checker) warn_or_error(message string, pos token.Pos, warn bool) { if kpos !in c.error_lines { c.error_lines[kpos] = true err := errors.Error{ - reporter: errors.Reporter.checker - pos: pos - file_path: file_path - message: message - details: details + reporter: errors.Reporter.checker + pos: pos + file_path: file_path + message: message + details: details + call_stack: actual_call_stack } c.file.errors << err c.errors << err diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 1063cf3cd..55728e888 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -1000,6 +1000,8 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. if no_exists { c.need_recheck_generic_fns = true } + full_fkey := if fn_name_has_dot { fkey } else { c.mod + '.' + fkey } + c.generic_call_positions[c.build_generic_call_key(full_fkey, concrete_types)] = node.pos } args_len := node.args.len if node.kind == .jsawait { diff --git a/vlib/v/checker/tests/compile_error_call_position.out b/vlib/v/checker/tests/compile_error_call_position.out new file mode 100644 index 000000000..f7e0819f2 --- /dev/null +++ b/vlib/v/checker/tests/compile_error_call_position.out @@ -0,0 +1,13 @@ +vlib/v/checker/tests/compile_error_call_position.vv:5:3: error: son only taken int as input + 3 | fn son[T](val T) { + 4 | $if T !is int { + 5 | $compile_error('son only taken int as input') + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 6 | } + 7 | } +called from vlib/v/checker/tests/compile_error_call_position.vv:10:2 + 8 | + 9 | fn main() { + 10 | son(false) + | ~~~~~~~~~~ + 11 | } diff --git a/vlib/v/checker/tests/compile_error_call_position.vv b/vlib/v/checker/tests/compile_error_call_position.vv new file mode 100644 index 000000000..08a1758a3 --- /dev/null +++ b/vlib/v/checker/tests/compile_error_call_position.vv @@ -0,0 +1,11 @@ +module main + +fn son[T](val T) { + $if T !is int { + $compile_error('son only taken int as input') + } +} + +fn main() { + son(false) +} diff --git a/vlib/v/checker/tests/compile_error_explicit_type.out b/vlib/v/checker/tests/compile_error_explicit_type.out new file mode 100644 index 000000000..ee803e29f --- /dev/null +++ b/vlib/v/checker/tests/compile_error_explicit_type.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/compile_error_explicit_type.vv:5:3: error: son only take `int` as input + 3 | fn son[T](val T) { + 4 | $if T !is int { + 5 | $compile_error('son only take `int` as input') + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 6 | } + 7 | } +called from vlib/v/checker/tests/compile_error_explicit_type.vv:11:2 + 9 | fn main() { + 10 | son[int](123) + 11 | son[bool](false) + | ~~~~~~~~~~~~~~~~ + 12 | son[int](456) + 13 | son[string]('hello') diff --git a/vlib/v/checker/tests/compile_error_explicit_type.vv b/vlib/v/checker/tests/compile_error_explicit_type.vv new file mode 100644 index 000000000..d8b024fb0 --- /dev/null +++ b/vlib/v/checker/tests/compile_error_explicit_type.vv @@ -0,0 +1,14 @@ +module main + +fn son[T](val T) { + $if T !is int { + $compile_error('son only take `int` as input') + } +} + +fn main() { + son[int](123) + son[bool](false) + son[int](456) + son[string]('hello') +} diff --git a/vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.out b/vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.out index 108dd4d61..523bfd3de 100644 --- a/vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.out +++ b/vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.out @@ -5,3 +5,9 @@ vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.vv:8 | ~~~~~~~~~~~~~~~~~~~~ 9 | println('not bool ${val}') 10 | } +called from vlib/v/checker/tests/comptime_branching_working_with_a_custom_compile_error.vv:15:10 + 13 | + 14 | fn main() { + 15 | println(create(123)) + | ~~~~~~~~~~~ + 16 | } diff --git a/vlib/v/checker/tests/comptime_else_compile_error_no_return.out b/vlib/v/checker/tests/comptime_else_compile_error_no_return.out index b34e500bc..659fcfef4 100644 --- a/vlib/v/checker/tests/comptime_else_compile_error_no_return.out +++ b/vlib/v/checker/tests/comptime_else_compile_error_no_return.out @@ -4,4 +4,10 @@ vlib/v/checker/tests/comptime_else_compile_error_no_return.vv:5:3: error: not an 5 | $compile_error('not an int') | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | } - 7 | } \ No newline at end of file + 7 | } +called from vlib/v/checker/tests/comptime_else_compile_error_no_return.vv:11:2 + 9 | fn main() { + 10 | onlyint(7) + 11 | onlyint([]int{}) + | ~~~~~~~~~~~~~~~~ + 12 | } diff --git a/vlib/v/checker/tests/sync_stdatomic_compile_err.out b/vlib/v/checker/tests/sync_stdatomic_compile_err.out index e762441bd..2389b03fd 100644 --- a/vlib/v/checker/tests/sync_stdatomic_compile_err.out +++ b/vlib/v/checker/tests/sync_stdatomic_compile_err.out @@ -5,3 +5,8 @@ vlib/sync/stdatomic/atomic.c.v:85:3: error: atomic: only support number, bool, a | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | } 87 | return unsafe { nil } +called from vlib/v/checker/tests/sync_stdatomic_compile_err.vv:3:6 + 1 | import sync.stdatomic {new_atomic} + 2 | + 3 | _ := new_atomic(`1`) + | ~~~~~~~~~~~~~~~ diff --git a/vlib/v/checker/tests/template_call_position.out b/vlib/v/checker/tests/template_call_position.out new file mode 100644 index 000000000..49a2e5823 --- /dev/null +++ b/vlib/v/checker/tests/template_call_position.out @@ -0,0 +1,14 @@ +template_call_position_test.txt:11:2: error: undefined ident: `unknown_var` (veb action: main__main) +called from vlib/v/checker/tests/template_call_position.vv:4:2 + 2 | + 3 | fn main() { + 4 | $tmpl('template_call_position_test.txt') + | ^ + 5 | } +template_call_position_test.txt:11:2: error: expression does not return a value (veb action: main__main) +called from vlib/v/checker/tests/template_call_position.vv:4:2 + 2 | + 3 | fn main() { + 4 | $tmpl('template_call_position_test.txt') + | ^ + 5 | } diff --git a/vlib/v/checker/tests/template_call_position.vv b/vlib/v/checker/tests/template_call_position.vv new file mode 100644 index 000000000..5203fdefd --- /dev/null +++ b/vlib/v/checker/tests/template_call_position.vv @@ -0,0 +1,5 @@ +module main + +fn main() { + $tmpl('template_call_position_test.txt') +} diff --git a/vlib/v/checker/tests/template_call_position_test.txt b/vlib/v/checker/tests/template_call_position_test.txt new file mode 100644 index 000000000..43e085733 --- /dev/null +++ b/vlib/v/checker/tests/template_call_position_test.txt @@ -0,0 +1,3 @@ +Hello World! +This is a template file. +@unknown_var diff --git a/vlib/v/errors/errors.v b/vlib/v/errors/errors.v index 3eb392c74..94bd44585 100644 --- a/vlib/v/errors/errors.v +++ b/vlib/v/errors/errors.v @@ -10,13 +10,21 @@ pub enum Reporter { gen } -pub struct CompilerMessage { +// CallStackItem represents a single location in the call stack +pub struct CallStackItem { pub: - message string - details string file_path string pos token.Pos - reporter Reporter +} + +pub struct CompilerMessage { +pub: + message string + details string + file_path string + pos token.Pos + reporter Reporter + call_stack []CallStackItem // call stack for compile-time errors } pub struct Error { diff --git a/vlib/v/parser/comptime.v b/vlib/v/parser/comptime.v index e8c1cb460..be9a0c235 100644 --- a/vlib/v/parser/comptime.v +++ b/vlib/v/parser/comptime.v @@ -6,6 +6,7 @@ module parser import os import v.ast import v.token +import v.errors const supported_comptime_calls = ['html', 'tmpl', 'env', 'embed_file', 'pkgconfig', 'compile_error', 'compile_warn', 'd', 'res'] @@ -375,6 +376,13 @@ fn (mut p Parser) comptime_call() ast.ComptimeCall { } mut file := parse_comptime(tmpl_path, v_code, mut p.table, p.pref, mut p.scope) file.path = tmpl_path + // Store call stack info for template errors + file.call_stack = [ + errors.CallStackItem{ + file_path: p.file_path + pos: start_pos + }, + ] return ast.ComptimeCall{ scope: unsafe { nil } is_vweb: true diff --git a/vlib/v/util/errors.v b/vlib/v/util/errors.v index 04a2902c8..87d9fff80 100644 --- a/vlib/v/util/errors.v +++ b/vlib/v/util/errors.v @@ -209,6 +209,19 @@ pub fn show_compiler_message(kind string, err errors.CompilerMessage) { if err.details.len > 0 { eprintln(bold('Details: ') + color('details', err.details)) } + // Display call stack if available + if err.call_stack.len > 0 { + for item in err.call_stack { + caller_path := path_styled_for_error_messages(item.file_path) + eprintln(bold('called from') + ' ${caller_path}:${item.pos.line_nr + + 1}:${int_max(1, item.pos.col + 1)}') + // Display code context for the caller location + scontext := source_file_context(kind, item.file_path, item.pos).join('\n') + if scontext.len > 0 { + eprintln(scontext) + } + } + } } pub struct JsonError { -- 2.39.5