From ed1549f6808d68defe18fc93b8940ef1e8aaf004 Mon Sep 17 00:00:00 2001 From: GGRei Date: Tue, 26 May 2026 00:18:54 +0200 Subject: [PATCH] v2: add module-scoped global variables (#27266) --- vlib/v2/ast/ast.v | 3 + vlib/v2/ast_dump/ast_dump.v | 10 + vlib/v2/builder/cache_headers.v | 11 +- vlib/v2/builder/module_storage_cache_test.v | 167 +++++++++ vlib/v2/builder/util_test.v | 71 ++++ vlib/v2/gen/cleanc/cleanc.v | 30 +- vlib/v2/gen/cleanc/cleanc_test.v | 26 +- vlib/v2/gen/cleanc/consts_and_globals.v | 41 ++- vlib/v2/gen/cleanc/expr.v | 72 ++-- vlib/v2/gen/cleanc/fn.v | 13 +- .../gen/cleanc/module_storage_codegen_test.v | 275 +++++++++++++++ vlib/v2/gen/cleanc/types.v | 17 +- vlib/v2/gen/v/gen.v | 13 + vlib/v2/parser/parser.v | 32 +- vlib/v2/ssa/builder.v | 164 +++++---- vlib/v2/ssa/module_storage_test.v | 198 +++++++++++ .../v2/tests/module_storage_example/README.md | 13 + vlib/v2/tests/module_storage_example/main.vv2 | 12 + .../module_storage_example/state/state.vv2 | 18 + vlib/v2/transformer/live.v | 5 +- vlib/v2/transformer/transformer.v | 3 + vlib/v2/transformer/transformer_test.v | 4 +- vlib/v2/types/checker.v | 165 ++++++++- vlib/v2/types/module_storage_test.v | 326 ++++++++++++++++++ vlib/v2/types/object.v | 5 +- 25 files changed, 1517 insertions(+), 177 deletions(-) create mode 100644 vlib/v2/builder/module_storage_cache_test.v create mode 100644 vlib/v2/gen/cleanc/module_storage_codegen_test.v create mode 100644 vlib/v2/ssa/module_storage_test.v create mode 100644 vlib/v2/tests/module_storage_example/README.md create mode 100644 vlib/v2/tests/module_storage_example/main.vv2 create mode 100644 vlib/v2/tests/module_storage_example/state/state.vv2 create mode 100644 vlib/v2/types/module_storage_test.v diff --git a/vlib/v2/ast/ast.v b/vlib/v2/ast/ast.v index e72e110fa..f3359190e 100644 --- a/vlib/v2/ast/ast.v +++ b/vlib/v2/ast/ast.v @@ -405,6 +405,8 @@ pub: typ Expr = empty_expr // can be empty as used for const (unless we use something else) value Expr = empty_expr attributes []Attribute + is_public bool + is_mut bool } pub struct FieldInit { @@ -908,6 +910,7 @@ pub struct GlobalDecl { pub: attributes []Attribute fields []FieldDecl + is_public bool } pub struct ImportStmt { diff --git a/vlib/v2/ast_dump/ast_dump.v b/vlib/v2/ast_dump/ast_dump.v index ca7585b32..624f99951 100644 --- a/vlib/v2/ast_dump/ast_dump.v +++ b/vlib/v2/ast_dump/ast_dump.v @@ -620,6 +620,9 @@ fn (mut jb JsonBuilder) write_global_decl(stmt ast.GlobalDecl) { jb.write_attributes(stmt.attributes) jb.sb.write_string(',\n') + jb.write_indent() + jb.sb.write_string('"is_public": ${stmt.is_public},\n') + jb.write_indent() jb.sb.write_string('"fields": ') jb.write_field_decls(stmt.fields) @@ -1279,6 +1282,13 @@ fn (mut jb JsonBuilder) write_field_decl(field ast.FieldDecl) { jb.write_indent() jb.sb.write_string('"attributes": ') jb.write_attributes(field.attributes) + jb.sb.write_string(',\n') + + jb.write_indent() + jb.sb.write_string('"is_public": ${field.is_public},\n') + + jb.write_indent() + jb.sb.write_string('"is_mut": ${field.is_mut}') jb.sb.write_string('\n') jb.indent-- diff --git a/vlib/v2/builder/cache_headers.v b/vlib/v2/builder/cache_headers.v index 457f97a66..863589e40 100644 --- a/vlib/v2/builder/cache_headers.v +++ b/vlib/v2/builder/cache_headers.v @@ -1540,12 +1540,15 @@ fn (b &Builder) build_header_ast_for_files(source_files []ast.File, module_name name: field.name typ: global_typ attributes: field.attributes + is_public: field.is_public + is_mut: field.is_mut } } if gfields.len > 0 { decl_stmts << ast.Stmt(ast.GlobalDecl{ attributes: stmt.attributes fields: gfields + is_public: stmt.is_public }) } } @@ -2505,7 +2508,7 @@ fn sanitize_header_source(source string, source_fn_returns map[string]string) st } } } - if trimmed == '__global (' { + if header_starts_global_block(trimmed) { in_global_block = true global_start_line = line global_body_lines = []string{} @@ -2550,6 +2553,10 @@ fn sanitize_header_source(source string, source_fn_returns map[string]string) st return out.join('\n') } +fn header_starts_global_block(trimmed string) bool { + return trimmed == '__global (' || trimmed == 'pub __global (' +} + fn repair_interface_method_fn_syntax(source string) string { lines := source.split_into_lines() mut out := []string{cap: lines.len} @@ -2597,7 +2604,7 @@ fn header_is_module_level_line(trimmed string) bool { || trimmed.starts_with('const ') || trimmed.starts_with('pub const ') || trimmed.starts_with('interface ') || trimmed.starts_with('pub interface ') || trimmed.starts_with('union ') || trimmed.starts_with('pub union ') - || trimmed.starts_with('__global') + || trimmed.starts_with('__global') || trimmed.starts_with('pub __global') } fn header_starts_type_block(trimmed string) bool { diff --git a/vlib/v2/builder/module_storage_cache_test.v b/vlib/v2/builder/module_storage_cache_test.v new file mode 100644 index 000000000..4374776bd --- /dev/null +++ b/vlib/v2/builder/module_storage_cache_test.v @@ -0,0 +1,167 @@ +module builder + +import os +import v2.ast +import v2.gen.v as gen_v +import v2.parser +import v2.pref +import v2.token + +fn module_storage_cache_tmp_dir(label string) string { + return os.join_path(os.temp_dir(), 'v2_module_storage_cache_${label}_${os.getpid()}') +} + +fn module_storage_cached_header_source_for_test() string { + tmp_dir := module_storage_cache_tmp_dir('source') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source_file := os.join_path(tmp_dir, 'state', 'state.v') + os.write_file(source_file, 'module state + +pub __global mut total = 0 +__global mut hidden = 0 +') or { + panic(err) + } + mut prefs := pref.new_preferences() + mut b := new_builder(&prefs) + source_files := b.parse_source_files_for_headers([source_file]) + header_ast := b.build_module_header_ast(source_files, 'state') or { + panic('missing state header') + } + mut gen := gen_v.new_gen(b.pref) + gen.gen(header_ast) + return sanitize_header_source(gen.output_string(), map[string]string{}) +} + +fn module_storage_cached_header_global(header_source string, name string) ast.FieldDecl { + tmp_dir := module_storage_cache_tmp_dir('parse') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + header_path := os.join_path(tmp_dir, 'state.vh') + os.write_file(header_path, header_source) or { panic(err) } + mut prefs := pref.new_preferences() + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(&prefs) + files := par.parse_files([header_path], mut file_set) + for stmt in files[0].stmts { + if stmt is ast.GlobalDecl { + for field in stmt.fields { + if field.name == name { + return field + } + } + } + } + panic('missing cached header field ${name}') +} + +fn module_storage_run_cached_header_check(label string, header_source string, main_source string) (int, string) { + tmp_dir := module_storage_cache_tmp_dir(label) + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(os.join_path(tmp_dir, 'state')) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + os.write_file(os.join_path(tmp_dir, 'state', 'state.v'), header_source) or { panic(err) } + main_path := os.join_path(tmp_dir, 'main.v') + os.write_file(main_path, main_source) or { panic(err) } + out_path := os.join_path(tmp_dir, 'out.txt') + cmd := '${os.quoted_path(@VEXE)} -v2 -backend v -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1' + res := os.execute(cmd) + return res.exit_code, res.output +} + +fn test_module_storage_cached_header_preserves_flags() { + header_source := module_storage_cached_header_source_for_test() + assert header_source.contains('pub __global (') + assert header_source.contains('mut total int') + assert header_source.contains('__global (') + assert header_source.contains('mut hidden int') + public_field := module_storage_cached_header_global(header_source, 'total') + assert public_field.is_public + assert public_field.is_mut + private_field := module_storage_cached_header_global(header_source, 'hidden') + assert !private_field.is_public + assert private_field.is_mut +} + +fn test_module_storage_cached_header_visibility_matches_source_module() { + header_source := module_storage_cached_header_source_for_test() + public_code, public_output := module_storage_run_cached_header_check('public', header_source, 'module main + +import state + +fn main() { + state.total += 1 +} +') + assert public_code == 0, public_output + + private_code, private_output := module_storage_run_cached_header_check('private', + header_source, 'module main + +import state + +fn main() { + state.hidden += 1 +} +') + assert private_code != 0, 'cached private module storage should fail' + assert private_output.contains('module global `state.hidden` is private'), private_output +} + +fn test_module_storage_c_extern_import_cache_uses_raw_symbol() { + tmp_dir := module_storage_cache_tmp_dir('c_extern_import') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(os.join_path(tmp_dir, 'ext')) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + mut prefs := pref.new_preferences() + mut cache_builder := new_builder(&prefs) + cache_dir := cache_builder.core_cache_dir() + os.rmdir_all(cache_dir) or {} + c_path := os.join_path(tmp_dir, 'raw_status.c') + os.write_file(c_path, 'int raw_status = 7;\n') or { panic(err) } + os.write_file(os.join_path(tmp_dir, 'ext', 'ext.v'), 'module ext + +@[c_extern] +pub __global raw_status int + +pub fn read_status() int { + return raw_status +} +') or { + panic(err) + } + main_path := os.join_path(tmp_dir, 'main.v') + os.write_file(main_path, 'module main + +import ext + +#flag ${c_path} + +fn main() { + _ = ext.raw_status + _ = ext.read_status() +} +') or { + panic(err) + } + out_path := os.join_path(tmp_dir, 'out') + cmd := '${os.quoted_path(@VEXE)} -v -v2 -keepc -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1' + res := os.execute(cmd) + assert res.exit_code == 0, res.output + ext_header := os.read_file(cache_builder.core_header_path('ext')) or { panic(err) } + assert ext_header.contains('@[c_extern]'), ext_header + imports_c := os.read_file(os.join_path(cache_dir, 'imports.c')) or { panic(err) } + assert imports_c.contains('return raw_status;'), imports_c + assert !imports_c.contains('ext__raw_status'), imports_c +} diff --git a/vlib/v2/builder/util_test.v b/vlib/v2/builder/util_test.v index 52c190711..3e1e6047d 100644 --- a/vlib/v2/builder/util_test.v +++ b/vlib/v2/builder/util_test.v @@ -2,6 +2,7 @@ module builder import os +import v2.ast import v2.pref fn test_get_v_files_from_dir_uses_target_os() { @@ -106,6 +107,76 @@ fn test_split_compile_and_link_flags_moves_c_sources_to_link_step() { assert link_flags == 'thirdparty/sqlite/sqlite3.c -L/tmp/lib -lsqlite3 foo.o' } +fn test_sanitize_header_source_preserves_public_module_storage() { + source := 'module state + +pub __global ( + mut count int + pub mut shared int +) + +pub __global mut total int +' + sanitized := sanitize_header_source(source, map[string]string{}) + assert sanitized.contains('pub __global (') + assert sanitized.contains('mut count int') + assert sanitized.contains('pub mut shared int') + assert sanitized.contains('pub __global mut total int') +} + +fn test_build_module_header_ast_preserves_module_storage_flags() { + mut prefs := pref.new_preferences() + mut b := new_builder(&prefs) + source_file := ast.File{ + mod: 'state' + name: 'state.v' + stmts: [ + ast.Stmt(ast.ModuleStmt{ + name: 'state' + }), + ast.Stmt(ast.GlobalDecl{ + is_public: true + fields: [ + ast.FieldDecl{ + name: 'private_count' + typ: ast.Ident{ + name: 'int' + } + is_mut: true + is_public: false + }, + ast.FieldDecl{ + name: 'shared_count' + typ: ast.Ident{ + name: 'int' + } + is_mut: true + is_public: true + }, + ] + }), + ] + } + header := b.build_module_header_ast([source_file], 'state') or { + panic('missing module storage header') + } + mut found := false + for stmt in header.stmts { + if stmt is ast.GlobalDecl { + found = true + assert stmt.is_public + assert stmt.fields.len == 2 + assert stmt.fields[0].name == 'private_count' + assert stmt.fields[0].is_mut + assert !stmt.fields[0].is_public + assert stmt.fields[1].name == 'shared_count' + assert stmt.fields[1].is_mut + assert stmt.fields[1].is_public + } + } + assert found +} + fn test_cflags_need_objc_mode_only_for_objc_inputs() { assert cflags_need_objc_mode('-framework Cocoa') assert cflags_need_objc_mode('/tmp/clipboard_darwin.m -framework Foundation') diff --git a/vlib/v2/gen/cleanc/cleanc.v b/vlib/v2/gen/cleanc/cleanc.v index 5b24298b8..7defca16d 100644 --- a/vlib/v2/gen/cleanc/cleanc.v +++ b/vlib/v2/gen/cleanc/cleanc.v @@ -104,7 +104,8 @@ mut: needed_ierror_wrapper_bases map[string]bool tmp_counter int cur_fn_mut_params map[string]bool // names of mut params in current function - global_var_modules map[string]string // global var name → module name + module_storage_vars map[string]string // qualified storage C name -> module name + c_extern_module_storage map[string]string // qualified storage C name -> raw C extern name global_var_types map[string]string // global var name → C type string primitive_type_aliases map[string]bool // type names that are aliases for primitive types emit_modules map[string]bool // when set, emit consts/globals/fns only for these modules @@ -416,6 +417,8 @@ fn new_gen_with_env_and_pref_impl(env &types.Environment, p &pref.Preferences) & emitted_option_structs: map[string]bool{} embedded_field_owner: map[string]string{} fixed_array_ret_wrappers: map[string]string{} + module_storage_vars: map[string]string{} + c_extern_module_storage: map[string]string{} emit_modules: map[string]bool{} type_modules: map[string]bool{} exported_const_seen: map[string]bool{} @@ -850,7 +853,7 @@ pub fn (mut g Gen) gen_passes_1_to_4() { g.register_builder_methods() stage_start = g.mark_cgen_step(stats_enabled, stats_scope, mut stats_sw, stage_start, 'setup') - // Pre-collect all global variable names so they can be module-qualified + // Pre-collect module storage names by qualified C name. for file in g.files { g.set_file_module(file) for stmt in file.stmts { @@ -859,9 +862,16 @@ pub fn (mut g Gen) gen_passes_1_to_4() { } if stmt is ast.GlobalDecl { for field in stmt.fields { - if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' { - g.global_var_modules[field.name] = g.cur_module + if module_storage_field_is_c_extern(stmt, field) { + if !field.name.starts_with('C.') { + qualified_name := module_storage_c_name(g.cur_module, field.name) + g.c_extern_module_storage[qualified_name] = module_storage_field_c_name(g.cur_module, + stmt, field) + } + continue } + name := module_storage_c_name(g.cur_module, field.name) + g.module_storage_vars[name] = g.cur_module } } } @@ -1603,12 +1613,10 @@ pub fn (mut g Gen) gen_pass5_pre() []int { for stmt in file.stmts { if stmt is ast.GlobalDecl { for field in stmt.fields { - gname := if g.cur_module != '' && g.cur_module != 'main' - && g.cur_module != 'builtin' { - '${g.cur_module}__${field.name}' - } else { - field.name + if module_storage_field_is_c_extern(stmt, field) { + continue } + gname := module_storage_c_name(g.cur_module, field.name) if gname !in g.global_owner_file { g.global_owner_file[gname] = fi } @@ -2500,7 +2508,9 @@ pub fn (g &Gen) new_pass5_worker(file_indices []int, worker_id int) &Gen { collected_fixed_array_types: g.collected_fixed_array_types.clone() collected_map_types: g.collected_map_types.clone() c_file_fn_keys: g.c_file_fn_keys.clone() - global_var_modules: g.global_var_modules.clone() + module_storage_vars: g.module_storage_vars.clone() + c_extern_module_storage: g.c_extern_module_storage.clone() + global_var_types: g.global_var_types.clone() const_exprs: g.const_exprs.clone() const_types: g.const_types.clone() const_c_names: g.const_c_names.clone() diff --git a/vlib/v2/gen/cleanc/cleanc_test.v b/vlib/v2/gen/cleanc/cleanc_test.v index 21954c898..6367407ca 100644 --- a/vlib/v2/gen/cleanc/cleanc_test.v +++ b/vlib/v2/gen/cleanc/cleanc_test.v @@ -231,20 +231,24 @@ fn f(fields []StructField) { assert csrc.contains('map__set(&field_usages') } -fn test_imported_module_global_ident_uses_declaring_module_prefix() { +fn test_module_storage_selector_uses_declaring_module_prefix() { mut g := Gen.new([]) g.cur_module = 'checker' g.cur_import_modules['ast'] = 'ast' - g.global_var_modules['global_table'] = 'ast' - g.expr(ast.Expr(ast.Ident{ - name: 'global_table' + g.expr(ast.Expr(ast.SelectorExpr{ + lhs: ast.Expr(ast.Ident{ + name: 'ast' + }) + rhs: ast.Ident{ + name: 'global_table' + } })) assert g.sb.str() == 'ast__global_table' mut local_g := Gen.new([]) local_g.cur_module = 'checker' local_g.cur_import_modules['ast'] = 'ast' - local_g.global_var_modules['global_table'] = 'ast' + local_g.module_storage_vars['ast__global_table'] = 'ast' local_g.runtime_local_types['global_table'] = 'int' local_g.expr(ast.Expr(ast.Ident{ name: 'global_table' @@ -252,19 +256,23 @@ fn test_imported_module_global_ident_uses_declaring_module_prefix() { assert local_g.sb.str() == 'global_table' } -fn test_imported_module_global_ident_type_resolves_receiver_methods() { +fn test_module_storage_selector_type_resolves_receiver_methods() { mut g := Gen.new([]) g.cur_module = 'transformer' g.cur_import_modules['ast'] = 'ast' - g.global_var_modules['global_table'] = 'ast' g.global_var_types['ast__global_table'] = 'ast__Table*' g.fn_return_types['ast__Table__type_to_str'] = 'string' g.fn_param_is_ptr['ast__Table__type_to_str'] = [true, false] g.fn_param_types['ast__Table__type_to_str'] = ['ast__Table*', 'ast__Type'] g.runtime_local_types['typ'] = 'ast__Type' g.call_expr(ast.Expr(ast.SelectorExpr{ - lhs: ast.Expr(ast.Ident{ - name: 'global_table' + lhs: ast.Expr(ast.SelectorExpr{ + lhs: ast.Expr(ast.Ident{ + name: 'ast' + }) + rhs: ast.Ident{ + name: 'global_table' + } }) rhs: ast.Ident{ name: 'type_to_str' diff --git a/vlib/v2/gen/cleanc/consts_and_globals.v b/vlib/v2/gen/cleanc/consts_and_globals.v index 76e6835aa..bbeeef96f 100644 --- a/vlib/v2/gen/cleanc/consts_and_globals.v +++ b/vlib/v2/gen/cleanc/consts_and_globals.v @@ -694,18 +694,35 @@ fn (mut g Gen) gen_const_decl_extern(node ast.ConstDecl) { } } +fn module_storage_c_name(module_name string, name string) string { + if name == '' || name.starts_with('C.') { + return name + } + if module_name != '' && module_name != 'main' && module_name != 'builtin' { + return '${module_name}__${name}' + } + return name +} + +fn module_storage_field_is_c_extern(node ast.GlobalDecl, field ast.FieldDecl) bool { + return field.name.starts_with('C.') || node.attributes.has('c_extern') + || field.attributes.has('c_extern') +} + +fn module_storage_field_c_name(module_name string, node ast.GlobalDecl, field ast.FieldDecl) string { + if module_storage_field_is_c_extern(node, field) { + return if field.name.starts_with('C.') { field.name.all_after('C.') } else { field.name } + } + return module_storage_c_name(module_name, field.name) +} + fn (mut g Gen) gen_global_decl(node ast.GlobalDecl) { for field in node.fields { - raw_name := if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' { - '${g.cur_module}__${field.name}' - } else { - field.name - } - name := if raw_name.starts_with('C.') { raw_name.all_after('C.') } else { raw_name } // Skip C globals that are already provided by C headers or cheaders. - if field.name.starts_with('C.') { + if module_storage_field_is_c_extern(node, field) { continue } + name := module_storage_field_c_name(g.cur_module, node, field) key := 'global_${name}' if key in g.emitted_types { continue @@ -784,16 +801,12 @@ fn (mut g Gen) gen_global_decl(node ast.GlobalDecl) { fn (mut g Gen) gen_global_decl_extern(node ast.GlobalDecl) { for field in node.fields { - raw_name := if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' { - '${g.cur_module}__${field.name}' - } else { - field.name - } - name := if raw_name.starts_with('C.') { raw_name.all_after('C.') } else { raw_name } - // Skip C globals that are already provided by C headers or cheaders. + // C.foo selectors are provided by C headers/macros; raw @[c_extern] + // globals below need an extern declaration, but C.foo does not. if field.name.starts_with('C.') { continue } + name := module_storage_field_c_name(g.cur_module, node, field) key := 'extern_global_${name}' if key in g.emitted_types { continue diff --git a/vlib/v2/gen/cleanc/expr.v b/vlib/v2/gen/cleanc/expr.v index b3095dfc5..536f651bb 100644 --- a/vlib/v2/gen/cleanc/expr.v +++ b/vlib/v2/gen/cleanc/expr.v @@ -784,6 +784,18 @@ fn (mut g Gen) known_module_runtime_symbol(module_name string, symbol_name strin return none } +fn (mut g Gen) module_selector_storage_c_name(module_name string, symbol_name string) string { + c_name := if symbol_name.starts_with('${module_name}__') { + symbol_name + } else { + '${module_name}__${symbol_name}' + } + if raw_c_name := g.c_extern_module_storage[c_name] { + return raw_c_name + } + return c_name +} + // expr_to_int_str_with_env resolves fixed-array size expressions, handling // both literal numbers and named constants (looked up via type environment). fn (g &Gen) expr_to_int_str_with_env(e ast.Expr) string { @@ -2078,34 +2090,32 @@ fn (mut g Gen) expr(node ast.Expr) { if handled_ident { return } - // Check global_var_modules first - globals may appear as types.Type in scope - // instead of types.Global, so is_local_var check would incorrectly block them - if !is_local_var && !node.name.contains('__') && node.name in g.global_var_modules - && g.is_current_or_imported_module(g.global_var_modules[node.name]) { - g.sb.write_string('${g.global_var_modules[node.name]}__${node.name}') + const_key := 'const_${g.cur_module}__${node.name}' + global_key := 'global_${g.cur_module}__${node.name}' + module_storage_name := module_storage_c_name(g.cur_module, node.name) + if !is_local_var && node.name in g.global_var_types + && module_storage_name !in g.global_var_types { + g.sb.write_string(node.name) + } else if !is_local_var && module_storage_name in g.module_storage_vars { + g.sb.write_string(module_storage_name) + } else if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' + && !node.name.contains('__') && !is_local_var + && ((const_key in g.emitted_types || global_key in g.emitted_types) + || g.is_module_local_const_or_global(node.name)) { + g.sb.write_string('${g.cur_module}__${node.name}') + } else if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' + && !node.name.contains('__') && !is_local_var && !g.is_module_ident(node.name) + && g.is_module_local_fn(node.name) && !g.is_type_name(node.name) { + g.sb.write_string('${g.cur_module}__${sanitize_fn_ident(node.name)}') } else { - const_key := 'const_${g.cur_module}__${node.name}' - global_key := 'global_${g.cur_module}__${node.name}' - if g.cur_module != '' && g.cur_module != 'main' && g.cur_module != 'builtin' - && !node.name.contains('__') && !is_local_var - && ((const_key in g.emitted_types || global_key in g.emitted_types) - || g.is_module_local_const_or_global(node.name)) { - g.sb.write_string('${g.cur_module}__${node.name}') - } else if g.cur_module != '' && g.cur_module != 'main' - && g.cur_module != 'builtin' && !node.name.contains('__') && !is_local_var - && !g.is_module_ident(node.name) && g.is_module_local_fn(node.name) - && !g.is_type_name(node.name) { - g.sb.write_string('${g.cur_module}__${sanitize_fn_ident(node.name)}') - } else { - mut ident_name := node.name - if g.cur_module != '' { - double_prefix := '${g.cur_module}__${g.cur_module}__' - if ident_name.starts_with(double_prefix) { - ident_name = ident_name[g.cur_module.len + 2..] - } + mut ident_name := node.name + if g.cur_module != '' { + double_prefix := '${g.cur_module}__${g.cur_module}__' + if ident_name.starts_with(double_prefix) { + ident_name = ident_name[g.cur_module.len + 2..] } - g.sb.write_string(c_local_name(ident_name)) } + g.sb.write_string(c_local_name(ident_name)) } } } @@ -2929,20 +2939,12 @@ fn (mut g Gen) expr(node ast.Expr) { is_local := g.local_var_c_type_for_expr(lhs_expr) != none if g.is_module_ident(lhs_ident.name) && !is_local { mod_name := g.resolve_module_name(lhs_ident.name) - if rhs_name.starts_with('${mod_name}__') { - g.sb.write_string(rhs_name) - } else { - g.sb.write_string('${mod_name}__${rhs_name}') - } + g.sb.write_string(g.module_selector_storage_c_name(mod_name, rhs_name)) return } if !is_local { if mod_name := g.known_module_runtime_symbol(lhs_ident.name, rhs_name) { - if rhs_name.starts_with('${mod_name}__') { - g.sb.write_string(rhs_name) - } else { - g.sb.write_string('${mod_name}__${rhs_name}') - } + g.sb.write_string(g.module_selector_storage_c_name(mod_name, rhs_name)) return } } diff --git a/vlib/v2/gen/cleanc/fn.v b/vlib/v2/gen/cleanc/fn.v index 34aa15bfa..e7a0129b5 100644 --- a/vlib/v2/gen/cleanc/fn.v +++ b/vlib/v2/gen/cleanc/fn.v @@ -8958,8 +8958,17 @@ fn (mut g Gen) call_expr(lhs ast.Expr, args []ast.Expr) { ident_name } // Try looking up in the declaring module's scope - global_mod := g.global_var_modules[short_name] or { '' } - for mod_name in [global_mod, g.cur_module, 'builtin'] { + mut module_names := []string{} + if module_name := g.module_storage_vars[ident_name] { + module_names << module_name + } + if ident_name.contains('__') { + module_names << ident_name.all_before_last('__') + } else { + module_names << g.cur_module + module_names << 'builtin' + } + for mod_name in module_names { if mod_name == '' { continue } diff --git a/vlib/v2/gen/cleanc/module_storage_codegen_test.v b/vlib/v2/gen/cleanc/module_storage_codegen_test.v new file mode 100644 index 000000000..59007425a --- /dev/null +++ b/vlib/v2/gen/cleanc/module_storage_codegen_test.v @@ -0,0 +1,275 @@ +module cleanc + +import os +import v2.parser +import v2.pref as vpref +import v2.token +import v2.transformer +import v2.types + +fn module_storage_csrc_for_test_sources(sources map[string]string) string { + tmp_dir := os.join_path(os.temp_dir(), 'v2_module_storage_codegen_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic('failed to create temp dir') } + defer { + os.rmdir_all(tmp_dir) or {} + } + mut paths := []string{} + for rel_path, code in sources { + tmp_file := os.join_path(tmp_dir, rel_path) + os.mkdir_all(os.dir(tmp_file)) or { panic('failed to create temp source dir') } + os.write_file(tmp_file, code) or { panic('failed to write temp source') } + paths << tmp_file + } + prefs := &vpref.Preferences{ + backend: .cleanc + no_parallel: true + } + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(prefs) + files := par.parse_files(paths, mut file_set) + env := types.Environment.new() + mut checker := types.Checker.new(prefs, file_set, env) + checker.check_files(files) + mut trans := transformer.Transformer.new_with_pref(files, env, prefs) + trans.set_file_set(file_set) + transformed_files := trans.transform_files(files) + mut gen := Gen.new_with_env_and_pref(transformed_files, env, prefs) + return gen.gen() +} + +fn test_generate_c_uses_module_prefix_for_qualified_module_storage() { + csrc := module_storage_csrc_for_test_sources({ + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'module main + +import report + +fn main() { + report.errors += 1 +} +' + }) + assert csrc.contains('int report__errors'), csrc + assert csrc.contains('report__errors'), csrc + assert !csrc.contains('\nint errors'), csrc +} + +fn test_generate_c_resolves_module_storage_import_alias() { + csrc := module_storage_csrc_for_test_sources({ + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'module main + +import report as r + +fn main() { + r.errors += 1 +} +' + }) + assert csrc.contains('report__errors'), csrc + assert !csrc.contains('r__errors'), csrc +} + +fn test_generate_c_keeps_same_short_module_storage_names_distinct() { + csrc := module_storage_csrc_for_test_sources({ + 'a/a.v': 'module a + +pub __global state = 1 +' + 'b/b.v': 'module b + +pub __global state = 2 +' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.state + b.state +} +' + }) + assert csrc.contains('int a__state = 1;'), csrc + assert csrc.contains('int b__state = 2;'), csrc + assert !csrc.contains('\nint state ='), csrc +} + +fn test_generate_c_prefixes_module_storage_names_containing_dunders() { + csrc := module_storage_csrc_for_test_sources({ + 'a/a.v': 'module a + +pub __global mut state__x = 1 + +pub fn bump() int { + state__x += 1 + return state__x +} + ' + 'b/b.v': 'module b + +pub __global mut state__x = 2 + +pub fn bump() int { + state__x += 1 + return state__x +} + ' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.bump() + b.bump() + _ = a.state__x + b.state__x +} +' + }) + assert csrc.contains('int a__state__x = 1;'), csrc + assert csrc.contains('int b__state__x = 2;'), csrc + assert csrc.contains('a__state__x += 1;'), csrc + assert csrc.contains('return a__state__x;'), csrc + assert csrc.contains('b__state__x += 1;'), csrc + assert csrc.contains('return b__state__x;'), csrc + assert !csrc.contains('\nint state__x ='), csrc + assert !csrc.contains('\n\tstate__x += 1;'), csrc + assert !csrc.contains('\n\treturn state__x;'), csrc +} + +fn test_generate_c_keeps_c_global_declaration_unmangled() { + csrc := module_storage_csrc_for_test_sources({ + 'main.v': 'module main + +@[c_extern] +__global C.errno int + +@[c_extern] +__global C.stdin voidptr + +@[c_extern] +__global C.stdout voidptr + +@[c_extern] +__global C.stderr voidptr + +fn main() { + _ = C.errno + _ = C.stdin + _ = C.stdout + _ = C.stderr +} +' + }) + assert !csrc.contains('main__C.errno'), csrc + assert !csrc.contains('main__errno'), csrc + assert !csrc.contains('main__stdin'), csrc + assert !csrc.contains('main__stdout'), csrc + assert !csrc.contains('main__stderr'), csrc + assert csrc.contains('errno'), csrc + assert csrc.contains('stdin'), csrc + assert csrc.contains('stdout'), csrc + assert csrc.contains('stderr'), csrc +} + +fn test_generate_c_keeps_c_extern_global_attributes_unmangled() { + csrc := module_storage_csrc_for_test_sources({ + 'main.v': 'module main + +@[c_extern; export: "raw_counter"; weak; hidden; markused] +__global raw_counter int + +fn main() { + _ = raw_counter +} +' + }) + assert csrc.contains('extern int raw_counter;'), csrc + assert csrc.contains('raw_counter'), csrc + assert !csrc.contains('main__raw_counter'), csrc +} + +fn test_generate_c_keeps_translated_c_v_extern_global_unmangled() { + csrc := module_storage_csrc_for_test_sources({ + 'cwrap/runtime.c.v': '@[translated] +module cwrap + +@[c_extern; hidden; markused] +__global raw_status int + +pub fn read_status() int { + return raw_status +} +' + 'main.v': 'module main + +import cwrap + +fn main() { + _ = cwrap.read_status() + _ = cwrap.raw_status +} +' + }) + assert csrc.contains('extern int raw_status;'), csrc + assert csrc.contains('return raw_status;'), csrc + assert csrc.count('raw_status') >= 3, csrc + assert !csrc.contains('cwrap__raw_status'), csrc +} + +fn test_generate_c_keeps_qualified_c_extern_global_alias_unmangled() { + csrc := module_storage_csrc_for_test_sources({ + 'cwrap/runtime.c.v': '@[translated] +module cwrap + +@[c_extern; hidden; markused] +__global raw_status int +' + 'main.v': 'module main + +import cwrap as cw + +fn main() { + _ = cw.raw_status +} +' + }) + assert csrc.contains('extern int raw_status;'), csrc + assert csrc.count('raw_status') >= 2, csrc + assert !csrc.contains('cwrap__raw_status'), csrc + assert !csrc.contains('cw__raw_status'), csrc +} + +fn test_generate_c_keeps_plain_module_c_extern_global_unmangled() { + csrc := module_storage_csrc_for_test_sources({ + 'ext/ext.v': 'module ext + +@[c_extern] +pub __global raw_status int + +pub fn read_status() int { + return raw_status +} +' + 'main.v': 'module main + +import ext + +fn main() { + _ = ext.raw_status + _ = ext.read_status() +} +' + }) + assert csrc.contains('extern int raw_status;'), csrc + assert csrc.contains('return raw_status;'), csrc + assert csrc.contains('(void)(raw_status);'), csrc + assert !csrc.contains('ext__raw_status'), csrc +} diff --git a/vlib/v2/gen/cleanc/types.v b/vlib/v2/gen/cleanc/types.v index 05a03d748..dbcfb24ed 100644 --- a/vlib/v2/gen/cleanc/types.v +++ b/vlib/v2/gen/cleanc/types.v @@ -2650,22 +2650,15 @@ fn (g &Gen) global_var_type_for_ident(name string) ?string { if global_type := g.global_var_types[name] { return global_type } - if name.contains('__') { - return none - } - if module_name := g.global_var_modules[name] { - if !g.is_current_or_imported_module(module_name) { - return none - } - qualified := if module_name != '' && module_name != 'main' && module_name != 'builtin' { - '${module_name}__${name}' - } else { - name - } + qualified := module_storage_c_name(g.cur_module, name) + if qualified in g.module_storage_vars { if global_type := g.global_var_types[qualified] { return global_type } } + if name.contains('__') { + return none + } return none } diff --git a/vlib/v2/gen/v/gen.v b/vlib/v2/gen/v/gen.v index b7a1099bb..cecac8f8a 100644 --- a/vlib/v2/gen/v/gen.v +++ b/vlib/v2/gen/v/gen.v @@ -277,10 +277,23 @@ fn (mut g Gen) stmt(stmt ast.Stmt) { g.expr(stmt.expr) } ast.GlobalDecl { + if stmt.attributes.len > 0 { + g.attributes(stmt.attributes) + g.writeln('') + } + if stmt.is_public { + g.write('pub ') + } g.writeln('__global (') g.indent++ for field in stmt.fields { // TODO + if field.is_public && !stmt.is_public { + g.write('pub ') + } + if field.is_mut { + g.write('mut ') + } g.write(field.name) // if field.value != none { if field.value !is ast.EmptyExpr { diff --git a/vlib/v2/parser/parser.v b/vlib/v2/parser/parser.v index 0adc676fc..1f8dd3e54 100644 --- a/vlib/v2/parser/parser.v +++ b/vlib/v2/parser/parser.v @@ -272,7 +272,7 @@ fn (mut p Parser) top_stmt() ast.Stmt { return p.fn_decl(false, []) } .key_global { - return p.global_decl([]) + return p.global_decl(false, []) } // NOTE: handling moved to parse_file // .key_import { @@ -296,6 +296,7 @@ fn (mut p Parser) top_stmt() ast.Stmt { .key_const { return p.const_decl(true) } .key_enum { return p.enum_decl(true, []) } .key_fn { return p.fn_decl(true, []) } + .key_global { return p.global_decl(true, []) } .key_interface { return p.interface_decl(true, []) } .key_struct, .key_union { return p.struct_decl(true, []) } .key_type { return p.type_decl(true) } @@ -342,7 +343,7 @@ fn (mut p Parser) stmt() ast.Stmt { } } .key_global { - return p.global_decl([]) + return p.global_decl(false, []) } .key_interface { return p.interface_decl(false, []) @@ -353,6 +354,7 @@ fn (mut p Parser) stmt() ast.Stmt { .key_const { return p.const_decl(true) } .key_enum { return p.enum_decl(true, []) } .key_fn { return p.fn_decl(true, []) } + .key_global { return p.global_decl(true, []) } .key_interface { return p.interface_decl(true, []) } .key_struct, .key_union { return p.struct_decl(true, []) } .key_type { return p.type_decl(true) } @@ -493,7 +495,7 @@ fn (mut p Parser) attribute_stmt() ast.Stmt { return p.fn_decl(is_pub, attributes) } .key_global { - return p.global_decl(attributes) + return p.global_decl(is_pub, attributes) } .key_interface { return p.interface_decl(is_pub, attributes) @@ -2811,7 +2813,7 @@ fn (mut p Parser) enum_decl(is_public bool, attributes []ast.Attribute) ast.Enum } } -fn (mut p Parser) global_decl(attributes []ast.Attribute) ast.GlobalDecl { +fn (mut p Parser) global_decl(is_public bool, attributes []ast.Attribute) ast.GlobalDecl { p.next() // NOTE: this got changed at some stage (or perhaps was never forced) // if p.tok != .lpar { @@ -2824,6 +2826,15 @@ fn (mut p Parser) global_decl(attributes []ast.Attribute) ast.GlobalDecl { } mut fields := []ast.FieldDecl{} for { + mut field_is_public := is_public + if p.tok == .key_pub { + p.next() + field_is_public = true + } + field_is_mut := p.tok == .key_mut + if field_is_mut { + p.next() + } mut name := p.expect_name() // Handle qualified names like C.errno, C.stdin, etc. for p.tok == .dot { @@ -2837,13 +2848,17 @@ fn (mut p Parser) global_decl(attributes []ast.Attribute) ast.GlobalDecl { value := p.expr(.lowest) p.in_top_level = prev_top_level fields << ast.FieldDecl{ - name: name - value: value + name: name + value: value + is_public: field_is_public + is_mut: field_is_mut } } else { fields << ast.FieldDecl{ - name: name - typ: p.expect_type() + name: name + typ: p.expect_type() + is_public: field_is_public + is_mut: field_is_mut } } p.expect(.semicolon) @@ -2858,6 +2873,7 @@ fn (mut p Parser) global_decl(attributes []ast.Attribute) ast.GlobalDecl { return ast.GlobalDecl{ attributes: attributes fields: fields + is_public: is_public } } diff --git a/vlib/v2/ssa/builder.v b/vlib/v2/ssa/builder.v index b9d7f0dd7..6a7f361f0 100644 --- a/vlib/v2/ssa/builder.v +++ b/vlib/v2/ssa/builder.v @@ -27,6 +27,24 @@ fn enum_field_symbol_name(name string) string { return escaped } +fn ssa_module_storage_name(module_name string, name string) string { + if name == '' || name.starts_with('C.') { + return name + } + if module_name == 'C' { + return 'C.${name}' + } + if module_name != '' && module_name != 'main' { + return '${module_name}__${name}' + } + return name +} + +fn ssa_module_storage_field_is_c_extern(node ast.GlobalDecl, field ast.FieldDecl) bool { + return field.name.starts_with('C.') || node.attributes.has('c_extern') + || field.attributes.has('c_extern') +} + struct DynConstArray { arr_global_name string // V array struct global name data_global_name string // raw data global name @@ -1483,12 +1501,18 @@ fn (mut b Builder) register_consts_and_globals(file ast.File) { } ast.GlobalDecl { for field in stmt.fields { - glob_name := if b.cur_module != '' && b.cur_module != 'main' { - '${b.cur_module}__${field.name}' - } else { - field.name - } glob_type := b.global_field_type(field) + if ssa_module_storage_field_is_c_extern(stmt, field) + && !field.name.starts_with('C.') { + glob_id := b.mod.add_external_global(field.name, glob_type) + b.global_refs[field.name] = glob_id + qualified_name := ssa_module_storage_name(b.cur_module, field.name) + if qualified_name != field.name { + b.global_refs[qualified_name] = glob_id + } + continue + } + glob_name := ssa_module_storage_name(b.cur_module, field.name) initial_value := if field.value != ast.empty_expr { b.try_eval_const_int(field.value) } else { @@ -2950,11 +2974,7 @@ fn (mut b Builder) build_assign(stmt ast.AssignStmt) { mut ptr := ValueID(0) if p := b.vars[ident.name] { ptr = p - } else if glob_id := b.find_global(ident.name) { - ptr = glob_id - } else if glob_id := b.find_global('${b.cur_module}__${ident.name}') { - ptr = glob_id - } else if glob_id := b.find_global('builtin__${ident.name}') { + } else if glob_id := b.find_global_ident(ident.name) { ptr = glob_id } if ptr != 0 { @@ -2984,11 +3004,7 @@ fn (mut b Builder) build_assign(stmt ast.AssignStmt) { mut ptr := ValueID(0) if p := b.vars[ident.name] { ptr = p - } else if glob_id := b.find_global(ident.name) { - ptr = glob_id - } else if glob_id := b.find_global('${b.cur_module}__${ident.name}') { - ptr = glob_id - } else if glob_id := b.find_global('builtin__${ident.name}') { + } else if glob_id := b.find_global_ident(ident.name) { ptr = glob_id } if ptr != 0 { @@ -3037,8 +3053,9 @@ fn (mut b Builder) build_assign(stmt ast.AssignStmt) { // Do NOT skip if RHS is a function call (e.g. new_array_from_c_array // for dynamic arrays — those need their _vinit assignment). if rhs is ast.ArrayInitExpr { + module_const_name := ssa_module_storage_name(b.cur_module, ident.name) if ident.name in b.const_array_globals - || '${b.cur_module}__${ident.name}' in b.const_array_globals + || module_const_name in b.const_array_globals || 'builtin__${ident.name}' in b.const_array_globals { continue } @@ -3046,11 +3063,7 @@ fn (mut b Builder) build_assign(stmt ast.AssignStmt) { mut ptr := ValueID(0) if p := b.vars[ident.name] { ptr = p - } else if glob_id := b.find_global(ident.name) { - ptr = glob_id - } else if glob_id := b.find_global('${b.cur_module}__${ident.name}') { - ptr = glob_id - } else if glob_id := b.find_global('builtin__${ident.name}') { + } else if glob_id := b.find_global_ident(ident.name) { ptr = glob_id } if ptr != 0 { @@ -4262,18 +4275,7 @@ fn (mut b Builder) build_ident(ident ast.Ident) ValueID { } } // Try as global variable - if glob_id := b.find_global(ident.name) { - glob_typ := b.mod.values[glob_id].typ - elem_typ := b.mod.type_store.types[glob_typ].elem_type - return b.mod.add_instr(.load, b.cur_block, elem_typ, [glob_id]) - } - // Try with prefixes for globals too - if glob_id := b.find_global(builtin_const) { - glob_typ := b.mod.values[glob_id].typ - elem_typ := b.mod.type_store.types[glob_typ].elem_type - return b.mod.add_instr(.load, b.cur_block, elem_typ, [glob_id]) - } - if glob_id := b.find_global(qualified_name) { + if glob_id := b.find_global_ident(ident.name) { glob_typ := b.mod.values[glob_id].typ elem_typ := b.mod.type_store.types[glob_typ].elem_type return b.mod.add_instr(.load, b.cur_block, elem_typ, [glob_id]) @@ -4299,17 +4301,24 @@ fn (mut b Builder) find_global(name string) ?ValueID { } fn (mut b Builder) find_global_ident(name string) ?ValueID { - if glob_id := b.find_global(name) { - return glob_id + if name.starts_with('C.') { + return b.find_global(name) } - qualified_name := '${b.cur_module}__${name}' - if glob_id := b.find_global(qualified_name) { - return glob_id + qualified_name := ssa_module_storage_name(b.cur_module, name) + if qualified_name != name { + if glob_id := b.find_global(qualified_name) { + return glob_id + } } - builtin_name := 'builtin__${name}' - if glob_id := b.find_global(builtin_name) { + if glob_id := b.find_global(name) { return glob_id } + builtin_name := ssa_module_storage_name('builtin', name) + if builtin_name != name && builtin_name != qualified_name { + if glob_id := b.find_global(builtin_name) { + return glob_id + } + } return none } @@ -6455,40 +6464,47 @@ fn (mut b Builder) build_selector(expr ast.SelectorExpr) ValueID { // Module-qualified constant/global access: os.args, pref.Backend, etc. // When LHS is a module name, resolve module__field as a constant or global. if expr.lhs is ast.Ident { - mod_name := expr.lhs.name.replace('.', '_') - qualified := '${mod_name}__${expr.rhs.name}' - // Try as float constant (inline as f64) - if fval := b.float_const_values[qualified] { - return b.mod.get_or_add_const(b.mod.type_store.get_float(64), fval) - } - // Try as compile-time constant - if qualified in b.const_values { - ct := if qualified in b.const_value_types { - b.const_value_types[qualified] - } else { - b.mod.type_store.get_int(64) + mut mod_name := '' + if resolved_mod := b.selector_module_name(expr) { + mod_name = resolved_mod + } else if b.env == unsafe { nil } { + mod_name = expr.lhs.name.replace('.', '_') + } + if mod_name != '' { + qualified := ssa_module_storage_name(mod_name, expr.rhs.name) + // Try as float constant (inline as f64) + if fval := b.float_const_values[qualified] { + return b.mod.get_or_add_const(b.mod.type_store.get_float(64), fval) } - return b.mod.get_or_add_const(ct, b.const_values[qualified].str()) - } - // Try as string constant - if qualified in b.string_const_values { - return b.build_string_literal(ast.StringLiteral{ - kind: .v - value: b.string_const_values[qualified] - }) - } - // Try as constant array global (return pointer directly for indexing) - if qualified in b.const_array_globals { + // Try as compile-time constant + if qualified in b.const_values { + ct := if qualified in b.const_value_types { + b.const_value_types[qualified] + } else { + b.mod.type_store.get_int(64) + } + return b.mod.get_or_add_const(ct, b.const_values[qualified].str()) + } + // Try as string constant + if qualified in b.string_const_values { + return b.build_string_literal(ast.StringLiteral{ + kind: .v + value: b.string_const_values[qualified] + }) + } + // Try as constant array global (return pointer directly for indexing) + if qualified in b.const_array_globals { + if glob_id := b.find_global(qualified) { + return glob_id + } + } + // Try as global variable (runtime-initialized constants like os.args) if glob_id := b.find_global(qualified) { - return glob_id + glob_typ := b.mod.values[glob_id].typ + elem_typ := b.mod.type_store.types[glob_typ].elem_type + return b.mod.add_instr(.load, b.cur_block, elem_typ, [glob_id]) } } - // Try as global variable (runtime-initialized constants like os.args) - if glob_id := b.find_global(qualified) { - glob_typ := b.mod.values[glob_id].typ - elem_typ := b.mod.type_store.types[glob_typ].elem_type - return b.mod.add_instr(.load, b.cur_block, elem_typ, [glob_id]) - } } // Check for const array global .len access — return compile-time element count @@ -8650,6 +8666,14 @@ fn (mut b Builder) build_addr(expr ast.Expr) ValueID { return 0 } ast.SelectorExpr { + if expr.lhs is ast.Ident { + if mod_name := b.selector_module_name(expr) { + qualified := ssa_module_storage_name(mod_name, expr.rhs.name) + if glob_id := b.find_global(qualified) { + return glob_id + } + } + } // Get address of base (not the loaded value) mut base := b.build_addr(expr.lhs) if base == 0 { diff --git a/vlib/v2/ssa/module_storage_test.v b/vlib/v2/ssa/module_storage_test.v new file mode 100644 index 000000000..84a14a250 --- /dev/null +++ b/vlib/v2/ssa/module_storage_test.v @@ -0,0 +1,198 @@ +module ssa + +import os +import v2.parser +import v2.pref +import v2.token +import v2.transformer +import v2.types + +fn module_storage_ssa_tmp_dir(label string) string { + return os.join_path(os.temp_dir(), 'v2_module_storage_ssa_${label}_${os.getpid()}') +} + +fn module_storage_ssa_for_test_sources(label string, sources map[string]string) &Module { + tmp_dir := module_storage_ssa_tmp_dir(label) + os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') } + defer { + os.rmdir_all(tmp_dir) or {} + } + mut paths := []string{} + for rel_path, code in sources { + path := os.join_path(tmp_dir, rel_path) + os.mkdir_all(os.dir(path)) or { panic('cannot create ${os.dir(path)}') } + os.write_file(path, code) or { panic('cannot write ${path}') } + paths << path + } + prefs := &pref.Preferences{} + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(prefs) + files := par.parse_files(paths, mut file_set) + env := types.Environment.new() + mut checker := types.Checker.new(prefs, file_set, env) + checker.check_files(files) + mut trans := transformer.Transformer.new_with_pref(files, env, prefs) + trans.set_file_set(file_set) + transformed_files := trans.transform_files(files) + mut mod := Module.new('module_storage_test') + mut builder := Builder.new_with_env(mod, env) + builder.build_all(transformed_files) + return mod +} + +fn module_storage_ssa_global_names(m &Module) []string { + mut names := []string{} + for global in m.globals { + names << global.name + } + return names +} + +fn module_storage_ssa_global_value_id(m &Module, name string) ?ValueID { + for value in m.values { + if value.kind == .global && value.name == name { + return value.id + } + } + return none +} + +fn module_storage_ssa_has_store_to_global(m &Module, name string) bool { + global_id := module_storage_ssa_global_value_id(m, name) or { return false } + for instr in m.instrs { + if instr.op == .store && instr.operands.len >= 2 && instr.operands[1] == global_id { + return true + } + } + return false +} + +fn test_module_storage_import_alias_resolves_to_declaring_module_in_ssa() { + m := module_storage_ssa_for_test_sources('alias', { + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'module main + +import report as r + +fn main() { + r.errors += 1 +} +' + }) + names := module_storage_ssa_global_names(m) + assert 'report__errors' in names + assert 'r__errors' !in names + assert module_storage_ssa_has_store_to_global(m, 'report__errors') +} + +fn test_module_storage_same_short_names_stay_distinct_in_ssa() { + m := module_storage_ssa_for_test_sources('collisions', { + 'a/a.v': 'module a + +pub __global state = 1 +' + 'b/b.v': 'module b + +pub __global state = 2 +' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.state + b.state +} +' + }) + names := module_storage_ssa_global_names(m) + assert 'a__state' in names + assert 'b__state' in names + assert 'state' !in names + a_id := module_storage_ssa_global_value_id(m, 'a__state') or { + panic('missing a__state global') + } + b_id := module_storage_ssa_global_value_id(m, 'b__state') or { + panic('missing b__state global') + } + assert a_id != b_id +} + +fn test_module_storage_bare_dunder_name_resolves_to_current_module_in_ssa() { + m := module_storage_ssa_for_test_sources('bare_dunder', { + 'a/a.v': 'module a + +pub __global mut state__x = 1 + +pub fn bump() int { + state__x += 1 + return state__x +} +' + 'b/b.v': 'module b + +pub __global mut state__x = 2 + +pub fn bump() int { + state__x += 1 + return state__x +} +' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.bump() + b.bump() + _ = a.state__x + b.state__x +} +' + }) + names := module_storage_ssa_global_names(m) + assert 'a__state__x' in names + assert 'b__state__x' in names + assert 'state__x' !in names + assert module_storage_ssa_has_store_to_global(m, 'a__state__x') + assert module_storage_ssa_has_store_to_global(m, 'b__state__x') + a_id := module_storage_ssa_global_value_id(m, 'a__state__x') or { + panic('missing a__state__x global') + } + b_id := module_storage_ssa_global_value_id(m, 'b__state__x') or { + panic('missing b__state__x global') + } + assert a_id != b_id +} + +fn test_module_storage_c_extern_raw_global_uses_c_symbol_in_ssa() { + m := module_storage_ssa_for_test_sources('c_extern_raw', { + 'ext/ext.v': 'module ext + +@[c_extern] +pub __global raw_status int + +pub fn read_status() int { + return raw_status +} +' + 'main.v': 'module main + +import ext + +fn main() { + _ = ext.raw_status + _ = ext.read_status() +} +' + }) + names := module_storage_ssa_global_names(m) + assert 'raw_status' in names + assert 'ext__raw_status' !in names + _ := module_storage_ssa_global_value_id(m, 'raw_status') or { + panic('missing raw_status C extern global') + } + assert module_storage_ssa_global_value_id(m, 'ext__raw_status') == none +} diff --git a/vlib/v2/tests/module_storage_example/README.md b/vlib/v2/tests/module_storage_example/README.md new file mode 100644 index 000000000..f2b92cdb3 --- /dev/null +++ b/vlib/v2/tests/module_storage_example/README.md @@ -0,0 +1,13 @@ +# Module Storage Example + +This is a V2 fixture for module-scoped `__global` storage. + +It is intentionally stored as `.vv2` source so shared V1/vfmt paths do not parse +V2-only public module-storage syntax directly. The regression test copies these +files to temporary `.v` files and compiles them with `-v2`. + +Run: + +```sh +./v test vlib/v2/types/module_storage_test.v +``` diff --git a/vlib/v2/tests/module_storage_example/main.vv2 b/vlib/v2/tests/module_storage_example/main.vv2 new file mode 100644 index 000000000..3126a993e --- /dev/null +++ b/vlib/v2/tests/module_storage_example/main.vv2 @@ -0,0 +1,12 @@ +module main + +import state + +fn main() { + assert state.total_ticks == 0 + assert state.tick() == 1 + assert state.tick() == 2 + state.total_ticks += 10 + assert state.total_ticks == 12 + assert state.private_tick_count() == 2 +} diff --git a/vlib/v2/tests/module_storage_example/state/state.vv2 b/vlib/v2/tests/module_storage_example/state/state.vv2 new file mode 100644 index 000000000..85379ec47 --- /dev/null +++ b/vlib/v2/tests/module_storage_example/state/state.vv2 @@ -0,0 +1,18 @@ +module state + +__global mut private_ticks = 0 +pub __global mut total_ticks = 0 + +fn bump_private() { + private_ticks += 1 +} + +pub fn tick() int { + bump_private() + total_ticks += 1 + return private_ticks +} + +pub fn private_tick_count() int { + return private_ticks +} diff --git a/vlib/v2/transformer/live.v b/vlib/v2/transformer/live.v index fae318503..fe7668387 100644 --- a/vlib/v2/transformer/live.v +++ b/vlib/v2/transformer/live.v @@ -529,8 +529,9 @@ fn mk_global_decl(name string, typ_name string) ast.Stmt { return ast.Stmt(ast.GlobalDecl{ fields: [ ast.FieldDecl{ - name: name - typ: mk_ident(typ_name) + name: name + typ: mk_ident(typ_name) + is_mut: true }, ] }) diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index 93348898a..fc3f5a422 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -3159,11 +3159,14 @@ fn (mut t Transformer) transform_global_decl(decl ast.GlobalDecl) ast.GlobalDecl typ: t.transform_expr(field.typ) value: t.transform_expr(field.value) attributes: field.attributes + is_public: field.is_public + is_mut: field.is_mut } } return ast.GlobalDecl{ attributes: decl.attributes fields: fields + is_public: decl.is_public } } diff --git a/vlib/v2/transformer/transformer_test.v b/vlib/v2/transformer/transformer_test.v index d1f9b6a9c..278ca54d1 100644 --- a/vlib/v2/transformer/transformer_test.v +++ b/vlib/v2/transformer/transformer_test.v @@ -989,7 +989,7 @@ pub type Type = u32 pub struct Table {} -__global global_table = &Table(unsafe { nil }) +pub __global global_table = &Table(unsafe { nil }) pub fn (t &Table) type_to_str(typ Type) string { _ = t @@ -1013,7 +1013,7 @@ struct FnDecl { } fn call(node &FnDecl) string { - return global_table.type_to_str(node.receiver.typ) + return ast.global_table.type_to_str(node.receiver.typ) } ' }, diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index c8fc6b215..1f28f4289 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -775,6 +775,125 @@ fn value_object_from_type(name string, typ Type) Object { return obj } +fn global_decl_field_is_c_extern(decl ast.GlobalDecl, field ast.FieldDecl) bool { + return field.name.starts_with('C.') || decl.attributes.has('c_extern') + || field.attributes.has('c_extern') +} + +fn module_storage_object(module_name string, decl ast.GlobalDecl, field ast.FieldDecl, typ Type) Global { + storage_module := if global_decl_field_is_c_extern(decl, field) { '' } else { module_name } + mut obj := Global{ + name: field.name + mod: storage_module + is_public: field.is_public + is_mut: field.is_mut + typ: Type(void_) + } + obj.typ = typ + return obj +} + +fn (mut c Checker) module_storage_predecl_type(field ast.FieldDecl) Type { + if field.typ !is ast.EmptyExpr { + return c.type_expr(field.typ) + } + match field.value { + ast.BasicLiteral, ast.StringLiteral { + return c.expr(field.value) + } + else {} + } + + return Type(int_) +} + +fn (mut c Checker) preregister_module_storage_decl(decl ast.GlobalDecl) { + for field in decl.fields { + field_type := c.module_storage_predecl_type(field) + obj := module_storage_object(c.cur_file_module, decl, field, field_type) + c.scope.insert(field.name, obj) + } +} + +fn (obj Global) is_module_storage() bool { + return obj.mod != '' +} + +fn (obj Global) requires_explicit_module_storage_mut() bool { + _ = obj + // This V2 palier keeps legacy `__global name` mutable on purpose, so + // existing V2/runtime sources do not need syntax that the V1 parser/vfmt + // still rejects. `is_mut` remains source metadata for the explicit spelling. + return false +} + +fn (mut c Checker) module_storage_for_lhs(expr ast.Expr) ?Global { + unwrapped := c.unwrap_expr(expr) + match unwrapped { + ast.Ident { + if obj := c.scope.lookup_parent(unwrapped.name, 0) { + if obj is Global && obj.is_module_storage() { + return obj + } + } + } + ast.SelectorExpr { + if unwrapped.lhs is ast.Ident { + lhs_ident := unwrapped.lhs as ast.Ident + if lhs_obj := c.scope.lookup_parent(lhs_ident.name, 0) { + if lhs_obj is Module { + if rhs_obj := lhs_obj.scope.lookup_parent(unwrapped.rhs.name, 0) { + if rhs_obj is Global && rhs_obj.is_module_storage() { + declaring_mod := if rhs_obj.mod != '' { + rhs_obj.mod + } else { + lhs_obj.name + } + if declaring_mod != c.cur_file_module && !rhs_obj.is_public { + c.error_with_pos('module global `${lhs_ident.name}.${unwrapped.rhs.name}` is private', + unwrapped.pos) + return none + } + return rhs_obj + } + } + } + if lhs_obj is Global && lhs_obj.is_module_storage() { + return lhs_obj + } + } + } + if storage := c.module_storage_for_lhs(unwrapped.lhs) { + return storage + } + } + ast.IndexExpr { + if storage := c.module_storage_for_lhs(unwrapped.lhs) { + return storage + } + } + else {} + } + + return none +} + +fn (mut c Checker) check_module_storage_assignment(lhs ast.Expr, op token.Token, pos token.Pos) { + if op == .decl_assign { + return + } + if storage := c.module_storage_for_lhs(lhs) { + if storage.requires_explicit_module_storage_mut() && !storage.is_mut { + mut display_name := storage.name + if storage.mod != '' && !storage.name.starts_with('${storage.mod}.') { + display_name = '${storage.mod}.${storage.name}' + } + c.error_with_pos('cannot assign to immutable module global `${display_name}`; declare it with `__global mut`', + pos) + } + } +} + fn is_empty_expr(e ast.Expr) bool { return e is ast.EmptyExpr } @@ -1120,6 +1239,9 @@ fn (mut c Checker) register_imported_symbols(files []ast.File) { for symbol in imp.symbols { if symbol is ast.Ident { if sym_obj := import_scope.lookup_parent(symbol.name, 0) { + if sym_obj is Global && sym_obj.is_module_storage() { + continue + } file_scope.insert(symbol.name, sym_obj) } } @@ -1316,6 +1438,9 @@ fn (mut c Checker) preregister_type_stmt(stmt ast.Stmt) { ast.ConstDecl, ast.EnumDecl, ast.InterfaceDecl, ast.StructDecl, ast.TypeDecl { c.decl(stmt) } + ast.GlobalDecl { + c.preregister_module_storage_decl(stmt) + } ast.ExprStmt { c.preregister_active_comptime_decl_stmt(stmt, true, false) } @@ -1376,6 +1501,11 @@ fn (mut c Checker) preregister_decl_stmts(stmts []ast.Stmt, want_types bool, wan c.decl(stmt) } } + ast.GlobalDecl { + if want_types { + c.preregister_module_storage_decl(stmt) + } + } ast.ExprStmt { c.preregister_active_comptime_decl_stmt(stmt, want_types, want_fns) } @@ -1441,11 +1571,8 @@ fn (mut c Checker) decl(decl ast.Stmt) { } else if field.value !is ast.EmptyExpr { field_type = c.expr(field.value) } - obj := Global{ - name: field.name - typ: field_type - } - c.scope.insert(field.name, obj) + obj := module_storage_object(c.cur_file_module, decl, field, field_type) + c.scope.insert_or_update(field.name, obj) } } ast.InterfaceDecl { @@ -2949,6 +3076,9 @@ fn (mut c Checker) expr_impl(expr ast.Expr) Type { return c.expr(expr.expr) } ast.PostfixExpr { + if expr.op in [.inc, .dec] { + c.check_module_storage_assignment(expr.expr, expr.op, expr.pos) + } typ := c.expr(expr.expr) // The `!` and `?` propagation operators unwrap result/option types. if expr.op in [.not, .question] { @@ -3196,6 +3326,9 @@ fn (mut c Checker) lookup_object_in_imported_modules(name string) ?Object { match obj { Module { if rhs_obj := obj.scope.lookup_parent(name, 0) { + if rhs_obj is Global && rhs_obj.is_module_storage() { + continue + } return rhs_obj } if obj.name == '' { @@ -3204,6 +3337,9 @@ fn (mut c Checker) lookup_object_in_imported_modules(name string) ?Object { if module_obj := c.scope.lookup_parent(obj.name, 0) { if module_obj is Module { if rhs_obj := module_obj.scope.lookup_parent(name, 0) { + if rhs_obj is Global && rhs_obj.is_module_storage() { + continue + } return rhs_obj } } @@ -3389,11 +3525,8 @@ fn (mut c Checker) stmt(stmt ast.Stmt) { } else { c.expr(field.value) } - obj := Global{ - name: field.name - typ: field_type - } - c.scope.insert(field.name, obj) + obj := module_storage_object(c.cur_file_module, stmt, field, field_type) + c.scope.insert_or_update(field.name, obj) } } ast.DeferStmt { @@ -3913,10 +4046,12 @@ fn (mut c Checker) check_pending_fn_body(pending PendingFnBody) { } if !has_decl_generic_params || c.env.cur_generic_types.len > 0 { prev_scope := c.scope + prev_module := c.cur_file_module prev_fn_root_scope := c.fn_root_scope prev_fallback_vars := c.fallback_vars.clone() prev_generic_params := c.generic_params.clone() c.scope = pending.scope + c.cur_file_module = pending.module_name c.fn_root_scope = pending.scope c.fallback_vars = map[string]Type{} for param in pending.typ.params { @@ -3969,6 +4104,7 @@ fn (mut c Checker) check_pending_fn_body(pending PendingFnBody) { c.fn_root_scope = prev_fn_root_scope c.env.set_fn_scope(pending.module_name, pending.scope_fn_name, pending.scope) c.scope = prev_scope + c.cur_file_module = prev_module } c.env.cur_generic_types = [] } @@ -4154,6 +4290,7 @@ fn (mut c Checker) assign_stmt(stmt ast.AssignStmt, unwrap_optional bool) { } else { c.expr(rx) } + c.check_module_storage_assignment(lx, stmt.op, stmt.pos) // if t := expected_type { // c.log('AssignStmt: setting expected_type to: ${t.name()}') // } else { @@ -6479,6 +6616,14 @@ fn (mut c Checker) selector_expr(expr ast.SelectorExpr) Type { c.error_with_pos('missing ${expr.lhs.name}.${expr.rhs.name}', expr.pos) return Type(void_) } + if rhs_obj is Global && rhs_obj.is_module_storage() { + declaring_mod := if rhs_obj.mod != '' { rhs_obj.mod } else { lhs_obj.name } + if declaring_mod != c.cur_file_module && !rhs_obj.is_public { + c.error_with_pos('module global `${expr.lhs.name}.${expr.rhs.name}` is private', + expr.pos) + return Type(void_) + } + } if expr.lhs.name == 'os' && expr.rhs.name == 'args' && rhs_obj.typ() is Void { return os_args_array_type() } diff --git a/vlib/v2/types/module_storage_test.v b/vlib/v2/types/module_storage_test.v new file mode 100644 index 000000000..f7a16f32f --- /dev/null +++ b/vlib/v2/types/module_storage_test.v @@ -0,0 +1,326 @@ +module types + +import os +import v2.parser +import v2.pref +import v2.token + +fn module_storage_tmp_dir(label string) string { + return os.join_path(os.temp_dir(), 'v2_module_storage_${label}_${os.getpid()}') +} + +fn write_module_storage_files(tmp_dir string, files map[string]string) []string { + mut paths := []string{} + for rel_path, code in files { + path := os.join_path(tmp_dir, rel_path) + os.mkdir_all(os.dir(path)) or { panic('cannot create ${os.dir(path)}') } + os.write_file(path, code) or { panic('cannot write ${path}') } + paths << path + } + return paths +} + +fn check_module_storage_files(files map[string]string) &Environment { + tmp_dir := module_storage_tmp_dir('direct') + os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') } + defer { + os.rmdir_all(tmp_dir) or {} + } + paths := write_module_storage_files(tmp_dir, files) + prefs := &pref.Preferences{} + mut file_set := token.FileSet.new() + mut par := parser.Parser.new(prefs) + parsed_files := par.parse_files(paths, mut file_set) + mut env := Environment.new() + mut checker := Checker.new(prefs, file_set, env) + checker.check_files(parsed_files) + return env +} + +fn module_storage_global(env &Environment, module_name string, name string) Global { + scope := env.get_scope(module_name) or { panic('missing module scope ${module_name}') } + obj := scope.lookup_parent(name, 0) or { panic('missing ${module_name}.${name}') } + assert obj is Global, '${module_name}.${name} should be a module storage object' + return obj as Global +} + +fn run_module_storage_v2_check(label string, files map[string]string, main_rel_path string) (int, string) { + tmp_dir := module_storage_tmp_dir(label) + os.mkdir_all(tmp_dir) or { panic('cannot create ${tmp_dir}') } + defer { + os.rmdir_all(tmp_dir) or {} + } + write_module_storage_files(tmp_dir, files) + main_path := os.join_path(tmp_dir, main_rel_path) + out_path := os.join_path(tmp_dir, 'out.txt') + cmd := '${os.quoted_path(@VEXE)} -v2 -backend v -o ${os.quoted_path(out_path)} ${os.quoted_path(main_path)} 2>&1' + res := os.execute(cmd) + return res.exit_code, res.output +} + +fn module_storage_v2_fixture_sources(name string) map[string]string { + fixture_dir := os.join_path(os.dir(os.dir(@FILE)), 'tests', name) + mut sources := map[string]string{} + files := os.walk_ext(fixture_dir, '.vv2') + for file in files { + rel_path := file.all_after(fixture_dir + os.path_separator).replace('.vv2', '.v') + sources[rel_path] = os.read_file(file) or { panic('cannot read ${file}') } + } + return sources +} + +fn test_module_storage_decl_metadata_keeps_spelling_out_of_semantics() { + env := check_module_storage_files({ + 'report/report.v': 'module report + +pub __global mut errors = 0 +__global hidden = 1 +' + }) + errors := module_storage_global(env, 'report', 'errors') + assert errors.mod == 'report' + assert errors.is_public + assert errors.is_mut + assert errors.name == 'errors' + hidden := module_storage_global(env, 'report', 'hidden') + assert hidden.mod == 'report' + assert !hidden.is_public + assert !hidden.is_mut +} + +fn test_module_storage_public_qualified_access_accepts_import_alias() { + code, output := run_module_storage_v2_check('public_alias', { + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'import report as r + +fn main() { + r.errors += 1 +} +' + }, 'main.v') + assert code == 0, output +} + +fn test_module_storage_private_same_module_files_accept_bare_access() { + check_module_storage_files({ + 'report/state.v': 'module report + +__global mut errors = 0 +' + 'report/api.v': 'module report + +fn inc() { + errors += 1 +} +' + }) +} + +fn test_module_storage_imported_names_are_not_bare_scope() { + code, output := run_module_storage_v2_check('bare_import', { + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'import report + +fn main() { + _ = errors +} +' + }, 'main.v') + assert code != 0, 'bare imported module storage should fail' + assert output.contains('unknown ident `errors'), output +} + +fn test_module_storage_private_qualified_access_fails() { + code, output := run_module_storage_v2_check('private_qualified', { + 'report/report.v': 'module report + +__global mut hidden = 0 +' + 'main.v': 'import report + +fn main() { + report.hidden += 1 +} +' + }, 'main.v') + assert code != 0, 'private module storage should not be reachable from another module' + assert output.contains('module global `report.hidden` is private'), output +} + +fn test_module_storage_selective_import_does_not_make_bare_global() { + code, output := run_module_storage_v2_check('selective_import', { + 'report/report.v': 'module report + +pub __global mut errors = 0 +' + 'main.v': 'import report { errors } + +fn main() { + _ = errors +} +' + }, 'main.v') + assert code != 0, 'selective import must not expose module storage by bare name' + assert output.contains('unknown ident `errors'), output +} + +fn test_module_storage_legacy_assignment_without_mut_is_allowed_for_compat() { + code, output := run_module_storage_v2_check('legacy_assignment', { + 'main.v': '__global frozen = 0 + +fn main() { + frozen = 1 +} +' + }, 'main.v') + assert code == 0, output +} + +fn test_module_storage_legacy_compound_mutation_without_mut_is_allowed_for_compat() { + code, output := run_module_storage_v2_check('legacy_compound', { + 'main.v': '__global frozen = 0 + +fn main() { + frozen += 1 +} +' + }, 'main.v') + assert code == 0, output +} + +fn test_module_storage_legacy_postfix_mutation_without_mut_is_allowed_for_compat() { + code, output := run_module_storage_v2_check('legacy_postfix', { + 'main.v': '__global frozen = 0 + +fn main() { + frozen++ +} +' + }, 'main.v') + assert code == 0, output +} + +fn test_module_storage_same_short_names_in_distinct_modules_are_allowed() { + env := check_module_storage_files({ + 'a/a.v': 'module a + +pub __global state = 1 +' + 'b/b.v': 'module b + +pub __global state = 2 +' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.state + b.state +} +' + }) + a_state := module_storage_global(env, 'a', 'state') + b_state := module_storage_global(env, 'b', 'state') + assert a_state.mod == 'a' + assert b_state.mod == 'b' +} + +fn test_module_storage_dunder_name_accepts_bare_access_in_declaring_module() { + env := check_module_storage_files({ + 'a/a.v': 'module a + +pub __global mut state__x = 1 + +pub fn bump() int { + state__x += 1 + return state__x +} +' + 'b/b.v': 'module b + +pub __global mut state__x = 2 + +pub fn bump() int { + state__x += 1 + return state__x +} +' + 'main.v': 'module main + +import a +import b + +fn main() { + _ = a.bump() + b.bump() + _ = a.state__x + b.state__x +} +' + }) + a_state := module_storage_global(env, 'a', 'state__x') + b_state := module_storage_global(env, 'b', 'state__x') + assert a_state.mod == 'a' + assert b_state.mod == 'b' + assert a_state.is_mut + assert b_state.is_mut +} + +fn test_module_storage_c_global_decl_keeps_c_selector_path() { + env := check_module_storage_files({ + 'main.v': '@[c_extern] +__global C.errno int + +@[c_extern] +__global C.stdin voidptr + +@[c_extern] +__global C.stdout voidptr + +@[c_extern] +__global C.stderr voidptr + +fn main() { + _ = C.errno + _ = C.stdin + _ = C.stdout + _ = C.stderr +} +' + }) + scope := env.get_scope('main') or { panic('missing main scope') } + for name in ['C.errno', 'C.stdin', 'C.stdout', 'C.stderr'] { + obj := scope.lookup_parent(name, 0) or { panic('missing ${name}') } + assert obj is Global + c_global := obj as Global + assert !c_global.is_module_storage() + } +} + +fn test_module_storage_c_extern_global_attributes_keep_c_path() { + env := check_module_storage_files({ + 'main.v': '@[c_extern; export: "raw_counter"; weak; hidden; markused] +__global raw_counter int + +fn main() { + _ = raw_counter +} +' + }) + scope := env.get_scope('main') or { panic('missing main scope') } + obj := scope.lookup_parent('raw_counter', 0) or { panic('missing raw_counter') } + assert obj is Global + c_global := obj as Global + assert !c_global.is_module_storage() +} + +fn test_module_storage_example_fixture_checks() { + code, output := run_module_storage_v2_check('example_fixture', + module_storage_v2_fixture_sources('module_storage_example'), 'main.v') + assert code == 0, output +} diff --git a/vlib/v2/types/object.v b/vlib/v2/types/object.v index f871a078e..34eb62276 100644 --- a/vlib/v2/types/object.v +++ b/vlib/v2/types/object.v @@ -23,7 +23,10 @@ pub: } struct Global { - name string + name string + mod string + is_public bool + is_mut bool mut: typ Type } -- 2.39.5