diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 41711e6fc..c71216f88 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -202,6 +202,13 @@ internal class VisualizerInfo public VisualizerType Visualizer { get; private set; } public Dictionary ScopedNames { get; private set; } + /// + /// Intrinsics defined in this type block, keyed by name. + /// Stored as IntrinsicType so that Parameter[] is available at call time + /// for argument substitution in parametrized intrinsics. + /// + public Dictionary Intrinsics { get; private set; } + public VisualizerId[] GetUIVisualizers() { return this.Visualizer.Items.Where((i) => i is UIVisualizerItemType).Select(i => @@ -220,12 +227,26 @@ public VisualizerInfo(VisualizerType viz, TypeName name) { ScopedNames["$T" + (i + 1).ToString(CultureInfo.InvariantCulture)] = name.Args[i].FullyQualifiedName; } + // collect intrinsics defined in this type block + Intrinsics = new Dictionary(); + if (viz.Items != null) + { + foreach (var item in viz.Items) + { + if (item is IntrinsicType intrinsic && !string.IsNullOrEmpty(intrinsic.Name)) + { + Intrinsics[intrinsic.Name] = intrinsic; + } + } + } } } private static Regex s_variableName = new Regex("[a-zA-Z$_][a-zA-Z$_0-9]*"); private static Regex s_subfieldNameHere = new Regex(@"\G((\.|->)[a-zA-Z$_][a-zA-Z$_0-9]*)+"); private static Regex s_expression = new Regex(@"^\{[^\}]*\}"); + private static readonly Regex s_dllQualifiedPrefix = new Regex(@"\w+(\.\w+)*\.dll!"); + private static readonly Regex s_intrinsicCallPattern = new Regex(@"\b([A-Za-z_]\w*)\s*\("); private List _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; @@ -456,11 +477,11 @@ private bool LoadFile(string path) { DisplayStringType display = item as DisplayStringType; // e.g. {{ size={_Mypair._Myval2._Mylast - _Mypair._Myval2._Myfirst} }} - if (!EvalCondition(display.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(display.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } - return (FormatValue(display.Value, variable, visualizer.ScopedNames), visualizer.GetUIVisualizers()); + return (FormatValue(display.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics), visualizer.GetUIVisualizers()); } } } @@ -582,17 +603,17 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) if (i is ItemType && !(variable is PaginatedVisualizerWrapper)) // we do not want to repeatedly display other ItemTypes when expanding the "[More...]" node { ItemType item = (ItemType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } - IVariableInformation expr = GetExpression(item.Value, variable, visualizer.ScopedNames, item.Name); + IVariableInformation expr = GetExpression(item.Value, variable, visualizer.ScopedNames, item.Name, visualizer.Intrinsics); children.Add(expr); } else if (i is ArrayItemsType) { ArrayItemsType item = (ArrayItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -606,7 +627,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) totalSize = 1; if (!int.TryParse(item.Rank, NumberStyles.None, CultureInfo.InvariantCulture, out rank)) { - string expressionValue = GetExpressionValue(item.Rank, variable, visualizer.ScopedNames); + string expressionValue = GetExpressionValue(item.Rank, variable, visualizer.ScopedNames, visualizer.Intrinsics); rank = Int32.Parse(expressionValue, CultureInfo.InvariantCulture); } if (rank <= 0) @@ -618,7 +639,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { // replace $i with Item.Rank here before passing it into GetExpressionValue string substitute = item.Size.Replace("$i", idx.ToString(CultureInfo.InvariantCulture)); - string val = GetExpressionValue(substitute, variable, visualizer.ScopedNames); + string val = GetExpressionValue(substitute, variable, visualizer.ScopedNames, visualizer.Intrinsics); uint tmp = MICore.Debugger.ParseUint(val, throwOnError: true); dimensions[idx] = tmp; totalSize *= tmp; @@ -626,7 +647,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); totalSize = MICore.Debugger.ParseUint(val, throwOnError: true); } @@ -639,9 +660,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) ValuePointerType[] vptrs = item.ValuePointer; foreach (var vp in vptrs) { - if (EvalCondition(vp.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(vp.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - IVariableInformation ptrExpr = GetExpression("*(" + vp.Value + ")", variable, visualizer.ScopedNames); + IVariableInformation ptrExpr = GetExpression("*(" + vp.Value + ")", variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); string typename = ptrExpr.TypeName; if (String.IsNullOrWhiteSpace(typename)) { @@ -669,7 +690,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) arrayBuilder.Append("))"); string arrayStr = arrayBuilder.ToString(); - IVariableInformation arrayExpr = GetExpression(arrayStr, variable, visualizer.ScopedNames); + IVariableInformation arrayExpr = GetExpression(arrayStr, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); arrayExpr.EnsureChildren(); if (arrayExpr.CountChildren != 0) { @@ -696,7 +717,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) else if (i is TreeItemsType) { TreeItemsType item = (TreeItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -709,7 +730,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { continue; } - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); uint size = MICore.Debugger.ParseUint(val, throwOnError: true); IVariableInformation headVal; if (variable is TreeContinueWrapper tcw) @@ -718,7 +739,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames); + headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } ulong head = MICore.Debugger.ParseAddr(headVal.Value); var content = new List(); @@ -736,9 +757,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { getValue = (v) => v.FindChildByName(item.ValueNode.Value); } - else if (GetExpression(item.ValueNode.Value, headVal, visualizer.ScopedNames) != null) + else if (GetExpression(item.ValueNode.Value, headVal, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics) != null) { - getValue = (v) => GetExpression(item.ValueNode.Value, v, visualizer.ScopedNames); + getValue = (v) => GetExpression(item.ValueNode.Value, v, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } if (goLeft == null || goRight == null || getValue == null) { @@ -770,7 +791,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) LinkedListItemsType item = (LinkedListItemsType)i; if (String.IsNullOrWhiteSpace(item.Condition)) { - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) continue; } if (String.IsNullOrWhiteSpace(item.HeadPointer) || String.IsNullOrWhiteSpace(item.NextPointer)) @@ -784,7 +805,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) uint size = MAX_EXPAND; if (!String.IsNullOrWhiteSpace(item.Size)) { - string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames); + string val = GetExpressionValue(item.Size, variable, visualizer.ScopedNames, visualizer.Intrinsics); size = MICore.Debugger.ParseUint(val); } IVariableInformation headVal; @@ -794,7 +815,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames); + headVal = GetExpression(item.HeadPointer, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } ulong head = MICore.Debugger.ParseAddr(headVal.Value); var content = new List(); @@ -813,10 +834,10 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) } else { - var value = GetExpression(item.ValueNode, headVal, visualizer.ScopedNames); + var value = GetExpression(item.ValueNode, headVal, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); if (value != null && !value.Error) { - getValue = (v) => GetExpression(item.ValueNode, v, visualizer.ScopedNames); + getValue = (v) => GetExpression(item.ValueNode, v, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); } } if (goNext == null || getValue == null) @@ -845,7 +866,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) // *(_M_vector._M_array[$i]) // IndexListItemsType item = (IndexListItemsType)i; - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -859,9 +880,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { if (string.IsNullOrWhiteSpace(s.Value)) continue; - if (EvalCondition(s.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(s.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - string val = GetExpressionValue(s.Value, variable, visualizer.ScopedNames); + string val = GetExpressionValue(s.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics); size = MICore.Debugger.ParseUint(val); break; } @@ -875,9 +896,9 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { if (string.IsNullOrWhiteSpace(v.Value)) continue; - if (EvalCondition(v.Condition, variable, visualizer.ScopedNames)) + if (EvalCondition(v.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { - string processedExpr = ReplaceNamesInExpression(v.Value, variable, visualizer.ScopedNames); + string processedExpr = ReplaceNamesInExpression(v.Value, variable, visualizer.ScopedNames, visualizer.Intrinsics); Dictionary indexDic = new Dictionary(); uint currentIndex = 0; if (variable is PaginatedVisualizerWrapper pvwVariable) @@ -917,7 +938,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) // if (item.Condition != null) { - if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames)) + if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) { continue; } @@ -926,7 +947,7 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable) { continue; } - var expand = GetExpression(item.Value, variable, visualizer.ScopedNames); + var expand = GetExpression(item.Value, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics); var eChildren = Expand(expand); if (eChildren != null) { @@ -1094,12 +1115,12 @@ private static string BaseName(string type) return type; } - private bool EvalCondition(string condition, IVariableInformation variable, IDictionary scopedNames) + private bool EvalCondition(string condition, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { bool res = true; if (!String.IsNullOrWhiteSpace(condition)) { - string exprValue = GetExpressionValue(condition, variable, scopedNames); + string exprValue = GetExpressionValue(condition, variable, scopedNames, intrinsics); bool exprBool = false; int exprInt = 0; @@ -1237,7 +1258,7 @@ private VisualizerInfo FindType(IVariableInformation variable) return null; } - private string FormatValue(string format, IVariableInformation variable, IDictionary scopedNames) + private string FormatValue(string format, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { if (String.IsNullOrWhiteSpace(format)) { @@ -1258,7 +1279,7 @@ private string FormatValue(string format, IVariableInformation variable, IDictio Match m = s_expression.Match(format.Substring(i)); if (m.Success) { - string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames); + string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames, intrinsics); value.Append(exprValue); i += m.Length - 1; } @@ -1390,8 +1411,176 @@ internal static bool IsPrecededByMemberAccessOperator(string expression, int ind return false; } - private string ReplaceNamesInExpression(string expression, IVariableInformation variable, IDictionary scopedNames) + /// + /// Find the index of the closing parenthesis that matches the opening paren at . + /// Returns -1 if not found. + /// + internal static int FindMatchingParen(string s, int openPos) + { + int depth = 0; + for (int i = openPos; i < s.Length; i++) + { + if (s[i] == '(') depth++; + else if (s[i] == ')') + { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /// + /// Split a comma-separated argument list (the text inside the parentheses) + /// at depth-zero commas only, so nested calls like f(a,b) are kept intact. + /// Only parentheses and square brackets are treated as nesting — angle brackets + /// are intentionally excluded because '>' is also a comparison operator and + /// NatVis intrinsic arguments are never C++ template types. + /// + internal static List SplitArguments(string argsText) + { + var result = new List(); + int depth = 0; + int start = 0; + for (int i = 0; i < argsText.Length; i++) + { + char c = argsText[i]; + if (c == '(' || c == '[') depth++; + else if (c == ')' || c == ']') depth--; + else if (c == ',' && depth == 0) + { + result.Add(argsText.Substring(start, i - start).Trim()); + start = i + 1; + } + } + string last = argsText.Substring(start).Trim(); + if (last.Length > 0 || result.Count > 0) + result.Add(last); + return result; + } + + /// + /// Substitute named parameters in an intrinsic expression with the supplied argument + /// values. Each parameter name is replaced as a whole word so that e.g. "val" inside + /// "interval" is not touched. + /// + internal static string SubstituteIntrinsicParameters(string body, ParameterType[] parameters, List args) { + if (parameters == null || parameters.Length == 0) + return body; + + string result = body; + for (int i = 0; i < parameters.Length && i < args.Count; i++) + { + string paramName = parameters[i].Name; + if (string.IsNullOrEmpty(paramName)) continue; + // whole-word replacement + result = Regex.Replace(result, @"\b" + Regex.Escape(paramName) + @"\b", args[i]); + } + return result; + } + + /// + /// Expand intrinsic calls in into their C++ equivalents. + /// For example, given an intrinsic day() = "jd - 2440588" the call day() + 1 + /// becomes (jd - 2440588) + 1. + /// Recurses up to times to handle chained calls. + /// + internal static string ResolveIntrinsicCalls(string expression, IDictionary intrinsics, int maxDepth = 20) + { + if (string.IsNullOrEmpty(expression) || intrinsics == null || intrinsics.Count == 0 || maxDepth <= 0) + return expression; + + bool anyReplaced = false; + string result = expression; + + // We scan left-to-right and build the output string incrementally. + // Using a loop rather than Regex.Replace because we need to consume the + // matched argument list (which the regex does not capture fully). + // s_intrinsicCallPattern matches a word immediately followed by '('; + // \b on the right side is intentionally absent — the '(' is the boundary. + int pos = 0; + var sb = new StringBuilder(); + + while (pos < result.Length) + { + Match m = s_intrinsicCallPattern.Match(result, pos); + if (!m.Success) break; + + string name = m.Groups[1].Value; + + // Skip if the identifier is a member or scope access (.name, ->name, ::name). + // \b matches after '.' / '>' / ':' because those are non-word characters, so + // we must guard here to avoid re-expanding e.g. _q_value.value() when "value" + // is also an intrinsic name. + if (IsPrecededByMemberAccessOperator(result, m.Index)) + { + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + if (!intrinsics.TryGetValue(name, out IntrinsicType intrinsic)) + { + // Not one of our intrinsics — skip past the identifier and keep going + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + // Found an intrinsic call. Locate the matching close paren. + int openParen = m.Index + m.Length - 1; // position of '(' + int closeParen = FindMatchingParen(result, openParen); + if (closeParen < 0) + { + // Malformed — leave as-is + sb.Append(result, pos, m.Index - pos + name.Length); + pos = m.Index + name.Length; + continue; + } + + // Append everything before the call + sb.Append(result, pos, m.Index - pos); + + // Extract and split arguments + string argsText = result.Substring(openParen + 1, closeParen - openParen - 1); + List args = string.IsNullOrWhiteSpace(argsText) + ? new List() + : SplitArguments(argsText); + + // Expand: substitute parameters into the intrinsic expression body + string body = intrinsic.Expression ?? string.Empty; + body = SubstituteIntrinsicParameters(body, intrinsic.Parameter, args); + + // Wrap in parens to preserve operator precedence + sb.Append('('); + sb.Append(body); + sb.Append(')'); + + pos = closeParen + 1; + anyReplaced = true; + } + + // Append any trailing text after the last match + sb.Append(result, pos, result.Length - pos); + result = sb.ToString(); + + // Recurse if we expanded anything (handles chained intrinsics) + if (anyReplaced) + result = ResolveIntrinsicCalls(result, intrinsics, maxDepth - 1); + + return result; + } + + private string ReplaceNamesInExpression(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) + { + // Strip Windows dll!-qualified type prefixes (e.g. Qt6Cored.dll!) + // for GDB/LLDB compatibility — meaningless outside Windows + expression = s_dllQualifiedPrefix.Replace(expression, ""); + + // Expand intrinsic calls (e.g. day(), memberOffset(3)) into plain C++ expressions + expression = ResolveIntrinsicCalls(expression, intrinsics); + return ProcessNamesInString(expression, new Substitute[] { (m)=> { @@ -1427,17 +1616,17 @@ private string ReplaceNamesInExpression(string expression, IVariableInformation /// /// /// - private IVariableInformation GetExpression(string expression, IVariableInformation variable, IDictionary scopedNames, string displayName = null) + private IVariableInformation GetExpression(string expression, IVariableInformation variable, IDictionary scopedNames, string displayName = null, IDictionary intrinsics = null) { - string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames); + string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames, intrinsics); IVariableInformation expressionVariable = new VariableInformation(processedExpr, variable, _process.Engine, displayName); expressionVariable.SyncEval(); return expressionVariable; } - private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames) + private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null) { - string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames); + string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames, intrinsics); IVariableInformation expressionVariable = new VariableInformation(processedExpr, variable, _process.Engine, null); expressionVariable.SyncEval(); diff --git a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs index e7b922409..264ebc4c5 100644 --- a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs +++ b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs @@ -1636,6 +1636,7 @@ public CustomVisualizerType[] CustomVisualizer { /// [System.Xml.Serialization.XmlElementAttribute("DisplayString", typeof(DisplayStringType))] [System.Xml.Serialization.XmlElementAttribute("Expand", typeof(ExpandType1))] + [System.Xml.Serialization.XmlElementAttribute("Intrinsic", typeof(IntrinsicType))] [System.Xml.Serialization.XmlElementAttribute("StringView", typeof(StringViewType))] [System.Xml.Serialization.XmlElementAttribute("UIVisualizer", typeof(UIVisualizerItemType))] public object[] Items { @@ -1822,4 +1823,96 @@ public string Value { } } } + + /// + /// Represents a <Parameter> child element of <Intrinsic>. + /// + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")] + public partial class ParameterType { + + private string nameField; + private string typeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Name { + get { return this.nameField; } + set { this.nameField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Type { + get { return this.typeField; } + set { this.typeField = value; } + } + } + + /// + /// Represents an <Intrinsic> element — an inline named expression that can be + /// called by name (with optional arguments) in any subsequent NatVis expression + /// within the same <Type> block. + /// + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")] + public partial class IntrinsicType { + + private string nameField; + private string expressionField; + private bool optionalField; + private bool optionalFieldSpecified; + private string categoryField; + private string moduleNameField; + private ParameterType[] parameterField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Name { + get { return this.nameField; } + set { this.nameField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Expression { + get { return this.expressionField; } + set { this.expressionField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public bool Optional { + get { return this.optionalField; } + set { this.optionalField = value; } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool OptionalSpecified { + get { return this.optionalFieldSpecified; } + set { this.optionalFieldSpecified = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Category { + get { return this.categoryField; } + set { this.categoryField = value; } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string ModuleName { + get { return this.moduleNameField; } + set { this.moduleNameField = value; } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("Parameter")] + public ParameterType[] Parameter { + get { return this.parameterField; } + set { this.parameterField = value; } + } + } } diff --git a/src/MIDebugEngine/Natvis.Impl/natvis.xsd b/src/MIDebugEngine/Natvis.Impl/natvis.xsd index 7b1bc61b1..c3c1b839f 100644 --- a/src/MIDebugEngine/Natvis.Impl/natvis.xsd +++ b/src/MIDebugEngine/Natvis.Impl/natvis.xsd @@ -577,6 +577,34 @@ + + + Defines a single parameter of an Intrinsic function, giving it a name and an optional C++ type hint. + + + + + + + + + Defines an inline named expression (an "intrinsic") that can be called by name within any subsequent + NatVis expression in the same <Type> block. For example: + <Intrinsic Name="day" Expression="jd - 2440588"/> + allows writing {day()} in a DisplayString. Parameters are substituted positionally: + <Intrinsic Name="offset" Expression="sizeof(int) * n"><Parameter Name="n" Type="int"/></Intrinsic> + + + + + + + + + + + + Specifies a visualizer entry which customizes the debugger view of a type. @@ -589,6 +617,7 @@ + diff --git a/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs b/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs new file mode 100644 index 000000000..6fee05ddd --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisIntrinsicTest.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using Xunit; +using Microsoft.MIDebugEngine.Natvis; + +namespace MIDebugEngineUnitTests +{ + /// + /// Unit tests for the NatVis <Intrinsic> expansion logic: + /// ResolveIntrinsicCalls and its helpers FindMatchingParen, + /// SplitArguments, and SubstituteIntrinsicParameters. + /// + public class NatvisIntrinsicTest + { + // ── helpers ────────────────────────────────────────────────────────── + + private static IntrinsicType MakeIntrinsic(string name, string expression, params (string name, string type)[] parameters) + { + var intrinsic = new IntrinsicType(); + intrinsic.Name = name; + intrinsic.Expression = expression; + if (parameters.Length > 0) + { + var ps = new ParameterType[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + ps[i] = new ParameterType(); + ps[i].Name = parameters[i].name; + ps[i].Type = parameters[i].type; + } + intrinsic.Parameter = ps; + } + return intrinsic; + } + + private static Dictionary Dict(params IntrinsicType[] intrinsics) + { + var d = new Dictionary(); + foreach (var i in intrinsics) + d[i.Name] = i; + return d; + } + + // ── FindMatchingParen ───────────────────────────────────────────────── + + [Fact] + public void FindMatchingParen_SimpleCall() + { + // "foo(bar)" — open paren at 3, close at 7 + Assert.Equal(7, Natvis.FindMatchingParen("foo(bar)", 3)); + } + + [Fact] + public void FindMatchingParen_NestedParens() + { + // "f(g(x))" — outer open at 1, outer close at 6 + Assert.Equal(6, Natvis.FindMatchingParen("f(g(x))", 1)); + } + + [Fact] + public void FindMatchingParen_EmptyArgs() + { + // "f()" — open at 1, close at 2 + Assert.Equal(2, Natvis.FindMatchingParen("f()", 1)); + } + + [Fact] + public void FindMatchingParen_Unmatched_ReturnsMinusOne() + { + Assert.Equal(-1, Natvis.FindMatchingParen("f(abc", 1)); + } + + // ── SplitArguments ──────────────────────────────────────────────────── + + [Fact] + public void SplitArguments_NoArgs_EmptyString() + { + // Empty string → no arguments. In practice ResolveIntrinsicCalls + // guards with IsNullOrWhiteSpace before calling SplitArguments, so + // zero-arg calls never reach it; but the helper itself should be consistent. + var result = Natvis.SplitArguments(""); + Assert.Empty(result); + } + + [Fact] + public void SplitArguments_SingleArg() + { + var result = Natvis.SplitArguments("42"); + Assert.Equal(new[] { "42" }, result); + } + + [Fact] + public void SplitArguments_MultipleArgs() + { + var result = Natvis.SplitArguments("a, b, c"); + Assert.Equal(new[] { "a", "b", "c" }, result); + } + + [Fact] + public void SplitArguments_NestedParens_NotSplit() + { + // "f(a, b), c" — the comma inside f(...) is not a split point + var result = Natvis.SplitArguments("f(a, b), c"); + Assert.Equal(new[] { "f(a, b)", "c" }, result); + } + + [Fact] + public void SplitArguments_ComparisonOperator_SplitsCorrectly() + { + // "a > 0, b" — '>' is a comparison operator, not a bracket; the comma is + // at depth 0 and must be treated as a split point. + // (Angle brackets are intentionally not tracked to avoid this ambiguity.) + var result = Natvis.SplitArguments("a > 0, b"); + Assert.Equal(new[] { "a > 0", "b" }, result); + } + + // ── SubstituteIntrinsicParameters ───────────────────────────────────── + + [Fact] + public void SubstituteIntrinsicParameters_NoParameters_BodyUnchanged() + { + string result = Natvis.SubstituteIntrinsicParameters("jd + 1", null, new List()); + Assert.Equal("jd + 1", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_SingleParam() + { + var ps = new[] { new ParameterType { Name = "count", Type = "int" } }; + string result = Natvis.SubstituteIntrinsicParameters("sizeof(int) * count", ps, new List { "3" }); + Assert.Equal("sizeof(int) * 3", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_WholeWordOnly() + { + // "val" should not be replaced inside "interval" + var ps = new[] { new ParameterType { Name = "val", Type = "int" } }; + string result = Natvis.SubstituteIntrinsicParameters("interval + val", ps, new List { "99" }); + Assert.Equal("interval + 99", result); + } + + [Fact] + public void SubstituteIntrinsicParameters_MultipleParams() + { + var ps = new[] + { + new ParameterType { Name = "a", Type = "int" }, + new ParameterType { Name = "b", Type = "int" } + }; + string result = Natvis.SubstituteIntrinsicParameters("a + b", ps, new List { "1", "2" }); + Assert.Equal("1 + 2", result); + } + + // ── ResolveIntrinsicCalls ───────────────────────────────────────────── + + [Fact] + public void ResolveIntrinsicCalls_NullDict_ReturnsUnchanged() + { + string result = Natvis.ResolveIntrinsicCalls("day() + 1", null); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_EmptyDict_ReturnsUnchanged() + { + string result = Natvis.ResolveIntrinsicCalls("day() + 1", new Dictionary()); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_UnknownName_ReturnsUnchanged() + { + var dict = Dict(MakeIntrinsic("month", "jd / 30")); + string result = Natvis.ResolveIntrinsicCalls("day() + 1", dict); + Assert.Equal("day() + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ZeroArgIntrinsic() + { + // day() with no parameters + var dict = Dict(MakeIntrinsic("day", "jd - 5")); + string result = Natvis.ResolveIntrinsicCalls("day() + 1", dict); + Assert.Equal("(jd - 5) + 1", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ParametrizedIntrinsic() + { + // memberOffset(3) where Expression = "sizeof(int) * count", param count + var dict = Dict(MakeIntrinsic("memberOffset", "sizeof(int) * count", ("count", "int"))); + string result = Natvis.ResolveIntrinsicCalls("memberOffset(3)", dict); + Assert.Equal("(sizeof(int) * 3)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ChainedIntrinsics() + { + // year() = N() + 1, N() = jd / 2 + var dict = Dict( + MakeIntrinsic("N", "jd / 2"), + MakeIntrinsic("year", "N() + 1") + ); + string result = Natvis.ResolveIntrinsicCalls("year()", dict); + Assert.Equal("((jd / 2) + 1)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_MemberAccessNotReExpanded() + { + // value() = _q_value.value() — after expansion the ".value()" must NOT + // be re-expanded even though "value" is in the intrinsics dictionary. + var dict = Dict(MakeIntrinsic("value", "_q_value.value()")); + string result = Natvis.ResolveIntrinsicCalls("value()", dict); + Assert.Equal("(_q_value.value())", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ArrowAccessNotReExpanded() + { + // ptr->get() — "get" is an intrinsic but must not expand when after "->" + var dict = Dict(MakeIntrinsic("get", "inner")); + string result = Natvis.ResolveIntrinsicCalls("ptr->get()", dict); + Assert.Equal("ptr->get()", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ParametrizedChained() + { + // isEmpty(size) = size==0; hasScheme() = !isEmpty(scheme_size) + var dict = Dict( + MakeIntrinsic("isEmpty", "size==0", ("size", "int")), + MakeIntrinsic("hasScheme", "!isEmpty(scheme_size)") + ); + string result = Natvis.ResolveIntrinsicCalls("hasScheme()", dict); + Assert.Equal("(!(scheme_size==0))", result); + } + + [Fact] + public void ResolveIntrinsicCalls_MultipleCallsInExpression() + { + var dict = Dict( + MakeIntrinsic("x", "a + 1"), + MakeIntrinsic("y", "b + 2") + ); + string result = Natvis.ResolveIntrinsicCalls("x() * y()", dict); + Assert.Equal("(a + 1) * (b + 2)", result); + } + + [Fact] + public void ResolveIntrinsicCalls_ExpressionWithNoCall_Unchanged() + { + var dict = Dict(MakeIntrinsic("day", "jd - 5")); + // Expression references "day" but without (), so no expansion + string result = Natvis.ResolveIntrinsicCalls("day + 1", dict); + Assert.Equal("day + 1", result); + } + } +}