From b0e0a1e9348ec11888beaaa21ea9f7ff6eeea0c4 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 28 May 2026 02:39:51 +0300 Subject: [PATCH] =?UTF-8?q?checker:=20accept=20`[]Alias`=20=E2=86=94=20`To?= =?UTF-8?q?kens`=20payload=20equivalence=20(fix=20#27006)=20(#27278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vlib/v/ast/table.v | 15 +++ vlib/v/checker/check_types.v | 9 ++ ...n_signature_alias_payload_mismatch_err.out | 6 + ...fn_signature_alias_payload_mismatch_err.vv | 20 +++ ...y_of_alias_passed_to_alias_of_array_test.v | 123 ++++++++++++++++++ 5 files changed, 173 insertions(+) create mode 100644 vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.out create mode 100644 vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.vv create mode 100644 vlib/v/tests/array_of_alias_passed_to_alias_of_array_test.v diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 4699cc3cd..505d1e93e 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -1087,9 +1087,24 @@ pub fn (t &Table) are_payloads_alias_compatible(a Type, b Type) bool { } a_unaliased := t.fully_unaliased_type(a) b_unaliased := t.fully_unaliased_type(b) + // `?T`/`!T` lower to a distinct `_option_T`/`_result_T` C struct per + // element type, so `?Alias` and `?T` (or container elements wrapping the + // same) are not layout-equivalent even if `Alias = T`. The top-level + // conversion in return.v / cgen.v (#27264) clears these flags before + // recursing — if they remain after unaliasing (including the case where + // the alias's parent carries the flag), reject (#27278 review). + if a_unaliased.has_option_or_result() || b_unaliased.has_option_or_result() { + return false + } if a_unaliased == b_unaliased { return true } + // `sym()` looks up by idx and drops `nr_muls()`, so without this guard a + // pointer like `&[]Token` would be reported as payload-compatible with + // `Tokens` (#27278 review). + if a_unaliased.nr_muls() != b_unaliased.nr_muls() { + return false + } a_sym := t.sym(a_unaliased) b_sym := t.sym(b_unaliased) if a_sym.kind != b_sym.kind { diff --git a/vlib/v/checker/check_types.v b/vlib/v/checker/check_types.v index 512c8b4cf..b61575cac 100644 --- a/vlib/v/checker/check_types.v +++ b/vlib/v/checker/check_types.v @@ -540,6 +540,15 @@ fn (mut c Checker) check_basic(got ast.Type, expected ast.Type) bool { if c.table.fully_unaliased_type(got) == c.table.fully_unaliased_type(expected) { return true } + // `[]Alias` ↔ `Tokens` where `type Alias = Parent` and `type Tokens = []Parent` + // (and the same for fixed arrays/maps): one side wraps the container in a + // top-level alias, the other doesn't, so `fully_unaliased_type` above only + // peels the alias side. The helper recurses through the container payload + // and rejects option/result wrappers itself (they lower to distinct + // `_option_*`/`_result_*` C structs per element type). + if c.table.are_payloads_alias_compatible(got, expected) { + return true + } unalias_got, unalias_expected := c.table.unalias_num_type(got), c.table.unalias_num_type(expected) if unalias_got.idx() == unalias_expected.idx() { // this is returning true even if one type is a ptr diff --git a/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.out b/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.out new file mode 100644 index 000000000..d366b0fd5 --- /dev/null +++ b/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.out @@ -0,0 +1,6 @@ +vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.vv:19:7: error: casting a function value from one function signature, to another function signature, should be done inside `unsafe{}` blocks + 17 | // Without `unsafe`, this must error: `fn () ![]Token` is *not* assignable + 18 | // to `fn () !Tokens` even though the payloads have the same layout. + 19 | _ := ReturnsTokens(returns_array_of_token) + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 20 | } diff --git a/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.vv b/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.vv new file mode 100644 index 000000000..503071a8c --- /dev/null +++ b/vlib/v/checker/tests/fn_signature_alias_payload_mismatch_err.vv @@ -0,0 +1,20 @@ +// Function-pointer signatures should NOT match across alias-equivalent +// result/option payloads, because the `_result_*` / `_option_*` C wrapper +// structs are distinct types — the conversion path used for `return some_call()` +// (#27264) doesn't run at the fn-signature boundary, so accepting these would +// pass V checking and then emit incompatible C function-pointer ABI. + +type Token = string +type Tokens = []string + +type ReturnsTokens = fn () !Tokens + +fn returns_array_of_token() ![]Token { + return [Token('hi')] +} + +fn main() { + // Without `unsafe`, this must error: `fn () ![]Token` is *not* assignable + // to `fn () !Tokens` even though the payloads have the same layout. + _ := ReturnsTokens(returns_array_of_token) +} diff --git a/vlib/v/tests/array_of_alias_passed_to_alias_of_array_test.v b/vlib/v/tests/array_of_alias_passed_to_alias_of_array_test.v new file mode 100644 index 000000000..eb07f64fa --- /dev/null +++ b/vlib/v/tests/array_of_alias_passed_to_alias_of_array_test.v @@ -0,0 +1,123 @@ +// Regression test for https://github.com/vlang/v/issues/27006: +// `[]Alias` should be assignable to `Tokens` (and vice versa) when +// `type Alias = T` and `type Tokens = []T`, since both describe the +// same in-memory payload. + +type Token = string +type Tokens = []string + +type Idx = int +type Idxs = []int + +struct Pair { + a int + b int +} + +type APair = Pair +type APairs = []Pair + +fn take_tokens(data Tokens) string { + return data.join(',') +} + +fn take_idxs(data Idxs) int { + mut sum := 0 + for v in data { + sum += v + } + return sum +} + +fn take_apairs(data APairs) int { + mut sum := 0 + for p in data { + sum += p.a + p.b + } + return sum +} + +fn make_tokens_return() Tokens { + mut tokens := []Token{} + tokens << Token('hello') + tokens << Token('world') + return tokens +} + +fn make_idxs_return() Idxs { + mut idxs := []Idx{} + idxs << Idx(1) + idxs << Idx(2) + return idxs +} + +fn test_array_of_alias_to_alias_of_array_arg() { + mut tokens := []Token{} + tokens << Token('hello') + tokens << Token('world') + assert take_tokens(tokens) == 'hello,world' +} + +fn test_array_of_alias_to_alias_of_array_int_arg() { + mut idxs := []Idx{} + idxs << Idx(10) + idxs << Idx(20) + assert take_idxs(idxs) == 30 +} + +fn test_array_of_alias_to_alias_of_array_struct_arg() { + mut pairs := []APair{} + pairs << APair(Pair{1, 2}) + pairs << APair(Pair{3, 4}) + assert take_apairs(pairs) == 10 +} + +fn test_array_of_alias_to_alias_of_array_return() { + t := make_tokens_return() + assert t.len == 2 + assert t[0] == 'hello' +} + +fn test_array_of_alias_to_alias_of_array_int_return() { + idxs := make_idxs_return() + assert idxs.len == 2 + assert idxs[0] == 1 +} + +fn test_array_of_alias_to_alias_of_array_assign() { + mut tokens := []Token{} + tokens << Token('a') + mut t2 := Tokens([]) + t2 = tokens + assert t2.len == 1 +} + +fn test_alias_of_array_to_array_of_alias_arg() { + tokens := Tokens(['x', 'y']) + assert take_array_of_token(tokens) == 'x|y' +} + +fn take_array_of_token(data []Token) string { + return data.join('|') +} + +fn test_fixed_array_of_alias() { + arr := [Token('a'), Token('b')]! + got := take_fixed_string(arr) + assert got == 'a-b' +} + +fn take_fixed_string(data [2]string) string { + return '${data[0]}-${data[1]}' +} + +type StrMap = map[string]string + +fn take_strmap(m StrMap) int { + return m.len +} + +fn test_map_with_alias_values() { + m := map[string]Token{} + assert take_strmap(m) == 0 +} -- 2.39.5