From ec451e67020f5bb2732fb69cd674827b95a934cd Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 24 Apr 2026 01:50:38 +0300 Subject: [PATCH] all: fix Ability to export a global symbol as a true (C-level) constant (fixes #20831) --- doc/docs.md | 10 +- vlib/v/ast/ast.v | 4 +- vlib/v/checker/checker.v | 7 ++ .../tests/global_const_call_init_err.out | 7 ++ .../tests/global_const_call_init_err.vv | 12 ++ vlib/v/fmt/fmt.v | 3 + vlib/v/gen/c/consts_and_globals.v | 115 ++++++++++++++++-- .../global_const_export_nix.c.must_have | 3 + .../gen/c/testdata/global_const_export_nix.vv | 23 ++++ vlib/v/parser/parser.v | 16 ++- 10 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 vlib/v/checker/tests/global_const_call_init_err.out create mode 100644 vlib/v/checker/tests/global_const_call_init_err.vv create mode 100644 vlib/v/gen/c/testdata/global_const_export_nix.c.must_have create mode 100644 vlib/v/gen/c/testdata/global_const_export_nix.vv diff --git a/doc/docs.md b/doc/docs.md index 7b3551a04..ee22153f0 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -7942,6 +7942,9 @@ specification – as in the example [above](#atomics). An initializer for global variables must be explicitly converted to the desired target type. If no initializer is given a default initialization is done. +Use `const` after `__global` (or inside the `__global ( ... )` block) when the symbol +must stay a true C-level constant. Non-extern const globals currently require an explicit +initializer that can be emitted directly in C global scope. Some objects like semaphores and mutexes require an explicit initialization *in place*, i.e. not with a value returned from a function call but with a method call by reference. A separate `init()` function can be used for this purpose – it will be called before `main()`: @@ -8204,9 +8207,10 @@ dump(f) ``` C globals can be exposed on the V side too. Use `@[c_extern] __global name C.Type` -when you want to redeclare an external symbol explicitly. When the type already comes -from the V context, direct references like `buffer = C.buffer` and `[4]u8(C.buffer)` -can be used as well. +when you want to redeclare an external symbol explicitly, or +`@[c_extern] __global const name C.Type` for an external `extern const` symbol. +When the type already comes from the V context, direct references like +`buffer = C.buffer` and `[4]u8(C.buffer)` can be used as well. **Example of using a C function from stdio, by redeclaring it on the V side** ```v diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 9d4259bb0..91ee6da3f 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1057,6 +1057,7 @@ pub: typ_pos token.Pos is_markused bool // an explicit `@[markused]` tag; the global will NOT be removed by `-skip-unused` is_volatile bool + is_const bool is_exported bool // an explicit `@[export]` tag; the global will NOT be removed by `-skip-unused` is_weak bool is_hidden bool @@ -1251,7 +1252,8 @@ pub fn (i &Ident) is_mut() bool { match i.obj { Var { return i.obj.is_mut } ConstField, EmptyScopeObject { return false } - AsmRegister, GlobalField { return true } + AsmRegister { return true } + GlobalField { return !i.obj.is_const } } } diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 2b1a35a73..97f1ed3f1 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1782,6 +1782,10 @@ fn (mut c Checker) fail_if_immutable(mut expr ast.Expr) (string, token.Pos) { // in translated code c.error('cannot modify constant `${expr.name}`', expr.pos) } + } else if expr.obj is ast.GlobalField && expr.obj.is_const { + if !c.pref.translated && c.mod != 'veb' { + c.error('cannot modify constant `${expr.name}`', expr.pos) + } } } ast.IndexExpr { @@ -4108,6 +4112,9 @@ fn (mut c Checker) global_decl(mut node ast.GlobalDecl) { c.table.export_names[field.name] = check_name } c.ensure_type_exists(field.typ, field.typ_pos) + if field.is_const && !field.is_extern && !field.has_expr { + c.error('const globals must have an explicit initializer', field.pos) + } if field.has_expr { if field.expr is ast.AnonFn && field.name == 'main' { c.error('the `main` function is the program entry point, cannot redefine it', diff --git a/vlib/v/checker/tests/global_const_call_init_err.out b/vlib/v/checker/tests/global_const_call_init_err.out new file mode 100644 index 000000000..09f87fc16 --- /dev/null +++ b/vlib/v/checker/tests/global_const_call_init_err.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/global_const_call_init_err.vv:8:16: cgen error: const global `bad` must be initialized with a C constant expression + 6 | } + 7 | + 8 | __global const bad = make_value() + | ~~~ + 9 | + 10 | fn main() { diff --git a/vlib/v/checker/tests/global_const_call_init_err.vv b/vlib/v/checker/tests/global_const_call_init_err.vv new file mode 100644 index 000000000..43caa4570 --- /dev/null +++ b/vlib/v/checker/tests/global_const_call_init_err.vv @@ -0,0 +1,12 @@ +@[has_globals] +module main + +fn make_value() int { + return 123 +} + +__global const bad = make_value() + +fn main() { + assert bad == 123 +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 52c3ab9f0..f8d7b03b4 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -1364,6 +1364,9 @@ pub fn (mut f Fmt) global_decl(node ast.GlobalDecl) { } for field in node.fields { f.comments(field.comments, same_line: true) + if field.is_const { + f.write('const ') + } if field.is_volatile { f.write('volatile ') } diff --git a/vlib/v/gen/c/consts_and_globals.v b/vlib/v/gen/c/consts_and_globals.v index eb27ed03e..b9b0c09de 100644 --- a/vlib/v/gen/c/consts_and_globals.v +++ b/vlib/v/gen/c/consts_and_globals.v @@ -426,6 +426,70 @@ fn (mut g Gen) const_decl_init_later_msvc_string_fixed_array(mod string, name st } } +fn (mut g Gen) global_const_expr_is_c_const(expr ast.Expr) bool { + match expr { + ast.BoolLiteral, ast.CharLiteral, ast.EnumVal, ast.FloatLiteral, ast.IntegerLiteral, + ast.Nil, ast.SizeOf, ast.StringLiteral { + return true + } + ast.ArrayInit { + if !expr.is_fixed || expr.has_len || expr.has_cap || expr.has_init || expr.has_index { + return false + } + for item in expr.exprs { + if !g.global_const_expr_is_c_const(item) { + return false + } + } + return true + } + ast.CastExpr { + return !expr.has_arg && g.global_const_expr_is_c_const(expr.expr) + } + ast.Ident { + return expr.kind == .function + || (expr.obj is ast.ConstField && expr.obj.is_simple_define_const()) + } + ast.InfixExpr { + if expr.left_type == ast.string_type || expr.right_type == ast.string_type + || expr.promoted_type == ast.string_type { + return false + } + return g.global_const_expr_is_c_const(expr.left) + && g.global_const_expr_is_c_const(expr.right) + } + ast.ParExpr { + return g.global_const_expr_is_c_const(expr.expr) + } + ast.PrefixExpr { + if expr.op == .amp { + return expr.right is ast.Ident || expr.right is ast.SelectorExpr + } + return g.global_const_expr_is_c_const(expr.right) + } + ast.SelectorExpr { + return expr.expr is ast.Ident && expr.expr.name == 'C' + } + ast.StructInit { + if expr.has_update_expr { + return false + } + for field in expr.init_fields { + if !g.global_const_expr_is_c_const(field.expr) { + return false + } + } + return true + } + ast.UnsafeExpr { + return g.global_const_expr_is_c_const(expr.expr) + } + else { + return false + } + } +} + fn (mut g Gen) global_decl(node ast.GlobalDecl) { // was static used here to to make code optimizable? it was removed when // 'extern' was used to fix the duplicate symbols with usecache && clang @@ -475,6 +539,47 @@ fn (mut g Gen) global_decl(node ast.GlobalDecl) { } } styp := g.styp(field.typ) + mut def_builder := strings.new_builder(100) + mut init := '' + extern := if field.is_extern { 'extern ' } else { '' } + field_visibility_kw := if field.is_extern { '' } else { visibility_kw } + mut qualifiers := '' + if field.is_const { + qualifiers += 'const ' + } + if field.is_volatile { + qualifiers += 'volatile ' + } + final_c_name := field.name.all_after('C.') + if field.is_const { + if field.is_extern || field_visibility_kw == 'extern ' { + def_builder.writeln('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}; // global 2') + g.global_const_defs[name] = GlobalConstDef{ + mod: node.mod + def: def_builder.str() + order: -1 + } + continue + } + if !field.has_expr { + g.error('const globals must have an explicit initializer', field.pos) + continue + } + if !g.global_const_expr_is_c_const(field.expr) { + g.error('const global `${field.name}` must be initialized with a C constant expression', + field.pos) + continue + } + def_builder.write_string('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}') + def_builder.write_string(' = ${g.expr_string(field.expr)}') + def_builder.writeln('; // global 2') + g.global_const_defs[name] = GlobalConstDef{ + mod: node.mod + def: def_builder.str() + order: -1 + } + continue + } mut anon_fn_expr := unsafe { field.expr } if field.has_expr && mut anon_fn_expr is ast.AnonFn { g.gen_anon_fn_decl(mut anon_fn_expr) @@ -486,14 +591,8 @@ fn (mut g Gen) global_decl(node ast.GlobalDecl) { } continue } - mut def_builder := strings.new_builder(100) - mut init := '' - extern := if field.is_extern { 'extern ' } else { '' } - field_visibility_kw := if field.is_extern { '' } else { visibility_kw } - modifier := if field.is_volatile { ' volatile ' } else { '' } - final_c_name := field.name.all_after('C.') if field.is_extern { - def_builder.writeln('${extern}${field_visibility_kw}${modifier}${styp} ${attributes}${final_c_name}; // global 2') + def_builder.writeln('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}; // global 2') g.global_const_defs[name] = GlobalConstDef{ mod: node.mod def: def_builder.str() @@ -503,7 +602,7 @@ fn (mut g Gen) global_decl(node ast.GlobalDecl) { } mut needs_ending_semicolon := false if field.language != .c || field.has_expr { - def_builder.write_string('${extern}${field_visibility_kw}${modifier}${styp} ${attributes}${final_c_name}') + def_builder.write_string('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}') needs_ending_semicolon = true } if field.has_expr || cinit { diff --git a/vlib/v/gen/c/testdata/global_const_export_nix.c.must_have b/vlib/v/gen/c/testdata/global_const_export_nix.c.must_have new file mode 100644 index 000000000..538f5c5f8 --- /dev/null +++ b/vlib/v/gen/c/testdata/global_const_export_nix.c.must_have @@ -0,0 +1,3 @@ +const int VV_EXP exported_const = ((int)(123)); +const main__Entry entry = ((main__Entry){.value = 123,}); +extern const int external_symbol; diff --git a/vlib/v/gen/c/testdata/global_const_export_nix.vv b/vlib/v/gen/c/testdata/global_const_export_nix.vv new file mode 100644 index 000000000..2bfafebd6 --- /dev/null +++ b/vlib/v/gen/c/testdata/global_const_export_nix.vv @@ -0,0 +1,23 @@ +// vtest vflags: -enable-globals +@[has_globals] +module main + +struct Entry { + value int +} + +@[export] +__global const exported_const = int(123) + +__global const entry = Entry{ + value: 123 +} + +@[c_extern] +__global const C.external_symbol int + +fn main() { + println(C.external_symbol) + println(exported_const) + println(entry.value) +} diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index e2fbf2ddd..4c561909e 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -2863,8 +2863,19 @@ fn (mut p Parser) global_decl() ast.GlobalDecl { mut comments := []ast.Comment{} for { comments = p.eat_comments() - is_volatile := p.tok.kind == .key_volatile - if is_volatile { + mut is_volatile := false + mut is_const := false + for p.tok.kind in [.key_const, .key_volatile] { + match p.tok.kind { + .key_const { + is_const = true + } + .key_volatile { + is_volatile = true + } + else {} + } + p.next() } if is_block && p.tok.kind == .eof { @@ -2928,6 +2939,7 @@ fn (mut p Parser) global_decl() ast.GlobalDecl { comments: comments is_markused: is_markused is_volatile: is_volatile + is_const: is_const is_exported: is_exported is_weak: is_weak is_hidden: is_hidden -- 2.39.5