using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace PoyoLang.Translator.SourceGenerator; [Generator] public class PoyoLangTranslatorGenerator : IIncrementalGenerator { private const char IndentChar = '\t'; public void Initialize(IncrementalGeneratorInitializationContext context) { var texts = context.AdditionalTextsProvider; // There will be only one of those but incremental generators work as pipelines var dictionaries = texts .Where(static text => text.Path.EndsWith("dictionary.txt")) .Select(static (text, _) => text.GetText()); var parsedDictionaries = dictionaries .Select(static (dictionary, _) => ReadCustomDictionary(dictionary!) ); var formattedDictionaries = parsedDictionaries .Select(static (dictionary, _) => { // Return normal and reverse dictionary order to have ngrams first return ( Normal: dictionary, Reversed: dictionary!.OrderBy(p => p.Value).ToDictionary(p => p.Value, p => p.Key) ); }); var prefixTrees = formattedDictionaries .Select(static (dictionaries, _) => ( Dictionary: dictionaries.Normal, PrefixTree: BuildPrefixTree(dictionaries.Reversed) )); context.RegisterSourceOutput(prefixTrees, static (sourceProductionContext, data) => { sourceProductionContext.AddSource("PoyoLangTranslator.g.cs", GenerateSource(data.Dictionary, data.PrefixTree)); }); } private static Dictionary ReadCustomDictionary(SourceText text) { var dictionary = new Dictionary(); foreach (var line in text.ToString().Split('\n')) { var span = line.TrimEnd('\r').AsSpan(); // Reached end of file if (span.Length < 1) { break; } var splitIndex = span.IndexOf('='); dictionary[span[..splitIndex].ToString()] = span[(splitIndex + 1)..].ToString(); } return dictionary; } private static List BuildPrefixTree(Dictionary dictionary) { var rootNodes = new List(); var firstNodes = dictionary.Where(p => p.Key.Length is 1); foreach (var firstNode in firstNodes) { var letter = firstNode.Key[0]; var target = firstNode.Value; var node = new Node(letter, target); rootNodes.Add(node); // Add sub-nodes ParseNodes(node, letter.ToString()); } return rootNodes; void ParseNodes(Node node, string prefix) { // Find nodes that have previous node as prefixed var subNodes = dictionary .Where(p => p.Key.StartsWith(prefix) && p.Key.Length == prefix.Length + 1); foreach (var subNode in subNodes) { var letter = subNode.Key[prefix.Length]; var target = subNode.Value; var newPrefix = $"{prefix}{letter}"; var newNode = new Node(letter, target); node.Nodes.Add(newNode); // Recursively add sub-nodes ParseNodes(newNode, newPrefix); } } } private static string GenerateSource(Dictionary dictionary, List rootNodes) { var source = new StringBuilder(); // Usings and namespace source.Append( """ using System; using System.Text; namespace PoyoLang.Translator; """ ); // Partial class definition source.Append( """ public partial class PoyoLangTranslator { """ ); GenerateNextLetterMethod(rootNodes, source); GenerateFromPoyoMethod(dictionary, source); // Partial class end source.Append( """ } """ ); return source.ToString(); } private static void GenerateNextLetterMethod(List rootNodes, StringBuilder source) { // Next letter method definition source.Append( """ private void NextLetter(ref ReadOnlySpan text, StringBuilder output) { """ ); // 0 length case and caps source.Append( """ if (text.Length < 1) { return; } var isCaps = char.IsUpper(text[0]); """ ); GenerateSwitchCases(rootNodes, depth: 0); // Next letter method end source.Append( """ // Punctuation/Unknown characters case output.Append(text[0]); text = text[1..]; } """ ); return; void GenerateSwitchCases(List nodes, int depth) { var indent = Indent(depth * 3); // Switch-case start source.Append( $$""" {{indent}}switch (text[{{depth}}]) {{indent}}{ """ ); foreach (var node in nodes) { var targetLower = node.Target; var targetUpper = ToTitleCase(targetLower); // Case start source.Append( $$""" {{indent}} case '{{node.Letter}}' or '{{char.ToUpper(node.Letter)}}': """ ); // Sub nodes handling if (node.Nodes.Count > 0) { source.Append( $$""" {{indent}} if (text.Length > {{depth + 1}}) {{indent}} { """ ); // Sub nodes GenerateSwitchCases(node.Nodes, depth + 1); source.Append( $$""" {{indent}} } """ ); } // Current node handling fallback source.Append( $$""" {{indent}} {{indent}} text = text[{{depth + 1}}..]; {{indent}} {{indent}} output.Append(isCaps ? "{{targetUpper}}" : "{{targetLower}}"); {{indent}} {{indent}} return; """ ); } // Switch-case end source.Append( $$""" {{indent}}} """ ); } } private static void GenerateFromPoyoMethod(Dictionary dictionary, StringBuilder source) { // From Poyo method definition source.Append( """ private void FromPoyo(ref ReadOnlySpan text, StringBuilder output) { """ ); // Initial cases source.Append( """ if (text.Length < 1) { return; } // This happens if the end of the text is not a poyo letter (punctuation for ex) if (text.Length < 4) { output.Append(text); text = text[^0..]; return; } var letter = text[..4]; """ ); GenerateReverseSwitchCases(); // From Poyo method end source.Append( """ // Advance in text text = text[4..]; } """ ); return; void GenerateReverseSwitchCases() { // Switch start source.Append( """ switch (letter) { """ ); foreach (var pair in dictionary) { // Non-caps case source.Append( $$""" case "{{pair.Key}}": output.Append("{{pair.Value}}"); break; """ ); // Caps case source.Append( $$""" case "{{ToTitleCase(pair.Key)}}": output.Append("{{ToTitleCase(pair.Value)}}"); break; """ ); } // Switch end source.Append( """ default: // Not a poyo letter, only read 1 character (could be punctuation for ex) output.Append(text[0]); text = text[1..]; return; } """ ); } } private static string ToTitleCase(string text) { return $"{char.ToUpper(text[0])}{text[1..]}"; } private static string Indent(int depth) => new(IndentChar, depth); }