Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bfc956b
Add depth limit to fully_unaliased_type function
tailsmails Jul 31, 2026
cc93f15
Implement depth limit for generic instantiation
tailsmails Jul 31, 2026
342bfe6
Reduce generic function postprocess iterations limit
tailsmails Jul 31, 2026
b9ebd72
Handle error reporting for generic function limit
tailsmails Jul 31, 2026
f9ccc1b
Merge branch 'vlang:master' into master
tailsmails Jul 31, 2026
cef9581
Add cutoff limits for various generic instantiation depths
tailsmails Jul 31, 2026
7a859cc
Decrease generic function postprocess iterations limit
tailsmails Jul 31, 2026
ca75f9a
Implement tests for nested generics and type aliases
tailsmails Aug 1, 2026
43d8c4f
v fmt table
tailsmails Aug 1, 2026
28924a8
v fmt checker
tailsmails Aug 1, 2026
be03e72
v fmt recursion_limits_test
tailsmails Aug 1, 2026
58e0fa6
Add depth tracking for generic type conversion
tailsmails Aug 1, 2026
ab9edce
Add max_postprocess_iterations constant
tailsmails Aug 1, 2026
489bc06
Increase cutoff limits for generic function and name
tailsmails Aug 1, 2026
a936e3d
Increase generic_fn_postprocess_iterations_cutoff_limit
tailsmails Aug 1, 2026
81a5f51
Refactor generic type conversion with depth handling
tailsmails Aug 1, 2026
5a3c4b2
Remove generic type depth fields from context
tailsmails Aug 1, 2026
6879480
Refactor generic parameter type handling in Table
tailsmails Aug 1, 2026
dc18b0b
Reduce generic function postprocess iterations limit
tailsmails Aug 1, 2026
b23be70
Merge branch 'vlang:master' into master
tailsmails Aug 1, 2026
3723a7e
Merge branch 'vlang:master' into master
tailsmails Aug 4, 2026
32fe5d7
Refactor limits to use configurable parameters
tailsmails Aug 4, 2026
1e3635c
Add generic function and instance limits to builder
tailsmails Aug 4, 2026
8d9b055
Refactor generic function post-processing limits
tailsmails Aug 4, 2026
243db77
Add tests for recursion and cutoff guard limits
tailsmails Aug 4, 2026
f926618
Delete vlib/v/checker/recursion_limits_test.v
tailsmails Aug 4, 2026
fa81479
Add resource limits and command line options
tailsmails Aug 4, 2026
643052f
Document resource limits for generics in build.txt
tailsmails Aug 4, 2026
067d1ec
Add flags for checker fixture and macOS compatibility
tailsmails Aug 5, 2026
dc3b065
Merge branch 'vlang:master' into master
tailsmails Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions vlib/v/ast/table.v
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import v.cflag
import v.util
import v.token

const alias_unwrap_depth_cutoff_limit = 100
const generic_inst_depth_cutoff_limit = 256
const struct_fields_depth_cutoff_limit = 100
const generic_fn_inst_cutoff_limit = 256
const generic_inst_name_len_cutoff_limit = 1_024
const max_postprocess_iterations = 100_000

