From 2543c5683ca41a6a38768900799c3629ba5ef0a7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 31 May 2026 01:18:19 +0300 Subject: [PATCH] checker, ast, cgen: allow chained type aliases (fix #27055) (#27296) --- vlib/v/ast/table.v | 54 ++++++++++++++----- vlib/v/checker/checker.v | 25 ++++++--- .../checker/tests/chained_alias_cycle_err.out | 11 ++++ .../checker/tests/chained_alias_cycle_err.vv | 5 ++ vlib/v/checker/tests/nested_aliases.out | 4 -- vlib/v/checker/tests/nested_aliases.vv | 2 - .../tests/use_byte_instead_of_u8_err.out | 5 -- vlib/v/gen/c/cgen.v | 49 ++++++++++++++++- vlib/v/tests/aliases/chained_alias_test.v | 53 ++++++++++++++++++ .../tests/modules/chained_alias_a_module/a.v | 5 ++ .../tests/modules/chained_alias_b_module/b.v | 3 ++ .../modules/chained_alias_reexport_test.v | 13 +++++ 12 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 vlib/v/checker/tests/chained_alias_cycle_err.out create mode 100644 vlib/v/checker/tests/chained_alias_cycle_err.vv delete mode 100644 vlib/v/checker/tests/nested_aliases.out delete mode 100644 vlib/v/checker/tests/nested_aliases.vv create mode 100644 vlib/v/tests/aliases/chained_alias_test.v create mode 100644 vlib/v/tests/modules/chained_alias_a_module/a.v create mode 100644 vlib/v/tests/modules/chained_alias_b_module/b.v create mode 100644 vlib/v/tests/modules/chained_alias_reexport_test.v diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 505d1e93e..c78b797ed 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -1007,16 +1007,31 @@ pub fn (t &Table) sym(typ Type) &TypeSymbol { return invalid_type_symbol } -// final_sym follows aliases until it gets to a "real" Type +// max_alias_chain_depth caps the recursive walk through chained aliases +// (#27055) so a malformed input — e.g. mutual aliases formed via placeholder +// rewriting — can't hang the compiler in the type-resolution helpers below. +const max_alias_chain_depth = 64 + +// final_sym follows aliases until it gets to a "real" Type. Chained aliases +// (`type B = A` where `A` is itself an alias, #27055) are walked recursively; +// a depth cap guards against pathological inputs where mutual aliases slip +// past the checker's cycle rejection. @[direct_array_access] pub fn (t &Table) final_sym(typ Type) &TypeSymbol { mut idx := typ.idx() if idx > 0 && idx < t.type_symbols.len { - cur_sym := t.type_symbols[idx] - if cur_sym.info is Alias { - idx = cur_sym.info.parent_type.idx() + mut cur_sym := t.type_symbols[idx] + for _ in 0 .. max_alias_chain_depth { + if cur_sym.info !is Alias { + break + } + idx = (cur_sym.info as Alias).parent_type.idx() + if idx <= 0 || idx >= t.type_symbols.len { + break + } + cur_sym = t.type_symbols[idx] } - return t.type_symbols[idx] + return cur_sym } if idx == 0 { return invalid_type_symbol @@ -1030,15 +1045,26 @@ pub fn (t &Table) final_sym(typ Type) &TypeSymbol { pub fn (t &Table) final_type(typ Type) Type { mut idx := typ.idx() if idx > 0 && idx < t.type_symbols.len { - cur_sym := t.type_symbols[idx] - if cur_sym.info is Alias { - idx = cur_sym.info.parent_type.idx() - aliased_sym := t.type_symbols[idx] - if aliased_sym.info is Enum { - return aliased_sym.info.typ - } - return cur_sym.info.parent_type - } else if cur_sym.info is Enum { + mut cur_sym := t.type_symbols[idx] + mut last_alias_parent := Type(0) + for _ in 0 .. max_alias_chain_depth { + if cur_sym.info !is Alias { + break + } + last_alias_parent = (cur_sym.info as Alias).parent_type + idx = last_alias_parent.idx() + if idx <= 0 || idx >= t.type_symbols.len { + break + } + cur_sym = t.type_symbols[idx] + } + if last_alias_parent != 0 { + if cur_sym.info is Enum { + return cur_sym.info.typ + } + return last_alias_parent + } + if cur_sym.info is Enum { return cur_sym.info.typ } } diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 91adbfcca..7fcceacb4 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -992,13 +992,24 @@ fn (mut c Checker) alias_type_decl(mut node ast.AliasTypeDecl) { c.error('unknown aliased type `${parent_typ_sym.name}`', node.type_pos) } .alias { - orig_sym := c.table.sym((parent_typ_sym.info as ast.Alias).parent_type) - if !node.name.starts_with('C.') - && parent_typ_sym.name !in ['strings.Builder', 'StringBuilder', 'builtin.StringBuilder'] { - // TODO: remove the whole check, or at least the need for special casing `strings.Builder` and `StringBuilder` here - // after more testing and bootstrapping of the strings.Builder -> builtin.StringBuilder change - c.error('type `${parent_typ_sym.str()}` is an alias, use the original alias type `${orig_sym.name}` instead', - node.type_pos) + // chained aliases (`type B = A` where `A` is itself an alias) are + // allowed; matches Go's alias semantics and lets modules re-export + // types from their dependencies without leaking the original module + // name to callers (#27055). Walk the parent chain to reject cycles + // such as `type A = B; type B = A`, which would otherwise hang + // downstream consumers that follow aliases unconditionally. + mut visited := []int{cap: 8} + visited << int(node.parent_type.idx()) + mut cur := parent_typ_sym + for cur.info is ast.Alias { + parent_idx := int(cur.info.parent_type.idx()) + if parent_idx in visited { + c.error('alias `${node.name}` forms a cycle through `${parent_typ_sym.name}`', + node.type_pos) + break + } + visited << parent_idx + cur = c.table.sym(cur.info.parent_type) } } .chan { diff --git a/vlib/v/checker/tests/chained_alias_cycle_err.out b/vlib/v/checker/tests/chained_alias_cycle_err.out new file mode 100644 index 000000000..8deb03cbf --- /dev/null +++ b/vlib/v/checker/tests/chained_alias_cycle_err.out @@ -0,0 +1,11 @@ +vlib/v/checker/tests/chained_alias_cycle_err.vv:1:14: error: alias `Alpha` forms a cycle through `Beta` + 1 | type Alpha = Beta + | ~~~~ + 2 | type Beta = Alpha + 3 | +vlib/v/checker/tests/chained_alias_cycle_err.vv:2:13: error: alias `Beta` forms a cycle through `Alpha` + 1 | type Alpha = Beta + 2 | type Beta = Alpha + | ~~~~~ + 3 | + 4 | fn main() { diff --git a/vlib/v/checker/tests/chained_alias_cycle_err.vv b/vlib/v/checker/tests/chained_alias_cycle_err.vv new file mode 100644 index 000000000..5c802288a --- /dev/null +++ b/vlib/v/checker/tests/chained_alias_cycle_err.vv @@ -0,0 +1,5 @@ +type Alpha = Beta +type Beta = Alpha + +fn main() { +} diff --git a/vlib/v/checker/tests/nested_aliases.out b/vlib/v/checker/tests/nested_aliases.out deleted file mode 100644 index da93210b9..000000000 --- a/vlib/v/checker/tests/nested_aliases.out +++ /dev/null @@ -1,4 +0,0 @@ -vlib/v/checker/tests/nested_aliases.vv:2:16: error: type `MyInt` is an alias, use the original alias type `int` instead - 1 | type MyInt = int - 2 | type MyMyInt = MyInt - | ~~~~~ diff --git a/vlib/v/checker/tests/nested_aliases.vv b/vlib/v/checker/tests/nested_aliases.vv deleted file mode 100644 index a21d0d365..000000000 --- a/vlib/v/checker/tests/nested_aliases.vv +++ /dev/null @@ -1,2 +0,0 @@ -type MyInt = int -type MyMyInt = MyInt diff --git a/vlib/v/checker/tests/use_byte_instead_of_u8_err.out b/vlib/v/checker/tests/use_byte_instead_of_u8_err.out index 8d77d56d0..71a57e44f 100644 --- a/vlib/v/checker/tests/use_byte_instead_of_u8_err.out +++ b/vlib/v/checker/tests/use_byte_instead_of_u8_err.out @@ -1,8 +1,3 @@ -vlib/v/checker/tests/use_byte_instead_of_u8_err.vv:1:13: error: type `byte` is an alias, use the original alias type `u8` instead - 1 | type Byte = byte - | ~~~~ - 2 | - 3 | fn foo(_ byte) {} vlib/v/checker/tests/use_byte_instead_of_u8_err.vv:3:10: error: byte is deprecated, use u8 instead 1 | type Byte = byte 2 | diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index a15ab02ba..81d1f1d13 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -2840,12 +2840,16 @@ pub fn (mut g Gen) write_typedef_types() { } } } + // Emit aliases parent-first so that chained aliases (`type B = A` where + // `A` is itself an alias, #27055) typedef in dependency order; otherwise C + // hits the unknown parent typedef and silently falls back to implicit int. + mut emitted_alias := map[int]bool{} for sym in g.table.type_symbols { if sym.kind == .alias && !sym.is_builtin && sym.name !in ['byte', 'i32'] { if g.pref.skip_unused && sym.idx !in g.table.used_features.used_syms { continue } - g.write_alias_typesymbol_declaration(sym) + g.emit_alias_typedef_chain(sym, mut emitted_alias) } } g.write_sorted_fn_typesymbol_declaration() @@ -2872,6 +2876,29 @@ pub fn (mut g Gen) write_typedef_types() { } } +// emit_alias_typedef_chain emits parent alias typedefs before child ones so a +// chain like `type Main = A; type A = B; type B = string` ends up as +// `typedef string B; typedef B A; typedef A Main;` in declaration order. +fn (mut g Gen) emit_alias_typedef_chain(sym ast.TypeSymbol, mut emitted map[int]bool) { + if emitted[sym.idx] { + return + } + emitted[sym.idx] = true + if sym.info is ast.Alias { + parent_idx := sym.info.parent_type.idx() + if parent_idx > 0 && parent_idx < g.table.type_symbols.len { + parent_sym := g.table.type_symbols[parent_idx] + if parent_sym.kind == .alias && !parent_sym.is_builtin + && parent_sym.name !in ['byte', 'i32'] { + if !(g.pref.skip_unused && parent_sym.idx !in g.table.used_features.used_syms) { + g.emit_alias_typedef_chain(parent_sym, mut emitted) + } + } + } + } + g.write_alias_typesymbol_declaration(sym) +} + pub fn (mut g Gen) write_alias_typesymbol_declaration(sym ast.TypeSymbol) { mut levels := 0 parent := g.table.type_symbols[sym.parent_idx] @@ -2885,6 +2912,26 @@ pub fn (mut g Gen) write_alias_typesymbol_declaration(sym ast.TypeSymbol) { } else { if sym.info is ast.Alias { parent_styp = g.styp(sym.info.parent_type) + // Chained aliases (#27055): if the parent is itself an alias whose + // ultimate base is a fixed array of non-builtin elements, the parent + // typedef has been deferred to `alias_definitions` so this child + // must also be deferred — otherwise `typedef Parent Child;` lands + // in `type_definitions` before `Parent` exists. + mut walk_typ := sym.info.parent_type + for { + walk_sym := g.table.sym(walk_typ) + if walk_sym.info is ast.Alias { + walk_typ = walk_sym.info.parent_type + continue + } + if walk_sym.info is ast.ArrayFixed { + base_elem_sym := g.fixed_array_base_elem_sym(walk_sym.info.elem_type) + if !base_elem_sym.is_builtin() { + is_fixed_array_of_non_builtin = true + } + } + break + } parent_sym := g.table.sym(sym.info.parent_type) if parent_sym.info is ast.ArrayFixed { mut elem_sym := g.table.sym(parent_sym.info.elem_type) diff --git a/vlib/v/tests/aliases/chained_alias_test.v b/vlib/v/tests/aliases/chained_alias_test.v new file mode 100644 index 000000000..9830c84e3 --- /dev/null +++ b/vlib/v/tests/aliases/chained_alias_test.v @@ -0,0 +1,53 @@ +// Chained aliases (`type B = A` where `A` is itself an alias, #27055) let +// modules re-export types from their dependencies without leaking the original +// module name to callers. + +type Id = int +type UserId = Id +type AdminId = UserId + +struct Box { + v int +} + +type Wrapped = Box +type DoubleWrapped = Wrapped + +fn test_primitive_chain() { + a := AdminId(42) + b := UserId(a) + c := Id(b) + assert int(c) == 42 + assert int(a) == 42 +} + +fn test_struct_chain() { + dw := DoubleWrapped{ + v: 7 + } + w := Wrapped(dw) + b := Box(w) + assert b.v == 7 + assert dw.v == 7 +} + +fn test_chain_assignment_compat() { + x := AdminId(1) + mut y := Id(0) + y = Id(x) + assert int(y) == 1 +} + +struct Item { + v int +} + +type ItemPair = [2]Item +type ItemPairAlias = ItemPair + +fn test_chain_through_fixed_array_of_non_builtin() { + a := ItemPair([Item{v: 1}, Item{v: 2}]!) + b := ItemPairAlias(a) + assert b[0].v == 1 + assert b[1].v == 2 +} diff --git a/vlib/v/tests/modules/chained_alias_a_module/a.v b/vlib/v/tests/modules/chained_alias_a_module/a.v new file mode 100644 index 000000000..ed494cfbe --- /dev/null +++ b/vlib/v/tests/modules/chained_alias_a_module/a.v @@ -0,0 +1,5 @@ +module chained_alias_a_module + +import chained_alias_b_module as b + +pub type ID = b.ID diff --git a/vlib/v/tests/modules/chained_alias_b_module/b.v b/vlib/v/tests/modules/chained_alias_b_module/b.v new file mode 100644 index 000000000..78387c95a --- /dev/null +++ b/vlib/v/tests/modules/chained_alias_b_module/b.v @@ -0,0 +1,3 @@ +module chained_alias_b_module + +pub type ID = string diff --git a/vlib/v/tests/modules/chained_alias_reexport_test.v b/vlib/v/tests/modules/chained_alias_reexport_test.v new file mode 100644 index 000000000..a6f11376e --- /dev/null +++ b/vlib/v/tests/modules/chained_alias_reexport_test.v @@ -0,0 +1,13 @@ +// Cross-module chained aliases: module `a` re-exports `b.ID` as `a.ID`, and a +// downstream caller only needs to know about `a` (#27055). +import chained_alias_a_module as a + +type ID = a.ID + +fn test_chained_alias_across_modules() { + id := ID('hello') + assert id.str() == 'hello' + b_id := a.ID('world') + mid := ID(b_id) + assert mid.str() == 'world' +} -- 2.39.5