Add translator source generator

This commit is contained in:
2025-05-15 21:03:35 +02:00
parent a25cbd6dde
commit 871f46b996
11 changed files with 686 additions and 127 deletions

View File

@@ -0,0 +1,15 @@
namespace PoyoLang.Translator.SourceGenerator;
public class Node
{
public char Letter { get; }
public string Target { get; }
public List<Node> Nodes { get; } = [];
public Node(char letter, string target)
{
Letter = letter;
Target = target;
}
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Text.Json" Version="9.0.5" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>
<ItemGroup>
<None Remove="dictionary.json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,225 @@
using System.Text;
using System.Text.Json;
using Microsoft.CodeAnalysis;
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.json"))
.Select(static (text, _) => text.GetText());
var parsedDictionaries = dictionaries
.Select(static (dictionary, _) =>
JsonSerializer.Deserialize<Dictionary<string, string>>(dictionary!.ToString())
);
var formattedDictionaries = parsedDictionaries
.Select(static (dictionary, _) =>
{
// Reverse dictionary order to have ngrams first
return dictionary!.OrderBy(p => p.Value).ToDictionary(p => p.Value, p => p.Key);
});
var prefixTrees = formattedDictionaries
.Select(static (formattedDictionary, _) => BuildPrefixTree(formattedDictionary));
context.RegisterSourceOutput(prefixTrees, static (sourceProductionContext, prefixTree) =>
{
sourceProductionContext.AddSource("PoyoLangTranslator.g.cs", GenerateSource(prefixTree));
});
}
private static List<Node> BuildPrefixTree(Dictionary<string, string> dictionary)
{
var rootNodes = new List<Node>();
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(List<Node> 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
{
"""
);
// Next letter method definition
source.Append(
"""
public void NextLetter(ref ReadOnlySpan<char> 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, source: source);
// Next letter method end
source.Append(
"""
// Punctuation/Unknown characters case
text = text[1..];
output.Append(text[0]);
}
"""
);
// Partial class end
source.Append(
"""
}
"""
);
return source.ToString();
}
private static void GenerateSwitchCases(List<Node> nodes, int depth, StringBuilder source)
{
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)}}':
{{indent}}
"""
);
// 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);
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 string ToTitleCase(string text)
{
return $"{char.ToUpper(text[0])}{text[1..]}";
}
private static string Indent(int depth) => new(IndentChar, depth);
}