// ----------------------------------------------------------------------------- // file="TextRegexSourceGen.GenClass" // Project: StgSharp // Copyright (c) Nitload. // SPDX-License-Identifier: MIT // ----------------------------------------------------------------------------- using Microsoft.CodeAnalysis.CSharp; using Nitload.RegularAnalysis.Abstraction; using System.Diagnostics.CodeAnalysis; namespace Nitload.RegularAnalysis.Text { internal partial class TextRegexSourceGen { internal static void GenerateMethodSource( RegexAstNode ast, in SequenceEmitter sc, in List> func_list, in SourceGenContext context, in ExitLabel exit_label = default, bool is_find = false ) { RegexAstNode root = ast; if (root.Label == RegexElementLabel.EPSILON) { return; } if (TryGenerateSpecialSource(root, sc, func_list, context, exit_label)) { return; } RegexElementLabel op = root.Label & (RegexElementLabel.VAST_OPERATOR); if (op != 0) { switch (op) { case RegexElementLabel.CONCAT: GenerateConcat(root, sc, func_list, context, exit_label); break; case RegexElementLabel.COUNT: GenerateCount(root, sc, func_list, context); break; case RegexElementLabel.GROUP_BEGIN: GenerateGroup(root, sc, func_list, context); break; case RegexElementLabel.ALT: GenerateAlt(root, sc, func_list, context); break; default: // TODO(NGRA): Report an unsupported operator instead of silently emitting // an incomplete matcher. break; } } else { op = root.Label & (RegexElementLabel.SEQUENCE); switch (op) { case RegexElementLabel.UNIT: GenerateUnit(root, sc, context, exit_label); break; case RegexElementLabel.UNIT_SET: GenerateUnitSet(root, sc, context, exit_label); break; case RegexElementLabel.UNIT_SPAN: GenerateUnitSpan(root, sc, context, exit_label); break; default: break; } // TODO(NGRA): Integrate is_find candidate search and continuation retries. // Leaf emission currently matches only at the active cursor. } } #region supporting method private static string FormatStringLiteral( string value ) { return SyntaxFactory.Literal(value).Text; } private static string FormatStringLiteral( ReadOnlySpan value ) { return SyntaxFactory.Literal(value.ToString()).Text; } private static string FormatCharLiteral( char value ) { return SyntaxFactory.Literal(value).Text; } private static string GenerateCharSetExpression( RegexCharSet set ) { string remain_span = SourceGenContext._remain_span; string accept = set.Accept ? string.Empty : "!"; switch (set.Type) { case RegexCharSetType.Single: return $"{accept}({FormatStringLiteral(set.Value)}.{nameof(MemoryExtensions.Contains)}({remain_span}[0]))"; case RegexCharSetType.Range: if (set.Value.Length != 2) { throw new InvalidOperationException("A charset range requires two endpoints."); } return $"{accept}({remain_span}[0] >= {FormatCharLiteral(set.Value[0])} && {remain_span}[0] <= {FormatCharLiteral(set.Value[1])})"; case RegexCharSetType.Set: switch (set.Value) { case "s": return $"{accept}char.IsWhiteSpace({remain_span}[0])"; case "w": return $"{accept}{Text_Regex}.{nameof(TextRegex.IsCharWord)}({remain_span}[0])"; case "d": return $"{accept}char.IsDigit({remain_span}[0])"; default: break; } goto default; case RegexCharSetType.Any: return set.Accept ? "true" : "false"; default: throw new NotSupportedException($"Unsupported charset rule: {set.Type} ({set.Value})."); } } #endregion #region UNIT direct matching private static void GenerateUnit( RegexAstNode value, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit ) { string literal = value.Value; if (literal.Length != 1) { throw new InvalidOperationException( $"UNIT requires one decoded character, but received {literal.Length}."); } string remain_span = context.RemainSpan; _ = sc.AppendLine($"if ({remain_span}.IsEmpty || {remain_span}[0] != {FormatCharLiteral(literal[0])})") .AppendLine("{") .AppendLine($"goto {exit.GetLabelName()};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice(1);"); } private static void GenerateUnitSet( RegexAstNode value, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit ) { RegexCharSetPayload payload = value.PayloadAs(); string[] rules = new string[payload.Set.Count]; for (int i = 0; i < rules.Length; i++) { rules[i] = GenerateCharSetExpression(payload.Set[i]); } // Combine rule-level complements before applying the enclosing charset complement. // An empty union accepts nothing; its complement accepts any available character. string predicate = rules.Length == 0 ? "false" : string.Join(" || ", rules); string mismatch = payload.Accept ? $"!({predicate})" : $"({predicate})"; string remain_span = context.RemainSpan; _ = sc.AppendLine($"if ({remain_span}.IsEmpty || {mismatch})") .AppendLine("{") .AppendLine($"goto {exit.GetLabelName()};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice(1);"); } private static void GenerateUnitSpan( RegexAstNode value, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit ) { string literal = value.Value; if (literal.Length == 0) { throw new InvalidOperationException("UNIT_SPAN requires a non-empty literal."); } // StartsWith also rejects input shorter than the decoded literal. string remain_span = context.RemainSpan; _ = sc.AppendLine($"if (!{remain_span}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(literal)}))") .AppendLine("{") .AppendLine($"goto {exit.GetLabelName()};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});"); } #endregion #region UNIT count matching private const int flattened_unit_count_threshold = 32; private static void GenerateUnitCount( RegexAstNode value, int min, int max, bool is_greedy, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit ) { if (min < 0 || (max >= 0 && max < min)) { throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); } string literal = value.Value; if (literal.Length != 1) { throw new InvalidOperationException( $"COUNT(UNIT) requires one decoded character, but received {literal.Length}."); } char expected = literal[0]; string expected_literal = FormatCharLiteral(expected); string remain_span = context.RemainSpan; // TODO(NGRA-NAMING): Allocate required_rest, required_matched, required_chunk_index, // and additional_count through the exclusive-name pool, including loop-local names. int scratch_scope_index = context.RequestScratchScopeIndex(); string exit_label_node = exit.GetLabelName(); string required_rest = $"__required_rest_{scratch_scope_index}"; string required_matched = $"__required_matched_{scratch_scope_index}"; string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; string additional_count = $"__additional_count_{scratch_scope_index}"; // TODO(NGRA): Replace this local COUNT exit with the inherited failure ExitLabel. // The temporary goto preserves the former local exit but still lets siblings execute. if (min <= flattened_unit_count_threshold) { if (min > 0) { string required_prefix = new(expected, min); _ = sc.AppendLine($"if (!{remain_span}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_prefix)}))") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice({min});"); } } else { // Keep large lower bounds vectorizable without emitting a giant literal. Validate // repeated threshold-sized chunks against a local span and commit the cursor only // after every chunk has matched. string required_chunk = new(expected, flattened_unit_count_threshold); int required_chunk_count = min / flattened_unit_count_threshold; int required_remainder_length = min % flattened_unit_count_threshold; _ = sc.AppendLine($"if ({remain_span}.Length < {min})") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{ROS_char} {required_rest} = {remain_span};") .AppendLine($"bool {required_matched} = true;") .AppendLine($"for (int {required_chunk_index} = 0; {required_chunk_index} < {required_chunk_count}; {required_chunk_index}++)") .AppendLine("{") .AppendLine($"if (!{required_rest}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_chunk)}))") .AppendLine("{") .AppendLine($"{required_matched} = false;") .AppendLine("break;") .AppendLine("}") .AppendLine($"{required_rest} = {required_rest}.Slice({flattened_unit_count_threshold});") .AppendLine("}"); if (required_remainder_length > 0) { string required_remainder = new(expected, required_remainder_length); _ = sc.AppendLine($"if ({required_matched} && !{required_rest}.{nameof(MemoryExtensions.StartsWith)}({FormatStringLiteral(required_remainder)}))") .AppendLine("{") .AppendLine($"{required_matched} = false;") .AppendLine("}"); } _ = sc.AppendLine($"if (!{required_matched})") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice({min});"); } int additional_limit = max < 0 ? -1 : max - min; if (is_greedy && additional_limit != 0) { _ = additional_limit < 0 ? sc.AppendLine($"while (!{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal})") .AppendLine("{") .AppendLine($"{remain_span} = {remain_span}.Slice(1);") .AppendLine("}") : sc.AppendLine($"for (int {additional_count} = 0; {additional_count} < {additional_limit} && !{remain_span}.IsEmpty && {remain_span}[0] == {expected_literal}; {additional_count}++)") .AppendLine("{") .AppendLine($"{remain_span} = {remain_span}.Slice(1);") .AppendLine("}"); } else if (!is_greedy && additional_limit != 0) { // Lazy COUNT deliberately stops at Min. Its optional tail is consumed only when // a future continuation attempt fails and requests another repetition. _ = sc.AppendLine("// TODO(NGRA): Retry the lazy COUNT tail from its continuation."); } } private static void GenerateUnitSetCount( RegexAstNode value, int min, int max, bool is_greedy, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit_label ) { if (min < 0 || (max >= 0 && max < min)) { throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); } string literal = value.Value; if (literal.Length != 1) { throw new InvalidOperationException( $"COUNT(UNIT) requires one decoded character, but received {literal.Length}."); } RegexCharSetPayload payload = value.PayloadAs(); IReadOnlyList charset = payload.Set; string remain_span = context.RemainSpan; // TODO(NGRA-NAMING): Allocate required_matched and additional_count through the // exclusive-name pool. Reserve required_rest/required_chunk_index if emitted later. int scratch_scope_index = context.RequestScratchScopeIndex(); string required_rest = $"__required_rest_{scratch_scope_index}"; string required_matched = $"__required_matched_{scratch_scope_index}"; string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; string additional_count = $"__additional_count_{scratch_scope_index}"; // TODO(NGRA): Route a failed minimum check through the inherited failure ExitLabel. // The break below only stops charset scanning and must remain local to that loop. bool accept = payload.Accept; RegexCharSet _set = charset[0]; _ = sc.AppendLine($"{ROS_char} {required_matched} = {remain_span}") .AppendLine($"int {additional_count} = 0;") .AppendLine($"for(; {additional_count} < {max}; {additional_count} ++)") .AppendLine("{"); if (charset.Count == 1) { // single charset rule _ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)})"); } else { // multi charset rule _ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)} ||"); for (int i = 1; i < charset.Count - 1; i++) { _set = charset[i]; _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ||"); } _set = charset[^1]; _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ){(accept ? string.Empty : ')')}"); } _ = sc .AppendLine("{") .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);") .AppendLine("}") .AppendLine("else") .AppendLine("{") .AppendLine($@"break;") .AppendLine("}") .AppendLine("}") .AppendLine($"if({additional_count} < {min})") .AppendLine("{") .AppendLine("}") .AppendLine("else") .AppendLine("{") .AppendLine("}"); } private static void GenerateUnitSpanCount( RegexAstNode value, int min, int max, bool is_greedy, [NotNull] in SequenceEmitter sc, in SourceGenContext context, in ExitLabel exit ) { if (min < 0 || (max >= 0 && max < min)) { throw new InvalidOperationException($"Invalid COUNT range: {min}..{max}."); } string literal = value.Value; if (literal.Length == 0) { throw new InvalidOperationException( "COUNT(UNIT_SPAN) requires a non-empty literal."); } string expected_literal = FormatStringLiteral(literal); string remain_span = context.RemainSpan; // TODO(NGRA-NAMING): Allocate required_rest, required_matched, required_chunk_index, // required_count, and additional_count through the exclusive-name pool. Flattened // sibling COUNT nodes share the declaration space even after their exit labels. int scratch_scope_index = context.RequestScratchScopeIndex(); string exit_label_node = exit.GetLabelName(); string required_rest = $"__required_rest_{scratch_scope_index}"; string required_matched = $"__required_matched_{scratch_scope_index}"; string required_chunk_index = $"__required_chunk_index_{scratch_scope_index}"; string required_count = $"__required_count_{scratch_scope_index}"; string additional_count = $"__additional_count_{scratch_scope_index}"; string starts_with = $"{nameof(MemoryExtensions.StartsWith)}"; // TODO(NGRA): Replace this local COUNT exit with the inherited failure ExitLabel. // The temporary goto preserves the former local exit but still lets siblings execute. if (literal.Length > flattened_unit_count_threshold) { // One repetition is already larger than the flattening threshold. Match the // original literal directly and keep all cursor mutations local until Min has // succeeded; the common tail below remains responsible for Min..Max. _ = sc.AppendLine($"int {required_count} = 0;") .AppendLine($"{ROS_char} {required_rest} = {remain_span};") .AppendLine($"for (; {required_count} < {min} && {required_rest}.{starts_with}({expected_literal}); {required_count}++)") .AppendLine("{") .AppendLine($"{required_rest} = {required_rest}.Slice({literal.Length});") .AppendLine("}") .AppendLine($"if ({required_count} != {min})") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{remain_span} = {required_rest};"); } else if (min <= flattened_unit_count_threshold / literal.Length) { if (min > 0) { Span required_prefix_span = stackalloc char[literal.Length * min]; for (int i = 0; i < min; i++) { literal.AsSpan().CopyTo(required_prefix_span[(i * literal.Length)..]); } string required_prefix = required_prefix_span.ToString(); _ = sc.AppendLine($"if (!{remain_span}.{starts_with}({FormatStringLiteral(required_prefix)}))") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{remain_span} = {remain_span}.Slice({min * literal.Length});"); } } else { // Keep large lower bounds vectorizable without emitting a giant literal. Validate // repeated threshold-sized chunks against a local span and commit the cursor only // after every chunk has matched. int count = flattened_unit_count_threshold / literal.Length; Span required_prefix_span = stackalloc char[literal.Length * count]; for (int i = 0; i < count; i++) { literal.AsSpan().CopyTo(required_prefix_span[(i * literal.Length)..]); } string required_chunk = required_prefix_span.ToString(); int required_chunk_count = min / count; int required_remainder_length = min % count; _ = sc.AppendLine($"{ROS_char} {required_rest} = {remain_span};") .AppendLine($"bool {required_matched} = true;") .AppendLine($"for (int {required_chunk_index} = 0; {required_chunk_index} < {required_chunk_count}; {required_chunk_index}++)") .AppendLine("{") .AppendLine($"if (!{required_rest}.{starts_with}({FormatStringLiteral(required_chunk)}))") .AppendLine("{") .AppendLine($"{required_matched} = false;") .AppendLine("break;") .AppendLine("}") .AppendLine($"{required_rest} = {required_rest}.Slice({count * literal.Length});") .AppendLine("}"); if (required_remainder_length > 0) { Span required_remainder_span = stackalloc char[literal.Length * required_remainder_length]; for (int i = 0; i < required_remainder_length; i++) { literal.AsSpan().CopyTo(required_remainder_span[(i * literal.Length)..]); } string required_remainder = required_remainder_span.ToString(); _ = sc.AppendLine($"if ({required_matched} && !{required_rest}.{starts_with}({FormatStringLiteral(required_remainder)}))") .AppendLine("{") .AppendLine($"{required_matched} = false;") .AppendLine("}") .AppendLine($"if ({required_matched})") .AppendLine("{") .AppendLine($"{required_rest} = {required_rest}.Slice({literal.Length * required_remainder_length});") .AppendLine("}"); } _ = sc.AppendLine($"if (!{required_matched})") .AppendLine("{") .AppendLine($"goto {exit_label_node};") .AppendLine("}") .AppendLine($"{remain_span} = {required_rest};"); } int additional_limit = max < 0 ? -1 : max - min; if (is_greedy && additional_limit != 0) { if (additional_limit < 0) { _ = sc.AppendLine($"while ({remain_span}.{starts_with}({expected_literal}))") .AppendLine("{") .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") .AppendLine("}"); } else { _ = sc.AppendLine($"for (int {additional_count} = 0; {additional_count} < {additional_limit} && {remain_span}.{starts_with}({expected_literal}); {additional_count}++)") .AppendLine("{") .AppendLine($"{remain_span} = {remain_span}.Slice({literal.Length});") .AppendLine("}"); } } else if (!is_greedy && additional_limit != 0) { // Lazy COUNT deliberately stops at Min. Its optional tail is consumed only when // a future continuation attempt fails and requests another repetition. _ = sc.AppendLine("// TODO(NGRA): Retry the lazy COUNT tail from its continuation."); } } #endregion #region UNIT find matching // TODO(NGRA): Add UNIT, UNIT_SET, and UNIT_SPAN candidate search emitters here. // Wire is_find dispatch and continuation retries when FIND matching is implemented. #endregion #region Operator generation private static void GenerateAlt( RegexAstNode node, [NotNull] in SequenceEmitter sc, in List> func_list, in SourceGenContext context ) { RegexAstNode current = node.Right; // TODO(NGRA-NAMING): Reserve exclusive names for future ALT cursor/capture snapshots; // their lifetime extends through retry labels, not just the candidate's emitted block. ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto); // TODO(NGRA): Walk every normalized ALT candidate exactly once. The final traversal // shape and failure continuation are not wired yet. while (RegexAstNode.IsNullOrEmpty(current)) { RegexElementLabel op = current.Label & RegexElementLabel.SEQUENCE; if (op != 0) { string code = current.SourceCode; switch (op) { case RegexElementLabel.UNIT: // TODO(NGRA): Guard against an empty remaining span before indexing it. _ = sc.AppendLine(@$"if({context.RemainSpan}[0] == {FormatCharLiteral(code[0])})") .AppendLine("{") .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);") .AppendLine($@"goto {exit_label};") .AppendLine("}"); break; case RegexElementLabel.UNIT_SET: // TODO(NGRA): Guard the remaining span, handle empty/Any sets, and // validate negated-set composition before emitting this predicate. RegexCharSetPayload payload = current.PayloadAs(); IReadOnlyList split = payload.Set; bool accept = payload.Accept; RegexCharSet _set = split[0]; _ = sc.AppendLine($"if({(accept ? string.Empty : "!(")} {GenerateCharSetExpression(_set)} ||"); for (int i = 1; i < split.Count - 1; i++) { _set = split[i]; _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ||"); } _set = split[^1]; _ = sc.AppendLine($" {GenerateCharSetExpression(_set)} ){(accept ? string.Empty : ')')}") .AppendLine("{") .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice(1);") .AppendLine($@"goto {exit_label};") .AppendLine("}"); break; case RegexElementLabel.UNIT_SPAN: // TODO(NGRA): Advance by code.Length rather than one character and // preserve the ALT rollback cursor on failure. string literal = FormatStringLiteral(code); _ = sc.AppendLine(@$"if({context.RemainSpan}.{MemoryExtensions.StartsWith}({literal}))") .AppendLine("{") .AppendLine($@"{context.RemainSpan} = {context.RemainSpan}.Slice({code.Length});") .AppendLine($@"goto {exit_label};") .AppendLine("}"); break; default: // TODO(NGRA): Route unsupported ALT candidates through the recursive // matcher or report a source-generation diagnostic. break; } // TODO(NGRA): Advance to the next ALT candidate after a scalar candidate // misses; the current sequence branch does not update current. } else { GenerateMethodSource(current, sc, func_list, context); current = current.Next; } } // TODO(NGRA): Emit the all-alternatives-failed continuation before the shared success // label, including restoration of the entry cursor and capture state. _ = sc.AppendLine($@"{exit_label}: {{ }}"); } private static void GenerateGroup( RegexAstNode node, [NotNull] in SequenceEmitter sc, in List> func_list, in SourceGenContext context ) { RegexAstNode left = node.Left; RegexAstNode right = node.Right; RegexGroupPayload config = node.PayloadAs(); // TODO(NGRA-NAMING): Reserve exclusive names for generated group snapshots and capture // temporaries. A capture's user-provided name is not a unique generated local name. // TODO(NGRA): Select the owned group body using Empty-sentinel checks, snapshot its // start cursor, and restore it when the child matcher fails. if (left is not null) { GenerateMethodSource(left, sc, func_list, context); } else if (right is not null) { GenerateMethodSource(right, sc, func_list, context); } string group_name = config.Name; // TODO(NGRA): Allocate numeric/named capture slots and emit the successful capture // span. Non-capturing groups still need a rollback scope but no stored capture. throw new NotImplementedException(); } private static void GenerateConcat( RegexAstNode node, [NotNull] in SequenceEmitter sc, in List> func_list, in SourceGenContext context, in ExitLabel exit_label = default ) { if (!RegexAstNode.IsNullOrEmpty(node.Left)) { GenerateMethodSource(node.Left, sc, func_list, context, exit_label); } if (!RegexAstNode.IsNullOrEmpty(node.Right)) { GenerateMethodSource(node.Right, sc, func_list, context, exit_label); } } private static void GenerateCount( RegexAstNode node, [NotNull] in SequenceEmitter sc, in List> func_list, in SourceGenContext context ) { RegexAstNode value = RegexAstNode.IsNullOrEmpty(node.Left) ? node.Right : node.Left; if (RegexAstNode.IsNullOrEmpty(value)) { throw new InvalidOperationException("COUNT requires an operand."); } RegexCountPayload payload = node.PayloadAs(); int min = payload.Min; int max = payload.Max; bool is_greedy = payload.IsGreedy; // TODO(NGRA-NAMING): Reserve exclusive names for future repetition/rollback state // across complex COUNT retries; recursive children must use the same method pool. ExitLabel exit_label = context.RequestExitLabel(ExitMode.Goto); if ((value.Label & RegexElementLabel.SEQUENCE) == 0) { // TODO(NGRA): Emit min/max repetition, zero-width progress protection, greedy vs. // lazy ordering, and per-attempt rollback for a complex COUNT body. GenerateMethodSource(value, sc, func_list, context); } else { switch (value.Label & RegexElementLabel.SEQUENCE) { case RegexElementLabel.UNIT: GenerateUnitCount(value, min, max, is_greedy, sc, context, exit_label); break; case RegexElementLabel.UNIT_SET: GenerateUnitSetCount(value, min, max, is_greedy, sc, context, exit_label); break; case RegexElementLabel.UNIT_SPAN: GenerateUnitSpanCount(value, min, max, is_greedy, sc, context, exit_label); break; default: // TODO(NGRA): Diagnose malformed COUNT children instead of emitting an // empty branch. break; } } // TODO(NGRA): Connect COUNT success/failure continuations to the surrounding rollback // scope; the label is currently only a placeholder. _ = sc.AppendLine($@"{exit_label}: {{ }}"); } #endregion } }