From 6b20a5ee7d2b42d98e601da4bef94a01d259e311 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 13 May 2026 22:16:55 +0300 Subject: [PATCH] builtin: prealloc + fasthttp/veb fixes; v2: ownership fixes --- vlib/builtin/prealloc.c.v | 144 +++++++-- vlib/fasthttp/README.md | 9 +- vlib/v/gen/c/cgen.v | 12 +- vlib/v/gen/c/spawn_and_go.v | 48 ++- vlib/v/gen/c/thread_bool_wait_codegen_test.v | 23 ++ vlib/v2/gen/cleanc/array.v | 63 +++- vlib/v2/gen/cleanc/assign.v | 41 ++- vlib/v2/gen/cleanc/cleanc.v | 52 +++- vlib/v2/gen/cleanc/expr.v | 62 +++- vlib/v2/gen/cleanc/flag_enum_codegen_test.v | 288 ++++++++++++++++++ vlib/v2/gen/cleanc/fn.v | 78 +++-- vlib/v2/gen/cleanc/for.v | 2 + vlib/v2/gen/cleanc/if.v | 14 +- .../gen/cleanc/result_option_codegen_test.v | 40 +++ vlib/v2/gen/cleanc/stmt.v | 53 +++- vlib/v2/gen/cleanc/struct.v | 64 +++- vlib/v2/gen/cleanc/types.v | 131 +++++++- vlib/v2/markused/markused.v | 117 +++++-- vlib/v2/markused/markused_test.v | 239 +++++++++++++++ vlib/v2/transformer/expr.v | 2 +- vlib/v2/transformer/fn.v | 66 +++- vlib/v2/transformer/if.v | 2 +- vlib/v2/transformer/struct.v | 6 + vlib/v2/transformer/transformer.v | 173 +++++++++++ vlib/v2/transformer/transformer_test.v | 78 +++++ vlib/v2/transformer/types.v | 2 +- vlib/v2/types/checker.v | 10 +- vlib/v2/types/types.v | 5 + vlib/veb/README.md | 14 +- 29 files changed, 1719 insertions(+), 119 deletions(-) diff --git a/vlib/builtin/prealloc.c.v b/vlib/builtin/prealloc.c.v index 22d78de81..63d726fdd 100644 --- a/vlib/builtin/prealloc.c.v +++ b/vlib/builtin/prealloc.c.v @@ -1,6 +1,13 @@ @[has_globals] module builtin +#insert "@VEXEROOT/vlib/builtin/prealloc_atomics.h" + +fn C.v_prealloc_atomic_add_i32(ptr &int, delta int) int +fn C.v_prealloc_atomic_load_i32(ptr &int) int +fn C.v_prealloc_atomic_store_i32(ptr &int, val int) int +fn C.v_prealloc_atomic_cas_i32(ptr &int, expected int, desired int) int + // With -prealloc, V calls libc's malloc to get chunks, each at least 16MB // in size, as needed. Once a chunk is available, all malloc() calls within // V code, that can fit inside the chunk, will use it instead, each bumping a @@ -28,11 +35,12 @@ __global g_memory_block &VMemoryBlock @[heap] struct VMemoryBlock { mut: - current &u8 = 0 // 8 - stop &u8 = 0 // 8 - start &u8 = 0 // 8 - previous &VMemoryBlock = 0 // 8 - next &VMemoryBlock = 0 // 8 + current &u8 = 0 // 8 + stop &u8 = 0 // 8 + start &u8 = 0 // 8 + previous &VMemoryBlock = 0 // 8 + next &VMemoryBlock = 0 // 8 + scope &VPreallocScope = 0 min_block_size isize is_scope bool id int // 4 @@ -42,8 +50,12 @@ mut: @[heap] struct VPreallocScope { mut: - previous &VMemoryBlock = 0 - first &VMemoryBlock = 0 + previous &VMemoryBlock = 0 + first &VMemoryBlock = 0 + refs int + free_requested int + abandoned int + finalized int } fn vmemory_abort_on_nil(p voidptr, bytes isize) { @@ -199,12 +211,16 @@ fn vmemory_block_malloc(n isize, align isize) &u8 { mut current := vmemory_align_up(g_memory_block.current, fixed_align) remaining := i64(g_memory_block.stop) - i64(current) if _unlikely_(remaining < n) { + was_scope := g_memory_block.is_scope + scope := g_memory_block.scope min_block_size := if g_memory_block.min_block_size > 0 { g_memory_block.min_block_size } else { isize(prealloc_block_size) } g_memory_block = vmemory_block_new_sized(g_memory_block, n, fixed_align, min_block_size) + g_memory_block.is_scope = was_scope + g_memory_block.scope = scope current = vmemory_align_up(g_memory_block.current, fixed_align) } res := &u8(current) @@ -381,6 +397,7 @@ pub fn prealloc_scope_begin() voidptr { scope.first = vmemory_block_new_sized(scope.previous, isize(prealloc_scope_block_size), 0, isize(prealloc_scope_block_size)) scope.first.is_scope = true + scope.first.scope = scope g_memory_block = scope.first prealloc_trace_scope(c'begin', scope) return scope @@ -430,6 +447,99 @@ fn prealloc_scope_free_blocks(scope &VPreallocScope) { } } +@[unsafe] +fn prealloc_scope_request_free(scope &VPreallocScope, abandoned bool) { + if scope == unsafe { nil } { + return + } + unsafe { + if abandoned { + C.v_prealloc_atomic_store_i32(&scope.abandoned, 1) + } + C.v_prealloc_atomic_store_i32(&scope.free_requested, 1) + prealloc_scope_finish_if_ready(scope) + } +} + +@[unsafe] +fn prealloc_scope_finish_if_ready(scope &VPreallocScope) { + if scope == unsafe { nil } { + return + } + unsafe { + if C.v_prealloc_atomic_load_i32(&scope.free_requested) == 0 { + return + } + if C.v_prealloc_atomic_load_i32(&scope.refs) != 0 { + return + } + if C.v_prealloc_atomic_cas_i32(&scope.finalized, 0, 1) == 0 { + return + } + if C.v_prealloc_atomic_load_i32(&scope.abandoned) == 0 { + prealloc_scope_free_blocks(scope) + } + C.free(scope) + } +} + +@[unsafe] +fn prealloc_scope_detach_current(scope &VPreallocScope) { + if scope == unsafe { nil } { + return + } + unsafe { + previous := scope.previous + if previous != 0 { + previous.next = nil + } + if g_memory_block != 0 && g_memory_block.is_scope && g_memory_block.scope == scope { + g_memory_block = previous + } + scope.previous = nil + } +} + +// prealloc_scope_retain_current keeps the current scoped arena alive after the +// owner calls `prealloc_scope_end`/`prealloc_scope_free_after`. It is used by +// generated `spawn` wrappers so detached threads can safely receive arguments +// allocated in a request arena. +@[unsafe] +pub fn prealloc_scope_retain_current() voidptr { + $if prealloc { + unsafe { + if g_memory_block == 0 || !g_memory_block.is_scope || g_memory_block.scope == 0 { + return nil + } + scope := g_memory_block.scope + C.v_prealloc_atomic_add_i32(&scope.refs, 1) + $if trace_prealloc ? { + prealloc_trace_scope(c'retain', scope) + } + return scope + } + } $else { + return unsafe { nil } + } +} + +@[unsafe] +pub fn prealloc_scope_release(scope_ptr voidptr) { + $if prealloc { + if scope_ptr == unsafe { nil } { + return + } + unsafe { + scope := &VPreallocScope(scope_ptr) + C.v_prealloc_atomic_add_i32(&scope.refs, -1) + $if trace_prealloc ? { + prealloc_trace_scope(c'release', scope) + } + prealloc_scope_finish_if_ready(scope) + } + } +} + // prealloc_scope_end frees a nested arena and restores the current thread arena // to the state before `prealloc_scope_begin`. @[unsafe] @@ -440,9 +550,8 @@ pub fn prealloc_scope_end(scope_ptr voidptr) { unsafe { scope := &VPreallocScope(scope_ptr) prealloc_trace_scope(c'end', scope) - prealloc_scope_free_blocks(scope) - g_memory_block = scope.previous - C.free(scope) + prealloc_scope_detach_current(scope) + prealloc_scope_request_free(scope, false) } } @@ -456,12 +565,7 @@ pub fn prealloc_scope_leave(scope_ptr voidptr) { unsafe { scope := &VPreallocScope(scope_ptr) prealloc_trace_scope(c'leave', scope) - previous := scope.previous - if previous != 0 { - previous.next = nil - } - g_memory_block = previous - scope.previous = nil + prealloc_scope_detach_current(scope) } } @@ -474,9 +578,10 @@ pub fn prealloc_scope_abandon(scope_ptr voidptr) { return } unsafe { - prealloc_trace_scope(c'abandon', &VPreallocScope(scope_ptr)) + scope := &VPreallocScope(scope_ptr) + prealloc_trace_scope(c'abandon', scope) prealloc_scope_leave(scope_ptr) - C.free(scope_ptr) + prealloc_scope_request_free(scope, true) } } @@ -491,8 +596,7 @@ pub fn prealloc_scope_free_after(scope_ptr voidptr) { unsafe { scope := &VPreallocScope(scope_ptr) prealloc_trace_scope(c'free-after', scope) - prealloc_scope_free_blocks(scope) - C.free(scope) + prealloc_scope_request_free(scope, false) } } diff --git a/vlib/fasthttp/README.md b/vlib/fasthttp/README.md index d7e52d0a8..625305278 100644 --- a/vlib/fasthttp/README.md +++ b/vlib/fasthttp/README.md @@ -185,9 +185,12 @@ the scope from the request thread and frees it after the write completes. This means request-local V allocations are cheap bump-pointer allocations, and freeing them does not require walking individual objects. Startup state, server state, and allocations made directly by C libraries are not part of a request -arena. Manual takeover responses transfer ownership to user code and currently -abandon the request arena, so long-lived takeover handlers should manage their -own allocation lifetime explicitly. +arena. If a handler starts V `spawn` work while the request scope is active, the +generated thread wrapper retains that scope until the spawned function returns; +void spawned functions also run inside their own scoped arena, which is freed at +thread exit. Manual takeover responses transfer ownership to user code and +currently abandon the request arena, so long-lived takeover handlers should +manage their own allocation lifetime explicitly. To inspect request arena usage while developing, build with: diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 06d3ea16a..6029557c4 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -1988,13 +1988,21 @@ fn (mut g Gen) register_thread_wait_call(eltyp string) { g.gowrappers.writeln('\tif (stat != WAIT_OBJECT_0) { builtin___v_panic(_S("error waiting thread")); }') g.gowrappers.writeln('\tCloseHandle(thread.handle);') g.gowrappers.writeln('\tres = *(${eltyp}*)(thread.ret_ptr);') - g.gowrappers.writeln('\tbuiltin___v_free(thread.ret_ptr);') + if g.pref.prealloc { + g.gowrappers.writeln('\tfree(thread.ret_ptr);') + } else { + g.gowrappers.writeln('\tbuiltin___v_free(thread.ret_ptr);') + } } else { g.gowrappers.writeln('\tvoid* ret_val;') g.gowrappers.writeln('\tint stat = pthread_join(thread, &ret_val);') g.gowrappers.writeln('\tif (stat != 0) { builtin___v_panic(_S("error waiting thread")); }') g.gowrappers.writeln('\tres = *(${eltyp}*)ret_val;') - g.gowrappers.writeln('\tbuiltin___v_free(ret_val);') + if g.pref.prealloc { + g.gowrappers.writeln('\tfree(ret_val);') + } else { + g.gowrappers.writeln('\tbuiltin___v_free(ret_val);') + } } g.gowrappers.writeln('\treturn res;') g.gowrappers.writeln('}') diff --git a/vlib/v/gen/c/spawn_and_go.v b/vlib/v/gen/c/spawn_and_go.v index adbc11d06..5a14e9ae0 100644 --- a/vlib/v/gen/c/spawn_and_go.v +++ b/vlib/v/gen/c/spawn_and_go.v @@ -89,7 +89,12 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { wrapper_fn_name := name + '_thread_wrapper' arg_tmp_var := 'arg_' + tmp if is_spawn { - g.writeln('${wrapper_struct_name} *${arg_tmp_var} = (${wrapper_struct_name} *) builtin___v_malloc(sizeof(thread_arg_${name}));') + if g.pref.prealloc { + g.writeln('${wrapper_struct_name} *${arg_tmp_var} = (${wrapper_struct_name} *) malloc(sizeof(thread_arg_${name}));') + g.writeln('if (${arg_tmp_var} == NULL) builtin___v_panic(_S("thread argument allocation failed"));') + } else { + g.writeln('${wrapper_struct_name} *${arg_tmp_var} = (${wrapper_struct_name} *) builtin___v_malloc(sizeof(thread_arg_${name}));') + } } else if is_go { g.writeln('${wrapper_struct_name} ${arg_tmp_var};') } @@ -127,6 +132,9 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { g.expr(arg.expr) g.writeln(';') } + if is_spawn && g.pref.prealloc { + g.writeln('${arg_tmp_var}->prealloc_scope = builtin__prealloc_scope_retain_current();') + } call_ret_type := if expr.is_fn_var && g.cur_fn != unsafe { nil } && g.cur_concrete_types.len > 0 && g.cur_fn.generic_names.len > 0 { // In generic contexts, node.call_expr.return_type may be stale from @@ -157,7 +165,12 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { } s_ret_typ := g.styp(g.unwrap_generic(call_ret_type)) if g.pref.os == .windows && call_ret_type != ast.void_type { - g.writeln('${arg_tmp_var}->ret_ptr = (void *) builtin___v_malloc(sizeof(${s_ret_typ}));') + if g.pref.prealloc { + g.writeln('${arg_tmp_var}->ret_ptr = (void *) malloc(sizeof(${s_ret_typ}));') + g.writeln('if (${arg_tmp_var}->ret_ptr == NULL) builtin___v_panic(_S("thread return allocation failed"));') + } else { + g.writeln('${arg_tmp_var}->ret_ptr = (void *) builtin___v_malloc(sizeof(${s_ret_typ}));') + } } gohandle_name := g.gen_gohandle_name(call_ret_type) if is_spawn { @@ -336,6 +349,9 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { g.type_definitions.writeln('\t${styp} arg${i + 1};') } } + if is_spawn && g.pref.prealloc { + g.type_definitions.writeln('\tvoid* prealloc_scope;') + } if need_return_ptr { g.type_definitions.writeln('\tvoid* ret_ptr;') } @@ -343,11 +359,19 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { thread_ret_type := if g.pref.os == .windows { 'u32' } else { 'void*' } g.waiter_fn_definitions.writeln('${g.static_non_parallel}${thread_ret_type} ${wrapper_fn_name}(${wrapper_struct_name} *arg);') g.gowrappers.writeln('${thread_ret_type} ${wrapper_fn_name}(${wrapper_struct_name} *arg) {') + if is_spawn && g.pref.prealloc && wrapper_return_type == ast.void_type { + g.gowrappers.writeln('\tvoid* thread_prealloc_scope = builtin__prealloc_scope_begin();') + } if wrapper_return_type != ast.void_type { if g.pref.os == .windows { g.gowrappers.write_string('\t*((${wrapper_s_ret_typ}*)(arg->ret_ptr)) = ') } else { - g.gowrappers.writeln('\t${wrapper_s_ret_typ}* ret_ptr = (${wrapper_s_ret_typ}*) builtin___v_malloc(sizeof(${wrapper_s_ret_typ}));') + if g.pref.prealloc { + g.gowrappers.writeln('\t${wrapper_s_ret_typ}* ret_ptr = (${wrapper_s_ret_typ}*) malloc(sizeof(${wrapper_s_ret_typ}));') + g.gowrappers.writeln('\tif (ret_ptr == NULL) builtin___v_panic(_S("thread return allocation failed"));') + } else { + g.gowrappers.writeln('\t${wrapper_s_ret_typ}* ret_ptr = (${wrapper_s_ret_typ}*) builtin___v_malloc(sizeof(${wrapper_s_ret_typ}));') + } $if tinyc && arm64 { g.gowrappers.write_string('\t${wrapper_s_ret_typ} tcc_bug_tmp_var = ') } $else { @@ -445,8 +469,18 @@ fn (mut g Gen) spawn_and_go_expr(node ast.SpawnExpr, mode SpawnGoMode) { g.gowrappers.writeln('\t*ret_ptr = tcc_bug_tmp_var;') } } + if is_spawn && g.pref.prealloc { + if wrapper_return_type == ast.void_type { + g.gowrappers.writeln('\tbuiltin__prealloc_scope_end(thread_prealloc_scope);') + } + g.gowrappers.writeln('\tbuiltin__prealloc_scope_release(arg->prealloc_scope);') + } if is_spawn { - g.gowrappers.writeln('\tbuiltin___v_free(arg);') + if g.pref.prealloc { + g.gowrappers.writeln('\tfree(arg);') + } else { + g.gowrappers.writeln('\tbuiltin___v_free(arg);') + } } if g.pref.os != .windows && wrapper_return_type != ast.void_type { g.gowrappers.writeln('\treturn ret_ptr;') @@ -535,7 +569,11 @@ fn (mut g Gen) create_waiter_handler(call_ret_type ast.Type, s_ret_typ string, g } if call_ret_type != ast.void_type { g.gowrappers.writeln('\t${s_ret_typ} ret = *ret_ptr;') - g.gowrappers.writeln('\tbuiltin___v_free(ret_ptr);') + if g.pref.prealloc { + g.gowrappers.writeln('\tfree(ret_ptr);') + } else { + g.gowrappers.writeln('\tbuiltin___v_free(ret_ptr);') + } g.gowrappers.writeln('\treturn ret;') } g.gowrappers.writeln('}') diff --git a/vlib/v/gen/c/thread_bool_wait_codegen_test.v b/vlib/v/gen/c/thread_bool_wait_codegen_test.v index 76e79c237..43cec8f0c 100644 --- a/vlib/v/gen/c/thread_bool_wait_codegen_test.v +++ b/vlib/v/gen/c/thread_bool_wait_codegen_test.v @@ -26,6 +26,29 @@ fn test_thread_bool_waiter_is_declared_before_array_waiter_uses_it_on_windows() assert wait_call_idx > array_wait_def_idx, res.output } +fn test_prealloc_spawn_args_use_c_malloc() { + tmp_dir := os.join_path(os.vtmp_dir(), 'prealloc_spawn_arg_codegen_test_${os.getpid()}') + os.mkdir_all(tmp_dir)! + defer { + os.rmdir_all(tmp_dir) or {} + } + source_path := os.join_path(os.real_path(tmp_dir), 'prealloc_spawn_arg.vv') + os.write_file(source_path, + "fn worker(s string) {\n\tprintln(s)\n}\n\nfn answer() int {\n\treturn 42\n}\n\nfn main() {\n\tt := spawn worker('ok')\n\tt.wait()\n\tt2 := spawn answer()\n\tprintln(t2.wait())\n}\n")! + cmd := '${os.quoted_path(thread_bool_wait_codegen_vexe)} -prealloc -o - ${os.quoted_path(source_path)}' + res := os.execute(cmd) + assert res.exit_code == 0, '${cmd}\n${res.output}' + assert res.output.contains('(thread_arg_main__worker *) malloc(sizeof(thread_arg_main__worker))'), res.output + assert res.output.contains('prealloc_scope = builtin__prealloc_scope_retain_current();'), res.output + assert res.output.contains('void* thread_prealloc_scope = builtin__prealloc_scope_begin();'), res.output + assert res.output.contains('builtin__prealloc_scope_end(thread_prealloc_scope);'), res.output + assert res.output.contains('builtin__prealloc_scope_release(arg->prealloc_scope);'), res.output + assert res.output.contains('free(arg);'), res.output + assert !res.output.contains('builtin___v_malloc(sizeof(thread_arg_main__worker))'), res.output + assert res.output.contains('int* ret_ptr = (int*) malloc(sizeof(int));'), res.output + assert res.output.contains('free(ret_ptr);'), res.output +} + fn find_generated_c_line(lines []string, needle string, start int) int { for idx := start; idx < lines.len; idx++ { if lines[idx] == needle { diff --git a/vlib/v2/gen/cleanc/array.v b/vlib/v2/gen/cleanc/array.v index 1b0ab0d39..5cb612bad 100644 --- a/vlib/v2/gen/cleanc/array.v +++ b/vlib/v2/gen/cleanc/array.v @@ -500,16 +500,23 @@ fn (mut g Gen) infer_array_elem_type_from_expr(arr_expr ast.Expr) string { } if arr_expr is ast.ArrayInitExpr { - elem_type := g.extract_array_elem_type(arr_expr.typ) - if elem_type != '' { - return elem_type - } + mut elem_type := g.extract_array_elem_type(arr_expr.typ) if arr_expr.exprs.len > 0 { - first_expr_type := g.get_expr_type(arr_expr.exprs[0]) + first_expr_type := g.array_init_elem_type_from_expr(arr_expr.exprs[0]) + if elem_type != '' && first_expr_type != '' + && first_expr_type.starts_with('${elem_type}_T_') { + return first_expr_type + } + if elem_type != '' && elem_type != 'int' && elem_type != 'int_literal' { + return elem_type + } if first_expr_type != '' && first_expr_type != 'int' && first_expr_type != 'int_literal' { return first_expr_type } } + if elem_type != '' { + return elem_type + } } // For slice calls, the return type is generic `array`; resolve from the source array. if arr_expr is ast.CallExpr { @@ -695,9 +702,12 @@ fn (mut g Gen) gen_array_init_expr(node ast.ArrayInitExpr) { // Fallback: if dynamic array but elem type couldn't be extracted from type annotation, // infer from the first expression (e.g., for [][2]int where inner [2]int has type info) mut final_elem := elem_type - if final_elem == '' && is_dyn && node.exprs.len > 0 { - inferred := g.get_expr_type(node.exprs[0]) - if inferred != '' && inferred != 'array' && inferred != 'int' { + if is_dyn && node.exprs.len > 0 { + inferred := g.array_init_elem_type_from_expr(node.exprs[0]) + should_use_inferred := final_elem == '' || final_elem == 'int' + || final_elem == 'int_literal' + || (inferred != '' && final_elem != '' && inferred.starts_with('${final_elem}_T_')) + if should_use_inferred && inferred != '' && inferred != 'array' && inferred != 'int' { final_elem = inferred } } @@ -759,6 +769,40 @@ fn (mut g Gen) gen_array_init_expr(node ast.ArrayInitExpr) { g.sb.write_string('(array){0}') } +fn (mut g Gen) array_init_elem_type_from_expr(expr ast.Expr) string { + if expr is ast.ModifierExpr { + return g.array_init_elem_type_from_expr(expr.expr) + } + if expr is ast.ParenExpr { + return g.array_init_elem_type_from_expr(expr.expr) + } + if expr is ast.PrefixExpr && expr.op == .amp { + return g.array_init_elem_type_from_expr(expr.expr) + } + if expr is ast.ArrayInitExpr { + return g.infer_array_elem_type_from_expr(expr) + } + if expr is ast.Ident { + if local_type := g.get_local_var_c_type(expr.name) { + return local_type + } + } + if expr is ast.IfExpr { + return g.get_if_expr_type(&expr) + } + if expr is ast.InitExpr { + init_type := g.expr_type_to_c(expr.typ) + return g.specialized_generic_c_name_from_type_expr(expr.typ, init_type) + } + mut inferred := g.get_expr_type(expr) + if (inferred == '' || inferred == 'int' || inferred == 'int_literal') && expr is ast.CallExpr { + if ret := g.get_call_return_type(expr.lhs, expr.args) { + inferred = ret + } + } + return inferred +} + // extract_array_elem_expr extracts the element type expression from an array type fn (g &Gen) extract_array_elem_expr(e ast.Expr) ast.Expr { @@ -782,7 +826,8 @@ fn (g &Gen) extract_array_elem_expr(e ast.Expr) ast.Expr { fn (mut g Gen) extract_array_elem_type(e ast.Expr) string { elem_expr := g.extract_array_elem_expr(e) if elem_expr != ast.empty_expr { - return g.expr_type_to_c(elem_expr) + elem_type := g.expr_type_to_c(elem_expr) + return g.specialized_generic_c_name_from_type_expr(elem_expr, elem_type) } return '' } diff --git a/vlib/v2/gen/cleanc/assign.v b/vlib/v2/gen/cleanc/assign.v index 0fc83db89..8854a6085 100644 --- a/vlib/v2/gen/cleanc/assign.v +++ b/vlib/v2/gen/cleanc/assign.v @@ -397,6 +397,12 @@ fn (mut g Gen) gen_assign_stmt(node ast.AssignStmt) { typ = cast_type } } + if rhs is ast.InitExpr { + init_type := g.expr_type_to_c(rhs.typ) + if init_type != '' && init_type !in ['int', 'void'] { + typ = g.specialized_generic_c_name_from_type_expr(rhs.typ, init_type) + } + } if rhs is ast.PrefixExpr && rhs.op == .mul && rhs.expr is ast.CastExpr { cast_type := g.expr_type_to_c(rhs.expr.typ) if cast_type.ends_with('*') { @@ -408,7 +414,11 @@ fn (mut g Gen) gen_assign_stmt(node ast.AssignStmt) { // prefer the scope-registered type over the RHS expression type. if name.starts_with('_or_t') || name.starts_with('_tmp_') || name.starts_with('_defer_t') { if raw_type := g.get_raw_type(lhs) { - scope_type := g.types_type_to_c(raw_type) + mut scope_type := g.types_type_to_c(raw_type) + elem_type := legacy_fixed_array_elem_type(scope_type) + if elem_type != '' { + scope_type = elem_type + } if scope_type != '' && scope_type != 'int' { generic_container_fallback := (typ == 'array' && scope_type.starts_with('Array_')) @@ -584,7 +594,11 @@ fn (mut g Gen) gen_assign_stmt(node ast.AssignStmt) { if obj !is types.Module { obj_type := obj.typ() if obj_type !is types.Alias { - scoped_type := g.types_type_to_c(obj_type) + mut scoped_type := g.types_type_to_c(obj_type) + elem_type := legacy_fixed_array_elem_type(scoped_type) + if elem_type != '' { + scoped_type = elem_type + } generic_container_fallback := (typ == 'array' && scoped_type.starts_with('Array_')) || (typ == 'map' && scoped_type.starts_with('Map_')) @@ -719,6 +733,10 @@ fn (mut g Gen) gen_assign_stmt(node ast.AssignStmt) { } if rhs is ast.IfExpr { if !g.if_expr_can_be_ternary(&rhs) && rhs.else_expr !is ast.EmptyExpr { + if_type := g.get_if_expr_type(&rhs) + if if_type != '' && if_type != 'int' && if_type != 'void' { + typ = if_type + } // If type is void/empty, infer from the branch's last expression if typ == 'void' || typ == '' { if rhs.stmts.len > 0 { @@ -885,6 +903,25 @@ fn (mut g Gen) gen_assign_stmt(node ast.AssignStmt) { if typ == '' || typ == 'void' { typ = 'int' } + if rhs is ast.IfExpr { + if_type := g.get_if_expr_type(&rhs) + if if_type != '' && if_type != 'int' && if_type != 'void' { + typ = if_type + } + } + if rhs is ast.InitExpr { + typ = g.specialized_generic_c_name_from_type_expr(rhs.typ, typ) + } + if rhs is ast.IndexExpr { + elem_type := g.infer_array_elem_type_from_expr(rhs.lhs) + if elem_type != '' && elem_type != 'array' && elem_type != 'int' { + typ = elem_type + } + } + legacy_elem_type := legacy_fixed_array_elem_type(typ) + if legacy_elem_type != '' { + typ = legacy_elem_type + } g.register_alias_type(typ) if typ.starts_with('Array_') && !typ.starts_with('Array_fixed_') && !typ.ends_with('*') { g.sb.writeln('typedef array ${typ};') diff --git a/vlib/v2/gen/cleanc/cleanc.v b/vlib/v2/gen/cleanc/cleanc.v index a2950966f..d608c1297 100644 --- a/vlib/v2/gen/cleanc/cleanc.v +++ b/vlib/v2/gen/cleanc/cleanc.v @@ -38,8 +38,9 @@ mut: comptime_field_raw_type types.Type = types.Struct{} // raw types.Type for comptime checks comptime_field_attrs []string // current field attributes comptime_field_idx int // current field index - comptime_val_var string // the struct variable being decoded (e.g., 'val') - comptime_val_type string // C type of val (e.g., 'Slack') + comptime_continue_label string + comptime_val_var string // the struct variable being decoded (e.g., 'val') + comptime_val_type string // C type of val (e.g., 'Slack') fixed_array_fields map[string]bool fixed_array_field_elem map[string]string @@ -70,6 +71,7 @@ mut: ierror_wrapper_bases map[string]bool needed_ierror_wrapper_bases map[string]bool tmp_counter int + runtime_loop_depth 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 global_var_types map[string]string // global var name → C type string @@ -998,6 +1000,7 @@ pub fn (mut g Gen) gen_passes_1_to_4() { if spec_key !in g.fn_owner_file { g.fn_owner_file[spec_key] = fi } + g.undef_possible_str_macro(spec.name) g.gen_fn_head_with_name(stmt, spec.name) g.sb.writeln(';') } @@ -1011,6 +1014,50 @@ pub fn (mut g Gen) gen_passes_1_to_4() { } continue } + recv_generic_params := receiver_generic_param_names(stmt) + if recv_generic_params.len > 0 { + all_bindings := g.get_all_receiver_generic_bindings(stmt) + if all_bindings.len > 0 { + prev_generic_types := g.active_generic_types.clone() + prev_generic_c_names := g.active_generic_c_names.clone() + for bindings in all_bindings { + g.active_generic_types = bindings.clone() + g.active_generic_c_names = g.generic_c_names_for_bindings(bindings) + fn_name := g.get_fn_name(stmt) + if fn_name == '' { + continue + } + fn_key := 'fn_${fn_name}' + if fn_key !in g.fn_owner_file { + g.fn_owner_file[fn_key] = fi + } + g.undef_possible_str_macro(fn_name) + g.gen_fn_head_with_name(stmt, fn_name) + g.sb.writeln(';') + } + g.active_generic_types = prev_generic_types.clone() + g.active_generic_c_names = prev_generic_c_names.clone() + continue + } else if bindings := g.get_receiver_generic_bindings(stmt) { + prev_generic_types := g.active_generic_types.clone() + prev_generic_c_names := g.active_generic_c_names.clone() + g.active_generic_types = bindings.clone() + g.active_generic_c_names = g.generic_c_names_for_bindings(bindings) + fn_name := g.get_fn_name(stmt) + if fn_name != '' { + fn_key := 'fn_${fn_name}' + if fn_key !in g.fn_owner_file { + g.fn_owner_file[fn_key] = fi + } + g.undef_possible_str_macro(fn_name) + g.gen_fn_head_with_name(stmt, fn_name) + g.sb.writeln(';') + } + g.active_generic_types = prev_generic_types.clone() + g.active_generic_c_names = prev_generic_c_names.clone() + continue + } + } fn_name := g.get_fn_name(stmt) if fn_name == '' { continue @@ -1034,6 +1081,7 @@ pub fn (mut g Gen) gen_passes_1_to_4() { g.cur_fn_scope = fn_scope } } + g.undef_possible_str_macro(fn_name) g.gen_fn_head(stmt) g.sb.writeln(';') } diff --git a/vlib/v2/gen/cleanc/expr.v b/vlib/v2/gen/cleanc/expr.v index 2bb889b70..97b9b261e 100644 --- a/vlib/v2/gen/cleanc/expr.v +++ b/vlib/v2/gen/cleanc/expr.v @@ -1407,7 +1407,7 @@ fn (mut g Gen) gen_infix_expr(node &ast.InfixExpr) { g.expr(node.rhs) } struct_type := g.lookup_struct_type_by_c_name(cmp_type) - if struct_type.fields.len > 0 && g.struct_has_ref_fields(struct_type) { + if struct_type.fields.len > 0 { eq_expr := g.gen_struct_field_eq_expr(struct_type, ltmp, rtmp) if node.op == .eq { g.sb.write_string('; ${eq_expr}; })') @@ -3034,6 +3034,21 @@ fn (mut g Gen) gen_index_expr(node ast.IndexExpr) { return } } + lhs_unwrapped := g.unwrap_parens(node.lhs) + if lhs_unwrapped is ast.CallExpr { + call_name := g.resolve_call_name(lhs_unwrapped.lhs, lhs_unwrapped.args.len) + if call_name.contains('array_from_c_array') { + elem_type := g.infer_array_elem_type_from_expr(lhs_unwrapped) + if elem_type != '' && elem_type != 'array' && elem_type != 'int' { + g.sb.write_string('((${elem_type}*)') + g.expr(lhs_unwrapped) + g.sb.write_string('.data)[') + g.gen_index_expr_value(node.expr) + g.sb.write_string(']') + return + } + } + } // Fixed-size array struct fields are emitted as plain C arrays. if node.lhs is ast.SelectorExpr && g.is_fixed_array_selector(node.lhs) { g.expr(node.lhs) @@ -3054,7 +3069,13 @@ fn (mut g Gen) gen_index_expr(node ast.IndexExpr) { } if raw_type is types.Array { // Dynamic arrays: ((elem_type*)arr.data)[idx] - elem_type := g.types_type_to_c(raw_type.elem_type) + mut elem_type := g.types_type_to_c(raw_type.elem_type) + if (elem_type == '' || elem_type == 'int') && node.lhs is ast.CallExpr { + call_elem := g.infer_array_elem_type_from_expr(node.lhs) + if call_elem != '' && call_elem != 'array' && call_elem != 'int' { + elem_type = call_elem + } + } g.sb.write_string('((${elem_type}*)') g.expr(node.lhs) g.sb.write_string('.data)[') @@ -3065,7 +3086,13 @@ fn (mut g Gen) gen_index_expr(node ast.IndexExpr) { if raw_type is types.Alias { match raw_type.base_type { types.Array { - elem_type := g.types_type_to_c(raw_type.base_type.elem_type) + mut elem_type := g.types_type_to_c(raw_type.base_type.elem_type) + if (elem_type == '' || elem_type == 'int') && node.lhs is ast.CallExpr { + call_elem := g.infer_array_elem_type_from_expr(node.lhs) + if call_elem != '' && call_elem != 'array' && call_elem != 'int' { + elem_type = call_elem + } + } g.sb.write_string('((${elem_type}*)') g.expr(node.lhs) g.sb.write_string('.data)[') @@ -3285,11 +3312,19 @@ fn (mut g Gen) gen_index_expr(node ast.IndexExpr) { if node.lhs is ast.CallExpr { call_name := g.resolve_call_name(node.lhs.lhs, node.lhs.args.len) if call_name.contains('array_from_c_array') && node.lhs.args.len >= 3 { + if node.lhs.args.len >= 4 { + call_elem := g.infer_array_elem_type_from_expr(node.lhs.args[3]) + if call_elem != '' && call_elem != 'array' && call_elem != 'int' { + elem_type = call_elem + } + } // Extract elem type from sizeof(T) in 3rd arg - sizeof_arg := node.lhs.args[2] - if sizeof_arg is ast.KeywordOperator && sizeof_arg.op == .key_sizeof { - if sizeof_arg.exprs.len > 0 { - elem_type = g.expr_type_to_c(sizeof_arg.exprs[0]) + if elem_type == '' || elem_type == 'int' { + sizeof_arg := node.lhs.args[2] + if sizeof_arg is ast.KeywordOperator && sizeof_arg.op == .key_sizeof { + if sizeof_arg.exprs.len > 0 { + elem_type = g.expr_type_to_c(sizeof_arg.exprs[0]) + } } } } else if node.lhs.args.len > 0 { @@ -3654,6 +3689,17 @@ fn (g &Gen) comptime_matches_keyword(typ types.Type, keyword string) bool { } } +fn comptime_attrs_array_c_expr(attrs []string) string { + if attrs.len == 0 { + return '__new_array_with_default_noscan(0, 0, sizeof(string), NULL)' + } + mut parts := []string{cap: attrs.len} + for attr in attrs { + parts << c_static_v_string_expr(attr) + } + return 'new_array_from_c_array(${attrs.len}, ${attrs.len}, sizeof(string), &(string[${attrs.len}]){${parts.join(', ')}})' +} + // gen_comptime_field_selector handles comptime field access patterns: // - field.name → V string literal with field name // - field.name.str → C string literal @@ -3715,7 +3761,7 @@ fn (mut g Gen) gen_comptime_field_selector(sel ast.SelectorExpr) bool { return true } 'attrs' { - g.sb.write_string('((Array_string){0})') + g.sb.write_string(comptime_attrs_array_c_expr(g.comptime_field_attrs)) return true } else {} diff --git a/vlib/v2/gen/cleanc/flag_enum_codegen_test.v b/vlib/v2/gen/cleanc/flag_enum_codegen_test.v index 206a26ab5..32b9ef977 100644 --- a/vlib/v2/gen/cleanc/flag_enum_codegen_test.v +++ b/vlib/v2/gen/cleanc/flag_enum_codegen_test.v @@ -149,3 +149,291 @@ fn main() { assert !csrc.contains('int item;') assert !csrc.contains('int ptr_value(int foo)') } + +fn test_generate_c_match_on_enum_does_not_constrain_branch_array_literals() { + csrc := generate_c_for_test(' +enum Choice { + color + none +} + +fn choices(id Choice) []string { + return match id { + .color { ["never", "auto"] } + .none { []string{} } + } +} +') + assert csrc.contains('sizeof(string)') + assert !csrc.contains('&(Choice[') +} + +fn test_generate_c_struct_eq_recurses_into_nested_string_fields() { + csrc := generate_c_for_test(' +struct Encoding { + label string +} + +enum Kind { + auto + disabled + some +} + +struct Mode { + kind Kind = .auto + encoding Encoding +} + +fn same(a Mode, b Mode) bool { + return a == b +} +') + assert csrc.contains('string__eq(') + assert csrc.contains('.encoding.label') +} + +fn test_generate_c_emits_generic_method_helper_specialization_body() { + csrc := generate_c_for_test(' +struct Mapper {} + +struct Schema {} + +fn (mut m Mapper) helper[T]() int { + return 1 +} + +fn (mut m Mapper) parse[T]() int { + return m.helper[T]() +} + +fn main() { + mut m := Mapper{} + _ = m.parse[Schema]() +} +') + assert csrc.contains('int Mapper__parse_T_Schema(Mapper* m) {') + assert csrc.contains('int Mapper__helper_T_Schema(Mapper* m) {') +} + +fn test_generate_c_rewrites_continue_in_generic_comptime_field_loop() { + code := [ + 'struct Schema {', + ' skip int', + ' keep int', + '}', + '', + 'fn count_fields[T]() int {', + ' mut n := 0', + ' @DLR@for field in T.fields {', + ' if field.name == @SQ@skip@SQ@ {', + ' continue', + ' }', + ' n++', + ' }', + ' return n', + '}', + '', + 'fn main() {', + ' _ = count_fields[Schema]()', + '}', + ].join('\n').replace('@DLR@', '$').replace('@SQ@', "'") + csrc := generate_c_for_test(code) + assert csrc.contains('goto __v_ctf_continue_') + assert csrc.contains('__v_ctf_continue_') +} + +fn test_generate_c_exposes_generic_comptime_field_attrs() { + code := [ + 'struct Schema {', + ' field int @[repeats; short: u]', + '}', + '', + 'fn attrs[T]() []string {', + ' mut out := []string{}', + ' @DLR@for field in T.fields {', + ' out = field.attrs', + ' }', + ' return out', + '}', + '', + 'fn main() {', + ' _ = attrs[Schema]()', + '}', + ].join('\n').replace('@DLR@', '$') + csrc := generate_c_for_test(code) + assert csrc.contains('"repeats"') + assert csrc.contains('"short: u"') +} + +fn test_generate_c_struct_default_does_not_write_string_value_to_pointer_field() { + csrc := generate_c_for_test(' +struct Builder[^a] { + glob &^a string +} + +fn make[^a]() &Builder[^a] { + return &Builder[^a]{} +} + +fn (mut b Builder[^a]) touch[^a]() &Builder[^a] { + return &b +} +') + assert csrc.contains('string* glob;') + assert !csrc.contains('.glob = (string)') +} + +fn test_generate_c_specializes_plain_method_on_each_generic_receiver_instance() { + csrc := generate_c_for_test(' +enum MatchKind { + none + hit +} + +struct ValueA {} +struct ValueB {} + +struct Match[T] { + kind MatchKind = .none + value T +} + +fn first() Match[ValueA] { + return Match[ValueA]{ + kind: .hit + value: ValueA{} + } +} + +fn second() Match[ValueB] { + return Match[ValueB]{ + kind: .hit + value: ValueB{} + } +} + +fn (m Match[T]) is_hit() bool { + return m.kind == .hit +} + +fn main() { + _ = first().is_hit() + _ = second().is_hit() +} +') + assert csrc.contains('bool Match__is_hit(Match m) {') + assert csrc.contains('bool Match_T_ValueB__is_hit(Match_T_ValueB m);') + assert csrc.contains('bool Match_T_ValueB__is_hit(Match_T_ValueB m) {') + assert csrc.contains('return ((Match_T_ValueB){.kind = MatchKind__hit') + assert csrc.contains('Match__is_hit(first())') + assert csrc.contains('Match_T_ValueB__is_hit(second())') + assert !csrc.contains('Match__is_hit(second())') +} + +fn test_generate_c_declares_generic_struct_literal_with_specialized_type() { + csrc := generate_c_for_test(' +struct Other {} +struct Value {} + +struct Match[T] { + value T +} + +fn make_other() Match[Other] { + return Match[Other]{} +} + +fn make_match() Match[Value] { + return Match[Value]{} +} + +fn main() { + _ = make_other() + mut mat := Match[Value]{} + mat = make_match() +} +') + assert csrc.contains('Match_T_Value mat = ((Match_T_Value){0});') + assert csrc.contains('mat = make_match();') + assert !csrc.contains('Match mat = ((Match_T_Value){0});') +} + +fn test_generate_c_declares_lifetime_generic_struct_literal_with_specialized_type() { + csrc := generate_c_for_test(' +struct Other {} +struct IgnoreMatch[^a] {} + +struct Match[T] { + value T +} + +fn make_other() Match[Other] { + return Match[Other]{} +} + +fn matched[^a]() Match[IgnoreMatch[^a]] { + _ = make_other() + mut mat := Match[IgnoreMatch[^a]]{} + return mat +} + +fn main() { + _ = matched() +} +') + assert csrc.contains('Match_T_IgnoreMatch matched()') + assert csrc.contains('Match_T_IgnoreMatch mat = ((Match_T_IgnoreMatch){0});') + assert !csrc.contains('\tMatch mat = ((Match_T_IgnoreMatch){0});') +} + +fn test_generate_c_uses_specialized_lifetime_generic_type_for_if_expr_and_array_literal() { + csrc := generate_c_for_test(' +struct Other {} +struct IgnoreMatch[^a] {} + +struct Match[T] { + value T +} + +fn make_other() Match[Other] { + return Match[Other]{} +} + +fn make_match[^a]() Match[IgnoreMatch[^a]] { + return Match[IgnoreMatch[^a]]{} +} + +fn matched[^a](flag bool) Match[IgnoreMatch[^a]] { + _ = make_other() + a := make_match() + b := make_match() + mut selected := if flag { + make_match() + } else { + Match[IgnoreMatch[^a]]{} + } + for mat in [a, b, selected] { + return mat + } + return Match[IgnoreMatch[^a]]{} +} +') + assert csrc.contains('Match_T_IgnoreMatch selected = ({ Match_T_IgnoreMatch _if_expr_t') + assert csrc.contains('new_array_from_c_array(3, 3, sizeof(Match_T_IgnoreMatch)') + assert csrc.contains('Match_T_IgnoreMatch mat = ') + assert !csrc.contains('new_array_from_c_array(3, 3, sizeof(int)') +} + +fn test_generate_c_fixed_array_index_expr_has_element_type() { + csrc := generate_c_for_test(' +const month_days = [31, 28, 31]! + +fn max_day(month int) int { + value := month_days[month - 1] + 1 + return value +} +') + assert csrc.contains('int value = (month_days') + assert !csrc.contains('fixed_int_3 value') +} diff --git a/vlib/v2/gen/cleanc/fn.v b/vlib/v2/gen/cleanc/fn.v index e0fa58cd7..b84dc5001 100644 --- a/vlib/v2/gen/cleanc/fn.v +++ b/vlib/v2/gen/cleanc/fn.v @@ -768,7 +768,9 @@ fn (mut g Gen) get_all_receiver_generic_bindings(node ast.FnDecl) []map[string]t if instances := g.generic_struct_instances[struct_name] { if instances.len > 0 { mut all_bindings := []map[string]types.Type{cap: instances.len} - all_bindings << g.primary_generic_struct_instance(struct_name, instances).bindings + for inst in instances { + all_bindings << inst.bindings.clone() + } return all_bindings } } @@ -777,7 +779,9 @@ fn (mut g Gen) get_all_receiver_generic_bindings(node ast.FnDecl) []map[string]t if instances := g.generic_struct_instances[recv_c_name] { if instances.len > 0 { mut all_bindings := []map[string]types.Type{cap: instances.len} - all_bindings << g.primary_generic_struct_instance(recv_c_name, instances).bindings + for inst in instances { + all_bindings << inst.bindings.clone() + } return all_bindings } } @@ -1583,9 +1587,6 @@ fn (mut g Gen) gen_fn_decl(node ast.FnDecl) { } fn (mut g Gen) gen_fn_decl_ptr(node &ast.FnDecl) { - if !g.should_emit_fn_decl(g.cur_module, *node) { - return - } // All other functions are resolved from the host executable. if g.pref != unsafe { nil } && g.pref.is_shared_lib { if !node.attributes.has('live') { @@ -1603,6 +1604,31 @@ fn (mut g Gen) gen_fn_decl_ptr(node &ast.FnDecl) { if node.language == .c && node.stmts.len == 0 { return } + if g.generic_fn_param_names(*node).len > 0 { + // maxof[T]/minof[T] are compile-time functions fully inlined by the transformer. + // Skip emitting stub bodies (which contain invalid C). + if node.name in ['maxof', 'minof'] { + return + } + prev_generic_types := g.active_generic_types.clone() + prev_generic_c_names := g.active_generic_c_names.clone() + specs := g.generic_fn_specializations(*node) + if specs.len > 0 { + for spec in specs { + g.active_generic_types = spec.generic_types.clone() + g.active_generic_c_names = g.generic_c_names_for_bindings(spec.generic_types) + g.gen_fn_decl_with_name_ptr(node, spec.name) + } + g.active_generic_types = prev_generic_types.clone() + g.active_generic_c_names = prev_generic_c_names.clone() + return + } + g.active_generic_types = prev_generic_types.clone() + g.active_generic_c_names = prev_generic_c_names.clone() + } + if !g.should_emit_fn_decl(g.cur_module, *node) { + return + } // eventbus module: all generic struct methods use T = string. // For subscribe_method/unsubscribe_method, the specialized name (_string suffix) is used. // For other methods (publish, has_subscriber, etc.), keep the base name but resolve T = string. @@ -1630,21 +1656,6 @@ fn (mut g Gen) gen_fn_decl_ptr(node &ast.FnDecl) { g.active_generic_types = prev_generic_types.clone() } if g.generic_fn_param_names(*node).len > 0 { - // maxof[T]/minof[T] are compile-time functions fully inlined by the transformer. - // Skip emitting stub bodies (which contain invalid C). - if node.name in ['maxof', 'minof'] { - return - } - prev_generic_types := g.active_generic_types.clone() - prev_generic_c_names := g.active_generic_c_names.clone() - specs := g.generic_fn_specializations(*node) - for spec in specs { - g.active_generic_types = spec.generic_types.clone() - g.active_generic_c_names = g.generic_c_names_for_bindings(spec.generic_types) - g.gen_fn_decl_with_name_ptr(node, spec.name) - } - g.active_generic_types = prev_generic_types.clone() - g.active_generic_c_names = prev_generic_c_names.clone() return } @@ -1695,6 +1706,14 @@ fn (mut g Gen) gen_fn_decl_with_name(node ast.FnDecl, fn_name string) { g.gen_fn_decl_with_name_ptr(&node, fn_name) } +fn (mut g Gen) undef_possible_str_macro(fn_name string) { + if fn_name.ends_with('__str') || fn_name.ends_with('_str') { + g.sb.writeln('#ifdef ${fn_name}') + g.sb.writeln('#undef ${fn_name}') + g.sb.writeln('#endif') + } +} + // Keep pass 5 on pointer-based FnDecl access; large by-value copies here // corrupt the self-hosted cleanc caller after the first emitted body. fn (mut g Gen) gen_fn_decl_with_name_ptr(node &ast.FnDecl, fn_name string) { @@ -1730,6 +1749,16 @@ fn (mut g Gen) gen_fn_decl_with_name_ptr(node &ast.FnDecl, fn_name string) { } else { 'void' } + if !is_c_main && node.typ.return_type !is ast.EmptyExpr { + mut declared_ret_type := g.expr_type_to_c(node.typ.return_type) + declared_ret_type = g.specialized_generic_c_name_from_type_expr(node.typ.return_type, + declared_ret_type) + if declared_ret_type != '' && declared_ret_type != 'void' && (g.cur_fn_ret_type == '' + || g.cur_fn_ret_type == 'int' + || declared_ret_type.starts_with('${g.cur_fn_ret_type}_T_')) { + g.cur_fn_ret_type = declared_ret_type + } + } g.cur_fn_ret_type = normalize_signature_type_name(g.cur_fn_ret_type, 'void') g.cur_fn_c_ret_type = g.c_fn_return_type_from_v(g.cur_fn_ret_type) if g.env != unsafe { nil } { @@ -1847,6 +1876,7 @@ fn (mut g Gen) gen_fn_decl_with_name_ptr(node &ast.FnDecl, fn_name string) { is_live_fn := !is_c_main && node.attributes.has('live') // Generate function header (with impl_live_ prefix for @[live] functions) + g.undef_possible_str_macro(fn_name) if is_live_fn { g.gen_fn_head_live_ptr(node, fn_name) } else { @@ -3275,6 +3305,7 @@ fn (mut g Gen) resolve_container_method_name(receiver ast.Expr, method_name stri } fn (mut g Gen) resolve_call_name(lhs ast.Expr, arg_count int) string { + _ = arg_count mut name := '' if lhs is ast.Ident { name = sanitize_fn_ident(lhs.name) @@ -4734,6 +4765,13 @@ fn (mut g Gen) call_expr(lhs ast.Expr, args []ast.Expr) { continue } } + if c_name == 'new_array_from_c_array' && i == 2 && call_args.len >= 4 { + elem_type := g.array_init_elem_type_from_expr(call_args[3]) + if elem_type != '' && elem_type != 'int' && elem_type != 'array' { + g.sb.write_string('sizeof(${elem_type})') + continue + } + } g.gen_call_arg(c_name, i, call_args[i]) } else { // Default arguments should be lowered by transformer; keep C generation moving. diff --git a/vlib/v2/gen/cleanc/for.v b/vlib/v2/gen/cleanc/for.v index 84447fb02..d1b6afbeb 100644 --- a/vlib/v2/gen/cleanc/for.v +++ b/vlib/v2/gen/cleanc/for.v @@ -42,7 +42,9 @@ fn (mut g Gen) gen_for_stmt(node ast.ForStmt) { // (e.g. _filter_it in nested map/filter) can shadow outer variables. // Without save/restore, the inner type overwrites the outer in the flat map. saved_local_types := g.runtime_local_types.clone() + g.runtime_loop_depth++ g.gen_stmts(node.stmts) + g.runtime_loop_depth-- g.runtime_local_types = saved_local_types.clone() g.not_local_var_cache = map[string]bool{} g.indent-- diff --git a/vlib/v2/gen/cleanc/if.v b/vlib/v2/gen/cleanc/if.v index 3074e2881..b88a5c95b 100644 --- a/vlib/v2/gen/cleanc/if.v +++ b/vlib/v2/gen/cleanc/if.v @@ -433,12 +433,24 @@ fn (mut g Gen) gen_if_expr_value(node &ast.IfExpr) { } fn (mut g Gen) get_if_expr_type(node &ast.IfExpr) string { + mut env_type := '' if t := g.get_expr_type_from_env(ast.Expr(*node)) { if t != '' { - return t + env_type = t } } branch_type := g.branch_result_type(node.stmts) + if branch_type != '' && branch_type != 'int' { + if env_type == '' || env_type == 'int' || branch_type.starts_with('${env_type}_T_') { + return branch_type + } + } + if env_type != '' { + if g.cur_fn_ret_type.starts_with('${env_type}_T_') { + return g.cur_fn_ret_type + } + return env_type + } if branch_type != '' && branch_type != 'int' { return branch_type } diff --git a/vlib/v2/gen/cleanc/result_option_codegen_test.v b/vlib/v2/gen/cleanc/result_option_codegen_test.v index c0083c944..0fd34a9a2 100644 --- a/vlib/v2/gen/cleanc/result_option_codegen_test.v +++ b/vlib/v2/gen/cleanc/result_option_codegen_test.v @@ -73,3 +73,43 @@ fn copy_bag(b Bag) Bag { assert csrc.contains('.state == 0') assert !csrc.contains('if (s)') } + +fn test_generate_c_wraps_struct_field_option_value() { + csrc := generate_result_option_c_for_test(' +struct Ref { + value int +} + +struct Holder { + item ?&Ref +} + +fn make(r &Ref) Holder { + return Holder{ + item: r + } +} +') + assert csrc.contains('_option_Refptr item;') + assert csrc.contains('_option_Refptr _opt = (_option_Refptr){ .state = 2 }; Ref* _val = r; _option_ok(&_val, (_option*)&_opt, sizeof(_val)); _opt;') + assert !csrc.contains('.item = r') +} + +fn test_generate_c_borrows_option_field_unwrap_payload_without_temp() { + csrc := generate_result_option_c_for_test(' +struct Holder { + value ?string +} + +fn (h &Holder) value_ref() ?&string { + if h.value != none { + return unsafe { &h.value? } + } + return none +} +') + assert !csrc.contains('.is_error') + assert !csrc.contains('_or_t') + assert csrc.contains('(h)->value.state != 0') + assert csrc.contains('&(*(string*)(((u8*)(&(h)->value.err)) + sizeof(IError)))') +} diff --git a/vlib/v2/gen/cleanc/stmt.v b/vlib/v2/gen/cleanc/stmt.v index 05ad2533d..1f525cdcf 100644 --- a/vlib/v2/gen/cleanc/stmt.v +++ b/vlib/v2/gen/cleanc/stmt.v @@ -391,7 +391,11 @@ fn (mut g Gen) gen_stmt(node ast.Stmt) { if node.op == .key_break { g.sb.writeln('break;') } else if node.op == .key_continue { - g.sb.writeln('continue;') + if g.comptime_continue_label != '' && g.runtime_loop_depth == 0 { + g.sb.writeln('goto ${g.comptime_continue_label};') + } else { + g.sb.writeln('continue;') + } } else if node.op == .key_goto { g.sb.writeln('goto ${node.label};') } @@ -485,6 +489,36 @@ fn (mut g Gen) gen_comptime_stmt(node ast.ComptimeStmt) { g.sb.writeln('/* [TODO] ComptimeStmt */') } +fn comptime_attr_value_string(expr ast.Expr) string { + match expr { + ast.BasicLiteral { + return expr.value.trim('\'"') + } + ast.StringLiteral { + return expr.value + } + ast.Ident { + return expr.name + } + else { + return expr.name().trim('\'"') + } + } +} + +fn comptime_attr_strings(attrs []ast.Attribute) []string { + mut out := []string{cap: attrs.len} + for attr in attrs { + value := comptime_attr_value_string(attr.value) + if attr.name != '' { + out << '${attr.name}: ${value}' + } else if value != '' { + out << value + } + } + return out +} + fn (mut g Gen) gen_comptime_if_stmt(node ast.IfExpr) { result := g.eval_comptime_cond(node.cond) if os.getenv('V2_DEBUG_COMPTIME') != '' { @@ -537,6 +571,13 @@ fn (mut g Gen) gen_comptime_for(node ast.ForStmt) { return } struct_type := concrete as types.Struct + mut attrs_by_field := map[string][]string{} + struct_c_name := g.types_type_to_c(concrete) + if decl_info := g.find_struct_decl_info_by_c_name(struct_c_name) { + for ast_field in decl_info.decl.fields { + attrs_by_field[ast_field.name] = comptime_attr_strings(ast_field.attributes) + } + } field_var := for_in.value.name() // Save comptime state prev_field_var := g.comptime_field_var @@ -545,21 +586,28 @@ fn (mut g Gen) gen_comptime_for(node ast.ForStmt) { prev_field_raw_type := g.comptime_field_raw_type prev_field_attrs := g.comptime_field_attrs prev_field_idx := g.comptime_field_idx + prev_continue_label := g.comptime_continue_label + continue_base := g.tmp_counter + g.tmp_counter++ g.comptime_field_var = field_var g.write_indent() g.sb.writeln('{ /* comptime for ${field_var} in ${type_name}.fields */') g.indent++ for i, field in struct_type.fields { + continue_label := '__v_ctf_continue_${continue_base}_${i}' g.comptime_field_name = field.name g.comptime_field_type = g.types_type_to_c(field.typ) g.comptime_field_raw_type = field.typ - g.comptime_field_attrs = [] + g.comptime_field_attrs = attrs_by_field[field.name] or { []string{} } g.comptime_field_idx = i + g.comptime_continue_label = continue_label g.write_indent() g.sb.writeln('{ /* field ${i}: ${field.name} */') g.indent++ // Use ForStmt.stmts for the loop body g.gen_stmts(node.stmts) + g.write_indent() + g.sb.writeln('${continue_label}:;') g.indent-- g.write_indent() g.sb.writeln('}') @@ -574,4 +622,5 @@ fn (mut g Gen) gen_comptime_for(node ast.ForStmt) { g.comptime_field_raw_type = prev_field_raw_type g.comptime_field_attrs = prev_field_attrs g.comptime_field_idx = prev_field_idx + g.comptime_continue_label = prev_continue_label } diff --git a/vlib/v2/gen/cleanc/struct.v b/vlib/v2/gen/cleanc/struct.v index fabc837f5..78ead4b93 100644 --- a/vlib/v2/gen/cleanc/struct.v +++ b/vlib/v2/gen/cleanc/struct.v @@ -1424,6 +1424,14 @@ fn struct_field_needs_explicit_default(field types.Field) bool { return field_type is types.Array || field_type is types.String } +fn (mut g Gen) struct_field_needs_explicit_default_for(struct_type string, env_struct types.Struct, field types.Field) bool { + expected_field_type := g.init_field_expected_type(struct_type, env_struct, field.name) + if expected_field_type.ends_with('*') || is_type_name_pointer_like(expected_field_type) { + return false + } + return struct_field_needs_explicit_default(field) +} + fn (mut g Gen) write_struct_field_default_value(field types.Field) bool { field_type := unwrap_alias_type(field.typ) if field_type is types.Array { @@ -1459,7 +1467,45 @@ fn (mut g Gen) gen_none_literal_for_type(type_name string) bool { return false } +fn (mut g Gen) gen_option_wrapped_value(type_name string, value ast.Expr) bool { + trimmed := type_name.trim_space() + if !trimmed.starts_with('_option_') { + return false + } + if is_none_like_expr(value) { + return g.gen_none_literal_for_type(trimmed) + } + value_type := option_value_type(trimmed) + if value_type == '' || value_type == 'void' { + return false + } + if g.get_expr_type(value) == trimmed { + g.expr(value) + return true + } + g.sb.write_string('({ ${trimmed} _opt = (${trimmed}){ .state = 2 }; ${value_type} _val = ') + if g.is_interface_type(value_type) && g.gen_interface_cast(value_type, value) { + // interface wrapping handled + } else { + g.expr(value) + } + g.sb.write_string('; _option_ok(&_val, (_option*)&_opt, sizeof(_val)); _opt; })') + return true +} + fn (mut g Gen) init_field_expected_type(type_name string, env_struct types.Struct, field_name string) string { + if decl_info := g.find_struct_decl_info_by_c_name(type_name) { + saved_module := g.cur_module + g.cur_module = decl_info.mod + defer { + g.cur_module = saved_module + } + for ast_field in decl_info.decl.fields { + if ast_field.name == field_name { + return g.expr_type_to_c(ast_field.typ) + } + } + } expected_key := '${type_name}.${field_name}' mut expected_field_type := g.struct_field_types[expected_key] or { '' } if expected_field_type == '' && type_name.contains('__') { @@ -1532,7 +1578,12 @@ fn (mut g Gen) gen_channel_init_expr(node ast.InitExpr) bool { } fn (mut g Gen) gen_init_expr(node ast.InitExpr) { - type_name := g.expr_type_to_c(node.typ) + mut type_name := g.expr_type_to_c(node.typ) + type_name = g.specialized_generic_c_name_from_type_expr(node.typ, type_name) + if g.cur_fn_ret_type != '' && type_name != g.cur_fn_ret_type + && g.cur_fn_ret_type.starts_with('${type_name}_T_') { + type_name = g.cur_fn_ret_type + } if g.gen_channel_init_expr(node) { return } @@ -1564,7 +1615,7 @@ fn (mut g Gen) gen_init_expr(node ast.InitExpr) { mut wrote_defaults := 0 g.sb.write_string('((${type_name}){') for field in env_struct.fields { - if !struct_field_needs_explicit_default(field) { + if !g.struct_field_needs_explicit_default_for(type_name, env_struct, field) { continue } if wrote_defaults > 0 { @@ -1579,7 +1630,7 @@ fn (mut g Gen) gen_init_expr(node ast.InitExpr) { for emb in env_struct.embedded { emb_name := emb.name.all_after_last('__') for field in emb.fields { - if !struct_field_needs_explicit_default(field) { + if !g.struct_field_needs_explicit_default_for(type_name, env_struct, field) { continue } if wrote_defaults > 0 { @@ -1691,6 +1742,9 @@ fn (mut g Gen) gen_init_expr(node ast.InitExpr) { if is_none_like_expr(field.value) && g.gen_none_literal_for_type(expected_field_type) { continue } + if g.gen_option_wrapped_value(expected_field_type, field.value) { + continue + } if g.is_fn_pointer_alias_type(expected_field_type) { if field.value is ast.SelectorExpr { sel := field.value as ast.SelectorExpr @@ -1762,7 +1816,7 @@ fn (mut g Gen) gen_init_expr(node ast.InitExpr) { if initialized_fields[field.name] { continue } - if !struct_field_needs_explicit_default(field) { + if !g.struct_field_needs_explicit_default_for(type_name, env_struct, field) { continue } if wrote_fields > 0 { @@ -1781,7 +1835,7 @@ fn (mut g Gen) gen_init_expr(node ast.InitExpr) { if initialized_fields[field.name] { continue } - if !struct_field_needs_explicit_default(field) { + if !g.struct_field_needs_explicit_default_for(type_name, env_struct, field) { continue } if wrote_fields > 0 { diff --git a/vlib/v2/gen/cleanc/types.v b/vlib/v2/gen/cleanc/types.v index 001f02841..75fb921d5 100644 --- a/vlib/v2/gen/cleanc/types.v +++ b/vlib/v2/gen/cleanc/types.v @@ -22,6 +22,18 @@ fn option_value_type(option_type string) string { return unmangle_c_ptr_type(option_type['_option_'.len..]) } +fn legacy_fixed_array_elem_type(name string) string { + if !name.starts_with('fixed_') { + return '' + } + rest := name['fixed_'.len..] + last_underscore := rest.last_index('_') or { -1 } + if last_underscore <= 0 { + return '' + } + return rest[..last_underscore] +} + fn (g &Gen) resolve_active_generic_type(name string) ?types.Type { if name == '' || g.active_generic_types.len == 0 { return none @@ -1041,6 +1053,10 @@ fn (mut g Gen) eq_expr_for_c_type(c_type string, va string, vb string) string { || c_type.ends_with('ptr') || g.is_enum_type(c_type) { return '${va} == ${vb}' } + struct_type := g.lookup_struct_type_by_c_name(c_type) + if struct_type.fields.len > 0 { + return g.gen_struct_field_eq_expr(struct_type, va, vb) + } return 'memcmp(&${va}, &${vb}, sizeof(${c_type})) == 0' } @@ -1057,15 +1073,10 @@ fn (mut g Gen) eq_expr_for_type(typ types.Type, va string, vb string) string { return g.eq_expr_for_c_type(c_type, va, vb) } types.Alias { - match typ.base_type { - types.String, types.Array, types.Map { - return g.eq_expr_for_type(typ.base_type, va, vb) - } - else {} - } - - c_type := g.types_type_to_c(typ) - return g.eq_expr_for_c_type(c_type, va, vb) + return g.eq_expr_for_type(typ.base_type, va, vb) + } + types.Struct { + return g.gen_struct_field_eq_expr(typ, va, vb) } types.Primitive, types.Pointer, types.Rune, types.Char, types.ISize, types.USize, types.Enum, types.Nil { @@ -1083,7 +1094,8 @@ fn (mut g Gen) eq_expr_for_type(typ types.Type, va string, vb string) string { fn (mut g Gen) gen_struct_field_eq_expr(s types.Struct, va string, vb string) string { mut parts := []string{} for field in s.fields { - parts << g.eq_expr_for_type(field.typ, '${va}.${field.name}', '${vb}.${field.name}') + field_name := escape_c_keyword(field.name) + parts << g.eq_expr_for_type(field.typ, '${va}.${field_name}', '${vb}.${field_name}') } if parts.len == 0 { return '1' @@ -1111,6 +1123,12 @@ fn (mut g Gen) method_receiver_base_type(expr ast.Expr) string { } } } + if expr is ast.CallExpr || expr is ast.CallOrCastExpr { + call_type := g.get_expr_type(expr).trim_right('*') + if call_type != '' && call_type != 'int' { + return call_type + } + } // Fast path: env pos.id O(1) lookup (covers most non-Ident receivers). if g.env != unsafe { nil } { pos := expr.pos() @@ -1959,6 +1977,12 @@ fn (mut g Gen) get_expr_type(node ast.Expr) string { } // Try environment lookup if t := g.get_expr_type_from_env(node) { + if node is ast.IndexExpr || node is ast.InfixExpr { + elem_type := legacy_fixed_array_elem_type(t) + if elem_type != '' { + return elem_type + } + } if node is ast.SelectorExpr { field_type_from_cast := g.selector_explicit_cast_field_type(node) if field_type_from_cast != '' { @@ -2096,6 +2120,11 @@ fn (mut g Gen) get_expr_type(node ast.Expr) string { if numeric_type != '' && numeric_type !in ['int', 'int_literal'] { return numeric_type } + } else if t == 'int' && node is ast.IndexExpr { + elem_type := g.infer_array_elem_type_from_expr(node.lhs) + if elem_type != '' && elem_type != 'array' && elem_type != 'int' { + return elem_type + } } else if node is ast.SelectorExpr && g.active_generic_types.len > 0 { // In specialized generic functions, the env may return the // substituted generic type param (e.g. Slack) instead of the actual @@ -2904,6 +2933,88 @@ fn (mut g Gen) resolve_generic_struct_c_name(base_name string, concrete_params [ return base_name } +fn (mut g Gen) generic_runtime_param_key_from_expr(expr ast.Expr) string { + match expr { + ast.GenericArgs { + base_name := g.expr_type_to_c(expr.lhs) + runtime_args := runtime_generic_args(expr.args) + if runtime_args.len == 0 { + return base_name + } + return '${base_name}_T_${g.generic_runtime_param_key_from_exprs(runtime_args)}' + } + ast.GenericArgOrIndexExpr { + base_name := g.expr_type_to_c(expr.lhs) + if expr.expr is ast.LifetimeExpr { + return base_name + } + return '${base_name}_T_${g.generic_runtime_param_key_from_expr(expr.expr)}' + } + ast.Type { + if expr is ast.GenericType { + base_name := g.expr_type_to_c(expr.name) + runtime_args := runtime_generic_args(expr.params) + if runtime_args.len == 0 { + return base_name + } + return '${base_name}_T_${g.generic_runtime_param_key_from_exprs(runtime_args)}' + } + return g.expr_type_to_c(expr) + } + else { + return g.expr_type_to_c(expr) + } + } +} + +fn (mut g Gen) generic_runtime_param_key_from_exprs(params []ast.Expr) string { + mut param_c_names := []string{cap: params.len} + for param in runtime_generic_args(params) { + param_c_names << g.generic_runtime_param_key_from_expr(param) + } + return param_c_names.join('_') +} + +fn (mut g Gen) specialized_generic_c_name_for_key(base_name string, key string, fallback string) string { + for inst in g.generic_struct_instances[base_name] { + if inst.params_key == key { + return inst.c_name + } + } + if key != '' && g.cur_fn_ret_type == '${base_name}_T_${key}' { + return g.cur_fn_ret_type + } + if fallback != '' && g.cur_fn_ret_type.starts_with('${fallback}_T_') { + return g.cur_fn_ret_type + } + return fallback +} + +fn (mut g Gen) specialized_generic_c_name_from_type_expr(type_expr ast.Expr, fallback string) string { + match type_expr { + ast.GenericArgs { + base_name := g.expr_type_to_c(type_expr.lhs) + key := g.generic_runtime_param_key_from_exprs(type_expr.args) + return g.specialized_generic_c_name_for_key(base_name, key, fallback) + } + ast.GenericArgOrIndexExpr { + base_name := g.expr_type_to_c(type_expr.lhs) + key := g.generic_runtime_param_key_from_expr(type_expr.expr) + return g.specialized_generic_c_name_for_key(base_name, key, fallback) + } + ast.Type { + if type_expr is ast.GenericType { + base_name := g.expr_type_to_c(type_expr.name) + key := g.generic_runtime_param_key_from_exprs(type_expr.params) + return g.specialized_generic_c_name_for_key(base_name, key, fallback) + } + } + else {} + } + + return fallback +} + // emit_late_generic_struct generates a struct definition for a non-primary generic // struct instantiation and adds it to late_struct_defs for insertion before option/result // wrapper emission. diff --git a/vlib/v2/markused/markused.v b/vlib/v2/markused/markused.v index e5abd850d..b8be648e1 100644 --- a/vlib/v2/markused/markused.v +++ b/vlib/v2/markused/markused.v @@ -187,6 +187,7 @@ fn (mut w Walker) seed_roots() bool { } } if has_root { + w.seed_generic_specialization_roots() // Also seed module init() functions (called from synthesized main) for i, info in w.fns { if is_module_init(info) { @@ -201,9 +202,39 @@ fn (mut w Walker) seed_roots() bool { has_root = true } } + if has_root { + w.seed_generic_specialization_roots() + } return has_root } +fn generic_base_name_from_key(key string) string { + mut name := key + bracket_idx := name.index_u8(`[`) + if bracket_idx > 0 { + name = name[..bracket_idx] + } + dot_idx := name.last_index_u8(`.`) + if dot_idx > 0 && dot_idx < name.len - 1 { + name = name[dot_idx + 1..] + } + return strip_generic_specialization_suffix(name) +} + +fn (mut w Walker) seed_generic_specialization_roots() { + if w.env == unsafe { nil } { + return + } + for key, _ in w.env.generic_types { + base_name := generic_base_name_from_key(key) + if base_name == '' { + continue + } + w.mark_lookup('fn:${base_name}') + w.mark_lookup('mname:${base_name}') + } +} + fn is_main_root(info FnInfo) bool { return !is_method_decl(info.decl) && info.mod == 'main' && info.decl.name == 'main' } @@ -305,30 +336,63 @@ fn type_name_candidates_from_type(mod_name string, typ types.Type) []string { return out } -fn receiver_names_from_decl(mod_name string, decl ast.FnDecl, env &types.Environment) []string { - mut out := []string{} - match decl.receiver.typ { +fn add_receiver_name_candidates(mut out []string, mod_name string, raw_name string) { + name := sanitize_receiver_name(raw_name) + if name == '' { + return + } + add_unique_string(mut out, name) + add_unique_string(mut out, maybe_trim_module_prefix(mod_name, name)) + if name.contains('__') { + add_unique_string(mut out, name.all_after_last('__')) + } else if mod_name != '' && mod_name != 'main' { + add_unique_string(mut out, '${mod_name}__${name}') + } +} + +fn receiver_type_expr_name(expr ast.Expr) string { + match expr { ast.Ident { - name := sanitize_receiver_name(decl.receiver.typ.name) - add_unique_string(mut out, name) - add_unique_string(mut out, maybe_trim_module_prefix(mod_name, name)) - if mod_name != '' && mod_name != 'main' { - add_unique_string(mut out, '${mod_name}__${name}') - } + return expr.name + } + ast.SelectorExpr { + return expr.rhs.name } ast.PrefixExpr { - if decl.receiver.typ.expr is ast.Ident { - name := sanitize_receiver_name(decl.receiver.typ.expr.name) - add_unique_string(mut out, name) - add_unique_string(mut out, maybe_trim_module_prefix(mod_name, name)) - if mod_name != '' && mod_name != 'main' { - add_unique_string(mut out, '${mod_name}__${name}') + if expr.op == .amp { + return receiver_type_expr_name(expr.expr) + } + } + ast.ModifierExpr { + return receiver_type_expr_name(expr.expr) + } + ast.GenericArgs { + return receiver_type_expr_name(expr.lhs) + } + ast.GenericArgOrIndexExpr { + return receiver_type_expr_name(expr.lhs) + } + ast.Type { + match expr { + ast.PointerType { + return receiver_type_expr_name(expr.base_type) } + ast.GenericType { + return receiver_type_expr_name(expr.name) + } + else {} } } else {} } + return '' +} + +fn receiver_names_from_decl(mod_name string, decl ast.FnDecl, env &types.Environment) []string { + mut out := []string{} + add_receiver_name_candidates(mut out, mod_name, receiver_type_expr_name(decl.receiver.typ)) + // Method on []ElemType — receiver name is Array_ElemType. // Use the string name which includes [] prefix for array types. recv_name := decl.receiver.typ.name() @@ -363,7 +427,8 @@ fn receiver_primary_name(mod_name string, decl ast.FnDecl, env &types.Environmen } fn normalize_method_name(name string) string { - return match name { + base_name := strip_generic_specialization_suffix(name) + return match base_name { '+' { 'plus' } '-' { 'minus' } '*' { 'mul' } @@ -375,7 +440,7 @@ fn normalize_method_name(name string) string { '>' { 'gt' } '<=' { 'le' } '>=' { 'ge' } - else { name } + else { base_name } } } @@ -551,6 +616,10 @@ fn (mut w Walker) mark_lookup(key string) { fn called_fn_name_candidates(name string) []string { mut out := []string{} add_unique_string(mut out, name) + generic_base := strip_generic_specialization_suffix(name) + if generic_base != name { + add_unique_string(mut out, generic_base) + } if name == 'builtin__new_array_from_c_array_noscan' { add_unique_string(mut out, 'new_array_from_c_array') return out @@ -573,6 +642,15 @@ fn called_fn_name_candidates(name string) []string { return out } +fn strip_generic_specialization_suffix(name string) string { + if idx := name.index('_T_') { + if idx > 0 { + return name[..idx] + } + } + return name +} + fn should_mark_ident_as_fn(name string) bool { if name == '' { return false @@ -854,8 +932,11 @@ fn (mut w Walker) mark_call_lhs(lhs ast.Expr, mod_name string) { } receivers := w.receiver_candidates_for_expr(lhs.lhs, mod_name) if receivers.len > 0 { + prev_queue_len := w.queue.len w.mark_method_name(method_name, receivers) - return + if w.queue.len > prev_queue_len { + return + } } w.mark_method_name_fallback(method_name) } diff --git a/vlib/v2/markused/markused_test.v b/vlib/v2/markused/markused_test.v index 9b696e44c..c36f54253 100644 --- a/vlib/v2/markused/markused_test.v +++ b/vlib/v2/markused/markused_test.v @@ -148,6 +148,245 @@ fn test_mark_used_tracks_method_calls_with_env_types() { assert !used[unused_key] } +fn test_mark_used_tracks_transformed_generic_method_calls() { + mut env := types.Environment.new() + env.set_expr_type(32, types.Struct{ + name: 'FlagMapper' + }) + env.generic_types['FlagMapper.build_schema[T]'] = [ + { + 'T': types.Type(types.Struct{ + name: 'TestSchema' + }) + }, + ] + files := [ + ast.File{ + mod: 'ownflag' + name: 'ownflag_test.v' + stmts: [ + ast.Stmt(ast.FnDecl{ + name: 'test_parse' + typ: ast.FnType{} + pos: pos(30) + stmts: [ + ast.Stmt(ast.ExprStmt{ + expr: ast.CallExpr{ + lhs: ast.SelectorExpr{ + lhs: ast.Ident{ + name: 'fm' + pos: pos(32) + } + rhs: ast.Ident{ + name: 'parse_T_ownflag_TestSchema' + pos: pos(33) + } + pos: pos(33) + } + pos: pos(33) + } + }), + ] + }), + ast.Stmt(ast.FnDecl{ + is_method: true + receiver: ast.Parameter{ + name: 'fm' + typ: ast.Ident{ + name: 'FlagMapper' + pos: pos(34) + } + pos: pos(34) + } + name: 'parse' + typ: ast.FnType{ + generic_params: [ + ast.Expr(ast.Ident{ + name: 'T' + pos: pos(35) + }), + ] + } + pos: pos(36) + stmts: [ + ast.Stmt(ast.ExprStmt{ + expr: ast.CallExpr{ + lhs: ast.SelectorExpr{ + lhs: ast.Ident{ + name: 'fm' + pos: pos(37) + } + rhs: ast.Ident{ + name: 'reset_state' + pos: pos(38) + } + pos: pos(38) + } + pos: pos(38) + } + }), + ast.Stmt(ast.ExprStmt{ + expr: ast.CallExpr{ + lhs: ast.SelectorExpr{ + lhs: ast.Ident{ + name: 'fm' + pos: pos(41) + } + rhs: ast.Ident{ + name: 'build_schema_T_ownflag_TestSchema' + pos: pos(42) + } + pos: pos(42) + } + pos: pos(42) + } + }), + ] + }), + ast.Stmt(ast.FnDecl{ + is_method: true + receiver: ast.Parameter{ + name: 'fm' + typ: ast.Ident{ + name: 'FlagMapper' + pos: pos(39) + } + pos: pos(39) + } + name: 'reset_state' + typ: ast.FnType{} + pos: pos(40) + }), + ast.Stmt(ast.FnDecl{ + is_method: true + receiver: ast.Parameter{ + name: 'fm' + typ: ast.Ident{ + name: 'FlagMapper' + pos: pos(43) + } + pos: pos(43) + } + name: 'build_schema' + typ: ast.FnType{ + generic_params: [ + ast.Expr(ast.Ident{ + name: 'T' + pos: pos(44) + }), + ] + } + pos: pos(45) + }), + ] + }, + ] + used := mark_used(files, env) + test_key := decl_key('ownflag', files[0].stmts[0] as ast.FnDecl, env) + parse_key := decl_key('ownflag', files[0].stmts[1] as ast.FnDecl, env) + reset_key := decl_key('ownflag', files[0].stmts[2] as ast.FnDecl, env) + build_schema_key := decl_key('ownflag', files[0].stmts[3] as ast.FnDecl, env) + assert used[test_key] + assert used[parse_key] + assert used[reset_key] + assert used[build_schema_key] +} + +fn test_mark_used_tracks_lifetime_generic_receiver_body_dependencies() { + mut env := types.Environment.new() + files := [ + ast.File{ + mod: 'globset' + name: 'glob.v' + stmts: [ + ast.Stmt(ast.StructDecl{ + name: 'GlobBuilder' + }), + ast.Stmt(ast.StructDecl{ + name: 'Tokens' + }), + ast.Stmt(ast.FnDecl{ + name: 'test_build' + typ: ast.FnType{} + pos: pos(50) + stmts: [ + ast.Stmt(ast.ExprStmt{ + expr: ast.CallExpr{ + lhs: ast.Ident{ + name: 'globset__GlobBuilder__build' + pos: pos(51) + } + pos: pos(51) + } + }), + ] + }), + ast.Stmt(ast.FnDecl{ + is_method: true + receiver: ast.Parameter{ + name: 'builder' + typ: ast.Type(ast.GenericType{ + name: ast.Expr(ast.Ident{ + name: 'GlobBuilder' + pos: pos(52) + }) + params: [ + ast.Expr(ast.LifetimeExpr{ + name: 'a' + pos: pos(53) + }), + ] + }) + pos: pos(52) + } + name: 'build' + typ: ast.FnType{} + pos: pos(54) + stmts: [ + ast.Stmt(ast.ExprStmt{ + expr: ast.CallExpr{ + lhs: ast.SelectorExpr{ + lhs: ast.Ident{ + name: 'Tokens' + pos: pos(55) + } + rhs: ast.Ident{ + name: 'default' + pos: pos(56) + } + pos: pos(56) + } + pos: pos(56) + } + }), + ] + }), + ast.Stmt(ast.FnDecl{ + is_method: true + is_static: true + receiver: ast.Parameter{ + typ: ast.Ident{ + name: 'Tokens' + pos: pos(57) + } + pos: pos(57) + } + name: 'default' + typ: ast.FnType{} + pos: pos(58) + }), + ] + }, + ] + used := mark_used(files, env) + test_key := decl_key('globset', files[0].stmts[2] as ast.FnDecl, env) + build_key := decl_key('globset', files[0].stmts[3] as ast.FnDecl, env) + default_key := decl_key('globset', files[0].stmts[4] as ast.FnDecl, env) + assert used[test_key] + assert used[build_key] + assert used[default_key] +} + fn test_mark_used_keeps_all_functions_when_no_entry_root_exists() { mut env := types.Environment.new() files := [ diff --git a/vlib/v2/transformer/expr.v b/vlib/v2/transformer/expr.v index b2a0059c7..6e8bb5fbe 100644 --- a/vlib/v2/transformer/expr.v +++ b/vlib/v2/transformer/expr.v @@ -2869,7 +2869,7 @@ fn (mut t Transformer) transform_infix_expr(expr ast.InfixExpr) ast.Expr { return range_check } // Map membership: key in map -> map__exists(&map, &key) - if rhs_type := t.get_expr_type(expr.rhs) { + if rhs_type := t.resolve_expr_type(expr.rhs) { if map_typ := t.unwrap_map_type(rhs_type) { if t.is_eval_backend() { return ast.InfixExpr{ diff --git a/vlib/v2/transformer/fn.v b/vlib/v2/transformer/fn.v index cde9fefc1..31800ef9a 100644 --- a/vlib/v2/transformer/fn.v +++ b/vlib/v2/transformer/fn.v @@ -132,6 +132,13 @@ fn (t &Transformer) expr_wrapper_type_for_or(expr ast.Expr) ?types.Type { if wrapper_type := t.channel_receive_wrapper_type(expr) { return wrapper_type } + if expr is ast.SelectorExpr { + if field_type := t.get_struct_field_type(expr) { + if field_type is types.OptionType || field_type is types.ResultType { + return field_type + } + } + } if ret_type := t.resolve_call_return_type(expr) { if ret_type is types.OptionType || ret_type is types.ResultType { return ret_type @@ -995,11 +1002,20 @@ fn normalize_blank_fn_parameters(decl ast.FnDecl) ast.FnDecl { } } +fn fn_decl_has_runtime_generic_params(decl ast.FnDecl) bool { + for param in decl.typ.generic_params { + if param !is ast.LifetimeExpr { + return true + } + } + return false +} + fn (mut t Transformer) transform_fn_decl(input_decl ast.FnDecl) ast.FnDecl { decl := normalize_blank_fn_parameters(input_decl) // Skip uninstantiated generic functions - their bodies were never type-checked // and they will never be called, so emit an empty body. - if decl.typ.generic_params.len > 0 { + if fn_decl_has_runtime_generic_params(decl) { mut has_generic_types := decl.name in t.env.generic_types if !has_generic_types { for key, _ in t.env.generic_types { @@ -1841,6 +1857,9 @@ fn (mut t Transformer) transform_call_expr(expr ast.CallExpr) ast.Expr { } } if !is_module_call { + if t.receiver_method_has_runtime_generic_params(sel.lhs, sel.rhs.name) { + return t.keep_generic_receiver_method_call(sel, expr.args, expr.pos) + } if resolved := t.resolve_method_call_name(sel.lhs, sel.rhs.name) { // Guard against misresolution: if the receiver is known to be a string // (e.g., tos2() returns string), ensure string methods aren't resolved to @@ -2561,6 +2580,51 @@ fn (t &Transformer) resolve_method_call_name(receiver ast.Expr, method_name stri return none } +fn (t &Transformer) receiver_method_has_runtime_generic_params(receiver ast.Expr, method_name string) bool { + mut recv_type_opt := t.get_expr_type(receiver) + if recv_type_opt == none && receiver is ast.SelectorExpr { + recv_type_opt = t.get_struct_field_type(receiver) + } + recv_type := recv_type_opt or { return false } + base_type := t.unwrap_alias_and_pointer_type(recv_type) + if base_type !is types.Struct { + return false + } + type_name := base_type.name() + mut lookup_names := []string{cap: 4} + lookup_names << type_name + if type_name.contains('__') { + lookup_names << type_name.all_after_last('__') + } else if t.cur_module != '' && t.cur_module != 'main' { + lookup_names << '${t.cur_module}__${type_name}' + } + for name in lookup_names { + if t.generic_receiver_methods['${name}__${method_name}'] { + return true + } + } + return false +} + +fn (mut t Transformer) keep_generic_receiver_method_call(sel ast.SelectorExpr, args []ast.Expr, pos token.Pos) ast.Expr { + call_args := t.lower_missing_call_args(ast.Expr(sel), args) + fn_info := t.lookup_call_fn_info(ast.Expr(sel)) + mut transformed_args := []ast.Expr{cap: call_args.len} + for i, arg in call_args { + transformed_args << t.transform_call_arg_with_sumtype_check(arg, fn_info, i) + } + transformed_args = t.lower_variadic_args(ast.Expr(sel), transformed_args) + return ast.CallExpr{ + lhs: ast.SelectorExpr{ + lhs: t.transform_expr(sel.lhs) + rhs: sel.rhs + pos: sel.pos + } + args: transformed_args + pos: pos + } +} + fn (t &Transformer) resolve_array_concrete_method_name(receiver ast.Expr, method_name string) ?string { mut recv_type_opt := t.get_expr_type(receiver) if recv_type_opt == none && receiver is ast.SelectorExpr { diff --git a/vlib/v2/transformer/if.v b/vlib/v2/transformer/if.v index 50fdcd0d0..2ae3be25b 100644 --- a/vlib/v2/transformer/if.v +++ b/vlib/v2/transformer/if.v @@ -518,7 +518,7 @@ fn (mut t Transformer) try_expand_if_guard_stmt(stmt ast.ExprStmt) ?[]ast.Stmt { // Transform to: if (i < arr.len) { x := arr[i]; use(x) } if rhs_is_map_index { rhs_idx := map_index_expr - if map_expr_typ := t.get_expr_type(rhs_idx.lhs) { + if map_expr_typ := t.resolve_expr_type(rhs_idx.lhs) { if map_type := t.unwrap_map_type(map_expr_typ) { // This is a map lookup - use map__get_check pattern temp_name := t.gen_temp_name() diff --git a/vlib/v2/transformer/struct.v b/vlib/v2/transformer/struct.v index ddc45fa18..3fbfc7e32 100644 --- a/vlib/v2/transformer/struct.v +++ b/vlib/v2/transformer/struct.v @@ -1838,6 +1838,9 @@ fn (mut t Transformer) add_missing_struct_field_defaults(struct_name string, fie } continue } + if t.is_pointer_type(struct_field.typ) { + continue + } field_type := t.unwrap_alias_and_pointer_type(struct_field.typ) if field_type is types.Map { if struct_name.contains('Scope') || struct_name.contains('Env') { @@ -1920,6 +1923,9 @@ fn (mut t Transformer) add_missing_struct_field_defaults(struct_name string, fie } continue } + if t.is_pointer_type(struct_field.typ) { + continue + } field_type := t.unwrap_alias_and_pointer_type(struct_field.typ) if field_type is types.Map { map_init := ast.Expr(ast.MapInitExpr{ diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index 54648923b..8ffdc592c 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -125,6 +125,9 @@ mut: cached_scopes map[string]&types.Scope cached_methods map[string][]&types.Fn cached_fn_scopes map[string]&types.Scope + // Methods declared on runtime-generic receivers must stay as selector calls + // until C generation, where concrete receiver instances are available. + generic_receiver_methods map[string]bool // Accumulated synth types for deferred application (thread-safe). // Instead of writing directly to env.set_expr_type during parallel transform, // store here and apply after merge. @@ -231,6 +234,7 @@ pub fn Transformer.new_with_pref(files []ast.File, env &types.Environment, p &pr static_local_renames: map[string]string{} decl_type_overrides: map[string]types.Type{} cur_import_aliases: map[string]string{} + generic_receiver_methods: map[string]bool{} synth_types: map[int]types.Type{} } t.comptime_vmodroot = resolve_comptime_vmodroot(files, p) @@ -257,6 +261,7 @@ pub fn (t &Transformer) new_worker_clone(worker_idx int) &Transformer { cached_scopes: t.cached_scopes cached_methods: t.cached_methods cached_fn_scopes: t.cached_fn_scopes + generic_receiver_methods: t.generic_receiver_methods synth_pos_counter: -(worker_idx * 100_000) needed_str_fns: map[string]string{} needed_clone_fns: map[string]string{} @@ -967,6 +972,106 @@ fn (t &Transformer) is_var_enum(name string) ?string { return none } +fn runtime_generic_receiver_base_name(expr ast.Expr) string { + match expr { + ast.Ident { + return expr.name.replace('.', '__') + } + ast.SelectorExpr { + if expr.lhs is ast.Ident { + return '${(expr.lhs as ast.Ident).name}__${expr.rhs.name}' + } + return expr.rhs.name + } + ast.GenericArgOrIndexExpr { + return runtime_generic_receiver_base_name(expr.lhs) + } + ast.GenericArgs { + return runtime_generic_receiver_base_name(expr.lhs) + } + ast.PrefixExpr { + return runtime_generic_receiver_base_name(expr.expr) + } + ast.ModifierExpr { + return runtime_generic_receiver_base_name(expr.expr) + } + ast.Type { + if expr is ast.GenericType { + return runtime_generic_receiver_base_name(expr.name) + } + if expr is ast.PointerType { + return runtime_generic_receiver_base_name(expr.base_type) + } + } + else {} + } + + return '' +} + +fn receiver_type_has_runtime_generic_param(expr ast.Expr) bool { + match expr { + ast.GenericArgOrIndexExpr { + return expr.expr !is ast.LifetimeExpr + } + ast.GenericArgs { + for arg in expr.args { + if arg !is ast.LifetimeExpr { + return true + } + } + } + ast.PrefixExpr { + return receiver_type_has_runtime_generic_param(expr.expr) + } + ast.ModifierExpr { + return receiver_type_has_runtime_generic_param(expr.expr) + } + ast.Type { + if expr is ast.GenericType { + for param in expr.params { + if param !is ast.LifetimeExpr { + return true + } + } + } + if expr is ast.PointerType { + return receiver_type_has_runtime_generic_param(expr.base_type) + } + } + else {} + } + + return false +} + +fn (mut t Transformer) collect_generic_receiver_methods(files []ast.File) { + mut methods := map[string]bool{} + for file in files { + for stmt in file.stmts { + if stmt is ast.FnDecl && stmt.is_method + && receiver_type_has_runtime_generic_param(stmt.receiver.typ) { + base_name := runtime_generic_receiver_base_name(stmt.receiver.typ) + if base_name == '' { + continue + } + methods['${base_name}__${stmt.name}'] = true + short_name := if base_name.contains('__') { + base_name.all_after_last('__') + } else { + base_name + } + methods['${short_name}__${stmt.name}'] = true + if file.mod != '' && file.mod != 'main' && file.mod != 'builtin' + && !base_name.contains('__') { + methods['${file.mod}__${base_name}__${stmt.name}'] = true + } + } + } + } + t.generic_receiver_methods = methods.clone() +} + // pre_pass runs the sequential pre-pass: builds elided_fns and collects runtime const inits. pub fn (mut t Transformer) pre_pass(files []ast.File) { // Pre-pass: scan all function declarations for conditional compilation attributes @@ -984,6 +1089,7 @@ pub fn (mut t Transformer) pre_pass(files []ast.File) { } } } + t.collect_generic_receiver_methods(files) // Pre-pass: collect const declarations that require runtime initialization. if !t.is_eval_backend() { t.collect_runtime_const_inits(files) @@ -6438,6 +6544,14 @@ fn (mut t Transformer) extract_or_expr(expr ast.Expr, mut prefix_stmts []ast.Stm return lowered } } + if expr.op == .amp && expr.expr is ast.PostfixExpr { + postfix := expr.expr as ast.PostfixExpr + if postfix.op == .question { + if lowered := t.try_expand_addr_of_option_unwrap(postfix, mut prefix_stmts) { + return lowered + } + } + } if expr.op == .arrow && expr.expr is ast.OrExpr { or_expr := expr.expr as ast.OrExpr rewritten_or := ast.OrExpr{ @@ -6537,6 +6651,65 @@ fn (mut t Transformer) extract_or_expr(expr ast.Expr, mut prefix_stmts []ast.Stm } } +fn addr_of_option_unwrap_can_borrow(expr ast.Expr) bool { + return match expr { + ast.Ident { + true + } + ast.SelectorExpr { + addr_of_option_unwrap_can_borrow(expr.lhs) + } + ast.ParenExpr { + addr_of_option_unwrap_can_borrow(expr.expr) + } + ast.PrefixExpr { + expr.op == .mul && addr_of_option_unwrap_can_borrow(expr.expr) + } + else { + false + } + } +} + +fn (mut t Transformer) try_expand_addr_of_option_unwrap(postfix ast.PostfixExpr, mut prefix_stmts []ast.Stmt) ?ast.Expr { + wrapper_type := t.expr_wrapper_type_for_or(postfix.expr) or { return none } + if wrapper_type !is types.OptionType { + return none + } + option_type := wrapper_type as types.OptionType + if !addr_of_option_unwrap_can_borrow(postfix.expr) { + return none + } + wrapper_expr := t.transform_expr(postfix.expr) + state_check := ast.Expr(ast.InfixExpr{ + op: .ne + lhs: t.synth_selector(wrapper_expr, 'state', types.Type(types.int_)) + rhs: ast.BasicLiteral{ + kind: .number + value: '0' + } + }) + or_stmts := [ + ast.Stmt(ast.ReturnStmt{ + exprs: [ast.Expr(ast.Ident{ + name: 'none' + })] + }), + ] + prefix_stmts << ast.Stmt(ast.ExprStmt{ + expr: ast.IfExpr{ + cond: state_check + stmts: t.transform_stmts(or_stmts) + } + }) + data_expr := t.synth_selector(wrapper_expr, 'data', option_type.base_type) + return ast.Expr(ast.PrefixExpr{ + op: .amp + expr: data_expr + pos: postfix.pos + }) +} + // expand_single_or_expr expands a single OrExpr and returns the data access expression fn (mut t Transformer) expand_single_or_expr(or_expr ast.OrExpr, mut prefix_stmts []ast.Stmt) ast.Expr { call_expr := or_expr.expr diff --git a/vlib/v2/transformer/transformer_test.v b/vlib/v2/transformer/transformer_test.v index fd56b5e4c..59e458958 100644 --- a/vlib/v2/transformer/transformer_test.v +++ b/vlib/v2/transformer/transformer_test.v @@ -1435,6 +1435,37 @@ fn test_transform_init_expr_adds_nested_struct_defaults() { assert (inner_init.fields[0].value as ast.BasicLiteral).value == '999999' } +fn test_transform_init_expr_skips_missing_default_for_pointer_string_field() { + builder_type := types.Type(types.Struct{ + name: 'Builder' + fields: [ + types.Field{ + name: 'glob' + typ: types.Type(types.Pointer{ + base_type: types.string_ + lifetime: 'a' + }) + }, + ] + }) + mut scope := types.new_scope(unsafe { nil }) + scope.insert('Builder', builder_type) + mut t := create_test_transformer() + t.cur_module = 'main' + t.scope = scope + t.cached_scopes = { + 'main': scope + } + result := t.transform_init_expr(ast.InitExpr{ + typ: ast.Expr(ast.Ident{ + name: 'Builder' + }) + }) + assert result is ast.InitExpr + init := result as ast.InitExpr + assert init.fields.len == 0 +} + fn test_transform_map_init_expr_non_empty_lowers_to_runtime_ctor() { mut t := create_test_transformer() @@ -2631,6 +2662,52 @@ fn test_transform_if_expr_map_guard_uses_map_path() { assert result_if.cond !is ast.IfGuardExpr } +fn test_transform_resolves_selector_map_for_membership_and_guard() { + store_type := types.Type(types.Struct{ + name: 'Store' + fields: [ + types.Field{ + name: 'entries' + typ: types.Type(types.Map{ + key_type: string_type() + value_type: types.Type(types.Array{ + elem_type: types.Type(types.int_) + }) + }) + }, + ] + }) + mut t := create_transformer_with_vars({ + 's': store_type + 'key': string_type() + }) + entries := ast.Expr(ast.SelectorExpr{ + lhs: ast.Ident{ + name: 's' + } + rhs: ast.Ident{ + name: 'entries' + } + }) + map_type_name := t.get_map_type_for_expr(entries) or { + assert false, 'expected selector field to resolve as a map' + '' + } + assert map_type_name == 'Map_string_Array_int' + + result := t.transform_infix_expr(ast.InfixExpr{ + op: .key_in + lhs: ast.Ident{ + name: 'key' + } + rhs: entries + }) + assert result is ast.CallExpr, 'expected selector map membership to become map__exists, got ${result.type_name()}' + call := result as ast.CallExpr + assert call.lhs is ast.Ident + assert (call.lhs as ast.Ident).name == 'map__exists' +} + fn test_transform_for_in_stmt_lowers_to_for_stmt() { mut t := create_test_transformer() result := t.transform_for_in_stmt(ast.ForInStmt{ @@ -4271,6 +4348,7 @@ fn main() { if stmt is ast.FnDecl && stmt.name == 'matched_dir_entry' { saw_method = true assert stmt.is_method + assert stmt.stmts.len > 0 assert stmt.receiver.typ is ast.Type receiver_type := stmt.receiver.typ as ast.Type assert receiver_type is ast.PointerType diff --git a/vlib/v2/transformer/types.v b/vlib/v2/transformer/types.v index d9a7daf95..b2ff9e6b5 100644 --- a/vlib/v2/transformer/types.v +++ b/vlib/v2/transformer/types.v @@ -3649,7 +3649,7 @@ fn (t &Transformer) normalize_array_type(array_type string) string { // get_map_type_for_expr returns the Map_K_V type string for an expression if it's a map. // Unwraps aliases and pointers (e.g., mut map parameters) before checking. fn (t &Transformer) get_map_type_for_expr(expr ast.Expr) ?string { - typ := t.get_expr_type(expr) or { return none } + typ := t.resolve_expr_type(expr) or { return none } unwrapped := t.unwrap_alias_and_pointer_type(typ) // Also unwrap aliases (unwrap_alias_and_pointer_type only handles pointers) base := if unwrapped is types.Alias { diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 80fcacce2..8840ebd49 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -3675,18 +3675,21 @@ fn (mut c Checker) if_expr(expr ast.IfExpr) Type { fn (mut c Checker) match_expr(expr ast.MatchExpr, used_as_expr bool) Type { expr_type := c.expr(expr.expr) expected_type := c.expected_type - if expr_type is Enum { - c.expected_type = to_optional_type(Type(expr_type)) + cond_expected_type := if expr_type is Enum { + to_optional_type(Type(expr_type)) } else { expr_base := expr_type.base_type() if expr_base is Enum { - c.expected_type = to_optional_type(Type(expr_base)) + to_optional_type(Type(expr_base)) + } else { + expected_type } } mut last_stmt_type := Type(void_) for _, branch in expr.branches { c.open_scope() for cond in branch.cond { + c.expected_type = cond_expected_type expr_unwrapped := c.unwrap_ident(expr.expr) cond_type := c.expr(cond) if cond is ast.Ident || cond is ast.SelectorExpr { @@ -3697,6 +3700,7 @@ fn (mut c Checker) match_expr(expr ast.MatchExpr, used_as_expr bool) Type { c.apply_smartcast(expr_unwrapped, cond_type) } } + c.expected_type = expected_type c.stmt_list(branch.stmts) // mut is_noreturn := false if used_as_expr { diff --git a/vlib/v2/types/types.v b/vlib/v2/types/types.v index d992dea43..25e9a8256 100644 --- a/vlib/v2/types/types.v +++ b/vlib/v2/types/types.v @@ -181,6 +181,11 @@ pub fn (f &FnType) is_noreturn() bool { return f.attributes.has(.noreturn) } +// get_generic_params returns this function's declared generic parameter names. +pub fn (f &FnType) get_generic_params() []string { + return f.generic_params.clone() +} + // get_generic_types returns the concrete generic instantiations inferred for this function. pub fn (f &FnType) get_generic_types() []map[string]Type { mut out := []map[string]Type{cap: f.generic_types.len} diff --git a/vlib/veb/README.md b/vlib/veb/README.md index 70f2f6559..09176b0bb 100644 --- a/vlib/veb/README.md +++ b/vlib/veb/README.md @@ -140,11 +140,15 @@ connection while `kqueue` finishes writing it; the arena is detached from the request thread and freed after the write completes. This keeps request-local allocations cheap while preserving response-buffer lifetime. -Do not store request-scoped strings, arrays, maps, `Context` values, or template -output in app fields, globals, caches, or other threads unless you deliberately -copy them into longer-lived storage. Process startup data, route tables, static -file maps, database pools, and allocations made directly by C libraries are not -part of the per-request arena. +When a handler starts V `spawn` work while the request scope is active, the +generated thread wrapper retains the request arena until the spawned function +returns, so veb app code does not need to manually copy request strings just to +pass them to a background task. Void spawned functions also run inside their own +scoped arena, which is freed at thread exit. Do not store request-scoped strings, +arrays, maps, `Context` values, or template output in app fields, globals, or +caches unless you deliberately copy them into longer-lived storage. Process +startup data, route tables, static file maps, database pools, and allocations +made directly by C libraries are not part of the per-request arena. To trace request arena allocation and free points while developing, build with: -- 2.39.5