From 3da68cc7ddde61b0d669ef796407b678ecc45ac4 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:35 +0000 Subject: [PATCH 1/5] cpp_demangle: support extended C++20 template-param mangling qualifiers --- src/ast.rs | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/src/ast.rs b/src/ast.rs index c64d698..73cdd2a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -3742,6 +3742,20 @@ impl Parse for TypeHandle { return Ok((handle, tail)); } + // C++20 placeholder type template-parameter declaration. This appears + // in contexts such as generic lambda signatures; parse it as `auto` + // so the stream remains well-formed. + // + // Like other template-parameter declaration qualifiers, this is + // treated as mangling syntax support and not represented as a distinct + // renderable AST construct. + if let Ok(tail) = consume(b"Ty", input) { + return Ok(( + TypeHandle::Builtin(BuiltinType::Standard(StandardBuiltinType::Auto)), + tail, + )); + } + // ::= // We don't have a separate type for the production. // Process these all up front, so that any ambiguity that might exist @@ -5747,6 +5761,56 @@ impl Parse for TemplateArg { ) -> Result<(TemplateArg, IndexStr<'b>)> { try_begin_parse!("TemplateArg", ctx, input); + // Parse C++20 template-parameter declarations that can prefix an + // actual template argument, such as: + // - `Tn ` + // - `Tk ` + // - `Tt + E` + // - `Tp ` + // - `Ty` + // + // We parse these qualifiers structurally for stream correctness, then + // continue with the following actual ``. + // + // These are parse-only mangling qualifiers: we intentionally do not + // preserve them as dedicated AST nodes for demangled rendering. + fn parse_template_param_decl<'a, 'b>( + ctx: &'a ParseContext, + subs: &'a mut SubstitutionTable, + input: IndexStr<'b>, + ) -> Result> { + if let Ok(tail) = consume(b"Ty", input) { + return Ok(tail); + } + + if let Ok(tail) = consume(b"Tn", input) { + let (_, tail) = TypeHandle::parse(ctx, subs, tail)?; + return Ok(tail); + } + + if let Ok(tail) = consume(b"Tk", input) { + let (_, tail) = Name::parse(ctx, subs, tail)?; + return Ok(tail); + } + + if let Ok(tail) = consume(b"Tp", input) { + return parse_template_param_decl(ctx, subs, tail); + } + + if let Ok(mut tail) = consume(b"Tt", input) { + loop { + if let Ok(next) = consume(b"E", tail) { + tail = next; + break; + } + tail = parse_template_param_decl(ctx, subs, tail)?; + } + return Ok(tail); + } + + Err(error::Error::UnexpectedText) + } + if let Ok(tail) = consume(b"X", input) { let (expr, tail) = Expression::parse(ctx, subs, tail)?; let tail = consume(b"E", tail)?; @@ -5757,6 +5821,35 @@ impl Parse for TemplateArg { return Ok((TemplateArg::SimpleExpression(expr), tail)); } + // C++20: a template-arg can also be prefixed by a constrained type + // template-parameter declaration (`Tk `). Parse this qualifier + // structurally and continue with the following actual template-arg. + if let Ok(tail) = consume(b"Tk", input) { + let (_, tail) = Name::parse(ctx, subs, tail)?; + return TemplateArg::parse(ctx, subs, tail); + } + + if let Ok(tail) = consume(b"Tt", input) { + let mut tail = tail; + loop { + if let Ok(next) = consume(b"E", tail) { + tail = next; + break; + } + tail = parse_template_param_decl(ctx, subs, tail)?; + } + return TemplateArg::parse(ctx, subs, tail); + } + + if let Ok(tail) = consume(b"Tp", input) { + let tail = parse_template_param_decl(ctx, subs, tail)?; + return TemplateArg::parse(ctx, subs, tail); + } + + if let Ok(tail) = consume(b"Ty", input) { + return TemplateArg::parse(ctx, subs, tail); + } + if let Ok((ty, tail)) = try_recurse!(TypeHandle::parse(ctx, subs, input)) { return Ok((TemplateArg::Type(ty), tail)); } @@ -10156,6 +10249,66 @@ mod tests { }); } + #[test] + fn parse_realworld_tfunctionrefcaller_full_mangled_name_probe() { + let mut subs = SubstitutionTable::new(); + let ctx = ParseContext::new(Default::default()); + let input = IndexStr::new( + b"_ZN2UE4Core7Private8Function18TFunctionRefCallerIZN26IEditorDataStorageProvider9AddColumnITkNS_6Editor11DataStorage15TDataColumnTypeE24FTypedElementLabelColumnEEvyOT_EUlPvRK13UScriptStructE_vJSB_SE_EE4CallESB_RSB_SE_", + ); + + match MangledName::parse(&ctx, &mut subs, input) { + Ok((_name, tail)) => assert!( + tail.is_empty(), + "tfunctionrefcaller full parse left tail: {:?}", + String::from_utf8_lossy(tail.as_ref()) + ), + Err(err) => panic!("failed tfunctionrefcaller full mangled name: {:?}", err), + } + } + + #[test] + fn parse_realworld_fmesh_deletevertexinstance_full_mangled_name_probe() { + let mut subs = SubstitutionTable::new(); + let ctx = ParseContext::new(Default::default()); + let input = IndexStr::new( + b"_ZN16FMeshDescription29DeleteVertexInstance_InternalITtTpTyE6TArrayEEv17FVertexInstanceIDPT_IJ9FVertexIDEE", + ); + + match MangledName::parse(&ctx, &mut subs, input) { + Ok((_name, tail)) => assert!( + tail.is_empty(), + "fmesh deletevertexinstance full parse left tail: {:?}", + String::from_utf8_lossy(tail.as_ref()) + ), + Err(err) => panic!( + "failed fmesh deletevertexinstance full mangled name: {:?}", + err + ), + } + } + + #[test] + fn parse_realworld_pcg_callbackwithrighttype_full_mangled_name_probe() { + let mut subs = SubstitutionTable::new(); + let ctx = ParseContext::new(Default::default()); + let input = IndexStr::new( + b"_ZN20PCGMetadataAttribute21CallbackWithRightTypeIZN18PCGPropertyHelpers35ExtractAttributeSetAsArrayOfStructsI26FPCGSelectGrammarCriterionEE6TArrayIT_22TSizedDefaultAllocatorILi32EEEPK13UPCGParamDataPK4TMapI5FName6TTupleIJSD_bEE20FDefaultSetAllocator27TDefaultMapHashableKeyFuncsISD_SF_Lb0EEEP11FPCGContextEUlTyS5_E_JEEEDctS5_DpOT0_", + ); + + match MangledName::parse(&ctx, &mut subs, input) { + Ok((_name, tail)) => assert!( + tail.is_empty(), + "pcg callbackwithrighttype full parse left tail: {:?}", + String::from_utf8_lossy(tail.as_ref()) + ), + Err(err) => panic!( + "failed pcg callbackwithrighttype full mangled name: {:?}", + err + ), + } + } + #[test] fn parse_template_arg() { assert_parse!(TemplateArg { From 65f2a34502c19d49e290714389421657d7ac15eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 14:18:16 +0000 Subject: [PATCH 2/5] fix PR3 template-param qualifier parser consistency --- src/ast.rs | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 73cdd2a..1dcb721 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5772,8 +5772,8 @@ impl Parse for TemplateArg { // We parse these qualifiers structurally for stream correctness, then // continue with the following actual ``. // - // These are parse-only mangling qualifiers: we intentionally do not - // preserve them as dedicated AST nodes for demangled rendering. + // This helper consumes qualifier prefixes without creating additional + // wrapper AST nodes for those prefixes in this path. fn parse_template_param_decl<'a, 'b>( ctx: &'a ParseContext, subs: &'a mut SubstitutionTable, @@ -5793,11 +5793,17 @@ impl Parse for TemplateArg { return Ok(tail); } + if let Ok((_param_decl, tail)) = TemplateParamDecl::parse(ctx, subs, input) { + return Ok(tail); + } + if let Ok(tail) = consume(b"Tp", input) { return parse_template_param_decl(ctx, subs, tail); } - if let Ok(mut tail) = consume(b"Tt", input) { + if let Ok(tail) = consume(b"Tt", input) { + // `Tt` requires one-or-more `` entries. + let mut tail = parse_template_param_decl(ctx, subs, tail)?; loop { if let Ok(next) = consume(b"E", tail) { tail = next; @@ -5830,7 +5836,8 @@ impl Parse for TemplateArg { } if let Ok(tail) = consume(b"Tt", input) { - let mut tail = tail; + // `Tt` requires one-or-more `` entries. + let mut tail = parse_template_param_decl(ctx, subs, tail)?; loop { if let Ok(next) = consume(b"E", tail) { tail = next; @@ -5841,6 +5848,16 @@ impl Parse for TemplateArg { return TemplateArg::parse(ctx, subs, tail); } + if let Ok((template_param_decl, tail)) = + try_recurse!(TemplateParamDecl::parse(ctx, subs, input)) + { + let (arg, tail) = TemplateArg::parse(ctx, subs, tail)?; + return Ok(( + TemplateArg::ParamDecl(template_param_decl, Box::new(arg)), + tail, + )); + } + if let Ok(tail) = consume(b"Tp", input) { let tail = parse_template_param_decl(ctx, subs, tail)?; return TemplateArg::parse(ctx, subs, tail); @@ -5854,16 +5871,6 @@ impl Parse for TemplateArg { return Ok((TemplateArg::Type(ty), tail)); } - if let Ok((template_param_decl, tail)) = - try_recurse!(TemplateParamDecl::parse(ctx, subs, input)) - { - let (arg, tail) = TemplateArg::parse(ctx, subs, tail)?; - return Ok(( - TemplateArg::ParamDecl(template_param_decl, Box::new(arg)), - tail, - )); - } - let tail = if input.peek() == Some(b'J') { consume(b"J", input)? } else { @@ -10425,11 +10432,12 @@ mod tests { Err => { b"..." => Error::UnexpectedText, b"X..." => Error::UnexpectedText, + b"TtES_..." => Error::UnexpectedText, b"J..." => Error::UnexpectedText, b"JS_..." => Error::UnexpectedText, // s that we don't implement yet. - b"Ty" => Error::UnexpectedText, - b"Tk" => Error::UnexpectedText, + b"Ty" => Error::UnexpectedEnd, + b"Tk" => Error::UnexpectedEnd, b"Tt" => Error::UnexpectedText, b"Tp" => Error::UnexpectedText, b"JS_" => Error::UnexpectedEnd, From 2621cc847e7d05c634551e96f7e420e7fde94b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 14:47:21 +0000 Subject: [PATCH 3/5] dedupe Tt template-param-decl list parsing --- src/ast.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 1dcb721..c26d690 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5802,21 +5802,27 @@ impl Parse for TemplateArg { } if let Ok(tail) = consume(b"Tt", input) { - // `Tt` requires one-or-more `` entries. - let mut tail = parse_template_param_decl(ctx, subs, tail)?; - loop { - if let Ok(next) = consume(b"E", tail) { - tail = next; - break; - } - tail = parse_template_param_decl(ctx, subs, tail)?; - } - return Ok(tail); + return parse_tt_param_decl_list(ctx, subs, tail); } Err(error::Error::UnexpectedText) } + fn parse_tt_param_decl_list<'a, 'b>( + ctx: &'a ParseContext, + subs: &'a mut SubstitutionTable, + input: IndexStr<'b>, + ) -> Result> { + // `Tt` requires one-or-more `` entries. + let mut tail = parse_template_param_decl(ctx, subs, input)?; + loop { + if let Ok(next) = consume(b"E", tail) { + return Ok(next); + } + tail = parse_template_param_decl(ctx, subs, tail)?; + } + } + if let Ok(tail) = consume(b"X", input) { let (expr, tail) = Expression::parse(ctx, subs, tail)?; let tail = consume(b"E", tail)?; @@ -5836,15 +5842,7 @@ impl Parse for TemplateArg { } if let Ok(tail) = consume(b"Tt", input) { - // `Tt` requires one-or-more `` entries. - let mut tail = parse_template_param_decl(ctx, subs, tail)?; - loop { - if let Ok(next) = consume(b"E", tail) { - tail = next; - break; - } - tail = parse_template_param_decl(ctx, subs, tail)?; - } + let tail = parse_tt_param_decl_list(ctx, subs, tail)?; return TemplateArg::parse(ctx, subs, tail); } From c276bf22c7ea67c66acf573bcf787ce2aeef0dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 15:05:53 +0000 Subject: [PATCH 4/5] add focused C++20 qualifier tests and EOF error propagation --- src/ast.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index c26d690..3d20b23 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5779,33 +5779,55 @@ impl Parse for TemplateArg { subs: &'a mut SubstitutionTable, input: IndexStr<'b>, ) -> Result> { - if let Ok(tail) = consume(b"Ty", input) { - return Ok(tail); + let mut saw_unexpected_end = false; + + match consume(b"Ty", input) { + Ok(tail) => return Ok(tail), + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - if let Ok(tail) = consume(b"Tn", input) { - let (_, tail) = TypeHandle::parse(ctx, subs, tail)?; - return Ok(tail); + match consume(b"Tn", input) { + Ok(tail) => { + let (_, tail) = TypeHandle::parse(ctx, subs, tail)?; + return Ok(tail); + } + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - if let Ok(tail) = consume(b"Tk", input) { - let (_, tail) = Name::parse(ctx, subs, tail)?; - return Ok(tail); + match consume(b"Tk", input) { + Ok(tail) => { + let (_, tail) = Name::parse(ctx, subs, tail)?; + return Ok(tail); + } + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - if let Ok((_param_decl, tail)) = TemplateParamDecl::parse(ctx, subs, input) { - return Ok(tail); + match TemplateParamDecl::parse(ctx, subs, input) { + Ok((_param_decl, tail)) => return Ok(tail), + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - if let Ok(tail) = consume(b"Tp", input) { - return parse_template_param_decl(ctx, subs, tail); + match consume(b"Tp", input) { + Ok(tail) => return parse_template_param_decl(ctx, subs, tail), + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - if let Ok(tail) = consume(b"Tt", input) { - return parse_tt_param_decl_list(ctx, subs, tail); + match consume(b"Tt", input) { + Ok(tail) => return parse_tt_param_decl_list(ctx, subs, tail), + Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, + Err(_) => {} } - Err(error::Error::UnexpectedText) + if saw_unexpected_end { + Err(error::Error::UnexpectedEnd) + } else { + Err(error::Error::UnexpectedText) + } } fn parse_tt_param_decl_list<'a, 'b>( @@ -9787,6 +9809,13 @@ mod tests { AbiTags::default())))))) ] } + b"Ty..." => { + TypeHandle::Builtin( + BuiltinType::Standard(StandardBuiltinType::Auto) + ), + b"...", + [] + } } Err => { b"P" => Error::UnexpectedEnd, @@ -10436,8 +10465,8 @@ mod tests { // s that we don't implement yet. b"Ty" => Error::UnexpectedEnd, b"Tk" => Error::UnexpectedEnd, - b"Tt" => Error::UnexpectedText, - b"Tp" => Error::UnexpectedText, + b"Tt" => Error::UnexpectedEnd, + b"Tp" => Error::UnexpectedEnd, b"JS_" => Error::UnexpectedEnd, b"X" => Error::UnexpectedEnd, b"J" => Error::UnexpectedEnd, @@ -10447,6 +10476,42 @@ mod tests { }); } + #[test] + fn parse_template_arg_cxx20_qualifier_prefix_forms() { + let mut subs = SubstitutionTable::new(); + let ctx = ParseContext::new(Default::default()); + + let input = IndexStr::new(b"Tk1ac..."); + let (arg, tail) = TemplateArg::parse(&ctx, &mut subs, input).unwrap(); + assert_eq!( + arg, + TemplateArg::Type(TypeHandle::Builtin(BuiltinType::Standard( + StandardBuiltinType::Char, + ))) + ); + assert_eq!(tail.as_ref(), b"..."); + + let input = IndexStr::new(b"TtTncEc..."); + let (arg, tail) = TemplateArg::parse(&ctx, &mut subs, input).unwrap(); + assert_eq!( + arg, + TemplateArg::Type(TypeHandle::Builtin(BuiltinType::Standard( + StandardBuiltinType::Char, + ))) + ); + assert_eq!(tail.as_ref(), b"..."); + + let input = IndexStr::new(b"TpTyc..."); + let (arg, tail) = TemplateArg::parse(&ctx, &mut subs, input).unwrap(); + assert_eq!( + arg, + TemplateArg::Type(TypeHandle::Builtin(BuiltinType::Standard( + StandardBuiltinType::Char, + ))) + ); + assert_eq!(tail.as_ref(), b"..."); + } + #[test] fn parse_expression() { assert_parse!(Expression { From 8e9fc28689da81d9fcb11d68efb14fec751a17c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Wed, 15 Apr 2026 19:32:37 +0000 Subject: [PATCH 5/5] refine qualifier parse EOF classification --- src/ast.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 3d20b23..56201b3 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -5779,11 +5779,13 @@ impl Parse for TemplateArg { subs: &'a mut SubstitutionTable, input: IndexStr<'b>, ) -> Result> { - let mut saw_unexpected_end = false; + // Only classify as UnexpectedEnd for genuinely truncated qualifier + // prefixes: empty input, or a lone leading `T`. + let saw_truncated_prefix = + input.is_empty() || (input.peek() == Some(b'T') && input.len() == 1); match consume(b"Ty", input) { Ok(tail) => return Ok(tail), - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } @@ -5792,7 +5794,6 @@ impl Parse for TemplateArg { let (_, tail) = TypeHandle::parse(ctx, subs, tail)?; return Ok(tail); } - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } @@ -5801,29 +5802,25 @@ impl Parse for TemplateArg { let (_, tail) = Name::parse(ctx, subs, tail)?; return Ok(tail); } - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } match TemplateParamDecl::parse(ctx, subs, input) { Ok((_param_decl, tail)) => return Ok(tail), - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } match consume(b"Tp", input) { Ok(tail) => return parse_template_param_decl(ctx, subs, tail), - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } match consume(b"Tt", input) { Ok(tail) => return parse_tt_param_decl_list(ctx, subs, tail), - Err(error::Error::UnexpectedEnd) => saw_unexpected_end = true, Err(_) => {} } - if saw_unexpected_end { + if saw_truncated_prefix { Err(error::Error::UnexpectedEnd) } else { Err(error::Error::UnexpectedText) @@ -10462,7 +10459,8 @@ mod tests { b"TtES_..." => Error::UnexpectedText, b"J..." => Error::UnexpectedText, b"JS_..." => Error::UnexpectedText, - // s that we don't implement yet. + // Qualifier prefixes are parsed structurally, but still + // require a following to succeed. b"Ty" => Error::UnexpectedEnd, b"Tk" => Error::UnexpectedEnd, b"Tt" => Error::UnexpectedEnd,