| File: System\Text\RegularExpressions\Symbolic\SymbolicRegexMatcher.Dgml.cs | Web Access |
| Project: src\runtime\src\libraries\System.Text.RegularExpressions\src\System.Text.RegularExpressions.csproj (System.Text.RegularExpressions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #if DEBUG using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; namespace System.Text.RegularExpressions.Symbolic { internal sealed partial class SymbolicRegexMatcher<TSet> { /// <inheritdoc cref="Regex.SaveDGML(TextWriter, int)"/> [ExcludeFromCodeCoverage(Justification = "Currently only used for testing")] public override void SaveDGML(TextWriter writer, int maxLabelLength) { lock (this) { if (maxLabelLength < 0) maxLabelLength = int.MaxValue; Dictionary<(int Source, int Target), (TSet Rule, List<int> NfaTargets)> transitions = GatherTransitions(this); writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); writer.WriteLine("<DirectedGraph xmlns=\"http://schemas.microsoft.com/vs/2009/dgml\" ZoomLevel=\"1.5\" GraphDirection=\"TopToBottom\" >"); writer.WriteLine(" <Nodes>"); writer.WriteLine(" <Node Id=\"dfa\" Label=\" \" Group=\"Collapsed\" Category=\"DFA\" DFAInfo=\"{0}\" />", FormatInfo(this, transitions.Count)); writer.WriteLine(" <Node Id=\"dfainfo\" Category=\"DFAInfo\" Label=\"{0}\"/>", FormatInfo(this, transitions.Count)); foreach (MatchingState<TSet> state in _stateCache.Values) { string info = CharKind.DescribePrev(state.PrevCharKind); string deriv = WebUtility.HtmlEncode(state.Node.ToString()); string nodeDgmlView = $"{(string.IsNullOrEmpty(info) ? info : $"Previous: {info} ")}{(string.IsNullOrEmpty(deriv) ? "()" : deriv)}"; writer.WriteLine(" <Node Id=\"{0}\" Label=\"{0}\" Category=\"State\" Group=\"Collapsed\" StateInfo=\"{1}\">", state.Id, nodeDgmlView); if (_stateFlagsArray[state.Id].IsInitial()) { writer.WriteLine(" <Category Ref=\"InitialState\" />"); } if (state.Node.CanBeNullable) { writer.WriteLine(" <Category Ref=\"FinalState\" />"); } writer.WriteLine(" </Node>"); writer.WriteLine(" <Node Id=\"{0}info\" Label=\"{1}\" Category=\"StateInfo\"/>", state.Id, nodeDgmlView); } writer.WriteLine(" </Nodes>"); writer.WriteLine(" <Links>"); foreach (MatchingState<TSet> initialState in GetInitialStates(this)) { writer.WriteLine(" <Link Source=\"dfa\" Target=\"{0}\" Label=\"\" Category=\"StartTransition\" />", initialState.Id); } writer.WriteLine(" <Link Source=\"dfa\" Target=\"dfainfo\" Label=\"\" Category=\"Contains\" />"); foreach (KeyValuePair<(int Source, int Target), (TSet Rule, List<int> NfaTargets)> transition in transitions) { string label = DescribeLabel(transition.Value.Rule, _builder); string info = ""; if (label.Length > maxLabelLength) { info = $"FullLabel = \"{label}\" "; label = string.Concat(label.AsSpan(0, maxLabelLength), ".."); } writer.WriteLine($" <Link Source=\"{transition.Key.Source}\" Target=\"{transition.Key.Target}\" Label=\"{label}\" Category=\"NonEpsilonTransition\" {info}/>"); // Render NFA transitions as labelless "epsilon" transitions (i.e. ones that don't consume a character) // from the target of the DFA transition. foreach (int nfaTarget in transition.Value.NfaTargets) { writer.WriteLine($" <Link Source=\"{transition.Key.Target}\" Target=\"{nfaTarget}\" Category=\"EpsilonTransition\"/>"); } } foreach (MatchingState<TSet> state in _stateCache.Values) { writer.WriteLine(" <Link Source=\"{0}\" Target=\"{0}info\" Category=\"Contains\" />", state.Id); } writer.WriteLine(" </Links>"); writer.WriteLine(" <Categories>"); writer.WriteLine(" <Category Id=\"DFA\" Label=\"DFA\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"EpsilonTransition\" Label=\"Epsilon transition\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"StartTransition\" Label=\"Initial transition\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"FinalLabel\" Label=\"Final transition\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"FinalState\" Label=\"Final\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"SinkState\" Label=\"Sink state\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"EpsilonState\" Label=\"Epsilon state\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"InitialState\" Label=\"Initial\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"NonEpsilonTransition\" Label=\"Nonepsilon transition\" IsTag=\"True\" />"); writer.WriteLine(" <Category Id=\"State\" Label=\"State\" IsTag=\"True\" />"); writer.WriteLine(" </Categories>"); writer.WriteLine(" <Styles>"); writer.WriteLine(" <Style TargetType=\"Node\" GroupLabel=\"InitialState\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('InitialState')\" />"); writer.WriteLine(" <Setter Property=\"Background\" Value=\"lightblue\" />"); writer.WriteLine(" <Setter Property=\"MinWidth\" Value=\"0\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Node\" GroupLabel=\"FinalState\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('FinalState')\" />"); writer.WriteLine(" <Setter Property=\"Background\" Value=\"lightgreen\" />"); writer.WriteLine(" <Setter Property=\"StrokeThickness\" Value=\"4\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Node\" GroupLabel=\"State\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('State')\" />"); writer.WriteLine(" <Setter Property=\"Stroke\" Value=\"black\" />"); writer.WriteLine(" <Setter Property=\"Background\" Value=\"white\" />"); writer.WriteLine(" <Setter Property=\"MinWidth\" Value=\"0\" />"); writer.WriteLine(" <Setter Property=\"FontSize\" Value=\"12\" />"); writer.WriteLine(" <Setter Property=\"FontFamily\" Value=\"Arial\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Link\" GroupLabel=\"NonEpsilonTransition\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('NonEpsilonTransition')\" />"); writer.WriteLine(" <Setter Property=\"FontSize\" Value=\"18\" />"); writer.WriteLine(" <Setter Property=\"FontFamily\" Value=\"Arial\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Link\" GroupLabel=\"StartTransition\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('StartTransition')\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Link\" GroupLabel=\"EpsilonTransition\" ValueLabel=\"True\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('EpsilonTransition')\" />"); writer.WriteLine(" <Setter Property=\"StrokeDashArray\" Value=\"8 8\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Link\" GroupLabel=\"FinalLabel\" ValueLabel=\"False\">"); writer.WriteLine(" <Condition Expression=\"HasCategory('FinalLabel')\" />"); writer.WriteLine(" <Setter Property=\"StrokeDashArray\" Value=\"8 8\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Node\" GroupLabel=\"StateInfo\" ValueLabel=\"True\">"); writer.WriteLine(" <Setter Property=\"Stroke\" Value=\"white\" />"); writer.WriteLine(" <Setter Property=\"FontSize\" Value=\"18\" />"); writer.WriteLine(" <Setter Property=\"FontFamily\" Value=\"Arial\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" <Style TargetType=\"Node\" GroupLabel=\"DFAInfo\" ValueLabel=\"True\">"); writer.WriteLine(" <Setter Property=\"Stroke\" Value=\"white\" />"); writer.WriteLine(" <Setter Property=\"FontSize\" Value=\"18\" />"); writer.WriteLine(" <Setter Property=\"FontFamily\" Value=\"Arial\" />"); writer.WriteLine(" </Style>"); writer.WriteLine(" </Styles>"); writer.WriteLine("</DirectedGraph>"); } // This function gathers all transitions in the given builder and groups them by (source,destination) state ID static Dictionary<(int Source, int Target), (TSet Rule, List<int> NfaTargets)> GatherTransitions(SymbolicRegexMatcher<TSet> matcher) { Dictionary<(int Source, int Target), (TSet Rule, List<int> NfaTargets)> result = new(); foreach (MatchingState<TSet> source in matcher._stateCache.Values) { // Get the span of entries in delta that gives the transitions for the different minterms ReadOnlySpan<int> deltas = matcher.GetDeltasFor(source); ReadOnlySpan<int[]?> nfaDeltas = matcher.GetNfaDeltasFor(source); Debug.Assert(deltas.Length == matcher._minterms.Length); for (int i = 0; i < deltas.Length; ++i) { // negative entries are transitions not explored yet, so skip them int targetId = deltas[i]; if (targetId >= 0) { // Get or create the data for this (source,destination) state ID pair (int Source, int Target) key = (source.Id, targetId); if (!result.TryGetValue(key, out (TSet Rule, List<int> NfaTargets) entry)) { entry = (matcher.Solver.Empty, new List<int>()); } // If this state has an NFA transition for the same minterm, then associate // those with the transition. if (nfaDeltas.Length > 0 && nfaDeltas[i] is int[] nfaTargets) { foreach (int nfaTarget in nfaTargets) { entry.NfaTargets.Add(matcher._nfaCoreIdArray[nfaTarget]); } } // Expand the rule for this minterm result[key] = (matcher.Solver.Or(entry.Rule, matcher._minterms[i]), entry.NfaTargets); } } } return result; } static string FormatInfo(SymbolicRegexMatcher<TSet> matcher, int transitionCount) { StringBuilder sb = new(); sb.Append($"States = {matcher._stateCache.Count} "); sb.Append($"Transitions = {transitionCount} "); sb.Append($"Min Terms ({matcher.Solver.GetMinterms()!.Length}) = ").AppendJoin(',', DescribeLabels(matcher.Solver.GetMinterms()!, matcher._builder)); return sb.ToString(); } static IEnumerable<string> DescribeLabels(IEnumerable<TSet> labels, SymbolicRegexBuilder<TSet> builder) { foreach (TSet label in labels) { yield return DescribeLabel(label, builder); } } static string DescribeLabel(TSet label, SymbolicRegexBuilder<TSet> builder) => WebUtility.HtmlEncode(builder._solver.PrettyPrint(label, builder._charSetSolver)); static IEnumerable<MatchingState<TSet>> GetInitialStates(SymbolicRegexMatcher<TSet> matcher) { foreach (MatchingState<TSet> state in matcher._dotstarredInitialStates) yield return state; foreach (MatchingState<TSet> state in matcher._initialStates) yield return state; foreach (MatchingState<TSet> state in matcher._reverseInitialStates) yield return state; } } } } #endif