From a9f51c26dd9b715639e09b76cea2833ea28e5161 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 3 Jul 2026 13:10:23 +0300 Subject: [PATCH 1/2] v3: fix self-host (C.termios redeclare, generic erasure, fixed-array eq convergence) Fixes four stacked bugs that broke `v3 -building-v`/`-selfhost`, each masking the next: - pref: add missing OS suffixes (_qnx/_serenity/_plan9/_vinix/_haiku/_termux) to file_has_incompatible_os_suffix, so termios_qnx.c.v no longer leaks into the `termios` module on non-QNX targets and collides with the darwin variant. - checker: scope the C-struct redeclaration check per module. A C struct name maps to one external C type, but different modules may each mirror it with their own field view (e.g. C.termios in both `term` and `term.termios`). Only same-module redeclarations are a real conflict. - transform: erase_generic_templates now uses the same fn_decl_has_unresolved_generics detector as the monomorphize path, so methods whose only generic param comes from the receiver struct (e.g. `fn (t ThreadLocalStorage[T]) set(value T)`) are erased in building_v mode instead of emitting a raw `_T_` body calling erased helpers. - cgen: gen_fixed_array_infix_eq detects fixed-array operands by the optional's presence instead of inspecting a default-constructed ArrayFixed{} sentinel. The zero value of the elem_type sum-type field is not reliably Void across bootstrap generations, which made a later generation miscompile scalar `!= 0` (e.g. @[flag] enum `.has()`) into a bogus memcmp against Array_fixed_int_0. Both self-host chains now converge byte-identically: -building-v (v4==v5==v6) and -selfhost (v5.c==v6.c, the test_all.vsh check). --- vlib/v3/gen/c/cleanc.v | 25 ++++++++++++++++++------- vlib/v3/pref/pref.v | 18 ++++++++++++++++++ vlib/v3/transform/monomorphize.v | 21 +++++++-------------- vlib/v3/types/checker.v | 14 ++++++++++---- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/vlib/v3/gen/c/cleanc.v b/vlib/v3/gen/c/cleanc.v index 0d9eab9f85c1e7..776ca5fb864c9e 100644 --- a/vlib/v3/gen/c/cleanc.v +++ b/vlib/v3/gen/c/cleanc.v @@ -4437,16 +4437,27 @@ fn (mut g FlatGen) gen_array_infix_eq(node flat.Node, lhs_id flat.NodeId, rhs_id } fn (mut g FlatGen) gen_fixed_array_infix_eq(node flat.Node, lhs_id flat.NodeId, rhs_id flat.NodeId, lhs_type types.Type, rhs_type types.Type) bool { - lhs_fixed := array_fixed_type(types.unwrap_pointer(lhs_type)) or { types.ArrayFixed{} } - rhs_fixed := array_fixed_type(types.unwrap_pointer(rhs_type)) or { types.ArrayFixed{} } - lhs_is_fixed := lhs_fixed.elem_type !is types.Void || lhs_fixed.len > 0 - || lhs_fixed.len_expr.len > 0 - rhs_is_fixed := rhs_fixed.elem_type !is types.Void || rhs_fixed.len > 0 - || rhs_fixed.len_expr.len > 0 + // Detect fixed-array operands by the optional's presence, not by inspecting a + // default-constructed `ArrayFixed{}` sentinel. The zero value of the `elem_type` + // sum-type field is not reliably `Void` across the self-host bootstrap, so the old + // `!is types.Void` check misfired for scalar comparisons (e.g. `(a.flags & 32) != 0` + // from `@[flag]` enum `.has()`), emitting a bogus `memcmp(..., sizeof(Array_fixed_int_0))`. + mut fixed := types.ArrayFixed{} + mut lhs_is_fixed := false + mut rhs_is_fixed := false + if f := array_fixed_type(types.unwrap_pointer(lhs_type)) { + fixed = f + lhs_is_fixed = true + } + if f := array_fixed_type(types.unwrap_pointer(rhs_type)) { + if !lhs_is_fixed { + fixed = f + } + rhs_is_fixed = true + } if !lhs_is_fixed && !rhs_is_fixed { return false } - fixed := if lhs_is_fixed { lhs_fixed } else { rhs_fixed } if !lhs_is_fixed && !g.expr_can_be_fixed_array_literal(lhs_id) { return false } diff --git a/vlib/v3/pref/pref.v b/vlib/v3/pref/pref.v index 7347fd2356d599..bcb3c483f531c7 100644 --- a/vlib/v3/pref/pref.v +++ b/vlib/v3/pref/pref.v @@ -181,6 +181,24 @@ pub fn file_has_incompatible_os_suffix(file string, current_os string) bool { if os_name != 'solaris' && file.contains('_solaris.') { return true } + if os_name != 'qnx' && file.contains('_qnx.') { + return true + } + if os_name != 'serenity' && file.contains('_serenity.') { + return true + } + if os_name != 'plan9' && file.contains('_plan9.') { + return true + } + if os_name != 'vinix' && file.contains('_vinix.') { + return true + } + if os_name != 'haiku' && file.contains('_haiku.') { + return true + } + if os_name != 'android' && os_name != 'termux' && file.contains('_termux.') { + return true + } if file.contains('.amd64.') || file.contains('_amd64.') || file.contains('.arm64.') || file.contains('_arm64.') { return true diff --git a/vlib/v3/transform/monomorphize.v b/vlib/v3/transform/monomorphize.v index 2db5b43f57858c..e52ba11668999f 100644 --- a/vlib/v3/transform/monomorphize.v +++ b/vlib/v3/transform/monomorphize.v @@ -138,7 +138,13 @@ fn (mut t Transformer) collect_generic_fn_decls_for_erasure() map[string]Generic cur_module = node.value } .fn_decl { - if !generic_fn_decl_needs_erasure_scan(node, t.a) { + // Use the same detector as the monomorphize path: a method whose + // only generic parameter comes from its receiver struct (e.g. + // `fn (t ThreadLocalStorage[T]) set(value T)`) has no method-level + // `generic_params` and no `generic` substring in its param types, so + // the old literal-substring scan missed it and cgen emitted a raw + // `_T_` template body calling erased helpers. + if !t.fn_decl_has_unresolved_generics(node, cur_module) { continue } key := t.generic_fn_decl_key(node, cur_module) @@ -156,19 +162,6 @@ fn (mut t Transformer) collect_generic_fn_decls_for_erasure() map[string]Generic return decls } -fn generic_fn_decl_needs_erasure_scan(node flat.Node, a &flat.FlatAst) bool { - if node.generic_params.len > 0 || node.typ.contains('generic') { - return true - } - for i in 0 .. node.children_count { - child := a.child_node(&node, i) - if child.kind == .param && child.typ.contains('generic') { - return true - } - } - return false -} - fn building_v_keeps_type_erased_generic_template(key string) bool { return key in ['token.new_keywords_matcher_trie', 'sync.pool.PoolProcessor.work_on_items'] } diff --git a/vlib/v3/types/checker.v b/vlib/v3/types/checker.v index 61e606edf00787..fdc57114212753 100644 --- a/vlib/v3/types/checker.v +++ b/vlib/v3/types/checker.v @@ -803,9 +803,15 @@ fn (mut tc TypeChecker) check_c_struct_redeclarations(a &flat.FlatAst) { if !qname.starts_with('C.') { continue } + // Scope the check per module: a C struct name refers to a single + // external C type, but different modules may each mirror it with + // their own field view (e.g. `C.termios` in both `term` and + // `term.termios`). Only redeclarations within the same module are + // a genuine conflict. + key := '${tc.cur_module}|${qname}' c_struct_sig := tc.c_struct_decl_signature(a, node) - if qname in c_struct_decl_sigs { - existing_sig := c_struct_decl_sigs[qname] + if key in c_struct_decl_sigs { + existing_sig := c_struct_decl_sigs[key] if !c_struct_decl_signatures_compatible(existing_sig, c_struct_sig) { tc.record_error_unfiltered(.duplicate_decl, 'cannot redeclare C struct `${qname}`', flat.NodeId(node_idx)) @@ -813,10 +819,10 @@ fn (mut tc TypeChecker) check_c_struct_redeclarations(a &flat.FlatAst) { existing_fields := c_struct_decl_signature_field_count(existing_sig) current_fields := c_struct_decl_signature_field_count(c_struct_sig) if current_fields > existing_fields { - c_struct_decl_sigs[qname] = c_struct_sig + c_struct_decl_sigs[key] = c_struct_sig } } else { - c_struct_decl_sigs[qname] = c_struct_sig + c_struct_decl_sigs[key] = c_struct_sig } } else {} From c04972cff11e24428e1dc6c18fbf6f4a0b562b9c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 3 Jul 2026 13:11:03 +0300 Subject: [PATCH 2/2] ok --- vlib/v/gen/c/infix.v | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/vlib/v/gen/c/infix.v b/vlib/v/gen/c/infix.v index 91b1c0a9fbecb9..0697d8841d1aaf 100644 --- a/vlib/v/gen/c/infix.v +++ b/vlib/v/gen/c/infix.v @@ -243,6 +243,66 @@ fn (mut g Gen) infix_expr_eq_op(node ast.InfixExpr) { g.expr(ast.Expr(node.right)) g.write(')') } + } else if has_defined_eq_operator && left_is_option && right_is_option { + // `?T == ?T` where the base type `T` has a defined `==` (e.g. string, or a + // struct with an operator overload). Compare option-awarely: both none -> equal; + // differing none-ness -> not equal; otherwise compare the unwrapped values using + // the base type's `==`. (Without this, the eq function name was derived from the + // option-wrapped type, producing an undefined `_option_T__eq`.) + old_inside_opt_or_res := g.inside_opt_or_res + g.inside_opt_or_res = true + inside_and_rhs := g.infix_left_var_name.len > 0 + mut lv := '' + mut rv := '' + if inside_and_rhs { + lv = g.expr_string(node.left) + rv = g.expr_string(node.right) + } else { + left_tmp := g.expr_to_ctemp_before_stmt(node.left, left_type) + right_tmp := g.expr_to_ctemp_before_stmt(node.right, right_type) + lv = left_tmp.name + rv = right_tmp.name + } + mut method_name := '' + if left.sym.kind == .struct && (left.sym.info as ast.Struct).generic_types.len > 0 { + concrete_types := (left.sym.info as ast.Struct).concrete_types + method_name = '${left.sym.cname}__eq' + if left.unaliased_sym.is_builtin() { + method_name = 'builtin__${method_name}' + } + method_name = g.generic_fn_name(concrete_types, method_name) + } else { + mut mn := if has_alias_eq_op_overload { + g.styp(left.typ.clear_flag(.option).set_nr_muls(0)) + } else { + g.styp(left.unaliased.clear_flag(.option).set_nr_muls(0)) + } + mut is_builtin_or_alias_to_builtin := left.sym.is_builtin() + if !has_alias_eq_op_overload && !is_builtin_or_alias_to_builtin + && left.sym.info is ast.Alias { + parent_sym := g.table.sym((left.sym.info as ast.Alias).parent_type) + is_builtin_or_alias_to_builtin = parent_sym.is_builtin() + } + if is_builtin_or_alias_to_builtin { + mn = 'builtin__${mn}' + } + method_name = '${mn}__eq' + } + base_styp := g.base_type(left_type) + if node.op == .eq { + g.write('(') + } else { + g.write('!(') + } + g.write('(${lv}.state == 2 && ${rv}.state == 2) || ') + g.write('(${lv}.state == ${rv}.state && ${lv}.state != 2 && ') + if eq_operator_expects_ptr { + g.write('${method_name}((${base_styp}*)&${lv}.data, (${base_styp}*)&${rv}.data)') + } else { + g.write('${method_name}(*(${base_styp}*)&${lv}.data, *(${base_styp}*)&${rv}.data)') + } + g.write('))') + g.inside_opt_or_res = old_inside_opt_or_res } else if has_defined_eq_operator { if node.op == .ne { g.write('!')