types: preserve caller types in imported composite generic arguments

This commit is contained in:
Alexander Medvednikov committed 2026-10-27 09:34:06 +03:00
1 parent efa3d1ce5e
commit 70076fe6f0
7 files changed
+176 -3

No files matched your search

@@ -10,6 +10,8 @@ pub:
id int
}
type RowVisitor = fn (Row) int
fn test_callback_parameter_infers_caller_type() {
rows := [Row{
id: 2
@@ -87,3 +89,56 @@ fn test_receiver_method_callback_keeps_caller_type() {
})
assert ids == [11]
}
fn test_function_type_generic_argument_keeps_caller_type() {
cell := genericmodule.cell_of(fn (row Row) Row {
return Row{ id: row.id + 1 }
})
assert cell.value(Row{ id: 12 }).id == 13
other := genericmodule.cell_of(fn (row genericmodule.Row) genericmodule.Row {
return row
})
assert other.value(genericmodule.Row{ name: 'module' }).name == 'module'
}
fn test_channel_type_generic_argument_keeps_caller_type() {
values := chan Row{cap: 1}
values <- Row{ id: 14 }
cell := genericmodule.cell_of(values)
row := <-cell.value
assert row.id == 14
}
fn test_nested_generic_argument_keeps_caller_type() {
inner := genericmodule.cell_of(Row{ id: 15 })
outer := genericmodule.cell_of(inner)
assert outer.value.value.id == 15
}
fn test_callback_alias_generic_argument_keeps_caller_type() {
visit := RowVisitor(fn (row Row) int {
return row.id
})
cell := genericmodule.cell_of(visit)
assert cell.value(Row{ id: 16 }) == 16
}
fn test_nested_callback_generic_argument_keeps_caller_type() {
cell := genericmodule.cell_of(fn (visit fn (Row) Row, row Row) Row {
return visit(row)
})
row := cell.value(fn (value Row) Row {
return Row{ id: value.id + 1 }
}, Row{ id: 17 })
assert row.id == 18
}
fn test_mutable_callback_generic_argument_keeps_caller_type() {
cell := genericmodule.cell_of(fn (mut rows []Row) int {
rows << Row{ id: 19 }
return rows.len
})
mut rows := []Row{}
assert cell.value(mut rows) == 1
assert rows[0].id == 19
}
+36 -2
View File
@@ -10020,6 +10020,14 @@ fn (t &Transformer) generic_arg_for_call_and_decl_module(arg_id u32, call_module
arg_comparison_type_id_10 := arg_id
arg_source_id_8 := arg_comparison_type_id_10
arg := flat.source_type_name(arg_source_id_8)
if call_module in ['', 'main'] {
// Lock caller types inside composites before normalization in the callee
// module can bind their bare names to its own declarations.
locked := t.lock_colliding_main_generic_type_text(arg_id, decl_module)
if locked != arg_id {
return locked
}
}
if call_module in ['', 'main'] && !arg.contains('.') {
if info := t.structs[trimmed_transform_text(arg)] {
if info.module in ['', 'main'] {
@@ -15843,8 +15851,9 @@ fn (t &Transformer) lock_colliding_main_generic_type_text_bytes(typ string, modu
mut changed := false
mut locked_params := []string{cap: params.len}
for param in params {
locked := t.lock_colliding_main_generic_type_text_bytes(param, module_name)
if locked != trimmed_transform_text(param) {
payload := generic_fn_type_param_mode_payload(param)
locked := t.lock_colliding_main_generic_type_text_bytes(payload, module_name)
if locked != payload {
changed = true
}
locked_params << locked
@@ -16887,6 +16896,31 @@ fn (t &Transformer) canonical_generic_specialization_arg(arg_id u32) u32 {
if clean.starts_with('[]') {
return flat.source_type_id('[]' + flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(clean[2..]))))
}
for prefix in ['chan ', 'thread '] {
if clean.starts_with(prefix) {
return flat.source_type_id(prefix + flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(clean[prefix.len..]))))
}
}
if clean.starts_with('fn(') || clean.starts_with('fn (') {
params, ret := fn_type_text_parts(clean) or { return clean_id }
mut canonical_params := []string{cap: params.len}
for param in params {
canonical_params << flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(generic_fn_type_param_mode_payload(param))))
}
canonical_ret := flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(ret)))
return flat.source_type_id('fn (${canonical_params.join(', ')})' + if canonical_ret.len > 0 {
' ' + canonical_ret
} else {
''
})
}
if clean.starts_with('(') && clean.ends_with(')') && clean.contains(',') {
mut parts := []string{}
for part in split_generic_args(clean[1..clean.len - 1]) {
parts << flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(part)))
}
return flat.source_type_id('(' + parts.join(', ') + ')')
}
// A fixed array can reach a second inference pass through its lowered C-name
// spelling (`_3int`). Restore the source spelling before using it as a
// specialization key, otherwise the duplicate key emits an invalid `_3int`
+17
View File
@@ -1386,3 +1386,20 @@ fn test_forwarded_generic_call_keeps_caller_context_identity() {
t.substitute_cloned_generic_call_type_args(index, mut children, [flat.source_type_id('Context')])
assert t.a.node(children[1]).value_text() == 'main.Context'
}
fn test_composite_generic_arguments_keep_callback_shape_and_modes() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
tc.structs['Row'] = []types.StructField{}
tc.structs['callee.Row'] = []types.StructField{}
mut t := new_transformer(mut a, &tc, map[string]bool{})
t.structs['Row'] = StructInfo{}
t.structs['callee.Row'] = StructInfo{ module: 'callee' }
t.qualified_types.set('Row', 'callee.Row')
assert flat.source_type_name(t.generic_arg_for_call_and_decl_module(flat.source_type_id('fn (row Row)'), 'main', 'callee')) == 'fn (main.Row)'
assert flat.source_type_name(t.generic_arg_for_call_and_decl_module(flat.source_type_id('fn (mut row Row, callee.Row) Row'), 'main', 'callee')) == 'fn (mut main.Row, callee.Row) main.Row'
assert flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id('fn (row callee.Row) callee.Row'))) == 'fn (callee.Row) callee.Row'
for spelling in ['fn (Row)', 'fn (mut Row)', 'fn (&Row)', 'chan Row', 'thread Row'] {
assert flat.source_type_name(t.canonical_generic_specialization_arg(flat.source_type_id(spelling))) == spelling
}
}
+3
View File
@@ -79,6 +79,9 @@ refer to the caller's payload. An explicit pointer payload keeps its declared po
Intern new source text with `flat.source_type_id(text)` at a parsing boundary. An AST annotation
already carries that ID in `node.typ`. Parsing still observes imports, generic bindings, and the
declaration context; identical spelling IDs can resolve to different semantic types.
Imported generic signatures retain caller-owned types inside callback signatures, channel
payloads, and nested generic arguments. A same-named type declared by the imported module
does not replace those caller types.
`Type.source_type_id()`, `TypeChecker.type_name(value)`, and `type_name_by_id(id)` return spelling
IDs. Resolve bytes with `flat.source_type_name(id)` for diagnostics, spelling syntax, or emission.
Builtin spelling constants in `v.flat` avoid repeated interning of scalar names.
+33
View File
@@ -11430,6 +11430,31 @@ fn (tc &TypeChecker) explicit_generic_concrete_arg_text_bytes(type_arg string) s
if clean.starts_with('[]') {
return '[]' + tc.explicit_generic_concrete_arg_text_bytes(clean[2..])
}
for prefix in ['mut ', 'shared ', 'atomic ', 'chan mut ', 'chan ', 'thread '] {
if clean.starts_with(prefix) {
return prefix + tc.explicit_generic_concrete_arg_text_bytes(clean[prefix.len..])
}
}
if is_fn_type_text(clean) {
if params, ret := fn_type_text_parts(clean) {
mut qualified_params := []string{cap: params.len}
for param in params {
qualified_params << tc.explicit_generic_concrete_arg_text_bytes(normalize_fn_type_param_text_preserving_mut(param))
}
return 'fn (${qualified_params.join(', ')})' + if ret.len > 0 {
' ' + tc.explicit_generic_concrete_arg_text_bytes(ret)
} else {
''
}
}
}
if clean.starts_with('(') && clean.ends_with(')') {
mut qualified_parts := []string{}
for part in split_params(clean[1..clean.len - 1]) {
qualified_parts << tc.explicit_generic_concrete_arg_text_bytes(part)
}
return '(' + qualified_parts.join(', ') + ')'
}
if clean.starts_with('map[') {
close := find_matching_bracket(clean, 3)
if close > 3 && close + 1 < clean.len {
@@ -11444,6 +11469,14 @@ fn (tc &TypeChecker) explicit_generic_concrete_arg_text_bytes(type_arg string) s
return clean[..close + 1] + tc.explicit_generic_concrete_arg_text_bytes(clean[close + 1..])
}
}
base, args, is_generic := generic_type_application_parts(clean)
if is_generic {
mut qualified_args := []string{cap: args.len}
for arg in args {
qualified_args << tc.explicit_generic_concrete_arg_text_bytes(arg)
}
return tc.explicit_generic_concrete_arg_text_bytes(base) + '[' + qualified_args.join(', ') + ']'
}
if !clean.contains('.') {
if tc.source_declares_type_in_scope(clean, tc.cur_file, tc.cur_module) {
return if tc.cur_module !in ['', 'main'] {
+3 -1
View File
@@ -21617,7 +21617,9 @@ fn (tc &TypeChecker) c_generic_struct_arg_name(arg string) string {
return fixed
}
}
if clean.contains('.') {
// A module separator inside a composite belongs to one of its components.
// Shortening the whole argument would turn `fn (mod.Row) mod.Row` into `Row`.
if clean.contains('.') && !clean.contains_any(' &?![](),') {
short := clean.all_after_last('.')
if target_source_id := tc.type_aliases.type_id(clean) {
target := flat.source_type_name(target_source_id)
+29
View File
@@ -0,0 +1,29 @@
module types
import v.flat
fn test_concrete_generic_arguments_lock_nested_caller_types() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.cur_module = 'main'
tc.structs['Row'] = []StructField{}
tc.structs['foreign.Row'] = []StructField{}
assert tc.explicit_generic_concrete_arg_text_bytes('fn (row Row) Row') == 'fn (main.Row) main.Row'
assert tc.explicit_generic_concrete_arg_text_bytes('fn (mut row Row) (Row, []Row)') == 'fn (mut main.Row) (main.Row, []main.Row)'
assert tc.explicit_generic_concrete_arg_text_bytes('chan Row') == 'chan main.Row'
for prefix in ['thread ', 'shared ', 'atomic ', 'chan mut '] {
assert tc.explicit_generic_concrete_arg_text_bytes(prefix + 'Row') == prefix + 'main.Row'
}
assert tc.explicit_generic_concrete_arg_text_bytes('foreign.Cell[Row]') == 'foreign.Cell[main.Row]'
assert tc.explicit_generic_concrete_arg_text_bytes('fn (foreign.Row) foreign.Row') == 'fn (foreign.Row) foreign.Row'
}
fn test_generic_struct_c_name_keeps_composite_argument_shape() {
a := flat.FlatAst.new()
mut tc := TypeChecker.new(&a)
tc.structs['Row'] = []StructField{}
tc.structs['foreign.Row'] = []StructField{}
for arg in ['fn (foreign.Row) foreign.Row', 'chan foreign.Row', 'foreign.Cell[foreign.Row]'] {
assert tc.c_generic_struct_arg_name(arg) == arg
}
}