diff --git a/doc/docs.md b/doc/docs.md index 6a0d9ccb87..e9dbc7db95 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -4763,6 +4763,21 @@ fn legacy_function() {} fn inlined_function() { } +// This function's calls will NOT be inlined. +[noinline] +fn function() { +} + +// This function will NOT return to its callers. +// Such functions can be used at the end of or blocks, +// just like exit/1 or panic/1. Such functions can not +// have return types, and should end either in for{}, or +// by calling other `[noreturn]` functions. +[noreturn] +fn forever() { + for {} +} + // The following struct must be allocated on the heap. Therefore, it can only be used as a // reference (`&Window`) or inside another reference (`&OuterStruct{ Window{...} }`). [heap] diff --git a/vlib/builtin/builtin.c.v b/vlib/builtin/builtin.c.v index 30e6f0fe39..d21afcc621 100644 --- a/vlib/builtin/builtin.c.v +++ b/vlib/builtin/builtin.c.v @@ -4,9 +4,16 @@ type FnExitCb = fn () fn C.atexit(f FnExitCb) int +[noreturn] +fn vhalt() { + for {} +} + // exit terminates execution immediately and returns exit `code` to the shell. +[noreturn] pub fn exit(code int) { C.exit(code) + vhalt() } fn vcommithash() string { @@ -17,6 +24,7 @@ fn vcommithash() string { // recent versions of tcc print nicer backtraces automatically // NB: the duplication here is because tcc_backtrace should be called directly // inside the panic functions. +[noreturn] fn panic_debug(line_no int, file string, mod string, fn_name string, s string) { // NB: the order here is important for a stabler test output // module is less likely to change than function, etc... @@ -52,14 +60,17 @@ fn panic_debug(line_no int, file string, mod string, fn_name string, s string) { C.exit(1) } } + vhalt() } +[noreturn] pub fn panic_optional_not_set(s string) { panic('optional not set ($s)') } // panic prints a nice error message, then exits the process with exit code of 1. // It also shows a backtrace on most platforms. +[noreturn] pub fn panic(s string) { $if freestanding { bare_panic(s) @@ -87,6 +98,7 @@ pub fn panic(s string) { C.exit(1) } } + vhalt() } // eprintln prints a message with a line end, to stderr. Both stderr and stdout are flushed. diff --git a/vlib/builtin/linux_bare/libc_impl.v b/vlib/builtin/linux_bare/libc_impl.v index bebfa31bfd..735cac681c 100644 --- a/vlib/builtin/linux_bare/libc_impl.v +++ b/vlib/builtin/linux_bare/libc_impl.v @@ -140,6 +140,7 @@ pub fn write(fd i64, buf &byte, count u64) i64 { return x } +[noreturn] fn bare_panic(msg string) { println('V panic' + msg) exit(1) @@ -150,6 +151,7 @@ fn bare_backtrace() string { } [export: 'exit'] +[noreturn] fn __exit(code int) { sys_exit(code) } diff --git a/vlib/builtin/linux_bare/linux_syscalls.v b/vlib/builtin/linux_bare/linux_syscalls.v index 5bd98ed5d4..b0b2c4724e 100644 --- a/vlib/builtin/linux_bare/linux_syscalls.v +++ b/vlib/builtin/linux_bare/linux_syscalls.v @@ -303,8 +303,10 @@ fn sys_execve(filename &byte, argv []&byte, envp []&byte) int { } // 60 sys_exit +[noreturn] fn sys_exit(ec int) { sys_call1(60, u64(ec)) + for {} } // 102 sys_getuid diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 2a56c4cd7b..7e1bd43f78 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -363,6 +363,7 @@ pub: is_pub bool is_variadic bool is_anon bool + is_noreturn bool // true, when [noreturn] is used on a fn is_manualfree bool // true, when [manualfree] is used on a fn is_main bool // true for `fn main()` is_test bool // true for `fn test_abcde` @@ -421,6 +422,7 @@ pub mut: is_method bool is_field bool // temp hack, remove ASAP when re-impl CallExpr / Selector (joe) is_keep_alive bool // GC must not free arguments before fn returns + is_noreturn bool // whether the function/method is marked as [noreturn] args []CallArg expected_arg_types []Type language Language diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 20ec6b1515..cfc1d04a04 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -73,6 +73,7 @@ pub: generic_names []string is_pub bool is_deprecated bool // `[deprecated] fn abc(){}` + is_noreturn bool // `[noreturn] fn abc(){}` is_unsafe bool // `[unsafe] fn abc(){}` is_placeholder bool is_main bool // `fn main(){}` diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 492753d780..aef270404f 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1936,6 +1936,7 @@ pub fn (mut c Checker) method_call(mut call_expr ast.CallExpr) ast.Type { } } if has_method { + call_expr.is_noreturn = method.is_noreturn if !method.is_pub && !c.pref.is_test && method.mod != c.mod { // If a private method is called outside of the module // its receiver type is defined in, show an error. @@ -2439,10 +2440,13 @@ pub fn (mut c Checker) fn_call(mut call_expr ast.CallExpr) ast.Type { c.table.fns[fn_name].usages++ } } + mut is_native_builtin := false if !found && c.pref.backend == .native { if fn_name in native.builtins { c.table.fns[fn_name].usages++ - return ast.void_type + found = true + func = c.table.fns[fn_name] + is_native_builtin = true } } if !found && c.pref.is_vsh { @@ -2457,6 +2461,9 @@ pub fn (mut c Checker) fn_call(mut call_expr ast.CallExpr) ast.Type { c.table.fns[os_name].usages++ } } + if is_native_builtin { + return ast.void_type + } // check for arg (var) of fn type if !found { if v := call_expr.scope.find_var(fn_name) { @@ -2493,6 +2500,7 @@ pub fn (mut c Checker) fn_call(mut call_expr ast.CallExpr) ast.Type { c.error('unknown function: $fn_name', call_expr.pos) return ast.void_type } + call_expr.is_noreturn = func.is_noreturn if !found_in_args { if _ := call_expr.scope.find_var(fn_name) { c.error('ambiguous call to: `$fn_name`, may refer to fn `$fn_name` or variable `$fn_name`', @@ -2906,13 +2914,13 @@ pub fn (mut c Checker) check_or_expr(or_expr ast.OrExpr, ret_type ast.Type, expr c.expected_or_type = ast.void_type type_fits := c.check_types(last_stmt_typ, ret_type) && last_stmt_typ.nr_muls() == ret_type.nr_muls() - is_panic_or_exit := is_expr_panic_or_exit(last_stmt.expr) - if type_fits || is_panic_or_exit { + is_noreturn := is_noreturn_callexpr(last_stmt.expr) + if type_fits || is_noreturn { return } expected_type_name := c.table.type_to_str(ret_type.clear_flag(.optional)) if last_stmt.typ == ast.void_type { - c.error('`or` block must provide a default value of type `$expected_type_name`, or return/exit/continue/break/panic', + c.error('`or` block must provide a default value of type `$expected_type_name`, or return/continue/break or call a [noreturn] function like panic(err) or exit(1)', last_stmt.pos) } else { type_name := c.table.type_to_str(last_stmt_typ) @@ -2942,7 +2950,7 @@ pub fn (mut c Checker) check_or_expr(or_expr ast.OrExpr, ret_type ast.Type, expr if last_stmt.typ == ast.void_type { return } - if is_expr_panic_or_exit(last_stmt.expr) { + if is_noreturn_callexpr(last_stmt.expr) { return } if c.check_types(last_stmt.typ, expr_return_type) { @@ -2959,11 +2967,11 @@ pub fn (mut c Checker) check_or_expr(or_expr ast.OrExpr, ret_type ast.Type, expr } } -fn is_expr_panic_or_exit(expr ast.Expr) bool { - match expr { - ast.CallExpr { return !expr.is_method && expr.name in ['panic', 'exit'] } - else { return false } +fn is_noreturn_callexpr(expr ast.Expr) bool { + if expr is ast.CallExpr { + return expr.is_noreturn } + return false } pub fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type { @@ -7572,9 +7580,11 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) { } c.fn_scope = node.scope c.stmts(node.stmts) - node.has_return = c.returns || has_top_return(node.stmts) + node_has_top_return := has_top_return(node.stmts) + node.has_return = c.returns || node_has_top_return + c.check_noreturn_fn_decl(mut node) if node.language == .v && !node.no_body && node.return_type != ast.void_type && !node.has_return - && (node.is_method || node.name !in ['panic', 'exit']) { + && !node.is_noreturn { if c.inside_anon_fn { c.error('missing return at the end of an anonymous function', node.pos) } else if !node.attrs.contains('_naked') { @@ -7594,20 +7604,27 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) { node.source_file = c.file } +// NB: has_top_return/1 should be called on *already checked* stmts, +// which do have their stmt.expr.is_noreturn set properly: fn has_top_return(stmts []ast.Stmt) bool { for stmt in stmts { - if stmt is ast.Return { - return true - } else if stmt is ast.Block { - if has_top_return(stmt.stmts) { + match stmt { + ast.Return { return true } - } else if stmt is ast.ExprStmt { - if stmt.expr is ast.CallExpr { - if !stmt.expr.is_method && stmt.expr.name in ['panic', 'exit'] { + ast.Block { + if has_top_return(stmt.stmts) { return true } } + ast.ExprStmt { + if stmt.expr is ast.CallExpr { + if stmt.expr.is_noreturn { + return true + } + } + } + else {} } } return false diff --git a/vlib/v/checker/noreturn.v b/vlib/v/checker/noreturn.v new file mode 100644 index 0000000000..ca4a2883f0 --- /dev/null +++ b/vlib/v/checker/noreturn.v @@ -0,0 +1,112 @@ +module checker + +import v.ast + +fn (mut c Checker) check_noreturn_fn_decl(mut node ast.FnDecl) { + if !node.is_noreturn { + return + } + if node.no_body { + return + } + if uses_return_stmt(node.stmts) { + c.error('[noreturn] functions cannot use return statements', node.pos) + } + if node.return_type != ast.void_type { + c.error('[noreturn] functions cannot have return types', node.pos) + } else { + if node.stmts.len != 0 { + mut is_valid_end_of_noreturn_fn := false + last_stmt := node.stmts.last() + match last_stmt { + ast.ExprStmt { + if last_stmt.expr is ast.CallExpr { + if last_stmt.expr.should_be_skipped { + c.error('[noreturn] functions cannot end with a skippable `[if ..]` call', + last_stmt.pos) + } + if last_stmt.expr.is_noreturn { + is_valid_end_of_noreturn_fn = true + } + } + } + ast.ForStmt { + if last_stmt.is_inf && last_stmt.stmts.len == 0 { + is_valid_end_of_noreturn_fn = true + } + } + else {} + } + if !is_valid_end_of_noreturn_fn { + c.error('[noreturn] functions should end with a call to another [noreturn] function, or with an infinite `for {}` loop', + last_stmt.pos) + } + } + } +} + +fn uses_return_stmt(stmts []ast.Stmt) bool { + if stmts.len == 0 { + return false + } + for stmt in stmts { + match stmt { + ast.Return { + return true + } + ast.Block { + if uses_return_stmt(stmt.stmts) { + return true + } + } + ast.ExprStmt { + match stmt.expr { + ast.CallExpr { + if uses_return_stmt(stmt.expr.or_block.stmts) { + return true + } + } + ast.MatchExpr { + for b in stmt.expr.branches { + if uses_return_stmt(b.stmts) { + return true + } + } + } + ast.SelectExpr { + for b in stmt.expr.branches { + if uses_return_stmt(b.stmts) { + return true + } + } + } + ast.IfExpr { + for b in stmt.expr.branches { + if uses_return_stmt(b.stmts) { + return true + } + } + } + else {} + } + } + ast.ForStmt { + if uses_return_stmt(stmt.stmts) { + return true + } + } + ast.ForCStmt { + if uses_return_stmt(stmt.stmts) { + return true + } + } + ast.ForInStmt { + if uses_return_stmt(stmt.stmts) { + return true + } + } + else {} + } + } + return false +} diff --git a/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.out b/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.out new file mode 100644 index 0000000000..6e7ec8df98 --- /dev/null +++ b/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.vv:4:6: error: [noreturn] functions should end with a call to another [noreturn] function, or with an infinite `for {}` loop + 2 | fn another() { + 3 | eprintln(@FN) + 4 | for { + | ^ + 5 | break + 6 | } diff --git a/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.vv b/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.vv new file mode 100644 index 0000000000..84f413cf8c --- /dev/null +++ b/vlib/v/checker/tests/noreturn_with_non_empty_loop_at_end.vv @@ -0,0 +1,19 @@ +[noreturn] +fn another() { + eprintln(@FN) + for { + break + } +} + +[noreturn] +fn abc() { + eprintln(@FN) + another() +} + +fn main() { + eprintln('start') + abc() + eprintln('done') +} diff --git a/vlib/v/checker/tests/noreturn_with_return.out b/vlib/v/checker/tests/noreturn_with_return.out new file mode 100644 index 0000000000..640c7555c9 --- /dev/null +++ b/vlib/v/checker/tests/noreturn_with_return.out @@ -0,0 +1,13 @@ +vlib/v/checker/tests/noreturn_with_return.vv:2:1: error: [noreturn] functions cannot use return statements + 1 | [noreturn] + 2 | fn another() { + | ~~~~~~~~~~~~ + 3 | eprintln(@FN) + 4 | // for{} +vlib/v/checker/tests/noreturn_with_return.vv:6:2: error: [noreturn] functions should end with a call to another [noreturn] function, or with an infinite `for {}` loop + 4 | // for{} + 5 | // exit(0) + 6 | return + | ~~~~~~ + 7 | } + 8 | diff --git a/vlib/v/checker/tests/noreturn_with_return.vv b/vlib/v/checker/tests/noreturn_with_return.vv new file mode 100644 index 0000000000..c5fdaa74ea --- /dev/null +++ b/vlib/v/checker/tests/noreturn_with_return.vv @@ -0,0 +1,19 @@ +[noreturn] +fn another() { + eprintln(@FN) + // for{} + // exit(0) + return +} + +[noreturn] +fn abc() { + eprintln(@FN) + another() +} + +fn main() { + eprintln('start') + abc() + eprintln('done') +} diff --git a/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.out b/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.out new file mode 100644 index 0000000000..afabf5f331 --- /dev/null +++ b/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.vv:3:2: error: [noreturn] functions should end with a call to another [noreturn] function, or with an infinite `for {}` loop + 1 | [noreturn] + 2 | fn another() { + 3 | eprintln(@FN) + | ~~~~~~~~~~~~~ + 4 | } + 5 | diff --git a/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.vv b/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.vv new file mode 100644 index 0000000000..d764d7df5b --- /dev/null +++ b/vlib/v/checker/tests/noreturn_without_loop_or_another_noreturn_at_end.vv @@ -0,0 +1,16 @@ +[noreturn] +fn another() { + eprintln(@FN) +} + +[noreturn] +fn abc() { + eprintln(@FN) + another() +} + +fn main() { + eprintln('start') + abc() + eprintln('done') +} diff --git a/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.run.out b/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.run.out new file mode 100644 index 0000000000..f114c0792b --- /dev/null +++ b/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.run.out @@ -0,0 +1 @@ +log_and_die: error: oh no diff --git a/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.vv b/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.vv new file mode 100644 index 0000000000..10cf22c37d --- /dev/null +++ b/vlib/v/checker/tests/run/noreturn_fn_can_be_used_instead_of_panic.vv @@ -0,0 +1,14 @@ +fn abc() ?int { + return error('oh no') +} + +[noreturn] +fn log_and_die(e IError) { + eprintln('${@FN}: error: $e') + exit(77) +} + +fn main() { + x := abc() or { log_and_die(err) } + println(x) +} diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index bfa539e6b4..5c16b8e546 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -5164,10 +5164,12 @@ fn (mut g Gen) assoc(node ast.Assoc) { } } +[noreturn] fn verror(s string) { util.verror('cgen error', s) } +[noreturn] fn (g &Gen) error(s string, pos token.Position) { ferror := util.formatted_error('cgen error:', s, g.file.path, pos) eprintln(ferror) diff --git a/vlib/v/gen/c/cheaders.v b/vlib/v/gen/c/cheaders.v index 1a6bd04944..7e71687113 100644 --- a/vlib/v/gen/c/cheaders.v +++ b/vlib/v/gen/c/cheaders.v @@ -165,11 +165,26 @@ const c_common_macros = ' #define _MOV #endif -#if defined(__TINYC__) && defined(__has_include) // tcc does not support has_include properly yet, turn it off completely +#if defined(__TINYC__) && defined(__has_include) #undef __has_include #endif +#if !defined(VNORETURN) + #if defined(__TINYC__) + #include + #define VNORETURN noreturn + #endif + # if !defined(__TINYC__) && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + # define VNORETURN _Noreturn + # elif defined(__GNUC__) && __GNUC__ >= 2 + # define VNORETURN __attribute__((noreturn)) + # endif + #ifndef VNORETURN + #define VNORETURN + #endif +#endif + //likely and unlikely macros #if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__) #define _likely_(x) __builtin_expect(x,1) diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index cf722d21c9..8ca8a3d1ee 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -333,6 +333,9 @@ fn (mut g Gen) gen_fn_decl(node &ast.FnDecl, skip bool) { prev_defer_stmts := g.defer_stmts g.defer_stmts = [] g.stmts(node.stmts) + if node.is_noreturn { + g.writeln('\twhile(1);') + } // clear g.fn_mut_arg_names if !node.has_return { @@ -1315,10 +1318,23 @@ fn (mut g Gen) write_fn_attrs(attrs []ast.Attr) string { 'inline' { g.write('inline ') } - 'no_inline' { + 'noinline' { // since these are supported by GCC, clang and MSVC, we can consider them officially supported. g.write('__NOINLINE ') } + 'noreturn' { + // a `[noreturn]` tag tells the compiler, that a function + // *DOES NOT RETURN* to its callsites. + // See: https://en.cppreference.com/w/c/language/_Noreturn + // Such functions should have no return type. They can be used + // in places where `panic(err)` or `exit(0)` can be used. + // panic/1 and exit/0 themselves will also be marked as + // `[noreturn]` soon. + // These functions should have busy `for{}` loops injected + // at their end, when they do not end by calling other fns + // marked by `[noreturn]`. + g.write('VNORETURN ') + } 'irq_handler' { g.write('__IRQHANDLER ') } diff --git a/vlib/v/gen/native/gen.v b/vlib/v/gen/native/gen.v index d8e75f863e..5817c199d0 100644 --- a/vlib/v/gen/native/gen.v +++ b/vlib/v/gen/native/gen.v @@ -428,7 +428,6 @@ fn (mut g Gen) postfix_expr(node ast.PostfixExpr) { } } -// not yet supported [noreturn] fn verror(s string) { util.verror('native gen error', s) diff --git a/vlib/v/parser/fn.v b/vlib/v/parser/fn.v index 0299a0df30..8a8d289bd3 100644 --- a/vlib/v/parser/fn.v +++ b/vlib/v/parser/fn.v @@ -185,8 +185,10 @@ fn (mut p Parser) fn_decl() ast.FnDecl { mut is_exported := false mut is_unsafe := false mut is_trusted := false + mut is_noreturn := false for fna in p.attrs { match fna.name { + 'noreturn' { is_noreturn = true } 'manualfree' { is_manualfree = true } 'deprecated' { is_deprecated = true } 'direct_array_access' { is_direct_arr = true } @@ -408,6 +410,7 @@ fn (mut p Parser) fn_decl() ast.FnDecl { generic_names: generic_names is_pub: is_pub is_deprecated: is_deprecated + is_noreturn: is_noreturn is_unsafe: is_unsafe is_main: is_main is_test: is_test @@ -449,6 +452,7 @@ fn (mut p Parser) fn_decl() ast.FnDecl { return_type: return_type return_type_pos: return_type_pos params: params + is_noreturn: is_noreturn is_manualfree: is_manualfree is_deprecated: is_deprecated is_exported: is_exported diff --git a/vlib/v/parser/tests/or_default_missing.out b/vlib/v/parser/tests/or_default_missing.out index a74648b0ea..82cfa59535 100644 --- a/vlib/v/parser/tests/or_default_missing.out +++ b/vlib/v/parser/tests/or_default_missing.out @@ -1,4 +1,4 @@ -vlib/v/parser/tests/or_default_missing.vv:4:3: error: `or` block must provide a default value of type `int`, or return/exit/continue/break/panic +vlib/v/parser/tests/or_default_missing.vv:4:3: error: `or` block must provide a default value of type `int`, or return/continue/break or call a [noreturn] function like panic(err) or exit(1) 2 | m := [3, 4, 5] 3 | el := m[4] or { 4 | println('error') diff --git a/vlib/v/util/errors.v b/vlib/v/util/errors.v index bb560ebc84..ba3b0da33d 100644 --- a/vlib/v/util/errors.v +++ b/vlib/v/util/errors.v @@ -141,6 +141,7 @@ pub fn source_context(kind string, source string, pos token.Position) []string { return clines } +[noreturn] pub fn verror(kind string, s string) { final_kind := bold(color(kind, kind)) eprintln('$final_kind: $s')