@[heap; minify]
pub struct UsedFeatures {
pub mut:
Expand Down Expand Up @@ -97,6 +104,8 @@ pub mut:
panic_handler FnPanicHandler = default_table_panic_handler
panic_userdata voidptr = unsafe { nil } // can be used to pass arbitrary data to panic_handler;
panic_npanics int
convert_generic_type_depth int
unwrap_generic_type_depth int
cur_fn &FnDecl = unsafe { nil } // previously stored in Checker.cur_fn and Gen.cur_fn
cur_lambda &LambdaExpr = unsafe { nil } // current lambda node
cur_concrete_types []Type // current concrete types, e.g. [int, string]
Expand Down Expand Up @@ -1547,12 +1556,14 @@ pub fn (t &Table) are_payloads_alias_compatible(a Type, b Type) bool {
pub fn (t &Table) fully_unaliased_type(typ Type) Type {
mut unaliased := typ
mut extra_flags := u32(typ) & 0xff00_0000
for {
mut depth := 0
for depth < alias_unwrap_depth_cutoff_limit {
sym := t.sym(unaliased)
if sym.info is Alias {
parent_typ := sym.info.parent_type
unaliased = Type(u32(parent_typ.set_nr_muls(parent_typ.nr_muls() + unaliased.nr_muls())) | extra_flags)
extra_flags |= u32(unaliased) & 0xff00_0000
depth++
continue
}
return unaliased
Expand Down Expand Up @@ -2306,6 +2317,9 @@ pub fn (mut t Table) find_or_register_generic_inst(parent_typ Type, concrete_typ
}
}
inst_name += ']'
if inst_name.len > generic_inst_name_len_cutoff_limit {
t.panic('generic instantiation name limit exceeded')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow finite generic instance names longer than 1 KiB

A finite generic instantiation can exceed 1,024 bytes without containing a cycle—for example, a generated generic with many concrete parameters or nested qualified type names—yet it now unconditionally panics here. Because V does not otherwise impose this identifier-length restriction, valid source that previously compiled now crashes; recursion should be bounded by the instantiation graph rather than the rendered name length.

Useful? React with 👍 / 👎.

}
existing_idx := t.type_idxs[inst_name]
if existing_idx > 0 {
if t.type_symbols[existing_idx].kind == .placeholder {
Expand Down Expand Up @@ -2409,6 +2423,9 @@ pub fn (mut t Table) register_fn_generic_types(fn_name string) {
}

pub fn (mut t Table) register_fn_concrete_types(fn_name string, types []Type) bool {
if t.fn_generic_types[fn_name].len > generic_fn_inst_cutoff_limit {
t.panic('generic function instantiation limit exceeded')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the total generic-instantiation cap with cycle detection

When a valid program specializes one generic function for 257 distinct concrete type lists, the map entry reaches length 257 and every subsequent call to register_fn_concrete_types—even a duplicate that would otherwise return false—panics here. The number of finite specializations is not evidence of recursive expansion, and generated serializers or dispatch code can legitimately exceed this threshold, so this turns valid compilation into a compiler crash; guard actual recursive growth instead of the accumulated specialization count.

Useful? React with 👍 / 👎.

}
if types.len == 0 {
return false
}
Expand Down Expand Up @@ -3016,8 +3033,15 @@ pub fn (mut t Table) convert_generic_static_type_name(fn_name string, generic_na
return void_type, fn_name
}

// convert_generic_type convert generics to real types (T => int) or other generics type.
pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []string, to_types []Type) ?Type {
t.convert_generic_type_depth++
if t.convert_generic_type_depth > generic_inst_depth_cutoff_limit {
t.convert_generic_type_depth--
return none
}
defer {
t.convert_generic_type_depth--
}
if generic_names.len != to_types.len {
return none
}
Expand Down Expand Up @@ -4013,11 +4037,22 @@ pub fn (mut t Table) unwrap_generic_type_ex(typ Type, generic_names []string, co
}

fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []string, concrete_types []Type, recheck_concrete_types bool, depth_guard []string) Type {
t.unwrap_generic_type_depth++
if t.unwrap_generic_type_depth > generic_inst_depth_cutoff_limit {
t.unwrap_generic_type_depth--
t.panic('generic instantiation depth limit exceeded')
}
defer {
t.unwrap_generic_type_depth--
}
mut final_concrete_types := []Type{}
mut fields := []StructField{}
mut nrt := ''
mut c_nrt := ''
mut new_depth_guard := []string{}
if depth_guard.len > generic_inst_depth_cutoff_limit {
t.panic('generic instantiation depth limit exceeded')
}
type_idx := typ.idx()
if type_idx == 0 || type_idx >= t.type_symbols.len {
return typ
Expand Down Expand Up @@ -4049,7 +4084,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str
return new_type(idx).derive_add_muls(typ).clear_flag(.generic)
}
Chan {
unwrap_typ := t.unwrap_generic_type(ts.info.elem_type, generic_names, concrete_types)
unwrap_typ := t.unwrap_generic_type_ex_with_depth(ts.info.elem_type, generic_names,
concrete_types, recheck_concrete_types, depth_guard)
idx := t.find_or_register_chan(unwrap_typ, unwrap_typ.nr_muls() > 0)
if idx <= 0 {
return typ
Expand Down Expand Up @@ -4087,8 +4123,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str
has_generic = true
}
if param.orig_typ.has_flag(.generic) || t.generic_type_names(param.orig_typ).len > 0 {
unwrapped_fn.params[i].orig_typ = t.unwrap_generic_type(param.orig_typ,
generic_names, concrete_types)
unwrapped_fn.params[i].orig_typ = t.unwrap_generic_type_ex_with_depth(param.orig_typ,
generic_names, concrete_types, recheck_concrete_types, depth_guard)
}
}
if unwrapped_fn.return_type.has_flag(.generic)
Expand Down Expand Up @@ -4134,8 +4170,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str
if !t_typ.has_flag(.generic) {
t_concrete_types << t_typ
} else {
t_concrete_types << t.unwrap_generic_type(t_typ, generic_names,
concrete_types)
t_concrete_types << t.unwrap_generic_type_ex_with_depth(t_typ,
generic_names, concrete_types, recheck_concrete_types, depth_guard)
}
}
}
Expand Down Expand Up @@ -4887,8 +4923,13 @@ fn (mut t Table) specialize_generic_fn_type_methods(parent_type Type, mut concre

// generic struct instantiations to concrete types
pub fn (mut t Table) generic_insts_to_concrete() {
mut cnt := 0
for mut sym in t.type_symbols {
if sym.kind == .generic_inst {
cnt++
if cnt > max_postprocess_iterations {
t.panic('generic_insts_to_concrete limit exceeded')
}
info := sym.info as GenericInst
if info.parent_idx <= 0 || info.parent_idx >= t.type_symbols.len {
continue
Expand Down Expand Up @@ -5040,6 +5081,29 @@ pub fn (mut t Table) generic_insts_to_concrete() {
if parent_info.generic_types.len == info.concrete_types.len {
mut fields := parent_info.fields.clone()
mut variants := parent_info.variants.clone()

// Prevent circular sum types from causing infinite loops
for variant in variants {
mut sym_name := t.sym(variant).name.trim_string_left(
t.sym(variant).mod + '.')
if sym_name.contains('[') {
sym_name = sym_name.all_before('[')
} else if sym_name.contains('<') {
sym_name = sym_name.all_before('<')
}

mut parent_name := parent.name.trim_string_left(parent.mod + '.')
if parent_name.contains('[') {
parent_name = parent_name.all_before('[')
} else if parent_name.contains('<') {
parent_name = parent_name.all_before('<')
}

if sym_name == parent_name {
return
}
}

generic_names := t.get_generic_names(parent_info.generic_types)
for i in 0 .. fields.len {
if t_typ := t.convert_generic_type(fields[i].typ, generic_names,
Expand Down
25 changes: 21 additions & 4 deletions vlib/v/checker/checker.v
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const type_level_cutoff_limit = 40 // it is very rarely deeper than 4
const iface_level_cutoff_limit = 100
const generic_fn_cutoff_limit_per_fn = 10_000 // how many times post_process_generic_fns, can visit the same function before bailing out

const generic_fn_postprocess_iterations_cutoff_limit = 1_000_000
const generic_fn_postprocess_iterations_cutoff_limit = 50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve finite generic chains beyond 50 rechecks

A valid chain of more than roughly 50 generic functions can require more than 50 passes when declarations are ordered from the innermost function to the outermost and only the outermost function is initially instantiated: each pass discovers the concrete type for a function that was already visited earlier in that pass. This fixed cutoff therefore emits an infinite-instantiation error for a finite chain that previously converged; use repeated-state/no-progress detection or a limit that does not reject ordinary finite propagation.

Useful? React with 👍 / 👎.


fn has_ascii_upper(s string) bool {
for ch in s {
Expand Down Expand Up @@ -804,6 +804,15 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) {
if !c.need_recheck_generic_fns {
break
}
if post_process_generic_fns_iterations == generic_fn_postprocess_iterations_cutoff_limit {
if c.file.generic_fns.len > 0 {
c.error('generic function post processing reached the cutoff limit of ${generic_fn_postprocess_iterations_cutoff_limit} iterations, probably due to an infinite generic instantiation loop',
c.file.generic_fns[0].pos)
} else {
c.error('generic function post processing reached the cutoff limit of ${generic_fn_postprocess_iterations_cutoff_limit} iterations, probably due to an infinite generic instantiation loop', token.Pos{})
}
break
}
c.need_recheck_generic_fns = false
post_process_generic_fns_iterations++
}
Expand Down Expand Up @@ -1179,7 +1188,7 @@ fn (mut c Checker) fn_type_decl(mut node ast.FnTypeDecl) {
fn (mut c Checker) sum_type_decl(mut node ast.SumTypeDecl) {
c.check_valid_pascal_case(node.name, 'sum type', node.pos)
if c.pref.is_vls && c.pref.linfo.method == .definition {
for variant in node.variants {
for mut variant in node.variants {
if c.vls_is_the_node(variant.pos) {
typ_str := c.table.type_to_str(variant.typ)
if np := c.name_pos_gotodef(typ_str) {
Expand All @@ -1192,7 +1201,7 @@ fn (mut c Checker) sum_type_decl(mut node ast.SumTypeDecl) {
}
}
mut names_used := []string{}
for variant in node.variants {
for mut variant in node.variants {
c.ensure_type_exists(variant.typ, variant.pos)
sym := c.table.sym(variant.typ)
if variant.typ.is_ptr() || (sym.info is ast.Alias && sym.info.parent_type.is_ptr()) {
Expand Down Expand Up @@ -1263,8 +1272,16 @@ and use a reference to the sum type instead: `var := &${node.name}(${variant_nam
}
c.check_any_type(variant.typ, sym, variant.pos)

if sym.name.trim_string_left(sym.mod + '.') == node.name {
mut clean_sym_name := sym.name.trim_string_left(sym.mod + '.')
if clean_sym_name.contains('[') {
clean_sym_name = clean_sym_name.all_before('[')
} else if clean_sym_name.contains('<') {
clean_sym_name = clean_sym_name.all_before('<')
}
if clean_sym_name == node.name {
c.error('sum type cannot hold itself', variant.pos)
variant.typ = ast.void_type
continue
} else if sym.kind == .sum_type && sym.info is ast.SumType {
// Check for circular references through other sum types
mut visited := map[int]bool{}
Expand Down
96 changes: 96 additions & 0 deletions vlib/v/checker/recursion_limits_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
fn generic_level_1[T](val T) T {
return val
}

fn generic_level_2[T](val T) T {
return generic_level_1[T](val)
}

fn generic_level_3[T](val T) T {
return generic_level_2[T](val)
}

fn generic_level_4[T](val T) T {
return generic_level_3[T](val)
}

fn test_valid_nested_generic_functions() {
res_int := generic_level_4[int](42)
assert res_int == 42

res_str := generic_level_4[string]('Vlang')
assert res_str == 'Vlang'
}

struct Box[T] {
pub:
val T
}

fn test_valid_nested_generic_structs() {
b1 := Box[int]{
val: 100
}
b2 := Box[Box[int]]{
val: b1
}
b3 := Box[Box[Box[int]]]{
val: b2
}
b4 := Box[Box[Box[Box[int]]]]{
val: b3
}

assert b4.val.val.val.val == 100
}

type Alias1 = int
type Alias2 = Alias1
type Alias3 = Alias2
type Alias4 = Alias3
type Alias5 = Alias4

fn test_valid_type_alias_chain() {
mut num := Alias5(10)
assert num == Alias5(10)

num += Alias5(20)
assert num == Alias5(30)
}

struct Some[T] {
pub:
val T
}

struct None {}

type MyOption[T] = None | Some[T]
type ComplexResult[T, E] = E | Some[T]

fn test_valid_generic_sum_types() {
opt_some := MyOption[int](Some[int]{
val: 99
})
if opt_some is Some[int] {
assert opt_some.val == 99
} else {
assert false
}

opt_none := MyOption[string](None{})
if opt_none is None {
assert true
} else {
assert false
}

res := ComplexResult[int, string](Some[int]{
val: 500
})
if res is Some[int] {
assert res.val == 500
} else {
assert false
}
}