From 0c30904170ea104a89d32f84e92f1e5eef67e0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Tue, 14 Apr 2026 14:36:38 +0000 Subject: [PATCH 1/6] cpp_demangle: add level-aware template param resolution --- src/ast.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index c64d698..63847b7 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -351,6 +351,11 @@ trait ArgScope<'me, 'ctx>: fmt::Debug { /// Get the current scope's leaf name. fn leaf_name(&'me self) -> Result>; + #[inline] + fn template_args(&'me self) -> Option<&'ctx TemplateArgs> { + None + } + /// Get the current scope's `index`th template argument. fn get_template_arg(&'me self, index: usize) -> Result<(&'ctx TemplateArg, &'ctx TemplateArgs)>; @@ -390,6 +395,12 @@ trait ArgScopeStackExt<'prev, 'subs>: Copy { &'prev self, item: &'subs dyn ArgScope<'subs, 'subs>, ) -> Option>; + + fn get_template_arg_by_level( + &'prev self, + level_from_current: Option, + idx: usize, + ) -> Result<(&'subs TemplateArg, &'subs TemplateArgs)>; } impl<'prev, 'subs> ArgScopeStackExt<'prev, 'subs> for Option> { @@ -404,6 +415,45 @@ impl<'prev, 'subs> ArgScopeStackExt<'prev, 'subs> for Option, + idx: usize, + ) -> Result<(&'subs TemplateArg, &'subs TemplateArgs)> { + let target_level = level_from_current.unwrap_or(0); + + let mut scope = self.as_ref(); + let mut template_level = 0usize; + + while let Some(s) = scope { + if let Some(template_args) = s.item.template_args() { + let _ = template_args; + + if template_level == target_level { + if let Ok((arg, args)) = s.item.get_template_arg(idx) { + if let Some((in_idx, in_args)) = s.in_arg { + if args as *const TemplateArgs == in_args as *const TemplateArgs + && in_idx <= idx + { + scope = s.prev; + template_level += 1; + continue; + } + } + return Ok((arg, args)); + } + return Err(error::Error::BadTemplateArgReference); + } + + template_level += 1; + } + + scope = s.prev; + } + + Err(error::Error::BadTemplateArgReference) + } } /// A stack of `ArgScope`s is itself an `ArgScope`! @@ -5362,6 +5412,43 @@ where #[derive(Clone, Debug, PartialEq, Eq)] pub struct TemplateParam(usize); +impl TemplateParam { + const EXPLICIT_LEVEL_FLAG: usize = 1usize << (usize::BITS as usize - 1); + const INDEX_BITS: usize = (usize::BITS as usize - 1) / 2; + const INDEX_MASK: usize = (1usize << Self::INDEX_BITS) - 1; + + #[inline] + fn implicit(index: usize) -> TemplateParam { + TemplateParam(index) + } + + #[inline] + fn explicit(level_from_current: usize, index: usize) -> TemplateParam { + debug_assert!(level_from_current <= Self::INDEX_MASK); + debug_assert!(index <= Self::INDEX_MASK); + let encoded = Self::EXPLICIT_LEVEL_FLAG | (level_from_current << Self::INDEX_BITS) | index; + TemplateParam(encoded) + } + + #[inline] + fn level_from_current(&self) -> Option { + if (self.0 & Self::EXPLICIT_LEVEL_FLAG) == 0 { + None + } else { + Some((self.0 >> Self::INDEX_BITS) & Self::INDEX_MASK) + } + } + + #[inline] + fn index(&self) -> usize { + if (self.0 & Self::EXPLICIT_LEVEL_FLAG) == 0 { + self.0 + } else { + self.0 & Self::INDEX_MASK + } + } +} + impl Parse for TemplateParam { fn parse<'a, 'b>( ctx: &'a ParseContext, @@ -5371,12 +5458,30 @@ impl Parse for TemplateParam { try_begin_parse!("TemplateParam", ctx, input); let input = consume(b"T", input)?; + + if let Ok(input) = consume(b"L", input) { + // C++20 can qualify template parameters with an explicit template + // nesting level (`TL_..._`). Level 0 means current + // (innermost) template parameter scope. + let (level, input) = parse_number(10, false, input)?; + let input = consume(b"_", input)?; + let (number, input) = match parse_number(10, false, input) { + Ok((number, input)) => ((number + 1) as _, input), + Err(_) => (0, input), + }; + let input = consume(b"_", input)?; + if level == 0 { + return Ok((TemplateParam::implicit(number), input)); + } + return Ok((TemplateParam::explicit(level as usize, number), input)); + } + let (number, input) = match parse_number(10, false, input) { Ok((number, input)) => ((number + 1) as _, input), Err(_) => (0, input), }; let input = consume(b"_", input)?; - Ok((TemplateParam(number), input)) + Ok((TemplateParam::implicit(number), input)) } } @@ -5394,7 +5499,7 @@ where ctx.push_demangle_node(DemangleNodeType::TemplateParam); let ret = if ctx.is_lambda_arg { // To match libiberty, template references are converted to `auto`. - write!(ctx, "auto:{}", self.0 + 1) + write!(ctx, "auto:{}", self.index() + 1) } else { let arg = self.resolve(scope)?; arg.demangle(ctx, scope) @@ -5409,8 +5514,14 @@ impl TemplateParam { &'subs self, scope: Option>, ) -> ::core::result::Result<&'subs TemplateArg, fmt::Error> { - scope - .get_template_arg(self.0) + let resolved = if let Some(level_from_current) = self.level_from_current() { + scope.get_template_arg_by_level(Some(level_from_current), self.index()) + } else { + // Preserve legacy semantics for implicit template params. + scope.get_template_arg(self.index()) + }; + + resolved .map_err(|e| { log!("Error obtaining template argument: {}", e); fmt::Error @@ -5697,6 +5808,10 @@ impl<'subs> ArgScope<'subs, 'subs> for TemplateArgs { Err(error::Error::BadLeafNameReference) } + fn template_args(&'subs self) -> Option<&'subs TemplateArgs> { + Some(self) + } + fn get_template_arg( &'subs self, idx: usize, @@ -11991,6 +12106,14 @@ mod tests { TemplateParam(4), b"..." } + b"TL0__..." => { + TemplateParam(0), + b"..." + } + b"TL1_3_..." => { + TemplateParam::explicit(1, 4), + b"..." + } } Err => { b"wtf" => Error::UnexpectedText, From 573f5d37609f14e9a8c2a1ebaf86cb36a9227888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Tue, 14 Apr 2026 15:56:08 +0000 Subject: [PATCH 2/6] cpp_demangle: render requires clauses in template args --- src/ast.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 63847b7..fe7d3bc 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -602,6 +602,9 @@ where // We are currently demangling a template-prefix in a nested-name. is_template_prefix_in_nested_name: bool, + // We are currently demangling a requires-clause expression. + is_requires_clause: bool, + // `PackExpansion`'s should only print '...', only when there is no template // argument pack. is_template_argument_pack: bool, @@ -667,6 +670,7 @@ where is_lambda_arg: false, is_template_prefix: false, is_template_prefix_in_nested_name: false, + is_requires_clause: false, is_template_argument_pack: false, is_explicit_obj_param: false, show_params: !options.no_params, @@ -5500,6 +5504,9 @@ where let ret = if ctx.is_lambda_arg { // To match libiberty, template references are converted to `auto`. write!(ctx, "auto:{}", self.index() + 1) + } else if ctx.is_requires_clause { + // Keep symbolic template parameters inside requires clauses. + write!(ctx, "T{}", self.index() + 1) } else { let arg = self.resolve(scope)?; arg.demangle(ctx, scope) @@ -5792,6 +5799,12 @@ where scope.in_arg = None; } + let saved_max_recursion = ctx.max_recursion; + ctx.max_recursion = core::cmp::max(saved_max_recursion, 256); + let ret = self.requires_clause.demangle(ctx, scope); + ctx.max_recursion = saved_max_recursion; + ret?; + // Ensure "> >" because old C++ sucks and libiberty (and its tests) // supports old C++. if ctx.last_char_written == Some('>') { @@ -6899,6 +6912,30 @@ impl Parse for ConstraintExpression { } } +impl<'subs, W> Demangle<'subs, W> for ConstraintExpression +where + W: 'subs + DemangleWrite, +{ + fn demangle<'prev, 'ctx>( + &'subs self, + ctx: &'ctx mut DemangleContext<'subs, W>, + scope: Option>, + ) -> fmt::Result { + let ctx = try_begin_demangle!(self, ctx, scope); + + if let ConstraintExpression(Some(ref expr)) = *self { + write!(ctx, " requires ")?; + let saved = ctx.is_requires_clause; + ctx.is_requires_clause = true; + let ret = expr.demangle(ctx, scope); + ctx.is_requires_clause = saved; + return ret; + } + + Ok(()) + } +} + /// The `` production. /// /// ```text @@ -8700,13 +8737,14 @@ mod tests { use super::{ AbiTag, AbiTags, ArrayType, BareFunctionType, BaseUnresolvedName, BuiltinType, CallOffset, ClassEnumType, ClosureTypeName, ConstraintExpression, CtorDtorName, CvQualifiers, - DataMemberPrefix, Decltype, DestructorName, Discriminator, Encoding, ExceptionSpec, - ExprPrimary, Expression, FoldExpr, FunctionParam, FunctionType, GlobalCtorDtor, Identifier, - Initializer, LambdaSig, LocalName, MangledName, MemberName, Name, NestedName, - NonSubstitution, Number, NvOffset, OperatorName, ParametricBuiltinType, Parse, - ParseContext, PointerToMemberType, Prefix, PrefixHandle, RefQualifier, ResourceName, SeqId, - SimpleId, SimpleOperatorName, SourceName, SpecialName, StandardBuiltinType, SubobjectExpr, - Substitution, TemplateArg, TemplateArgs, TemplateParam, TemplateParamDecl, + DataMemberPrefix, Decltype, Demangle, DemangleContext, DemangleOptions, DestructorName, + Discriminator, Encoding, ExceptionSpec, ExprPrimary, Expression, FoldExpr, FunctionParam, + FunctionType, GlobalCtorDtor, Identifier, Initializer, LambdaSig, LocalName, MangledName, + MemberName, Name, NestedName, NonSubstitution, Number, NvOffset, OperatorName, + ParametricBuiltinType, Parse, ParseContext, PointerToMemberType, Prefix, PrefixHandle, + RefQualifier, ResourceName, SeqId, SimpleId, SimpleOperatorName, SourceName, SpecialName, + StandardBuiltinType, SubobjectExpr, Substitution, TemplateArg, TemplateArgs, TemplateParam, + TemplateParamDecl, TemplateTemplateParam, TemplateTemplateParamHandle, Type, TypeHandle, UnnamedTypeName, UnqualifiedName, UnresolvedName, UnresolvedQualifierLevel, UnresolvedType, UnresolvedTypeHandle, UnscopedName, UnscopedTemplateName, UnscopedTemplateNameHandle, @@ -8716,6 +8754,7 @@ mod tests { use crate::error::Error; use crate::index_str::IndexStr; use crate::subs::{Substitutable, SubstitutionTable}; + use crate::Symbol; use alloc::boxed::Box; use alloc::string::String; use core::fmt::Debug; @@ -10271,6 +10310,40 @@ mod tests { }); } + #[test] + fn demangle_template_args_renders_requires_clause() { + let parse_ctx = ParseContext::new(Default::default()); + let mut subs = SubstitutionTable::new(); + let input = b"IiQtrE"; + let (template_args, tail) = TemplateArgs::parse(&parse_ctx, &mut subs, IndexStr::new(input)) + .expect("template args with requires-clause should parse"); + assert!(tail.is_empty(), "unexpected parse tail"); + + let mut out = String::new(); + let mut demangle_ctx = DemangleContext::new(&subs, input, DemangleOptions::default(), &mut out); + template_args + .demangle(&mut demangle_ctx, None) + .expect("requires-clause demangle should succeed"); + + assert!( + out.contains("requires"), + "demangled template args should render requires clause: {}", + out + ); + } + + #[test] + fn demangle_realworld_tfunction_requires_clause_probe() { + let mangled = b"_ZN9TFunctionIFvR21FMassExecutionContextEEC2IZN22UMassTestProcessorBaseC1EvE3$_0Qaantntaantsr12TIsTFunctionINSt3__15decayITL0__E4typeEEE5Valuesr3stdE16is_invocable_r_vIT_SB_DpT0_Esr2UE4Core7PrivateE19BoolIdentityConceptILb1EEEEOSC_"; + let sym = Symbol::new(&mangled[..]).expect("symbol parse"); + let demangled = sym.demangle().expect("requires-clause demangle should succeed"); + assert!( + demangled.contains("requires"), + "expected requires clause in demangled output: {}", + demangled + ); + } + #[test] fn parse_template_arg() { assert_parse!(TemplateArg { From 12ad2076e026146d7371e9621bc99546ebb4f5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 10:28:53 +0000 Subject: [PATCH 3/6] cpp_demangle: harden TL parsing and respect recursion limits --- src/ast.rs | 58 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index fe7d3bc..a67a947 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -8,6 +8,7 @@ use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; use core::cell::Cell; +use core::convert::TryFrom; #[cfg(feature = "logging")] use core::cell::RefCell; use core::fmt::{self, Write}; @@ -427,15 +428,12 @@ impl<'prev, 'subs> ArgScopeStackExt<'prev, 'subs> for Option ArgScope<'prev, 'subs> for Option while let Some(s) = scope { if let Ok((arg, args)) = s.item.get_template_arg(idx) { if let Some((in_idx, in_args)) = s.in_arg { - if args as *const TemplateArgs == in_args as *const TemplateArgs - && in_idx <= idx - { + if core::ptr::eq(args, in_args) && in_idx <= idx { return Err(error::Error::ForwardTemplateArgReference); } } @@ -5420,12 +5416,22 @@ impl TemplateParam { const EXPLICIT_LEVEL_FLAG: usize = 1usize << (usize::BITS as usize - 1); const INDEX_BITS: usize = (usize::BITS as usize - 1) / 2; const INDEX_MASK: usize = (1usize << Self::INDEX_BITS) - 1; + const IMPLICIT_INDEX_MASK: usize = Self::EXPLICIT_LEVEL_FLAG - 1; #[inline] fn implicit(index: usize) -> TemplateParam { TemplateParam(index) } + #[inline] + fn checked_implicit(index: usize) -> Result { + if index > Self::IMPLICIT_INDEX_MASK { + Err(error::Error::Overflow) + } else { + Ok(TemplateParam::implicit(index)) + } + } + #[inline] fn explicit(level_from_current: usize, index: usize) -> TemplateParam { debug_assert!(level_from_current <= Self::INDEX_MASK); @@ -5434,6 +5440,15 @@ impl TemplateParam { TemplateParam(encoded) } + #[inline] + fn checked_explicit(level_from_current: usize, index: usize) -> Result { + if level_from_current > Self::INDEX_MASK || index > Self::INDEX_MASK { + Err(error::Error::Overflow) + } else { + Ok(TemplateParam::explicit(level_from_current, index)) + } + } + #[inline] fn level_from_current(&self) -> Option { if (self.0 & Self::EXPLICIT_LEVEL_FLAG) == 0 { @@ -5468,24 +5483,33 @@ impl Parse for TemplateParam { // nesting level (`TL_..._`). Level 0 means current // (innermost) template parameter scope. let (level, input) = parse_number(10, false, input)?; + let level = usize::try_from(level).map_err(|_| error::Error::Overflow)?; let input = consume(b"_", input)?; let (number, input) = match parse_number(10, false, input) { - Ok((number, input)) => ((number + 1) as _, input), + Ok((number, input)) => { + let number = usize::try_from(number).map_err(|_| error::Error::Overflow)?; + let number = number.checked_add(1).ok_or(error::Error::Overflow)?; + (number, input) + } Err(_) => (0, input), }; let input = consume(b"_", input)?; if level == 0 { - return Ok((TemplateParam::implicit(number), input)); + return Ok((TemplateParam::checked_implicit(number)?, input)); } - return Ok((TemplateParam::explicit(level as usize, number), input)); + return Ok((TemplateParam::checked_explicit(level, number)?, input)); } let (number, input) = match parse_number(10, false, input) { - Ok((number, input)) => ((number + 1) as _, input), + Ok((number, input)) => { + let number = usize::try_from(number).map_err(|_| error::Error::Overflow)?; + let number = number.checked_add(1).ok_or(error::Error::Overflow)?; + (number, input) + } Err(_) => (0, input), }; let input = consume(b"_", input)?; - Ok((TemplateParam::implicit(number), input)) + Ok((TemplateParam::checked_implicit(number)?, input)) } } @@ -5799,11 +5823,7 @@ where scope.in_arg = None; } - let saved_max_recursion = ctx.max_recursion; - ctx.max_recursion = core::cmp::max(saved_max_recursion, 256); - let ret = self.requires_clause.demangle(ctx, scope); - ctx.max_recursion = saved_max_recursion; - ret?; + self.requires_clause.demangle(ctx, scope)?; // Ensure "> >" because old C++ sucks and libiberty (and its tests) // supports old C++. @@ -12192,6 +12212,8 @@ mod tests { b"wtf" => Error::UnexpectedText, b"Twtf" => Error::UnexpectedText, b"T3wtf" => Error::UnexpectedText, + b"TL2147483648_0_..." => Error::Overflow, + b"T9223372036854775807_..." => Error::Overflow, b"T" => Error::UnexpectedEnd, b"T3" => Error::UnexpectedEnd, b"" => Error::UnexpectedEnd, From 9d5ca7cd9513c06171ce487b783fb3e693e27f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 15:17:32 +0000 Subject: [PATCH 4/6] fix ConstraintExpression demangle borrow pattern --- src/ast.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast.rs b/src/ast.rs index a67a947..5fcf72e 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -6943,7 +6943,7 @@ where ) -> fmt::Result { let ctx = try_begin_demangle!(self, ctx, scope); - if let ConstraintExpression(Some(ref expr)) = *self { + if let Some(expr) = self.0.as_ref() { write!(ctx, " requires ")?; let saved = ctx.is_requires_clause; ctx.is_requires_clause = true; From 7f02deb3f66d242f7c748eba71481391c71d2886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 18:01:56 +0000 Subject: [PATCH 5/6] tighten TemplateParam parse error propagation --- src/ast.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 5fcf72e..5c236ee 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5480,7 +5480,7 @@ impl Parse for TemplateParam { if let Ok(input) = consume(b"L", input) { // C++20 can qualify template parameters with an explicit template - // nesting level (`TL_..._`). Level 0 means current + // nesting level (`TL_..._`). Level 0 means current // (innermost) template parameter scope. let (level, input) = parse_number(10, false, input)?; let level = usize::try_from(level).map_err(|_| error::Error::Overflow)?; @@ -5491,7 +5491,8 @@ impl Parse for TemplateParam { let number = number.checked_add(1).ok_or(error::Error::Overflow)?; (number, input) } - Err(_) => (0, input), + Err(error::Error::UnexpectedText) => (0, input), + Err(err) => return Err(err), }; let input = consume(b"_", input)?; if level == 0 { @@ -5506,7 +5507,8 @@ impl Parse for TemplateParam { let number = number.checked_add(1).ok_or(error::Error::Overflow)?; (number, input) } - Err(_) => (0, input), + Err(error::Error::UnexpectedText) => (0, input), + Err(err) => return Err(err), }; let input = consume(b"_", input)?; Ok((TemplateParam::checked_implicit(number)?, input)) From e968ea411f0a48af253dce98bd3cca0618019f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Fri, 17 Apr 2026 09:13:51 +0000 Subject: [PATCH 6/6] Apply rustfmt --- src/ast.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 5c236ee..e0a7904 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -8,9 +8,9 @@ use alloc::boxed::Box; use alloc::string::String; use alloc::vec::Vec; use core::cell::Cell; -use core::convert::TryFrom; #[cfg(feature = "logging")] use core::cell::RefCell; +use core::convert::TryFrom; use core::fmt::{self, Write}; use core::hash::{Hash, Hasher}; use core::mem; @@ -429,7 +429,6 @@ impl<'prev, 'subs> ArgScopeStackExt<'prev, 'subs> for Option